In this post we’re going to build our actual TCP proxy server in Python. You might use one for forwarding traffic to bounce from host to host, or when assessing network-based software. When performing penetration tests in enterprise environments, you probably won’t be able to run Wireshark; nor will you be able to load drivers to sniff the loopback on Windows, and network segmentation will prevent you from running your tools directly against your target host. We’ve built simple Python proxies, like this one, in various cases to help you understand unknown protocols, modify traffic being sent to an application, and create test cases for fuzzers.
The proxy has a few moving parts. Let’s summarize the four main functions we need to write. We need to display the communication between the local and remote machines to the console (hexdump). We need to receive data from an incoming socket from either the local or remote machine (receive_from). We need to manage the traffic direction between remote and local machines (proxy_handler). Finally, we need to set up a listening socket and
pass it to our proxy_handler (server_loop).
Let’s get to it. Open a new file called proxy.py:
import sys
import socket
import threading
HEX_FILTER = ''.join(
[(len(repr(chr(i))) == 3) and chr(i) or '.' for i in range(256)])
def hexdump(src, length=16, show=True):
if isinstance(src, bytes):
src = src.decode()
results = list()
for i in range(0, len(src), length):
word = str(src[i:i+length])
printable = word.translate(HEX_FILTER)
hexa = ' '.join([f'{ord(c):02X}' for c in word])
hexwidth = length*3
results.append(f'{i:04x} {hexa:<{hexwidth}} {printable}')
if show:
for line in results:
print(line)
else:
return results
We start with a few imports. Then we define a hexdump function that takes some input as bytes or a string and prints a hexdump to the console. That is, it will output the packet details with both their hexadecimal values and ASCII-printable characters. This is useful for understanding unknown protocols, finding user credentials in plaintext protocols, and much more. We create a HEXFILTER string 1 that contains ASCII printable characters, if one exists, or a dot (.) if such a representation doesn’t exist. For an example of what this string could contain, let’s look at the character representations of two integers, 30 and 65, in an interactive Python shell:
>>> chr(65)
'A'
>>> chr(30)
'\x1e'
>>> len(repr(chr(65)))
3
>>> len(repr(chr(30)))
6
The character representation of 65 is printable and the character representation of 30 is not. As you can see, the representation of the printable character has a length of 3. We use that fact to create the final HEXFILTER string: provide the character if possible and a dot (.) if not.
The list comprehension used to create the string employs a Boolean short-circuit technique, which sounds pretty fancy. Let’s break it down: for each integer in the range of 0 to 255, if the length of the corresponding character equals 3, we get the character (chr(i)). Otherwise, we get a dot (.).
Then we join that list into a string so it looks something like this:
'................................ !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJK
LMNOPQRSTUVWXYZ[.]^_`abcdefghijklmnopqrstuvwxyz{|}~...........................
.......¡¢£¤¥¦§¨©ª«¬.®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæç
èéêëìíîïðñòóôõö÷øùúûüýþÿ'
The list comprehension gives a printable character representation of the first 256 integers. Now we can create the hexdump function. First, we make sure we have a string, decoding the bytes if a byte string was passed. Then we grab a piece of the string to dump and put it into the word variable. We use the translate built-in function to substitute the string
representation of each character for the corresponding character in the raw
string (printable). Likewise, we substitute the hex representation of the
integer value of every character in the raw string (hexa). Finally, we create a
new array to hold the strings, result, that contains the hex value of the index
of the first byte in the word, the hex value of the word, and its printable representation. The output looks like this:
>> hexdump('python rocks\n and proxies roll\n')
0000 70 79 74 68 6F 6E 20 72 6F 63 6B 73 0A 20 61 6E python rocks. an
0010 64 20 70 72 6F 78 69 65 73 20 72 6F 6C 6C 0A d proxies roll.
This function provides us with a way to watch the communication going through the proxy in real time. Now let’s create a function that the two ends of the proxy will use to receive data:
def receive_from(connection):
buffer = b""
connection.settimeout(5)
try:
while True:
data = connection.recv(4096)
if not data:
break
buffer += data
except Exception as e:
pass
return buffer
For receiving both local and remote data, we pass in the socket object to be used. We create an empty byte string, buffer, that will accumulate responses from the socket. By default, we set a five-second time-out, which might be aggressive if you’re proxying traffic to other countries or over lossy networks, so increase the time-out as necessary. We set up a loop to read response data into the buffer until there’s no more data, or we time out.
Finally, we return the buffer byte string to the caller, which could be either the local or remote machine.
Sometimes you may want to modify the response or request packets before the proxy sends them on their way. Let’s add a couple of functions (request_handler and response_handler) to do just that:
def request_handler(buffer):
# perform packet modifications
return buffer
def response_handler(buffer):
# perform packet modifications
return buffer
Inside these functions, you can modify the packet contents, perform fuzzing tasks, test for authentication issues, or do whatever else your heart desires. This can be useful, for example, if you find plaintext user credentials being sent and want to try to elevate privileges on an application by passing in admin instead of your own username.
Let’s dive into the proxy_handler function now by adding this code:
def proxy_handler(client_socket, remote_host, remote_port, receive_first):
remote_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
remote_socket.connect((remote_host, remote_port))
if receive_first:
remote_buffer = receive_from(remote_socket)
hexdump(remote_buffer)
remote_buffer = response_handler(remote_buffer)
if len(remote_buffer):
print("[<==] Sending %d bytes to localhost." % len(remote_buffer))
client_socket.send(remote_buffer)
while True:
local_buffer = receive_from(client_socket)
if len(local_buffer):
line = "[==>]Received %d bytes from localhost." % len(local_buffer)
print(line)
hexdump(local_buffer)
local_buffer = request_handler(local_buffer)
remote_socket.send(local_buffer)
print("[==>] Sent to remote.")
remote_buffer = receive_from(remote_socket)
if len(remote_buffer):
print("[<==] Received %d bytes from remote." % len(remote_buffer))
hexdump(remote_buffer)
remote_buffer = response_handler(remote_buffer)
client_socket.send(remote_buffer)
print("[<==] Sent to localhost.")
if not len(local_buffer) or not len(remote_buffer):
client_socket.close()
remote_socket.close()
print("[*] No more data. Closing connections.")
break
This function contains the bulk of the logic for our proxy. To start off, we connect to the remote host. Then we check to make sure we don’t need to first initiate a connection to the remote side and request data before going into the main loop. Some server daemons will expect you to do this (FTP servers typically send a banner first, for example). We then
use the receive_from function for both sides of the communication. It accepts
Basic Networking Tools 23 a connected socket object and performs a receive. We dump the contents of the packet so that we can inspect it for anything interesting. Next, we hand the
output to the response_handler function and then send the received buffer to the local client. The rest of the proxy code is straightforward: we set up our loop to continually read from the local client, process the data, send it to the remote client, read from the remote client, process the data, and send it to the local client until we no longer detect any data. When there’s no data to send on either side of the connection, we close both the local and
remote sockets and break out of the loop.
Let’s put together the server_loop function to set up and manage the connection:
def server_loop(local_host, local_port,
remote_host, remote_port, receive_first):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
server.bind((local_host, local_port))
except Exception as e:
print('problem on bind: %r' % e)
print("[!!] Failed to listen on %s:%d" % (local_host, local_port))
print("[!!] Check for other listening sockets or correct permissions.")
sys.exit(0)
print("[*] Listening on %s:%d" % (local_host, local_port))
server.listen(5)
while True:
client_socket, addr = server.accept()
# print out the local connection information
line = "> Received incoming connection from %s:%d" % (addr[0], addr[1])
print(line)
# start a thread to talk to the remote host
proxy_thread = threading.Thread(
target=proxy_handler,
args=(client_socket, remote_host,
remote_port, receive_first))
proxy_thread.start()
The server_loop function creates a socket and then binds to the local host and listens. In the main loop, when a fresh connection request comes in, we hand it off to the proxy_handler in a new thread, which does all of the sending and receiving of juicy bits to either side of the data stream.
The only part left to write is the main function:
def main():
if len(sys.argv[1:]) != 5:
print("Usage: ./proxy.py [localhost] [localport]", end='')
print("[remotehost] [remoteport] [receive_first]")
print("Example: ./proxy.py 127.0.0.1 9000 10.12.132.1 9000 True")
sys.exit(0)
local_host = sys.argv[1]
local_port = int(sys.argv[2])
remote_host = sys.argv[3]
remote_port = int(sys.argv[4])
receive_first = sys.argv[5]
if "True" in receive_first:
receive_first = True
else:
receive_first = False
server_loop(local_host, local_port,
remote_host, remote_port, receive_first)
if __name__ == '__main__':
main()
In the main function, we take in some command line arguments and then fire up the server loop that listens for connections.