FTPoverEmail Alpha





0/5 (0 vote)
FTPoverEmail Alpha
Overview
Ever find yourself on a computer where you only have access to e-mail? That’s where FTPoverEmail shines. I frequently find myself in situations where I only have access to e-mail, but I still want to interact with my home server in an FTP-like fashion. You may be in a hotel business center or you may be a pentester and you want to drop FTPoverEmail on a box so that you have reached back via a protocol that’s a bit more innocuous. In the future, I plan on adding SSHoverEmail
capability, which makes the application all the more practical for a pentesting scenario and more useful for the common case.
How It Works
I wrote FTPoverEmail in Python. The server reaches out to an email account the user specifies and polls the account via imap for commands. Right now, the server supports the following commands:
FTP cd path
FTP dir [path]
FTP pwd
FTP get [path/]file_name
FTP put [path/]file_name
The server requires the FTP prefix before the command. I added this because I plan to add other protocols in the future and the server needs a way to distinguish between them. The user simply sends any of these commands in the body of an email to the address the user set the server to poll. The subject of the e-mail doesn’t matter. The user can send from any e-mail address they choose and the server responds to that address. I plan to add an authentication feature to add a measure of protection to the service. The response includes any output from the command. If the user performed a get
command, the server includes the file as an attachment in the response. If the user performs a put
command, the server places the file wherever the command specifies and sends a response acknowledging the command completed successfully.
Future Work
Below is a list of features I plan to add:
Support for the following commands:
- FTP delete file_name
- FTP mkdir directory
- FTP rmdir directory
- FTP ls <—– This will just be an alias for dir, but it makes life easier
- FTP mget file_list
- A configuration file where the user can set any pertinent variables.
- Support for all services instead of just gmail.
- A fix for the Windows Phone, which adds some funky formatting I haven’t figured out
- A GUI interface in the browser
- Database support
- SSH over Email capability
The Code
I did my best to comment thoroughly to make the program as readable as possible. Comment if you have questions. Here is a link to the download:
# __author__ = 'gecman' import smtplib import email from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.base import MIMEBase from email import encoders import mimetypes import os import imaplib import time import sys # TODO: Windows-1252 encoding adds an = sign which the program can't handle. # Find a way to take care of it. # TODO: Test more rapidly sent email # Global Variables from_name = '' to_name = '' subject = '' user = "whatever_email@gmail.com" password = "whatever_email_password" # sendresponse sends text based responses to the client # response: The response text to send in the body of the mail def sendresponse(response): # Create a response email from the response string response_email = MIMEText(response) print(subject) # Configure the message header fields response_email['Subject'] = subject response_email['From'] = to_name response_email['To'] = from_name # Declare an instance of the SMTP class and connect to the smtp server smtp = smtplib.SMTP('smtp.gmail.com') # Start the TLS connection smtp.starttls() # Login to the server smtp.login(user, password) # Attempt to send the email in the try block # noinspection PyBroadException try: # Send the response email smtp.sendmail(to_name, from_name, response_email.as_string()) # Catch all exceptions. I could have only done SMTP exceptions, # but I'm not sure if they'll add more in the future # so I thought it better to just catch all of them for this one line. except: # The following lines dump the details of the error to stdout print( "Server encountered the following error while attempting to send email: \nType: {0}\nValue: {1}\nTraceback: {2}".format( str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2]))) finally: # Close the smtp connection smtp.close() # sendfile Instead of just sending text, sends a file. Primarily used for the FTP get functionality. # file_name: The name of the file to be attached to the email. def sendfile(file_name): # Declare the mime class and message header fields response = MIMEMultipart() response['Subject'] = subject response['From'] = to_name response['To'] = from_name # Attach the text portion of the message. Alerts the user of the file name and that it is attached. response.attach(MIMEText("Grabbed file " + file_name + ". See attachment.")) # Define the major and minor mime type # attachment = MIMEBase('application', "octet-stream") # This command uses the mimetypes module to attempt to guess the mimetype of the file. # The strict=False argument # tells the mimetypes module to include modules not officially registered with IANA # The file_type variable will be in the format of a tuple #major type#/#sub type# , #encoding# file_type = mimetypes.guess_type(file_name, strict=False) main_type = None # mimetypes.guess_type returns None if it doesn't recognize the file type. # In this case I chose to use the generic # application octect-stream type, which should be safe. if file_type is None: attachment = MIMEBase('application', 'octet-stream') else: main_type = file_type[0].split('/')[0] sub_type = file_type[0].split('/')[1] # This pulls the major type and sub type and splits them around the # '/' character providing the major type in the # first argument and the sub type in the second argument attachment = MIMEBase(main_type, sub_type) # Read the file from disk and set it as the payload for the email # The with statement ensures the file is properly closed even if an exception is raised. try: f = open(file_name, "rb") attachment.set_payload(f.read()) f.close() except OSError: print( "Server encountered the following error while attempting to open the file you asked for: \nType: {0}\nValue: {1}\nTraceback: {2}".format( str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2]))) # TODO: Fix the fact that this shows the newlines as \n instead of properly adding newlines sendresponse( "Server encountered the following error while attempting to open the file you asked for: \nType: {0}\nValue: {1}\nTraceback: {2}".format( str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2]))) # Check to see if the attachment requires encoding if main_type == 'application' or main_type == 'audio' or main_type == 'image': # Encode the attachment encoders.encode_base64(attachment) # This sets the header. The output will be in the following format: # Content-Disposition: attachment; file_name="the_file_name" # The os.path.basename(file_name) portion strips the leading part of the # file path and just gives the actual file name attachment.add_header('Content-Disposition', 'attachment; file_name="%s"' % os.path.basename(file_name)) # A ttach the attachment to the response message response.attach(attachment) # Declare an instance of the SMTP class and connect to the smtp server smtp = smtplib.SMTP('smtp.gmail.com') # Start the TLS connection smtp.starttls() # Login to the server smtp.login(user, password) # Attempt to send the email # noinspection PyBroadException try: # Send the response email smtp.sendmail(to_name, from_name, response.as_string()) # Close the smtp connection smtp.close() # Catch all exceptions. I could have only done SMTP exceptions, # but I'm not sure if they'll add more in the future # so I thought it better to just catch all of them for this one line. except: # Close the smtp connection smtp.close() # The following lines print all of the error details to stdout and # also send them back to the user print( "Server encountered the following error while attempting to send email: \nType: {0}\nValue: {1}\nTraceback: {2}".format( str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2]))) # TODO: Fix the fact that this shows the newlines as \n instead of properly adding newlines sendresponse( "Server encountered the following error while attempting to send email: \nType: {0}\nValue: {1}\nTraceback: {2}".format( str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2]))) # Responsible for connecting to the server via IMAP and actually grabbing the e-mail. # It then passes the text or # content of the e-mail to proccommand for processing. def getemail(): # Declare as global variables. They are originally defined at the beginning. # Without these the values won't # carry over to other functions. global from_name global to_name global subject # Connect to Google's imap server # TODO: This needs to be generic mail = imaplib.IMAP4_SSL('imap.gmail.com') # Login to the imap server mail.login(user, password) # List all mailbox (folder) names. In gmail these are called labels. mail.list() # Select the inbox folder mail.select("inbox") # Grab an ordered list of all mail. The search function returns an ordered list # from newest to oldest. Only grab new mail. result, data = mail.search(None, "UNSEEN") # Check to see if there was new mail or not. If there wasn't return None. if not data[0].split(): return None # Data[0] is a list of email IDs in a space separated string. id_list = data[0].split() # Grab the latest email # TODO add support for queuing latest_email_id = id_list[-1] # Fetch the email body for the given ID result, data = mail.fetch(latest_email_id, "(RFC822)") raw_email = data[0][1] # Grab the body of the email including the raw text, headers, and payloads # The following line converts the email from a string into an email message object email_message = email.message_from_bytes(raw_email) if email_message['To'] is not None: print(email_message['To']) to_name = email_message['To'] else: print("Received email with an empty To address. Ignoring the message.") return if email_message['Return-Path'] is not None: print(email_message['Return-Path']) from_name = email_message['Return-Path'] else: print("Received email with an empty Return-Path. Ignoring the message.") return if email_message['Subject'] is not None: # If there is already a Re in the subject don't add it again if email_message['Subject'].find("Re:") == -1: subject = "Re: " + email_message['Subject'] else: subject = email_message['Subject'] print(subject) else: subject = '' # Check to see if this is a multipart email message. If it isn't just return the text portion. if email_message.get_content_maintype() == 'multipart': # Used to determine whether a command was found in the email found_command = False # Walk the various sections of the email. # The enumerate function here returns a number for each email part walked through. # IE 0 for the first part in # the email, 1 for the next, and so on. for index, part in enumerate(email_message.walk()): # The multipart section is just a container so we can skip it. if part.get_content_maintype() is 'multipart': continue # Check to see if we have reached the text body of the message if part.get_content_type() == 'text/plain': found_command = True message = part.get_payload() proccommand(message.split('\n')[0].rstrip(), index, email_message) #This does not need to continue after this run break # If a command was found and successfully processed simply return if found_command: return else: sendresponse("Error: No command was found in multipart email. Was it there?") print("Error: Server encountered a multipart email that did not appear to have a command in it") # Return the text payload elif email_message.get_content_main_type() == 'text': print(email_message.get_payload()) # Pass the command to proccommand. rstrip removes any whitespace characters from the tail. proccommand(email_message.get_payload().split('\n')[0].rstrip()) # It wasn't text - do not process the email else: print("Error processing email in getemail. Encountered unrecognized content type.") sendresponse("Error processing email in getemail. Encountered unrecognized content type.") return # Processes the command retrieved from the email server # message: The message from the email server in the form of [type] [command(s)] # index: This is an index into a multipart email's parts. # getemail may have already processed some of them and it is # unnecessary to reprocess those parts # email_message: This is an email_message passed from getemail. # In terms of FTP, this is used for the put command def proccommand(message, index=None, email_message=None): command = message.split(' ') # Used for logging to determine whether a command was successful or not errors = False # Process an FTP command if command[0].upper() == "FTP": # cd command --------------------------- # TODO: Test cd with shortcuts such as .. # Process the command "cd" if command[1].lower() == "cd": # Ensure the correct number of arguments was passed" if len(command) != 3: sendresponse("Error: Incorrect number of arguments for cd") errors = True # Make sure the path exists elif not os.path.exists(command[2]): sendresponse("Error: The path: \"" + command[2] + "\" does not exist") errors = True # Process the "cd" command else: os.chdir(command[2]) sendresponse("CD completed successfully. Directory is now: " + os.getcwd()) # dir command --------------------------- # Process the command "dir" if command[1].lower() == "dir": # Ensure the correct number of arguments was passed" if len(command) < 2 or len(command) > 3: sendresponse("Error: Incorrect number of arguments for dir") errors = True else: # If no arguments are passed to dir then list the current directory if len(command) == 2: response = "" for file in os.listdir(os.getcwd()): # Contains newline delimited directory listing at end response = response + file + "\n" sendresponse(response) # Process the dir command with directory argument else: # Make sure the path exists if not os.path.exists(command[2]): sendresponse("Error: The path: \"" + command[2] + "\" does not exist") errors = True # If the path does exist then list the directory else: response = "" for file in os.listdir(command[2]): # Contains newline delimited directory listing at end response = response + file + "\n" sendresponse(response) # pwd command --------------------------- # Process the pwd command if command[1].lower() == "pwd": # Ensure correct number of arguments was passed if len(command) != 2: sendresponse("Error: Incorrect number of arguments for pwd") errors = True else: # Get current directory and respond sendresponse(os.getcwd()) # get command --------------------------- # Process the get command if command[1].lower() == "get": # Ensure correct number of arguments was passed if len(command) != 3: sendresponse("Error: Incorrect number of arguments for get") errors = True else: # Make sure the file exists if not os.path.exists(command[2]): sendresponse("Error: The path: \"" + command[2] + "\" does not exist") errors = True else: sendfile(command[2]) # put command --------------------------- if command[1].lower() == "put": # Ensure correct number of arguments was passed if len(command) != 3: sendresponse("Error: Incorrect number of arguments for put") errors = True else: # There may be a better way to do this, but I'm using this # to inform the functionality below whether it # should actually execute. This defaults to true, # but gets set to False if the path is determined to be # non-valid valid_path = True # The line below splits command[2] into two pieces. For example say the path is # C:\Users\randomUser\Downloads\tryMe.exe. The split command will split this into # C:\Users\randomUser\Downloads\ and tryMe.exe. # The [0] index grabs just the path prefix. In this case # bool will return true IF there is a path prefix and false if there isn't. # We don't want to check if the path exists if the user didn't provide # any path (because a path of '' will return false. If the # user didn't provide a path we want to just dump the file # in the current directory whatever it is. if bool(os.path.split(command[2])[0]): # Make sure the path exists exists if not os.path.exists(os.path.split(command[2])[0]): valid_path = False sendresponse("Error: The path: \"" + os.path.split(command[2])[0] + "\" does not exist") errors = True # Only execute this if a valid path is supplied (or no path as the case may be) if valid_path: # getemail already parsed some of the sections. # The index comes from the getemail function and # allows this section to skip the parts of the email that were already processed. # email_message.walk() is a generator, but we want to index into it # so we use the list function. # This turns it into a list, which we are permitted to index into for part in list(email_message.walk())[index:]: # The multipart section is just a container so we can skip it. if part.get_content_maintype() is 'multipart': continue # Grab the file name from the section file_name = part.get_filename() # Make sure the file name actually exists and if it does write the file. if bool(file_name): try: fp = open(command[2], 'wb') # Get the payload of the email and write it. # The decode=True argument basically says if # the payload is encoded, decode it first otherwise, # it gets returned as is. fp.write(part.get_payload(decode=True)) fp.close() sendresponse("Successfully procced the put command: " + message) except OSError: errors = True print( "Server encountered the following error while attempting to write the file you uploaded: \nType: {0}\nValue: {1}\nTraceback: {2}".format( str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2]))) # TODO: Fix the fact that this shows the newlines as \n # instead of properly adding newlines sendresponse( "Server encountered the following error while attempting to write the file you uploaded: \nType: {0}\nValue: {1}\nTraceback: {2}".format( str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2]))) # Break out of the for loop. We should only need to write one file. break # If the protocol or desired action did not match anything... else: sendresponse("Error: Bad or nonexistent command: " + message) print("Server received an email which matched no commands.") # Used server side for logging purposes if errors: print("Failed to process command \"" + message + "\"") else: print("Successfully processed command: " + message) # This is the main section while 1: getemail() time.sleep(5)