diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/README.md b/README.md index 575c134..65011fd 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,81 @@ # Office 365 User Enumeration -Enumerate valid usernames from Office 365 using the office.com login page. +Enumerate valid usernames from Office 365 using the office.com login page while optionally dodging throttling by rotating IPs with each request through Fireprox APIs. + +# WARNING! This Repository Is Deprecated + +> This repository is now considered deprecated and will no longer receive further support. This tool has been refactored into a module within Stratustryke (https://github.com/vexance/Stratustryke). Refer to the Stratustryke repository and leverage the `m365/enum/unauth/m365_enum_users_managed` module for similar functionality. ## Usage o365enum will read usernames from the file provided as first parameter. The file should have one username per line. ``` -python3.6 o365enum.py -h -usage: o365enum.py [-h] -u USERLIST [-v] - +python3 o365enum.py --help +usage: o365enum.py [-h] [-u USERS] [-d DOMAIN] [--static] [-v] [-o OUTFILE] [--profile PROFILE] [--access-key ACCESS_KEY] [--secret-key SECRET_KEY] [--session-token SESSION_TOKEN] [--region REGION] command + Office365 User Enumeration Script +positional arguments: + command Module / command to run [list,delete,enum] + optional arguments: -h, --help show this help message and exit - -u USERLIST, --userlist USERLIST - username list one per line (default: None) - -v, --verbose Enable verbose output at urllib level (default: False) + -u USERS, --users USERS + Required for 'enum' module; File containing list of users / emails to enumerate + -d DOMAIN, --domain DOMAIN + Email domain if not already included within user file + --static Disable IP rotation via Fireprox APIs; O365 will throttle after ~100 requests + -v, --verbose Enable verbose output at urllib level + -o, --outfile File to output results to [default: None] + --profile PROFILE AWS profile within ~/.aws/credentials to use [default: default] + --access-key ACCESS_KEY + AWS access key id for fireprox API creation + --secret-key SECRET_KEY + AWS secret access key for fireprox API creation + --session-token SESSION_TOKEN + AWS session token for assumed / temporary roles + --region REGION AWS region to which fireprox API will be deployed [default: us-east-1] +``` + +Example O365 username enumeration +``` +./o365enum.py enum -u users.txt +Creating => https://login.microsoftonline.com/common/GetCredentialType?mkt=en-US... +[2021-09-09 22:07:00-04:00] (abcdefghijklmno) fireprox_microsoftonline => https://abcdefghijklmno.execute-api.us-east-1.amazonaws.com/fireprox/ (https://login.microsoftonline.com/common/GetCredentialType?mkt=en-US) +[-] first.last@example.com - Invalid user +[*] flast@example.com - Valid user with different IDP +[-] first.last2@example.com - Invalid user +[+] flast2@example.com - Valid user +[+] flast3@example.com - Valid user +[!] f.last@nonexistant.example.com. - Domain type 'UNKNOWN' not supported +[+] flast4@example.com - Valid user +[-] first.last3@example.com - Invalid user +[+] flast5@example.com - Valid user +[-] f.last2@example.com - Invalid user +[!] f.last3@example.com - Possible throttle detected on request +[-] f.last3@example.com - Invalid user ``` -Example run: +Example Fireprox API Listing +``` +python3 o365enum.py list +[2021-09-09 22:05:34-04:00] (abcdefghijklmno) fireprox_microsoftonline: https://abcdefghijklmno.execute-api.us-east-1.amazonaws.com/fireprox/ => https://login.microsoftonline.com/common/GetCredentialType?mkt=en-US/ +``` +Example Deleation of all Fireprox APIs ``` -./o365enum.py -u users.txt -nonexistent@contoso.com INVALID_USER -existing@contoso.com VALID_USER -possible@federateddomain.com DOMAIN_NOT_SUPPORTED -notreal@badomain.com UNKNOWN_DOMAIN +python3 o365enum.py delete +[+] Listing Fireprox APIs prior to deletion +[2021-09-09 22:05:34-04:00] (abcdefghijklmno) fireprox_microsoftonline: https://abcdefghijklmno.execute-api.us-east-1.amazonaws.com/fireprox/ => https://login.microsoftonline.com/common/GetCredentialType?mkt=en-US/ +[2021-09-09 22:07:00-04:00] (qwertyuiop) fireprox_microsoftonline: https://qwertyuiop.execute-api.us-east-1.amazonaws.com/fireprox/ => https://login.microsoftonline.com/common/GetCredentialType?mkt=en-US/ +[+] Attempting to delete API 'abcdefghijklmno' +[+] Attempting to delete API 'qwertyuiop' +[+] Fireprox APIs following deletion: ``` +> __Note:__
+> o365enum *should* automatically delete all Fireprox APIs when complete or in the event of an exception / keyboard interrupt during execution + ## Office.com Enumeration @@ -206,3 +254,4 @@ Content-Length: 579 * [@jenic](https://github.com/jenic) - Arguments parsing and false negative reduction. * [@gremwell](https://github.com/gremwell) - Original script author * [@BarrelTit0r](https://github.com/BarrelTit0r) - Enhancement and refinement of user enumeration functionality +* [@Vexance](https://github.com/vexance) - IP rotation through FireProx APIs and outfile / output reformatting diff --git a/fire.py b/fire.py new file mode 100644 index 0000000..d449eef --- /dev/null +++ b/fire.py @@ -0,0 +1,445 @@ +from multiprocessing import Pool +from pathlib import Path +import shutil +import tldextract +import boto3 +import os +import sys +import datetime +import tzlocal +import argparse +import json +import configparser +from typing import Tuple, Callable + + +class FireProx(object): + def __init__(self, arguments: argparse.Namespace, help_text: str): + self.profile_name = arguments.profile_name + self.access_key = arguments.access_key + self.secret_access_key = arguments.secret_access_key + self.session_token = arguments.session_token + self.region = arguments.region + self.command = arguments.command + self.api_id = arguments.api_id + self.url = arguments.url + self.api_list = [] + self.client = None + self.help = help_text + + if self.access_key and self.secret_access_key: + if not self.region: + self.error('Please provide a region with AWS credentials') + + if not self.load_creds(): + self.error('Unable to load AWS credentials') + + if not self.command: + self.error('Please provide a valid command') + + def __str__(self): + return 'FireProx()' + + def _try_instance_profile(self) -> bool: + """Try instance profile credentials + :return: + """ + try: + if not self.region: + self.client = boto3.client('apigateway') + else: + self.client = boto3.client( + 'apigateway', + region_name=self.region + ) + self.client.get_account() + self.region = self.client._client_config.region_name + return True + except: + return False + + def load_creds(self) -> bool: + """Load credentials from AWS config and credentials files if present. + :return: + """ + # If no access_key, secret_key, or profile name provided, try instance credentials + if not any([self.access_key, self.secret_access_key, self.profile_name]): + return self._try_instance_profile() + # Read in AWS config/credentials files if they exist + credentials = configparser.ConfigParser() + credentials.read(os.path.expanduser('~/.aws/credentials')) + config = configparser.ConfigParser() + config.read(os.path.expanduser('~/.aws/config')) + # If profile in files, try it, but flow through if it does not work + config_profile_section = f'[{self.profile_name}]' + if self.profile_name in credentials: + # This section seemed to break everything; not sure of its purpose if region is supplied in __init__() + # if config_profile_section not in config: + # print(f'Please create a section for {self.profile_name} in your ~/.aws/config file') + # return False + # self.region = config[config_profile_section].get('region', 'us-east-1') + try: + self.client = boto3.session.Session(profile_name=self.profile_name).client('apigateway') + self.client.get_account() + return True + except: + pass + # Maybe had profile, maybe didn't + if self.access_key and self.secret_access_key: + try: + self.client = boto3.client( + 'apigateway', + aws_access_key_id=self.access_key, + aws_secret_access_key=self.secret_access_key, + aws_session_token=self.session_token, + region_name=self.region + ) + self.client.get_account() + self.region = self.client._client_config.region_name + # Save/overwrite config if profile specified + if self.profile_name: + if config_profile_section not in config: + config.add_section(config_profile_section) + config[config_profile_section]['region'] = self.region + with open(os.path.expanduser('~/.aws/config'), 'w') as file: + config.write(file) + if self.profile_name not in credentials: + credentials.add_section(self.profile_name) + credentials[self.profile_name]['aws_access_key_id'] = self.access_key + credentials[self.profile_name]['aws_secret_access_key'] = self.secret_access_key + if self.session_token: + credentials[self.profile_name]['aws_session_token'] = self.session_token + else: + credentials.remove_option(self.profile_name, 'aws_session_token') + with open(os.path.expanduser('~/.aws/credentials'), 'w') as file: + credentials.write(file) + return True + except: + return False + else: + return False + + def error(self, error): + print(self.help) + sys.exit(error) + + def get_template(self): + url = self.url + if url[-1] == '/': + url = url[:-1] + + title = 'fireprox_{}'.format( + tldextract.extract(url).domain + ) + version_date = f'{datetime.datetime.now():%Y-%m-%dT%XZ}' + template = ''' + { + "swagger": "2.0", + "info": { + "version": "{{version_date}}", + "title": "{{title}}" + }, + "basePath": "/", + "schemes": [ + "https" + ], + "paths": { + "/": { + "get": { + "parameters": [ + { + "name": "proxy", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "X-My-X-Forwarded-For", + "in": "header", + "required": false, + "type": "string" + } + ], + "responses": {}, + "x-amazon-apigateway-integration": { + "uri": "{{url}}/", + "responses": { + "default": { + "statusCode": "200" + } + }, + "requestParameters": { + "integration.request.path.proxy": "method.request.path.proxy", + "integration.request.header.X-Forwarded-For": "method.request.header.X-My-X-Forwarded-For" + }, + "passthroughBehavior": "when_no_match", + "httpMethod": "ANY", + "cacheNamespace": "irx7tm", + "cacheKeyParameters": [ + "method.request.path.proxy" + ], + "type": "http_proxy" + } + }, + "post": { + "parameters": [ + { + "name": "proxy", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "X-My-X-Forwarded-For", + "in": "header", + "required": false, + "type": "string" + } + ], + "responses": {}, + "x-amazon-apigateway-integration": { + "uri": "{{url}}/", + "responses": { + "default": { + "statusCode": "200" + } + }, + "requestParameters": { + "integration.request.path.proxy": "method.request.path.proxy", + "integration.request.header.X-Forwarded-For": "method.request.header.X-My-X-Forwarded-For" + }, + "passthroughBehavior": "when_no_match", + "httpMethod": "ANY", + "cacheNamespace": "irx7tm", + "cacheKeyParameters": [ + "method.request.path.proxy" + ], + "type": "http_proxy" + } + } + }, + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "proxy", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "X-My-X-Forwarded-For", + "in": "header", + "required": false, + "type": "string" + } + ], + "responses": {}, + "x-amazon-apigateway-integration": { + "uri": "{{url}}/{proxy}", + "responses": { + "default": { + "statusCode": "200" + } + }, + "requestParameters": { + "integration.request.path.proxy": "method.request.path.proxy", + "integration.request.header.X-Forwarded-For": "method.request.header.X-My-X-Forwarded-For" + }, + "passthroughBehavior": "when_no_match", + "httpMethod": "ANY", + "cacheNamespace": "irx7tm", + "cacheKeyParameters": [ + "method.request.path.proxy" + ], + "type": "http_proxy" + } + } + } + } + } + ''' + template = template.replace('{{url}}', url) + template = template.replace('{{title}}', title) + template = template.replace('{{version_date}}', version_date) + + return str.encode(template) + + def create_api(self, url) -> str: + if not url: + self.error('Please provide a valid URL end-point') + + print(f'Creating => {url}...') + + template = self.get_template() + response = self.client.import_rest_api( + parameters={ + 'endpointConfigurationTypes': 'REGIONAL' + }, + body=template + ) + resource_id, proxy_url = self.create_deployment(response['id']) + self.store_api( + response['id'], + response['name'], + response['createdDate'], + response['version'], + url, + resource_id, + proxy_url + ) + return proxy_url + + def update_api(self, api_id, url): + if not any([api_id, url]): + self.error('Please provide a valid API ID and URL end-point') + + if url[-1] == '/': + url = url[:-1] + + resource_id = self.get_resource(api_id) + if resource_id: + print(f'Found resource {resource_id} for {api_id}!') + response = self.client.update_integration( + restApiId=api_id, + resourceId=resource_id, + httpMethod='ANY', + patchOperations=[ + { + 'op': 'replace', + 'path': '/uri', + 'value': '{}/{}'.format(url, r'{proxy}'), + }, + ] + ) + return response['uri'].replace('/{proxy}', '') == url + else: + self.error(f'Unable to update, no valid resource for {api_id}') + + def delete_api(self, api_id): + if not api_id: + self.error('Please provide a valid API ID') + items = self.list_api(api_id, silenced=True) + for item in items: + item_api_id = item['id'] + if item_api_id == api_id: + response = self.client.delete_rest_api( + restApiId=api_id + ) + return True + return False + + def list_api(self, deleted_api_id=None, silenced=False): + response = self.client.get_rest_apis() + for item in response['items']: + try: + created_dt = item['createdDate'] + api_id = item['id'] + name = item['name'] + proxy_url = self.get_integration(api_id).replace('{proxy}', '') + url = f'https://{api_id}.execute-api.{self.region}.amazonaws.com/fireprox/' + if not silenced: + print(f'[{created_dt}] ({api_id}) {name}: {url} => {proxy_url}') + except: + pass + + return response['items'] + + def store_api(self, api_id, name, created_dt, version_dt, url, + resource_id, proxy_url): + print( + f'[{created_dt}] ({api_id}) {name} => {proxy_url} ({url})' + ) + + def create_deployment(self, api_id): + if not api_id: + self.error('Please provide a valid API ID') + + response = self.client.create_deployment( + restApiId=api_id, + stageName='fireprox', + stageDescription='FireProx Prod', + description='FireProx Production Deployment' + ) + resource_id = response['id'] + return (resource_id, + f'https://{api_id}.execute-api.{self.region}.amazonaws.com/fireprox/') + + def get_resource(self, api_id): + if not api_id: + self.error('Please provide a valid API ID') + response = self.client.get_resources(restApiId=api_id) + items = response['items'] + for item in items: + item_id = item['id'] + item_path = item['path'] + if item_path == '/{proxy+}': + return item_id + return None + + def get_integration(self, api_id): + if not api_id: + self.error('Please provide a valid API ID') + resource_id = self.get_resource(api_id) + response = self.client.get_integration( + restApiId=api_id, + resourceId=resource_id, + httpMethod='ANY' + ) + return response['uri'] + + +def parse_arguments() -> Tuple[argparse.Namespace, str]: + """Parse command line arguments and return namespace + :return: Namespace for arguments and help text as a tuple + """ + parser = argparse.ArgumentParser(description='FireProx API Gateway Manager') + parser.add_argument('--profile_name', + help='AWS Profile Name to store/retrieve credentials', type=str, default=None) + parser.add_argument('--access_key', + help='AWS Access Key', type=str, default=None) + parser.add_argument('--secret_access_key', + help='AWS Secret Access Key', type=str, default=None) + parser.add_argument('--session_token', + help='AWS Session Token', type=str, default=None) + parser.add_argument('--region', + help='AWS Region', type=str, default=None) + parser.add_argument('--command', + help='Commands: list, create, delete, update', type=str, default=None) + parser.add_argument('--api_id', + help='API ID', type=str, required=False) + parser.add_argument('--url', + help='URL end-point', type=str, required=False) + return parser.parse_args(), parser.format_help() + + +def main(): + """Run the main program + :return: + """ + args, help_text = parse_arguments() + fp = FireProx(args, help_text) + if args.command == 'list': + print(f'Listing API\'s...') + result = fp.list_api() + + elif args.command == 'create': + result = fp.create_api(fp.url) + + elif args.command == 'delete': + result = fp.delete_api(fp.api_id) + success = 'Success!' if result else 'Failed!' + print(f'Deleting {fp.api_id} => {success}') + + elif args.command == 'update': + print(f'Updating {fp.api_id} => {fp.url}...') + result = fp.update_api(fp.api_id, fp.url) + success = 'Success!' if result else 'Failed!' + print(f'API Update Complete: {success}') + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/o365enum.py b/o365enum.py index 933b251..1451699 100644 --- a/o365enum.py +++ b/o365enum.py @@ -3,65 +3,75 @@ Office365 User Enumeration script. Enumerate valid usernames from Office 365 using the office.com login page. -Author: Quentin Kaiser -Author: Cameron Geehr @BarrelTit0r +Author: Quentin Kaiser (original) +Author: Cameron Geehr @BarrelTit0r (enhanced / refined functionality) +Author: Vexance @vexance (integration of fireprox APIs and reformatted output) + +Shoutout to @ustayready for the implementation of Fireprox APIs (fire.py) ''' -import random -import re -import string -import argparse -import logging -import requests -import xml.etree.ElementTree as ET +import io, logging, random, re, requests, string, termcolor, threading +from queue import Queue +import argparse, botocore, boto3 +import fire + +mutex = threading.Lock() +count_queue = Queue() +search_results = set() try: import http.client as http_client except ImportError: import httplib as http_client -def load_usernames(usernames_file): - ''' - Loads a list of usernames from `usernames_file`. - Args: - usernames_file(str): filename of file holding usernames +def print_status(status_type: str, text: str, file_handle: io.TextIOWrapper = None) -> None: + '''Print text to terminal / append to file if specified''' + if status_type != '': + status_type = f'[{status_type.lower()}]' + + color = 'white' + if status_type == '[!]' or status_type == '[x]': + color = 'yellow' + elif status_type == '[-]': + color = 'red' + elif status_type == '[*]': + color = 'blue' + elif status_type == '[+]': + color = 'green' + print(f'{termcolor.colored(status_type, color)} {text}') - Returns: - usernames(list): a list of usernames - ''' + if file_handle: + file_handle.write(f'{termcolor.colored(status_type, color)} {text}\n') + + return None + +def load_usernames(usernames_file: str, domain: str = False) -> list: + ''' Load usernames from provided file; returns usernames as list''' + user_list = [] with open(usernames_file) as file_handle: - return [line.strip() for line in file_handle.readlines()] - -def o365enum_office(usernames): - ''' - Checks if `usernames` exists using office.com method. - - Args: - usernames(list): list of usernames to enumerate - ''' - headers = { - "User-Agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"\ - " (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" - } + for line in file_handle: + user = line.strip() + if domain: + user = f'{user}@{domain}' + user_list.append(user) + return user_list + + +def o365enum_office(usernames: list, fireprox_url: str, fhandle: io.TextIOWrapper = None) -> None: + '''Check a list of usernames for validity via office.com method''' + headers = { "User-Agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" } + # first we open office.com main page session = requests.session() - response = session.get( - "https://www.office.com", - headers=headers - ) + response = session.get("https://www.office.com", headers=headers) # we get the application identifier and session identifier client_id = re.findall(b'"appId":"([^"]*)"', response.content) - # then we request the /login page which will redirect us to the authorize - # flow - response = session.get( - "https://www.office.com/login?es=Click&ru=/&msafed=0", - headers=headers, - allow_redirects=True - ) + # then we request the /login page which will redirect us to the authorize flow + response = session.get("https://www.office.com/login?es=Click&ru=/&msafed=0", headers=headers, allow_redirects=True) hpgid = re.findall(b'hpgid":([0-9]+),', response.content) hpgact = re.findall(b'hpgact":([0-9]+),', response.content) - if not client_id or not hpgid or not hpgact: + if not all([client_id, hpgid, hpgact]): raise Exception("An error occured when generating headers.") # we setup the right headers to blend in @@ -104,17 +114,10 @@ def o365enum_office(usernames): # If it's managed, it's good to go and we can proceed # If it's anything else, don't bother checking # If it hasn't been checked yet, look up that user and get the domain info back - if username.index("@") > 0: # don't crash the program with an index out of bounds exception if a bad email is entered - domain = username.split("@")[1] - else: - domain = " " + domain = username[username.rfind('@')+1:] if ('@' in username) else '' if not domain in environments or environments[domain] == "MANAGED": payload["username"] = username - response = session.post( - "https://login.microsoftonline.com/common/GetCredentialType?mkt=en-US", - headers=headers, - json=payload - ) + response = session.post(fireprox_url, headers=headers, json=payload) if response.status_code == 200: throttleStatus = int(response.json()['ThrottleStatus']) ifExistsResult = str(response.json()['IfExistsResult']) @@ -123,34 +126,132 @@ def o365enum_office(usernames): if environments[domain] == "MANAGED": # NotThrottled:0,AadThrottled:1,MsaThrottled:2 if not throttleStatus == 0: - print("POSSIBLE THROTTLE DETECTED ON REQUEST FOR {}".format(username)) - print("{} {}".format(username, ifExistsResultCodes[ifExistsResult])) + print_status('!', f'{username} - Possible throttle detected on request', fhandle) + if ifExistsResult in ['0', '6']: #Valid user found! + print_status('+', f'{username} - Valid user', fhandle) + elif ifExistsResult == '5': # Different identity provider, but still a valid email address + print_status('*', f'{username} - Valid user with different IDP', fhandle) + elif ifExistsResult == '1': + print_status('-', f'{username} - Invalid user', fhandle) + else: + print_status('!', f'{username} - {ifExistsResultCodes[ifExistsResult]}', fhandle) else: - print("{} DOMAIN TYPE {} NOT SUPPORTED".format(username, environments[domain])) + print_status('!', f'{username} - Domain type \'{environments[domain]}\' not supported', fhandle) else: - print("{} REQUEST ERROR".format(username)) + print_status('!', f'{username} - Request error', fhandle) else: - print("{} DOMAIN TYPE {} NOT SUPPORTED".format(username, environments[domain])) + print_status('!', f'{username} - Domain type \'{environments[domain]}\' not supported', fhandle) + + +def prep_proxy(args: argparse.Namespace, url: str) -> fire.FireProx: + """Prepares Fireprox proxy object based off supplied / located AWS keys""" + ns = argparse.Namespace() + ns.profile_name = args.profile # ('--profile_name',default=args.profile) + ns.access_key = args.access_key # parser.add_argument('--access_key',default=args.access_key) + ns.secret_access_key = args.secret_key # parser.add_argument('--secret_access_key',default=args.secret_key) + ns.session_token = args.session_token # parser.add_argument('--session_token',default=args.session_token) + ns.region = args.region # parser.add_argument('--region',default=args.region) + ns.command = 'create' # parser.add_argument('--command',default='create') + ns.api_id = None # parser.add_argument('--api_id',default=None) + ns.url = url # parser.add_argument('--url', default=url) + return fire.FireProx(ns, 'This is a useless help message :(') + + +def list_fireprox_apis(fp: fire.FireProx) -> list: + """Lists active Fireprox APIs within the account; returns API Ids of said proxies""" + res = fp.list_api() + return [entry.get('id', '') for entry in res] + + +def delete_fireprox_apis(fp: fire.FireProx) -> list: + """Removes all Fireprox APIs within an AWS account""" + print_status('*', 'Listing Fireprox APIs prior to deletion') + #print('[+] Listing Fireprox APIs prior to deletion') + ids = list_fireprox_apis(fp) + for prox in ids: + print_status(f'!', f'Attempting to delete API \'{prox}\'') + fp.delete_api(prox) + print_status('*', 'Fireprox APIs following deletion:') + return list_fireprox_apis(fp) # Should be empty list [] + if __name__ == "__main__": - parser = argparse.ArgumentParser( - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - description='Office365 User Enumeration Script') - parser.add_argument('-u', '--userlist', required=True, type=str,\ - help='username list one per line') - parser.add_argument('-v', '--verbose', default=False, action='store_true',\ - help='Enable verbose output at urllib level') + parser = argparse.ArgumentParser(description='Office365 User Enumeration Script') + parser.add_argument('command',help='Module / command to run [list,delete,enum]') + parser.add_argument('-u', '--users',default=False,required=False,help="Required for 'enum' module; File containing list of users / emails to enumerate") + parser.add_argument('-d', '--domain',default=False,required=False,help="Email domain if not already included within user file") + parser.add_argument('--static', default=False,required=False,action='store_true',help="Disable IP rotation via Fireprox APIs; O365 will throttle after ~100 requests") + parser.add_argument('-v', '--verbose', default=False, action='store_true',help='Enable verbose output at urllib level') + parser.add_argument('-o', '--outfile', default=None, help='File to output results to [default: None') + parser.add_argument('--profile',default='default',help='AWS profile within ~/.aws/credentials to use [default: default]') + parser.add_argument('--access-key', default=None,required=False,help='AWS access key id for fireprox API creation') + parser.add_argument('--secret-key',default=None,required=False,help='AWS secret access key for fireprox API creation') + parser.add_argument('--session-token',default=None,required=False,help='AWS session token for assumed / temporary roles') + parser.add_argument('--region',default='us-east-1',required=False,help='AWS region to which fireprox API will be deployed [default: us-east-1]') args = parser.parse_args() + # Verbosity settings if args.verbose: http_client.HTTPConnection.debuglevel = 1 - logging.basicConfig( - format="%(asctime)s: %(levelname)s: %(module)s: %(message)s" - ) + logging.basicConfig(format="%(asctime)s: %(levelname)s: %(module)s: %(message)s") logging.getLogger().setLevel(logging.DEBUG) requests_log = logging.getLogger("requests.packages.urllib3") requests_log.setLevel(logging.DEBUG) requests_log.propagate = True - o365enum_office(load_usernames(args.userlist)) + # Fetch AWS access key info + if (not args.static): + try: + if (any([args.access_key, args.secret_key, args.session_token])): + aws_session = boto3.Session(args.access_key, args.secret_key, args.session_token) + else: + aws_session = boto3.Session(profile_name=args.profile) + args.access_key = aws_session.get_credentials().access_key + args.secret_key = aws_session.get_credentials().secret_key + args.session_token = aws_session.get_credentials().token + except botocore.exceptions.ProfileNotFound as err: + print(f'[x] {err}. Specify credentials here or include them as command arguments') + args.access_key = input('\tAWS Access Key Id: ') + args.secret_key = input('\tAWS Secret Access Key: ') + + fp = prep_proxy(args, 'https://login.microsoftonline.com/common/GetCredentialType?mkt=en-US') + + # Run 'module' + if (args.command == 'list' and (not args.static)): + list_fireprox_apis(fp) + elif (args.command == 'delete' and (not args.static)): + delete_fireprox_apis(fp) + elif (args.command == 'enum'): + outfile_handle = None + # Select either a fireprox API to rotate IPs or use the client's actual public IP + try: + if (args.static): + endpoint = 'https://login.microsoftonline.com/common/GetCredentialType?mkt=en-US' + else: + endpoint = fp.create_api('https://login.microsoftonline.com/common/GetCredentialType?mkt=en-US') + + users = load_usernames(args.users, args.domain) + + if (args.outfile): + with open(args.outfile, 'w+') as outfile_handle: + o365enum_office(users, endpoint, outfile_handle) + else: + o365enum_office(users, endpoint) + if (not args.static): + delete_fireprox_apis(fp) + except Exception as err: # Cleanup proxies after each run + if (not args.static): + print_status('x', f'{err}; Deleting Fireprox APIs') + delete_fireprox_apis(fp) + else: + print_status('x', f'{err}') + except KeyboardInterrupt as err: + if (not args.static): + print('!', 'Interrupt detected - deleting Fireprox APIs - CTRL-C again to force quit') + delete_fireprox_apis(fp) + + else: + print_status('x', f'Invalid option \'{args.command}\' is not in [list,delete,enum]') + parser.print_help() + exit() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4f498cf --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +boto3 +tldextract +tzlocal +bs4 +lxml +requests +termcolor