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:
- hexdump: Display the communication between the local and remote machines in the console and deal with hexa data of course π€
- receive_from: Receive data from an incoming socket, whether from the local or remote machine
- proxy_handler: Manage the traffic direction between the remote and local machines
- 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 :
- 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 :
- Establishing Connections:
-
Connecting to both the local client and the remote server.
-
Managing Data Flow:
-
Facilitating the bidirectional flow of data between the client and the server.
-
Processing Data:
-
Allowing for inspection and modification of data passing through the proxy via
hexdump
,request_handler
, andresponse_handler
. -
Handling Protocols:
-
Accommodating protocols that require initial data exchange (
receive_first
) before entering the main communication loop. -
Ensuring Robustness:
- 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.
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 π₯²
Run the FTP with this command :
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.
- Save the proxy script to
proxy.py
. - Run the script, replacing the
local_host
andremote_host
as needed.
For example, if the FTP server is running locally on port 21, you can run the script like this:
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()
Alternatively, you can use
Enter the username (ftp
from the command line: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 π