Python TCP reverse shell: compromising a Kali machine using a Mac

Nimish Mishra
The Startup
Published in
10 min readJun 21, 2020
Source: https://www.netsparker.com/blog/web-security/understanding-reverse-shells/

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.

Sample of a reverse shell. Source: https://resources.infosecinstitute.com/icmp-reverse-shell/#gref

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 side script

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:

target side script for establishing connection

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.

attacker side script enabled for sending and receiving data

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:

target side script for sending/receiving data

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.

target side code for executing commands

And call the above function from our previous receive_data() on the target side.

target side code calling run_command()

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:

attacker side script for handling commands

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.

target side script for handling cd command. A thing to note is this code will capture all commands that have cd anywhere in them, like mkdir abcd. A better alternative is to make sure cd is the first word to be encountered.

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:

target side script for navigation of directories

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.

attacker side script for capturing the file command

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.

attacker side file handler

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:

target side code for handling file commands

Quite intuitively, this captures the file command (like it does for cd) and transfers control to a separately defined function.

main file handler for the target

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:

attacker side complete script

Target side script:

target side complete script

Sample runs on same host

Some basic commands…

Successful directory navigation and manipulation and file creation
Creating and writing to a python file and running python code remotely.
Some move file and directory manipulation commands
curl and reading downloaded file. A problem I witnessed is that the entire file could not be read in one go. But that can be fixed easily.
Commands to know the surface of the target’s machine configuration. I tested out more intricate commands; they run too.
Process lists

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:

Attacker listening (Mac)
Client (Kali) runs the script (this is the phase you need to social engineer your client into: to run the script)
Attacker (Mac) runs remotely a series of commands.
Client machine (Kali) responds to it.
Just in case the client notices a weird looking code they didn’t write, and runs it.

Conclusion

There are many ways to improve this. Some can be:

  1. Running commands that require additional input from the user. For instance, the first time git push or any sudo requires additional input from the user after running the command. That needs to be managed.
  2. Commands as ping did not run well. That needs to be fixed.
  3. If the connection breaks, make the client retry connection repeatedly.
  4. (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 :)

--

--

The Startup
The Startup

Published in The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +772K followers.