How to Build an Evasive Shell in Python, Part 2: Building the Shell
Welcome back, everybody. In the previous article, we covered the ideas and concepts of well-known ports and trust exploitation in order to evade a firewall. Today, we'll be building the shell, the part that receives commands, executes them, and sends the output back to the attacker.
This portion of our shell is relatively simple, but we still have something we need to cover before we jump in. We'll be using UDP as the transport protocol for moving our commands and output in and out of the shell. This is actually important, so let's explain in a bit more detail.
You may be asking yourself, "What's so important about UDP?" Or you may not even know what UDP is. Well, UDP stands for User Datagram Protocol. The special thing about UDP is that it is connectionless. No connection is required to send data with UDP, so it's a prime candidate for our evasion shell.
UDP behaves drastically from its transport counterpart, TCP, which cares about what gets delivered and will retransmit if need be. On the other hand, UDP will just transmit everything at once, and doesn't care if the destination received the full transmission. This can make UDP a bit difficult to work with, but it will be worth it in the end.
So let's get to making our shell now!
When scripting with Python (or any language for that matter), the first things we need to do when creating a script are set the interpreter path and import whatever modules we need into the script. Let's do this now:
Setting the interpreter path lets the file know where the Python interpreter is located so that we can execute it as a normal executable instead of a Python script.
Let's quickly run through the modules that we'll be using and their purpose:
- socket - This will be used to receive commands and send the output back to the attacker.
- os - This will be used to execute the commands and return the output.
- sys - This will be used to exit the script, killing the shell.
- platform - This will be used to gather information such as the OS; it will play a key role in creating a prompt for the attacker.
Now that we know what the interpreter path does, and what our modules are for, let's move on.
We're going to be creating a function to initialize communication with the attacker. It will create the socket, and wait for the attacker to start interaction.
When using UDP with Python sockets, we must use the recvfrom() and sendto() functions in order to properly initialize the interaction. Using recvfrom() will return the source address and port number of the data received. Since we're spoofing our IP, we'll need to remember where to send data. This is perfect because we can store these values into their own variables for later use. Let's go ahead and break down this function:
Here can see we've defined our function as launch(). First, we make a socket object and store it in s. You can see that this is in fact a UDP socket as it uses socket.SOCK_DGRAM.
The socket is then bound to port 80 on all interfaces. Binding to a well-known port will decrease the likelihood that we'll be suspected by that pesky firewall.
We then call the previously discussed recvfrom() function. This will wait for data to come from any source; it will then extract the source address and source port numbers and store them in addr and port. It will then send a confirmation string of "hello master" back to the attacker—confirmation that initialization has been successful.
It will then return the socket, the address, and the port number so that we may use them later.
Our next function will gather system information and build a proper prompt for the attacker to use. This function will be called getsysinfo(). This is where we'll be using our platform module, so let's take a look and break it down:
We can see above that the shell will wait for the attacker to send something before building and sending the prompt. This is to insure that the attacker is ready to receive the data.
We then use os.getuid() in order to retrieve the permissions of the current user. If the result is zero, that means the shell is running as root and the prompt needs to reflect this, so we put root@ and a pound sign (#) inside of our prompt list. If the user is anything but root, we'll simply make the prompt start with user@ and end with $.
Now that we've got our permissions down, we need to know the OS of the victim. This is where the platform module comes into play.
If we call the platform.dist() function out of the platform module, it will return a list with some information about the host, including the OS name. We'll insert whatever this string may be between the two elements of our list. We'll use the .join() method to make them into a single string, then we'll send this string back to the attacker.
Now that we've sent the prompt back to the attacker, we need to be able to execute commands. This is the purpose of this third function, named shell.
This function will receive commands from the attacker, run them using os.popen(), collect the output using the .readlines() method, and will send this output back to the attacker. There's just one thing about this: if we use a command that doesn't give us any output, then the shell will hang. Most commands will still work and won't be an issue, but there is one command that is very crucial—cd.
We'll need to test for this command before executing the received command. If the attacker wants to change directories, we'll need to use the os.chdir() function out of the os module. We'll need to test for this command every time a new command is received.
Let's go ahead and take a look at our shell() function:
This may look complicated, but it's really simple when we break it down.
First, we enter an infinite loop; this will allow us to execute commands as long as we want. Then we set everything within a try to catch any errors that occur. If an error does occur, we simply send an error message to the attacker and ignore the error.
Once we receive a command, we split it at every space using the .split() method. We then test to see if the first element of this list is the string "cd". If it is, then we'll remove this portion of the string and call os.chdir with the remainder of the received command. This will allow us to change directories!
We then test to see if the the string "goodbye" was sent. If it was, we send the string "Goodbye master" and close the socket. This will be used by the attacker as a way of terminating the shell.
Finally, if the received command isn't "cd" or "goodbye" we'll just execute it as a normal command. We'll store the object created by calling os.popen() in a variable called proc (short for process), we'll then define a variable to store the output in the conveniently named output, and we'll use the .readlines() method to store the command output in this variable. We then send the new value of this output string back to the attacker.
It is highly recommended not to use os.popen(). It has been replaced with subprocess.Popen(). But this method of executing commands has many issues executing commands that don't give output, so we'll stick with os.popen() for now. But remember, this is not recommended.
That's it for this portion of the shell, now let's wrap things, shall we?
Today, we built the victim's portion of our evasion shell. In the next article, we'll build the attackers portion, which will initialize interaction and send commands to the victim.
If you have any questions, leave them in the comments below, and I'll do my absolute best to answer them and to clear up any confusion.
Thank you for reading!