What Is A TCP Proxy
A TCP proxy is a server that acts as an intermediary between a client and the destination server. Clients establish connections to the TCP proxy server, which then establishes a connection to the destination server. TCP proxy supports Window Scale (WS) option that are carried by SYN and SYN ACK packets.
Building A TCP Proxy With Python
There are a number of reasons to have a TCP proxy in your tool belt, both
for forwarding traffic to bounce from host to host, but also when assessing
network-based software. When performing penetration tests in enterprise
environments, you’ll commonly be faced with the fact that you can’t run
Wireshark, that you can’t load drivers to sniff the loopback on Windows, or
that network segmentation prevents you from running your tools directly
against your target host. I have employed a simple Python proxy in a num-
ber of cases to help understand unknown protocols, modify traffic being
sent to an application, and create test cases for fuzzers. Let’s get to it.
import sys import socket import threading 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: 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 print "[==>] Received incoming connection from %s:%d" % ¬ (addr,addr) # 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() def main(): # no fancy command-line parsing here if len(sys.argv[1:]) != 5: print "Usage: ./proxy.py [localhost] [localport] [remotehost] ¬ [remoteport] [receive_first]" print "Example: ./proxy.py 127.0.0.1 9000 10.12.132.1 9000 True" sys.exit(0) # setup local listening parameters local_host = sys.argv local_port = int(sys.argv) # setup remote target remote_host = sys.argv remote_port = int(sys.argv) # this tells our proxy to connect and receive data # before sending to the remote host receive_first = sys.argv if "True" in receive_first: receive_first = True else: receive_first = False # now spin up our listening socket server_loop(local_host,local_port,remote_host,remote_port,receive_first) main()
Most of this should look familiar: we take in some command-line arguments and then fire up a server loop that listens for connections. When a fresh connection request comes in, we hand it off to our proxy_handler, which does all of the sending and receiving of juicy bits to either side of the data stream.
Let’s dive into the proxy_handler function now by adding the following code above our main function.
def proxy_handler(client_socket, remote_host, remote_port, receive_first): # connect to the remote host remote_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) remote_socket.connect((remote_host,remote_port)) # receive data from the remote end if necessary if receive_first:
This function contains the bulk of the logic for our proxy. To start off, we check to make sure we don’t need to first initiate a connection to the remote side and request data before going into our main loop. Some server daemons will expect you to do this first (FTP servers typically send a banner first, for example).
remote_buffer = receive_from(remote_socket)
We then use our receive_from function, which we reuse for both sides of the communication; it simply takes in a connected socket object and performs a receive.
We then dump the contents of the packet so that we can inspect it for anything interesting.
# send it to our response handler remote_buffer = response_handler(remote_buffer) # if we have data to send to our local client, send it if len(remote_buffer): print "[<==] Sending %d bytes to localhost." % len(remote_buffer) client_socket.send(remote_buffer) # now lets loop and read from local, # send to remote, send to local # rinse, wash, repeat while True: # read from local host local_buffer = receive_from(client_socket) if len(local_buffer): print "[==>] Received %d bytes from localhost." % len(local_buffer) hexdump(local_buffer) # send it to our request handler local_buffer = request_handler(local_buffer) # send off the data to the remote host remote_socket.send(local_buffer) print "[==>] Sent to remote." # receive back the response remote_buffer = receive_from(remote_socket) if len(remote_buffer): print "[<==] Received %d bytes from remote." % len(remote_buffer) hexdump(remote_buffer) # send to our response handler remote_buffer = response_handler(remote_buffer) # send the response to the local socket client_socket.send(remote_buffer) print "[<==] Sent to localhost."
Next we hand the output to our response_handler function x. Inside this function, you can modify the packet contents, perform fuzzing tasks, test for authentication issues, or whatever else your heart desires. There is a complimentary request_handler function that does the same for modifying outbound traffic as well.
# if no more data on either side, close the connections if not len(local_buffer) or not len(remote_buffer): client_socket.close() remote_socket.close() print "[*] No more data. Closing connections." break
The final step is to send the received buffer to our local client. The rest of the proxy code is straightforward: we continually read from local, process, send to remote, read from remote, process, and send to local until there is no more data detected.
Let’s put together the rest of our functions to complete our proxy.
# this is a pretty hex dumping function directly taken from # the comments here: # http://code.activestate.com/recipes/142812-hex-dumper/ def hexdump(src, length=16): result =  digits = 4 if isinstance(src, unicode) else 2 for i in xrange(0, len(src), length): s = src[i:i+length] hexa = b' '.join(["%0*X" % (digits, ord(x)) for x in s]) text = b''.join([x if 0x20 <= ord(x) < 0x7F else b'.' for x in s]) result.append( b"%04X %-*s %s" % (i, length*(digits + 1), hexa,text) ) print b'\n'.join(result)
This is the final chunk of code to complete our proxy. First we create our hex dumping function u that will simply 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.
def receive_from(connection): buffer = "" # We set a 2 second timeout; depending on your # target, this may need to be adjusted connection.settimeout(2) try: # keep reading into the buffer until # there's no more data # or we time out while True: data = connection.recv(4096) if not data: break buffer += data except: pass return buffer
The receive_from function is used both for receiving local and remote data, and we simply pass in the socket object to be used. By default, there is a two-second timeout set, which might be aggressive if you are proxying traffic to other countries or over lossy net- (increase the timeout as necessary). The rest of the function simply handles receiving data until more data is detected on the other end of the connection.
# modify any requests destined for the remote host def request_handler(buffer): # perform packet modifications return buffer # modify any responses destined for the local host def response_handler(buffer): # perform packet modifications return buffer
Our last two functions enable you to modify any traffic that is destined for either end of the proxy. This can be useful, for example, if plaintext user credentials are being sent and you want to try to elevate privileges on an application by passing in admin instead of justin. Now that we have our proxy set up, let’s take it for a spin.
Now Let’s Check Our TCP Proxy
Now that we have our core proxy loop and the supporting functions in place, let’s test this out against an FTP server. Fire up the proxy with the following options:
thedarktech$ sudo ./proxy.py 127.0.0.1 21 ftp.target.ca 21 True
We used sudo here because port 21 is a privileged port and requires administrative or root privileges in order to listen on it. Now take your favorite FTP client and set it to use localhost and port 21 as its remote host and port. Of course, you’ll want to point your proxy to an FTP server that will actually respond to you. When I ran this against a test FTP server, I got the following result:
[*] Listening on 127.0.0.1:21 [==>] Received incoming connection from 127.0.0.1:59218 0000 32 32 30 20 50 72 6F 46 54 50 44 20 31 2E 33 2E 220 ProFTPD 1.3. 0010 33 61 20 53 65 72 76 65 72 20 28 44 65 62 69 61 3a Server (Debia 0020 6E 29 20 5B 3A 3A 66 66 66 66 3A 35 30 2E 35 37 n) [::ffff:22.22 0030 2E 31 36 38 2E 39 33 5D 0D 0A .22.22].. [<==] Sending 58 bytes to localhost. [==>] Received 12 bytes from localhost. 0000 55 53 45 52 20 74 65 73 74 79 0D 0A USER testy.. [==>] Sent to remote. [<==] Received 33 bytes from remote. 0000 33 33 31 20 50 61 73 73 77 6F 72 64 20 72 65 71 331 Password req 0010 75 69 72 65 64 20 66 6F 72 20 74 65 73 74 79 0D uired for testy. 0020 0A . [<==] Sent to localhost. [==>] Received 13 bytes from localhost. 0000 50 41 53 53 20 74 65 73 74 65 72 0D 0A PASS tester.. [==>] Sent to remote. [*] No more data. Closing connections.
You can clearly see that we are able to successfully receive the FTP banner and send in a username and password, and that it cleanly exits when the server punts us because of incorrect credentials.