Monday, November 8, 2010

Making Very Simple Honeypots in Python

If you've ever wondered how Honeypots work, or needed a small implementation of a standard protocol (like SMTP) to test your program on; Python is the answer.  This tutorial will show you how to write a simple implementation of a protocol in 100 lines of Python!
Essentially what you need three things to create a simple honeypot:
  • A way to listen
  • A way to log
  • A way to answer
The listener will wait for connections from an outside source, then depending on the protocol either wait for information or send some initial information.  Each time the client sends some data the program will look up how to answer, then send back the answer.  The logger should document the entire conversation.

The entire code is at the end, but first I'll go over each bit:

#!/usr/bin/env python
import socket
import time

First you will need two basic imports, socket is used to create TCP/UDP sockets on your system, and time is used for logging (you did expect the logfiles to have time right?)

Next we need some default variables:

TCP_IP = socket.gethostname()  #Bind externally 
TCP_PORT = 25  #25 is the standard for smtp.
BUFFER_SIZE = 1024
LOG_FILENAME = "SMTP.log"

The IP here is actually the hostname for your computer, this makes sure that when the socket is bound it becomes externally visible.  The BUFFER_SIZE could be smaller, but slows the program down because each time the buffer fills the program assumes it is your turn to talk back.  Obviously the log file will be stored in a file named SMTP.log .

Next we define a class called SMTP.  In it will be four functions, start, proc_data, log and log_time.
The log function will open the log file, write the given input and close, the log_time function will output the current time to the logfile, the proc_data function will parse data received from the client and return a response.  Finally the start function will create the socket, listen for connections, log data, and return responses.

log_time function:
def log_time(self): 
'''Logs the current time.'''
self.log('Time UTC: ' + str(time.asctime(time.gmtime())) + "\n")
self.log('Time Local: ' + str(time.asctime(time.localtime())) + "\n")

The function logs both UTC (GMT) time and Local time to provide you and anyone you send your logs to information about the connection.

log function:

def log(self, data):
'''Logs any data sent to the file specified by LOG_FILENAME.'''
print str( data.replace("\n", "") ) #Give visual feedback.

log_file = open(LOG_FILENAME, 'a')
log_file.write( str(data) )
log_file.close()

This function simply opens the logfile writes to it and closes it, the print statement is for visual feedback in the terminal, this is handy while testing.


proc_data function:
def proc_data(self, data):
''' Processes the data. '''
data = data.replace("\n", " ")
data = data.replace("\r", " ")
data_array = data.split(" ")
#Do HELO
if data_array[0] == "HELO":
try:
return "250 Hello " + data_array[1] + ", I am glad to meet you\n"
except:
return "250 Hello, I am glad to meet you\n"

#I added EHLO for newer clients.
if data_array[0] == "EHLO":
return "250 SIZE 1000000\n"

if data_array[0] == "MAIL":
return "250 Ok\n"

if data_array[0] == "RCPT":
return "250 Ok\n"

if data_array[0] == "DATA":
return "354 End data with <CR><LF>.<CR><LF>\n"

if data_array[0] == "." or data_array[-1].endswith("."):
return "250 Ok\n"

if data_array[0] == "RSET":
return "250 Ok\n"

if data_array[0] == "QUIT":
return "221 Bye\n"

if data_array[0] == "HELP": #I learned this one from the Gmail servers.
return "214 http://www.ietf.org/rfc/rfc2821.txt\n"
else:
#Return nothing if the request was unintelligible, this
#usually means that the user is inputting data.
return ""

This function is the real workhorse of the program, it first removes all newlines and carriage returns; this is important for parsing.  It then splits the client's information and uses the first part to figure out what the client wanted and returns a fake response based off that information.  I used if statements because they were more readable than a large dictionary.  I implemented the main commands from the RFC 2821 (SMTP).  Most of the commands are the same HELO should be taken note of though, many times a connector will give you their name, if so it is customary to return it to them, although the second return would be just fine.


start function:

def start(self):
'''Starts the SMTP honeypot and gets it listening.'''
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind( (TCP_IP, TCP_PORT) )
s.listen(1)

while 1:#Accept Unlimited Connections
try: #If the connection closes before ready it will cause an error.
conn, addr = s.accept()

#Begin Log
self.log( "\n\n\n" )
self.log( "=" * 80 + "\n" ) #Log a horizontal separator.
self.log( 'Connection Address: ' + str(addr) + "\n" )

self.log_time()

#Send the headers
self.log("220 smtp.example.com ESMTP Postfix\n")
conn.send("220 smtp.example.com ESMTP Postfix\n")
while 1: #Accept unlimited data from a connection.
#Receive data
data = conn.recv(BUFFER_SIZE)
self.log(data)

if not data: break

