Python TCP reverse shell: compromising a Kali machine using a Mac
Imagine a target node in a network behind a firewall that specifically blocks access from your node. What can be a possible way to compromise such a system even though you can’t establish connection to it? A basic yet powerful approach is to social engineer the target to somehow install and run a malicious script (or rather a script that probably even does some additional good in perspective of the target) that connects back to the attacking machine. Since the firewall doesn’t care where the nodes inside the network are connecting to (concretely, it doesn’t care about outgoing connections), it won’t stop such a connection back to the attacker.
There are various tools available to administer such an attack. They are beyond the scope of this article. We’ll rather focus on building our own toy reverse shell in Python and adding some functionality that enables us to execute commands, write files, remotely execute additional code, and other cool stuff on the target’s compromised system.
Overview of the task at hand
Limited by the hardware requirements, I (at least here) will be demonstrating code that communicates, through a TCP connection, between two ports on the same node/host as well as between two different hosts but on the same network. Different hosts in my case will be connected by a Wi-Fi, hence we will need to use the local IP of our server, instead of the global IP. I will talk about it when we get to the section (Sample Runs).
Overviewing the task at hand, few stages of development come to light. First stage would obviously be to build a proper send-receive relationship between the attacker and the target, where we establish connection and begin proper communication. Next stage would be writing code to drive the commands the attacker sends, and to send back the result/output to the attacker (the latter part of this stage is precisely a component of the communication part of the first stage, so we need to just integrate the two). Lastly, we wish to add functionality about some file transfers, so we’ll do that.
Prerequisites: It will, although be clear how it works, be beneficial if the reader is familiar with basics about a TCP connection and how to build one in Python.
Establishing connection
We begin by the basic setup on the attacker side.
attacker_server_binder()
servers to bind the server socket on the specified port on the attacker’s machine. Once bound, target_client_connection_receiver
blocks until a successful connection is established. target_client
is the socket object that stores all the information we’ll need about the target, as well as is the key to sending and receiving data.
Moving on the target side:
Extra imports in face of subprocess
and os
are for later. localhost
here implies connect to the port = 1234
on the same physical machine. In case of an actual attack, you replace this with the IP address of the attacker’s machine.
Sending/Receiving
Moving on to enabling basic communication, here is the attacker side script.
Although in a TCP connection, the node sending data receives an implicit acknowledgement flag (see TCP flags), we implement an explicitly communicated ACK
that acknowledges to the attacker the sent data has been received at the target end. The receive_data()
loop is standard for receiving data in the sense that it allows the assumption that the target needs to send data that spans several bytes (or even KB or higher) that can’t be fit in the buffer length (denoted byBUFFER_SIZE
) provided. To maintain coherence, we keep on receiving until we obtain a packet which has not utilised the entire buffer. We assume this is an indication that the current transmission has no more data to send (else it would have fit that data into the leftover buffer space) and terminate the reception loop for that transmission. Notice that after receiving an ACK
, we invariably enter into receive_data()
. This is intentional: we shall be sending commands, the acknowledgement of which when received, we expect the target machine to transmit the output of the command (STDOUT
on the target) back to the attacker.
Similar script for the target side:
Largely similar with two differences. First, no ACK
for sending data on the target side. We might include that as a safety faucet for ensuring the attacker receives in legible format what they intended for; I omitted that for sake of clarity here, partly assuming a high probability of successful TCP transmissions (you may omit the ACK
on the server side as well). Second, receive_data()
has additional work of sending a ACK
, doing something on the data, and sending back the response.
In both cases, data sent/received over transmission is in bytes
. This is intentional and works well, partly because there is easy decoding on the recipient end: decode('utf-8')
. utf-8
is the particular encoding used.
Executing commands
Now we are done with the basic setup (a setup which we shall follow in almost all attacks similar/dissimilar to the one described below) for communication, we move on to the fun part.
On the target side, we implement code for executing the commands the attacker sends.
And call the above function from our previous receive_data()
on the target side.
rstrip()
allows to delete trailing spaces and newline characters if any. This, though trivial looking, becomes specially important if the attacker is not vigilant of the trailing characters that crop in while inputting commands. Running commands involves subprocess
that we imported before. It allows creating a new process that runs the sent command and sends the output back to the attacker (precisely sends back to receive_data()
function which in turn sends the data back to the attacker over the connection).
Adapting the attacker side script:
This is quite simple. We take input the command and send it to the target. Everything occurs on the target end.
Managing directories
A thing I observed is the above setup fails to perform one of the most crucial things we would like it to: navigation around the directory system, ideally the cd
command. We now build extra functionality for that to occur.
On the target side, update the run_command()
function to capture whenever a command with cd
pops up.
Whenever we get a command that has cd
(ideally it should have been whenever we get cd
in the beginning of the command), we pass control to another routine instead of subprocess
. The routine is as such:
The above code is simply extracting the destination directory and applying the os.chdir()
inbuilt function to change current directory. Note from the code added in run_command()
that we return the current working directory ( os.getcwd()
) back to the attacker for reference.
Nothing has to be changed on the attacker side for this.
Managing file transfers
Remotely accessing files is important. We build that functionality here. Define the following command (or any command format of your choice):file file_name mode
where file
denotes the command, file_name
denotes the name of the file to operate upon, and mode
denotes the mode of opening the file. This is also a framework for creating your own commands that you wish to be specifically included in your reverse shell.
We update the following on the attacker side.
Since we have explicitly defined this file
command, we capture it differently (like we did with the cd
command in the previous section). I remark that it is better to test if the string file
comes up in the beginning of the inputted command, denoted by data
here. Anyway, if we capture a file
command, we fire a separate function to handle it.
After sending the command and receiving the ACK
, we move on to find the mode. If reading, we simply delegate to the attacker side receive_data()
we defined earlier. If writing however, we continuously take input and send to the target machine. A special flag FILE_UPDATE_QUIT
is designed keeping in mind the scenario where the attacker would rather like to send chunks of data to be written/appended to the file. These chunks may be less than the BUFFER_SIZE
defined earlier, still we do not want to cut the loop yet. Hence, the need for another flag to denote the attacker is done updating. Such a setting allows the attacker to even write small strings of much fewer bytes than BUFFER_SIZE
. After writing is done, we ready the attacker machine to receive some confirmation from the client, hence the call to receive_data()
.
On the client side, the following changes are made:
Quite intuitively, this captures the file
command (like it does for cd
) and transfers control to a separately defined function.
After some initial checks for the format of the specified command: file file_name mode
, the code moves on to try opening the file. If successful, it proceeds based on the mode
provided. Reading the file is a simple read()
and sending the data back to the attacker. Writing is again simply receiving everything the attacker has to send until a fresh FILE_UPDATE_QUIT
is encountered. The data is written/appended to the file, and a confirmation is sent back to the attacker.
Bringing it all together
The code can be found on Github. The entire code:
Attacker side:
Target side script:
Sample runs on same host
Some basic commands…
Targeting another host on the same network
All commands tested on the previous section are equally valid here.
We will be targeting a Kali machine from a Mac :)
There is a subtle difference between what you will know as the local IP address and the global IP address. Local IP addresses are closely related to commonly used IP addresses that are related to communication within a network. Refer to non-routable address spaces for more information.
My nodes are connected by the same Wi-Fi (or are within the same network). Hence, we shall use the local IP address of the attacker at appropriate points in both our scripts. Replace localhost
in both scripts (in bind
function in the attacker script and in connect_ex
in the target script).
To find about the local IP, execute the following on the attacker machine :
ip a s | grep -w inet | awk '{print $2}'
This is a special case of pipes ( or |). The output from first command ip a s
is fed as input to the next, grep
captures relevant information based on the parameters provided, and provides the output as input to awk
which prints them out.
If this doesn’t work out, simply type ifconfig
and search for inet
addresses of the form 192.168.x.x
or 172.x.x.x
or 10.x.x.x
(link). Mine turned out to be 192.168.43.34
for the Mac and 192.168.43.38
( 192.168.x.x
turn out to be smaller networks than the other two. Details lie in the subnet mask; intuition, however, can be built by seeing this address space has two x
rather than three as in others. This means this can accommodate less number of nodes in the local network.)
Since we are trying to attack the Kali machine using the Mac, I put in 192.168.43.34
in the relevant localhost
positions in both the scripts. Sample runs are as follows:
Conclusion
There are many ways to improve this. Some can be:
- Running commands that require additional input from the user. For instance, the first time
git push
or anysudo
requires additional input from the user after running the command. That needs to be managed. - Commands as
ping
did not run well. That needs to be fixed. - If the connection breaks, make the client retry connection repeatedly.
- (Obviously): Remove the
print
statements from the client side. They are currently just for test purposes.
There can be several ways to hide the script and make a non-vigilant user run it. Note that the connection exists as long as the client is connected to the attackers. A good way to ensure longevity is to hide the client script into a package (that looks good to the client, does some good work for client, or so). When the client runs the good script, it does its job, and additionally fires our malicious script and thrusts into the background with custom event scheduler independent of the original script with small changes in the run_command()
code above. Even though the good script terminates, your malicious TCP client script keeps on running.
Have a good day :)