Skip to content

Building a python mini proxy 😈

Having a TCP proxy in your toolkit is invaluable for various reasons. You might use it to forward traffic between hosts or to analyze and assess network-based applications.

When performing penetration tests in enterprise environments, you may not be able to run tools like Wireshark or load drivers to sniff loopback interfaces on Windows. Network segmentation might also prevent you from running your tools directly against the target host. In such cases, building a simple Python proxy can be extremely helpful. It allows you to understand unknown protocols, modify traffic sent to an application, and create test cases for fuzzers.

Our proxy will consist of several components. Let's summarize the four main functions we'll need to implement:

  1. hexdump: Display the communication between the local and remote machines in the console and deal with hexa data of course πŸ€“
  2. receive_from: Receive data from an incoming socket, whether from the local or remote machine
  3. proxy_handler: Manage the traffic direction between the remote and local machines
  4. server_loop: Set up a listening socket and pass connections to our proxy_handler

Let's get started by opening a new file called proxy.py.

Decode hexa data

As I said, display the communication between the local and remote machines in the console will be our first challenge. Since we are handling packets data ==> encoded in hexa so we need to be able to decode this shit

import sys
import socket
import threading


#The list comprehension gives a printable character representation of the first 256 integers.
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

You can test this function by running the command : python -i proxy.py, you should see something like this :

>>> hexdump("this is working well brooo !! (; ")
0000 74 68 69 73 20 69 73 20 77 6F 72 6B 69 6E 67 20  this is working 
0010 77 65 6C 6C 20 62 72 6F 6F 6F 20 21 21 20 28 3B  well brooo !! (;
0020 20                                                
>>> 

The hexdump function enables us to monitor the communication passing through the proxy in real time.

Receive data

Next, let's create a function that both 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

def request_handler(buffer):
    # perform packet modifications
    return buffer

def response_handler(buffer):
    # perform packet modifications
    return buffer

Proxy handler function

Let's code the proxy_handler now ! This function is central to managing the communication between the client and the remote server through the proxy.

Certainly! Let's break down the proxy_handler function to understand its role within your TCP proxy implementation. This function is central to managing the communication between the client and the remote server through the proxy. Here's a step-by-step explanation of what proxy_handler does. First let's explain the parameters :

def proxy_handler(client_socket, remote_host, remote_port, receive_first):
    ...
  • Parameters:
  • client_socket: The socket object representing the connection from the local client.
  • remote_host: The hostname or IP address of the remote server you want to connect to.
  • remote_port: The port number on the remote server to connect to.
  • receive_first: A boolean flag indicating whether the proxy should first receive data from the remote server before entering the main communication loop. This is useful for protocols where the server sends an initial message (e.g., FTP banners).

The proxy_handler function will be acting as the core component of our TCP proxy by :

  1. Establishing Connections:
  2. Connecting to both the local client and the remote server.

  3. Managing Data Flow:

  4. Facilitating the bidirectional flow of data between the client and the server.

  5. Processing Data:

  6. Allowing for inspection and modification of data passing through the proxy via hexdump, request_handler, and response_handler.

  7. Handling Protocols:

  8. Accommodating protocols that require initial data exchange (receive_first) before entering the main communication loop.

  9. Ensuring Robustness:

  10. Monitoring the connections and gracefully closing them when communication ends or an error occurs.

In summary, the proxy_handler enables our proxy to effectively monitor, analyze, and manipulate network traffic, which is essential for tasks like penetration testing, debugging, and protocol analysis.

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))
    #Check if 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)
    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

Make it loopy

Now 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):
    #create socket, bind it to the host then listen 
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        server.bind((local_host, local_port))
    except:
        #TODO: capture exception 
        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)
    #listen connection --> when comes in start a new thread
    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()


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)

That's it you are basically an hacker for now πŸ˜‚

Testing against ftp target

Ok we have coded this but how about testing it and celebrate !

Step 1: Run some FTP server locally

Set up our target and run some FTP server directly on your machine using a simple FTP server package or Docker

Using docker to run our FTP

If you prefer using Docker, you can run an FTP server like vsftpd.

docker run -d -p 21:21 -p 21000-21010:21000-21010 -e FTP_USER=user -e FTP_PASS=pass fauria/vsftpd

This command runs a vsftpd server with: - Username: user - Password: pass - FTP server running on port 21 (you can expose more passive ports as well if needed).

