Building Your Own Safe, Secure SMTP Proxy

by sail0r  sail0r@creepjoint.net)

Shows of power are very common in the business world when a new executive takes power.  The reasoning, I have been told, is to make a powerful first impression that you are the big, bad new boss.

Recently, the CEO of the small software company I work for was replaced.  After several weeks, the new CEO began to wield the ax.  At first, a few fringe benefits such as catered meetings and periodic team gatherings at the local pub were gone.  Next came the restrictions on Internet use.  Internet traffic is now logged and filtered.  AOL IM traffic is now routed through a proxy and logged.  And, closest to my heart, SMTP restrictions are now in place; all outgoing mail must now be sent through the company's SMTP server where, of course, it is logged.

Until this point, I had been happily accessing and sending my personal e-mail with Mozilla Thunderbird and my personal IMAP and SMTP servers.  Webmail services such as Gmail are out of the question as alternatives, as these sites are now blocked and HTTP requests for these sites are now logged.

Furthermore, since all network traffic is now being logged, any attempt to circumvent these restrictions must be disguised as valid network traffic.  Since SSH and SCP must remain open for valid business use, I have devised a method whereby I may continue to use Thunderbird to send my personal email from work but use SSH and SCP to proxy the outgoing messages through a personal shell account.  This has the excellent added benefit of encrypting my outgoing mail, hiding it from corporate snoops.

The method I use consists of four parts:

1.)  A custom SMTP server runs on my local machine on some arbitrary port.  To make detection slightly more difficult, I do not use the default SMTP port of 25.  As of this writing, there are no in-house port scans.  Management does actually realize that developers writing network code often have works-in-progress running on multiple high numbered ports.

2.)  The custom SMTP server will accept messages like a normal SMTP server.  The message is then copied with SCP to a directory on a machine on which I have remote shell access.

3.)  A cron job runs every minute to poll the message directory and send the messages to their destinations.

4.)  After each message is sent, it is moved to an archive directory.

After I had a rough outline of my approach in my head, I decided to actually implement the idea using the Python programming language.  I made this choice because I have found that Python's compact and powerful language constructs often make coding go faster than it would with other languages.  Also, there are many ready-built APIs available.

I was certain that I could easily find code which would handle the SSH and SCP as well as the eventual SMTP connection.  The only code I would have to write would be to glue together ready made pieces.  In this regard, Python certainly met my expectations.  The code which I wrote is a good example of code re-use and the power of Python.

To begin, the install following non-standard Python modules: PyDNS, smtps.py, and Pexpect.

Obviously, you will need a shell account ona machine someplace that has a SSH daemon running.  Also, it is assumed that you have the SSH and SCP command line tools installed on your local system and in your path.  One security note: for the SSH and SCP commands, you will either need to put your password in the script or use SSH keys.

In the included code, I embed my password in the script.  If you do this, you must chmod your script to be 0700, so no other users of your system can read your password.  I believe that this is just as good security as the use of SSH keys.  If someone got root on your system, then they would be able to use your SSH key to login to your remote server just as easily as using your password.  Use whichever you are most comfortable with.

After you have installed the prerequisite modules, the SMTP proxy consists of two programs.  SMTPLocalService.py is to be run locally.  I run this on the same machine I am running Thunderbird on.  SMTPLocalService.py runs via cron on the remote host.

For the sake of simplicity, I just start the script SMTPLocalService.py from the command line as follows:

$ python SMTPLocalService.py 1234

In this example, I set the port to be 1234.  Of course, you may choose any port you wish.

SMTPRemoteService.py is set up via crontab.  Here is how it looks in my /etc/crontab file:

* * * * * /usr/local/bin/python /home/sail0r/smtp_out/SMTPRemoteService.py

Note that this just runs every minute of every hour of every day.  Again, how often this runs is up to your discretion.

Finally, you must tell Thunderbird about your new SMTP server.

