Intro

In my previous post, we investigated the inner workings of the File Transfer Protocol, at least the subset of commands that we deemed necessary for our purposes. Now we will take that information and implement it in Scala to create our rudimentary honeypot.

I will be leveraging the Akka and laying out most of the system through their Actor system. This approach will allow many different pieces to run concurrently and give us an excellent basis to extend this honeypot in the future to handle more protocols. To keep this post concise, I won’t go into depth about setting up the project or the Akka library’s specifics. The entire project exists on my local git repository.

Basic Architecture

At the core of this honeypot, implementation is the idea of having a root Supervisor, which will read a configuration file to create child actors responsible for handling server input for one mock protocol. I chose to use JSON for the configuration file since I’m not too fond of whitespace and curly braces spark joy in me, but if you aren’t so inclined, I’m sure switching to YML would be trivial.

Supervisor

Within the Supervisor we can see the looping over configuration file and instantiate of each components, for each component we send ourselves a MStartComponent message containing the information necessary to create it, using self ! MStartComponent(...). Most of this is straightforward with the interesting function being startComponent. This is where we will extend support for further protocols in the future, but right now only supports FTP.

1
2
3
4
5
6
7
private def startComponent(msg: MStartComponent) {
log.info("starting component :: {}", msg.name)
msg.ctype match {
case "ftp" => context.actorOf(FtpListener.props(msg.port), name = msg.name)
case _ => log.error("unknown component type: ", msg.ctype, msg.name);
}
}

FTP Package

Within the FTP Package of our project lies all the real goodness that mimics an FTP server’s functionality enough to accept logins and files. The FtpListener is the entry file that is stood up by the Supervisor and is responsible for handling initial FTP connections into our system. Just a quick FYI also, this is configured to run on port 21, just like a real default FTP server, to make it easy for hackers to identify what the server is for, but this may require root privileges depending on your system.

Our receive function is where most of the magic happens, which is true for most actors. We can see that most of this is boilerplate stuff for handling a UDP connection. Under the case for the Connected message, we are creating a handler, and sending our FTP version vulnFTPd 2.0.1. The version string should eventually be configurable and probably accept a list of names it can randomly choose, but for now, it just hardcoded to this value.

1
2
3
4
5
6
7
8
9
10
override def receive: Receive = {
case Bound(localAddress) =>
log.info("listening on {}", localAddress)
case CommandFailed(_: Bind) => context.stop(self)
case Connected(_, _) =>
val connection = sender()
val handler = context.actorOf(FtpHandler.props(connection), name = "handler")
connection ! Register(handler)
connection ! Write(ByteString.apply("220 (vulnFTPd 2.0.1)\n"))
}

Once FtpHandler is instantiated and registered to handle the connection, it will begin parsing data sent from the client using the parse function. Inside this function are all the supported commands, most of which will be familiar to anybody who read the previous post. Most of this information is just output to the console; in the future, it will need to be collected and sent somewhere else (the observant may notice an add_reporting branch in the repository).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def parse(msg: Array[String]): String = msg(0) match {
case "user" =>
log.info("attempted login with username: {}", msg(1))
"331 Please specify password.\n"
case "pass" =>
log.info("attempted login with password: {}", msg(1))
"230 Login successful.\n"
case "pwd" => "257 \"/\" is the current directory\n"
case "quit" => "221 Goodbye.\n"
case "pasv" =>
log.info("entering passive mode")
val r = new Random()
val p1 = r.nextInt(200)
val p2 = r.nextInt(200)
context.actorOf(FtpFileReceiver.props(p1 * 256 + p2, client), name = "passive-connection-" + (p1 * 256 + p2))
Thread.sleep(256)
"227 entering passive mode (127,0,0,1," + p1 + "," + p2 + ")\n"
case "stor" =>
log.info("stor: {}", msg(1))
"150 File status okay; about to open data connection.\n"
case _ =>
log.info("unsupported command received: {}", msg.mkString(" "))
"451 Requested action aborted. Local error in processing.\n"
}

The last piece of this package is the FtpFileReceiver. This component is responsible for handling the passive data connection from a client. You’ll remember that the passive connection means the server sends an IP and port for the client to connect to and transfer data. It currently just stores the binaries with a randomly generated UUID in a specific folder on the machine. Eventually, the destination for recieved files will be configurable.

Outro

Well that is just a current update on the status of my honeypot. As I hinted at before, I have begun adding some reporting features to allow the exporting of events to various other services (discord notifier?!). I will continue work on the FTP aspect of the project, but look forward to another recon post where I will look into implementing a whole new protocol!

2020.09.30

⬆︎TOP