Very original as you can see πŸ˜…

Or using a python-based FTP server (geeky-way)

Alternatively, you can also install an FTP server on your local machine with python if you feel like a brave dev πŸ₯²

pip install pyftpdlib

Run the FTP with this command :

python -m pyftpdlib --port 21 --user user --password pass

And that's it you have a simple FTP server running on port 21 with the username user and password pass.

Be aware of your network security solution installed on your machine like firewalls ⚠️

Step 2: Run our Python script locally

Now, you can run the Python proxy script that we'bve seen earlier on your local machine, setting it up to intercept FTP traffic.

  1. Save the proxy script to proxy.py.
  2. Run the script, replacing the local_host and remote_host as needed.

For example, if the FTP server is running locally on port 21, you can run the script like this:

python proxy.py 127.0.0.1 2121 127.0.0.1 21 True

This command sets up the proxy on port 2121 on your local machine (you can adjust it on your needs of course), and it will forward traffic to the FTP server running on port 21 on the same machine (127.0.0.1).

Step 3: Test the Proxy by set up an FTP client

You can use a simple Python FTP client script, or an FTP client tool like FileZilla or ftp command-line utility of you feel linux brave now to connect to the proxy πŸ₯·πŸΌ

import ftplib

def ftp_connect():
    ftp = ftplib.FTP()
    ftp.connect('127.0.0.1', 2121)  # Connect to the proxy container 
    ftp.login('user', 'pass')  # Login with the credentials for the FTP server
    print(ftp.getwelcome())
    ftp.quit()

if __name__ == "__main__":
    ftp_connect()
And that's it, an other python package magic tricks from the hood πŸ₯·πŸΌ

Alternatively, you can use ftp from the command line:

ftp 127.0.0.1 2121
Enter the username (user) and password (pass) when prompted.

Step 4: Celebration : inspect traffic

Once you run the FTP client, the proxy script running on port 2121 for our example will intercept the traffic. You can modify the script to inspect, manipulate, or simply forward the traffic between the client and the FTP server.

Now returning on our proxy shell you should see something like this :

[*] Listening on 127.0.0.1:2121
> Received incoming connection from 127.0.0.1:64332
    0000 32 32 30 20 28 76 73 46 54 50 64 20 33 2E 30 2E  220 (vsFTPd 3.0.
0010 32 29 0D 0A                                      2)..
[<==] Sending 20 bytes to localhost.
[==>]Received 11 bytes from localhost.
0000 55 53 45 52 20 75 73 65 72 0D 0A                 USER user..
[==>] Sent to remote.
[<==] Received 34 bytes from remote.
0000 33 33 31 20 50 6C 65 61 73 65 20 73 70 65 63 69  331 Please speci
0010 66 79 20 74 68 65 20 70 61 73 73 77 6F 72 64 2E  fy the password.
0020 0D 0A                                            ..
[<==] Sent to localhost.
[==>]Received 11 bytes from localhost.
0000 50 41 53 53 20 70 61 73 73 0D 0A                 PASS pass..
[==>] Sent to remote.
[<==] Received 23 bytes from remote.
0000 32 33 30 20 4C 6F 67 69 6E 20 73 75 63 63 65 73  230 Login succes
0010 73 66 75 6C 2E 0D 0A                             sful...
[<==] Sent to localhost.
[==>]Received 6 bytes from localhost.
0000 51 55 49 54 0D 0A                                QUIT..
[==>] Sent to remote.
[<==] Received 14 bytes from remote.
0000 32 32 31 20 47 6F 6F 64 62 79 65 2E 0D 0A        221 Goodbye...
[<==] Sent to localhost.
[*] No more data. Closing connections.

Isn't that great you can see all informations clearly like a real hacker shinobi, congrats πŸŽ‰

Be aware of firewalls and security sofware you have on your machines ⚠️

Key testing points

  • FTP Server: Running on port 21 (Docker or local installation as you like if you're a VMWare πŸ‘¨β€πŸ³).
  • Proxy: Running on port 2121 on your local machine.
  • FTP Client: Connects to port 2121 (the proxy), which forwards the traffic to the FTP server on port 21.

Deeper into the rabit hole

Hope you have learn a thing or two and enjoyed the ride, in the next section we will go deeper into packet manipulation 😎

Remember above we just write two functions request_handler and response_handler and if you had take a good look you know that's here where the fun begin πŸš€