Go to the Tools menu and select Account Settings....  From the menu on the left-hand side of the window, choose Outgoing Server (SMTP).  Select the Add... button.

Now, simply enter the name of the machine running SMTPLocalService.py and the port that you have chosen.  Set this as your default SMTP server, and now you will be able to send outgoing safe, secure outgoing mail, free from prying eyes!

Please be aware that I make no claim that this is bulletproof code.

Astute readers will notice that mail is now being sent asynchronously.  Failures in SMTPLocalService.py usually, but not always, cause an error to be propagated back to Thunderbird.  Failures in sending the mail from your shell host will need to be debugged from the server by manually running SMTPRemoteService.py.

In this article, I exclusively use Thunderbird as an example, but this should work just as easily with any email client assuming you configure the SMTP settings correctly.  These scripts were developed on UNIX; however, they will easily work with only slight modification with Python on Windows.

Anyone with further questions or comments may contact me in #2600 on the 2600 IRC network, via ICQ 490590026, or at sail0r@creepjoint.net.

SMTPLocalService.py:

import sys, smtps, string, smtplib, rfc822, StringIO,time,pexpect,os
# This extends the smtps.SMTPServerInterface and specializes it to
# proxy requests onwards. 
class SMTPLocalService(smtps.SMTPServerInterface):
    def __init__(self):
        self.user="MY_USERNAME"
        self.password="MY_PASSWORD"
        self.scpServer="host.domain.tld"
        self.scpServerPath="/path/to/your/message_directory/"
        self.savedTo = []
        self.savedMailFrom = ""
        self.shutdown = 0
        
    def mailFrom(self, args):
        # Stash who its from for later
        self.savedMailFrom = smtps.stripAddress(args)
        
    def rcptTo(self, args):
        # Stashes multiple RCPT TO: addresses
        self.savedTo.append(args)
        
    def data(self, args):
        # Write the mail to a file in /tmp
        # and then scp the file.
        # After scp-ing the file delete it from /tmp
        timeStamp=time.strftime("%m%d%y_%I%M%S%p",time.localtime()) # just name the message files with the current time
        data=self.frobData(args)
        filename=timeStamp
        localFilePath="/tmp/"
        filename_new=filename+".new"
        filename_msg=filename+".msg"
        file=open(localFilePath+filename_new,"w")
        file.writelines(self.savedTo)
        file.write("FROM:<"+self.savedMailFrom+">\n")
        file.write(data)
        file.close()
        # We do not want incomplete messages to be process on the server
        # to avoid this we first send the message with a .new extension
        # after transfer is complete the message is renamed with a .msg extension
        # The remote side only processes files ending in .msg        
        cmd=pexpect.spawn('scp -r %s %s@%s:%s' % (localFilePath+filename_new,self.user,self.scpServer,self.scpServerPath))
        cmd.expect('.ssword:*') 
        cmd.sendline(self.password)
        cmd.interact()     
        # Here we rename the file
        cmd=pexpect.spawn('ssh %s@%s mv %s %s' % (self.user,self.scpServer,self.scpServerPath+filename_new,self.scpServerPath+filename_msg))
        cmd.expect('.ssword:*')
        cmd.sendline(self.password)
        cmd.interact()
        os.remove(localFilePath+filename_new)
        self.savedTo = []
        
    def quit(self, args):
        if self.shutdown:
            print("Shutdown at user request")
            sys.exit(0)

    def frobData(self, data):
        hend = string.find(data, '\n\r')
        if hend != -1:
            rv = data[:hend]
        else:
            rv = data[:]
        rv = rv + 'X-PySpy: Python SMTP Proxy Frobnication'
        rv = rv + data[hend:]
        return rv

def Usage():
    print("Usage SMTPLocalService.py port\nWhere: port = Client SMTP Port number (ie 25).")
    sys.exit(1)
    
if __name__ == '__main__':
    if len(sys.argv) != 2:
        Usage()
    port = int(sys.argv[1])
    service = SMTPLocalService()
    server = smtps.SMTPServer(port)
    server.serve(service)