data = self.proc_data( data )
self.log(data)
conn.send(data)

#Close on bye
if data.startswith("221"):
break
self.log("Connection Closed\n")
self.log_time()

conn.close()
except: #End Except
self.log("Connection error, possibly a portscan.")
self.log_time()

NMAP or other portscanners on your machine while this program is running it will cause an exception that is caught because portscanners don't normally cleanly exit.

Example Session:

220 smtp.example.com ESMTP Postfix
HELO onehourhacks.blogspot.com
250 Hello onehourhacks.blogspot.com, I am glad to meet you
MAIL FROM:<joe@onehourhacks.blogspot.com>
250 Ok
RCPT TO:<someone@example.com>             
250 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
From: "Joe" <joe@onehourhacks.blogspot.com>
To: "Someone" <someone@example.com>
Subject: Test message

Hello Someone.
This is a test message.
Your friend,
Joe
.
250 Ok
QUIT
221 Bye


The server's responses are in italics.  You may notice that the server didn't say anything during the DATA transmission, this is because it didn't recognize any of the first words, Hello, This, Your, Joe, Subject:, To:, or From:.  If I had typed HELO as the beginning word on a line, the server would have sent another HELO message, this tells any spammers right away that they are dealing with a fake system, however this is unlikely to happen because a) most of the spam sent is from automated services, and they don't have the time to check for reality, and b) it is quite unlikely that any of these lines occur in normal email.

If you want to preform more extensive tests on the server hook up your mail client to it, then attempt to send some mail.

Source Code:
#!/usr/bin/env python
'''
Copyright (c) 2010 Joseph Lewis &lt;joehms22@gmail.com&gt;
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
'''
import socket
import time

TCP_IP = socket.gethostname() #Bind externally
TCP_PORT = 25 #25 is the standard for smtp.
BUFFER_SIZE = 1024
LOG_FILENAME = "SMTP.log"

class SMTP():
''' This class is for creating a fake SMTP server, but is may be only
convincing to novices or bots. Hey I like bots though, and novices.'''

def start(self):
'''Starts the SMTP honeypot and gets it listening.'''
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind( (TCP_IP, TCP_PORT) )
s.listen(1)

while 1:#Accept Unlimited Connections
try: #If the connection closes before ready it will cause an error.
conn, addr = s.accept()
self.log( "\n\n\n" )
self.log( "="*80+"\n" )
self.log( 'Connection Address: ' + str(addr) + "\n" )
self.log_time()

#Send the headders
self.log("220 smtp.example.com ESMTP Postfix\n")
conn.send("220 smtp.example.com ESMTP Postfix\n")
while 1: #Accept unlimited data from a connection.
#Receive data
data = conn.recv(BUFFER_SIZE)
self.log(data)

if not data: break

data = self.proc_data( data )
self.log(data)
conn.send( data )

#Close on bye
if data.startswith("221"):
break
self.log("Connection Closed\n")
self.log_time()

conn.close()
except: #End Except
self.log("Connection error, possibly a portscan.")
self.log_time()

def log_time(self):
'''Logs the current time.'''
self.log('Time UTC: ' + str(time.asctime(time.gmtime())) + "\n")
self.log('Time Local: ' + str(time.asctime(time.localtime())) + "\n")

def log(self, data):
'''Logs any data sent to the file specified by LOG_FILENAME.'''

print str( data.replace("\n", "") ) #Give visual feedback.

log_file = open(LOG_FILENAME, 'a')
log_file.write( str(data) )
log_file.close()

def proc_data(self, data):
''' Processes the data. '''
data = data.replace("\n", " ")
data = data.replace("\r", " ")
data_array = data.split(" ")
#Do HELO
if data_array[0] == "HELO":
try:
return "250 Hello "+data_array[1]+", I am glad to meet you\n"
except:
return "250 Hello, I am glad to meet you\n"

#I added EHLO for newer clients.
if data_array[0] == "EHLO":
return "250 SIZE 1000000\n"

if data_array[0] == "MAIL":
return "250 Ok\n"

if data_array[0] == "RCPT":
return "250 Ok\n"

if data_array[0] == "DATA":
return "354 End data with &lt;cr&gt;&lt;lf&gt;.&lt;cr&gt;&lt;lf&gt;\n"

if data_array[0] == "." or data_array[-1].endswith("."):
return "250 Ok\n"

if data_array[0] == "RSET":
return "250 Ok\n"

if data_array[0] == "QUIT":
return "221 Bye\n"

if data_array[0] == "HELP": #I learned this one from the Gmail servers.
return "214 http://www.ietf.org/rfc/rfc2821.txt\n"
else:
#Return nothing if the request was unintelligible, this
#usually means that the user is inputting data.
return ""

if __name__ == "__main__":
SMTP().start()

No comments:

Post a Comment