Writing a Simple Telnet Honeypot in Python
Introduction
A honeypot is a device that poses as a vulnerable system, making it an irresistible target for an attacker. But in reality, the honeypot is a trap that can be used to detect an attacker. This makes honeypots an extremely valuable tool for detecting when your network has been compromised since attackers will often try to pivot from one machine to another within a network.
Honeypots also have uses in malware research and threat intelligence because they can be used to capture malicious software or gather information about attackers while posing as a vulnerable system.
My First Honeypot
One Friday night in 2017 while I was on a bus headed home from university, I popped open my slider phone, ssh’d into a VPS I was using as a game server and began writing some Python code. This was shortly after the source code for Mirai was leaked to the public and incredible numbers of poorly-secured internet of things devices were being infected and used in some of the most high-profile DDoS attacks ever seen. I was curious about how this piece of malware behaved, so I cobbled together a Python program that posed as a vulnerable IoT device with telnet accessible to the internet. Within seconds, I had infected devices trying to connect to my honeypot. After some refinement, I was able to successfully identify a command and control server (which was promptly reported to the ISP serving it).
This was a lot of fun, but the honeypot was very much a cobbled-together mess and I recently thought it might be fun to revisit it in a sort-of tutorial format.
The Telnet Server
In order to write a Telnet honeypot, you must first write a Telnet server. Telnet is a relatively simple protocol defined in RFC854. Telnet allows both data and commands to be sent over the same TCP socket, but for the purposes of this simple server we will just ignore the commands for now. Most automated scanners don’t send telnet commands, anyways.
The server will consist of three classes: A server class that handles incoming connections, a client connection class that handles individual clients and a class for logging activity. Create a new Python file called telnetsvr.py
and we will begin adding code to it:
This class is responsible for setting up a TCP socket on the host and listening for incoming connections. Our main loop will eventually instantiate this class, call listen()
and then call accept()
in a loop in order accept incoming telnet connections. When an telnet client connects to the server, the accept()
function in TelnetServer returns a TelnetServerClient object. Let’s create that next.
First, add the following constants to the top of telnetsvr.py
:
Next, Create the TelnetServerClient class:
This class is initialized by the TelnetServer class and has functions to send to or receive from the socket. The recvstr() function is a convenience function that automatically converts the received data to a string. This is a bit of a hack and might break if the remote client sends a telnet command, but we will ignore this for now in the interests of keeping this server simple. Both of these functions automatically perform some logging (which we will add later) if a log object was specified.
This is good, but it would be nice if we could send or receive one line at a time from the client since commands are typically line terminated. In order to do that, we will need a buffer to store the data we’ve received. We’ll also add a queue since it’s a convenient way of storing lines.
Add the following to the __init__()
function of TelnetServerClient
:
Next add a function to TelnetServerClient
to receive a data from the connected client into the buffer we just created:
Now for the complicated part: Parsing the lines.
Add the following functions to TelnetServerClient
:
readLine()
is a function that reads one line sent by the remote system from the queue if a line is available and _splitLines()
is a function that is used to tokenize lines from a string, such as the buffer we created earlier.
If you’re familiar with Python, you might be tempted to simply use line.split('\n')
to separate the buffer into individual lines. There’s a small complication we deal with in _splitLines()
, though! In the telnet protocol, lines may be either CR-LF or CR-NULL terminated and we must consider both cases. Additionally, we may wish to know the termination type as each of these have different meanings. So, we check for both of those cases in the while
loop of this function in order to account for this. When a line is found, it is appended to a list along with the line termination. When this while loop finishes executing, any leftover
data in the buffer is returned along with the list of lines.
Now reading a line is fairly simple. readLine()
processes any data in the buffer, adds any lines it finds to the queue then attempts to read from the queue. If there is nothing in the queue to return, it reads data from the socket into the buffer and tries again. The effect of this is that this function blocks until there’s a line available to be read.
Next, let’s deal with sending a line and closing the socket. Add the following to TelnetServerClient
:
This is fairly straightforward, sendLine()
simply appends a line terminator to a string and sends it over the socket. close()
closes the connection and log file if a log object was defined.
Finally, let’s add some logging capabilities to telnetsvr.py
by adding this TelnetLogger
class:
This class makes use of the built in csvwriter module, which is a simple and convenient log file format. You could certainly make some improvements here like log to a database, but I decided to keep things fairly simple with one file per connection. The try/except blocks around the writer are there to catch attempts to write to a closed log. These writes are simply ignored.
Main loop
Now that we have the telnet server taken care of, we need some code to make use of it. let’s begin by making a new file called honey.py
and begin with a little bit of code to start and test the server:
Run this code and try connecting to the server using telnet. On most Linux systems, you can do this with:
$ telnet localhost 7000
If everything works, you should see “Hello, World!” printed to the terminal.
Making it useful
We can now accept incoming connections, but can’t do much else beyond that. To fix that, let’s create a fake login prompt and a fake shell. Add the following to honey.py
:
So what’s going on here? FakeGetty
is instantiated by FakeShell
, prints a banner and then prompts the client for a user name and password. If the user name and password are both “root”, FakeShell
presents the client with, well, a fake shell.
FakeShell
needs to be instantiated with a TelnetServerClient
, so we shall do so in our main loop after a client connects. Let’s update the main loop in honey.py
to properly instantiate the FakeShell:
You should now be able to run honey.py and you will be prompted with a password when you connect with telnet. After “logging in” you should be greeted with a shell prompt. Of course, it still doesn’t do much beside allow you to exit or print the banner again.
Adding a program
Let’s extend the functionality of the honeypot by adding a program that emulates the functionality of a common *nix utility, echo.
Add the following class to honey.py:
FakeEcho simply echos back the arguments that were passed to it. These arguments are passed in the form of an array of strings, much in the way they would in a C program. "".join(self.cmdline)
simply concatenates all of these arguments into a string, which isn’t quite how echo works, but this works sufficiently well for illustrative purposes.
Now let’s make it possible to run FakeEcho
from our FakeShell
. Update the if
statement in runCommand()
to the following:
You should now be able to log in to the honeypot and run the echo
command from the shell.
Conclusion
Thank you for reading. I hope you found this post helpful, or at least interesting. Keep in mind that the honeypot presented here isn’t particularly robust and does have a few flaws such as a lack of an upper bound on the size of the receive buffer, so I would suggest being careful if you do decide to expose it to the internet.
Now that you have a basic telnet honeypot together though, you might try extending it with more commands, implementing a fake filesystem or making the server threaded so that it can service multiple client connections as well.
If you’re really feeling up for a challenge, you might extend the server and shell to emulate process signaling or try implementing pipes.