SMTPRemoteService.py:

import sys, smtps, string, smtplib, rfc822, StringIO,os,glob,DNS,time
# Designed to run via cron. Will check a directory and
# if any new message files are there it will send them of and
# move them to an archive sub-directory
# It uses DNS to resolve each RCPT TO:
# address, then uses smtplib to forward the client mail on the
# resolved mailhost.
class SMTPRemoteService():
    def __init__(self):
        self.dnshost="199.45.32.41" #Some dns that belongs to verizon. :)
                                    #this should work but if you want to 
                                    #use another one feel free
        self.pollDir="/path/to/your/message_directory/"
        self.archiveDir=self.pollDir+"msg_archive/"
        self.fileList=[]
        self.savedTo = []
        self.savedMailFrom = ''

    def mxlookup(self,host):
        global DNSHOST
        a = DNS.DnsRequest(host, qtype = 'mx', server=self.dnshost).req().answers
        l = map(lambda x:x['data'], a)
        l.sort()
        return l

    def mailFrom(self, args):
        # Stash who its from for later
        self.savedMailFrom = smtps.stripAddress(args)

    def rcptTo(self, args):
        # Stashes multiple RCPT TO: addresses
        self.savedTo.append(args)

    def data(self, args):
        # Process client mail data. It inserts a silly X-Header, then
        # does a MX DNS lookup for each TO address stashed by the
        # rcptTo method above. Messages are logged to the console as
        # things proceed. 
        data = self.frobData(args)
        for addressee in self.savedTo:
            toHost, toFull = smtps.splitTo(addressee)
            # Treat this TO address speciallt. All because of a
            # WINSOCK bug!
            if toFull == 'shutdown@shutdown.now':
                self.shutdown = 1
                return
            sys.stdout.write('Resolving ' + toHost + '...')
            resolved = self.mxlookup(toHost)
            if len(resolved):
                sys.stdout.write(' found. Sending ...')
                mxh = resolved[0][1]
                for retries in range(3):
                    try:
                        smtpServer = smtplib.SMTP(mxh)
                        smtpServer.set_debuglevel(0)
                        smtpServer.helo(mxh)
                        smtpServer.sendmail(self.savedMailFrom, toFull, data)
                        smtpServer.quit()
                        print(' Sent TO:', toFull, mxh)
                        break
                    except e:
                        print('*** SMTP FAILED', retries, mxh, sys.exc_info()[1])
                        continue
            else:
                print('*** NO MX HOST for :', toFull)
        self.savedTo = []

    def frobData(self, data):
        hend = string.find(data, '\n\r')
        if hend != -1:
            rv = data[:hend]
        else:
            rv = data[:]
        rv = rv + 'X-PySpy: Python SMTP Proxy Frobnication'
        rv = rv + data[hend:]
        return rv

    def archiveFile(self,msgfile):
       timeStamp=time.strftime("%m%d%y_%I%M%S%p",time.localtime())
       in_msg=open(msgfile, "rb")
       outfile=open(self.archiveDir+timeStamp, "wb")
       outfile.write(in_msg.read())
       outfile.close()
       in_msg.close()
       os.remove(msgfile)

    def run(self):
        contents=""
        inContents=False
        self.fileList=glob.glob(self.pollDir+'*.msg')
        for filename in self.fileList:
            for line in open(filename,"r").readlines():
                if line.startswith("RCPT"):
                    self.rcptTo(line)
                if line.startswith("FROM:"):
                    self.mailFrom(line[5:])
                if line.startswith("Message-ID"):
                    inContents=True
                if inContents:
                    contents=contents+line
            self.data(contents)
            inContents=False
            contents=""
            self.archiveFile(filename)


if __name__ == '__main__':
    service = SMTPRemoteService()
    service.run()

Code: SMTPLocalService.py

Code: SMTPRemoteService.py

Return to $2600 Index