From 85d5025320bc3d3ddb677a3c557573bb012c15d6 Mon Sep 17 00:00:00 2001 From: Vexance Date: Thu, 9 Sep 2021 20:59:13 -0400 Subject: [PATCH 01/18] Ignore pycache --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore 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 From edec14a02b4edf15641a5438f6f1c1185e297941 Mon Sep 17 00:00:00 2001 From: Vexance Date: Thu, 9 Sep 2021 20:59:49 -0400 Subject: [PATCH 02/18] Upload fire.py and remove config parser check --- fire.py | 409 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 fire.py diff --git a/fire.py b/fire.py new file mode 100644 index 0000000..a1e9d7e --- /dev/null +++ b/fire.py @@ -0,0 +1,409 @@ +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" + } + } + }, + "/{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 From b53595b05c1a85e9aecb74430666c0dd4c3bd370 Mon Sep 17 00:00:00 2001 From: Vexance Date: Thu, 9 Sep 2021 21:00:20 -0400 Subject: [PATCH 03/18] necessary packages --- requirements.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e897500 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +boto3 +tldextract +tzlocal +bs4 +lxml +requests From 57fc32cdf836df512f99f819f308b3f73b81de78 Mon Sep 17 00:00:00 2001 From: Vexance Date: Thu, 9 Sep 2021 21:00:45 -0400 Subject: [PATCH 04/18] fireprox integration - troubleshooting auth header --- o365enum.py | 146 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 109 insertions(+), 37 deletions(-) diff --git a/o365enum.py b/o365enum.py index 933b251..03881f0 100644 --- a/o365enum.py +++ b/o365enum.py @@ -13,36 +13,42 @@ import logging import requests import xml.etree.ElementTree as ET +from queue import Queue +import threading +import datetime +import time +import sys +from bs4 import BeautifulSoup +import fire +import boto3, botocore + +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 - - Returns: - usernames(list): a list of usernames - ''' +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) -> 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( @@ -110,11 +116,8 @@ def o365enum_office(usernames): domain = " " 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) + print(response.content) if response.status_code == 200: throttleStatus = int(response.json()['ThrottleStatus']) ifExistsResult = str(response.json()['IfExistsResult']) @@ -132,25 +135,94 @@ def o365enum_office(usernames): else: print("{} DOMAIN TYPE {} NOT SUPPORTED".format(username, environments[domain])) + +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('[+] Listing Fireprox APIs prior to deletion') + ids = list_fireprox_apis(fp) + for prox in ids: + print(f'[+] Attempting to delete API \'{prox}\'') + fp.delete_api(prox) + print('[+] 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('-v', '--verbose', default=False, action='store_true',help='Enable verbose output at urllib level') + 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 + # Fetch AWS access key info + 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'): + list_fireprox_apis(fp) + elif (args.command == 'delete'): + delete_fireprox_apis(fp) + elif (args.command == 'enum'): + fireprox_url = fp.create_api('https://login.microsoftonline.com/common/GetCredentialType?mkt=en-US') + users = load_usernames(args.users, args.domain) + #try: + o365enum_office(users, fireprox_url) + #except Exception as err: + # print(f'[x] {err}; Deleting Fireprox APIs') + # delete_fireprox_apis(fp) + else: + print(f'[x] Invalid option \'{args.command}\' is not in [list,delete,enum]') + parser.print_help() + exit() + o365enum_office(load_usernames(args.userlist)) + exit() From f5ae7f5d76dd42165bc479a99bbfb0df2259b6e9 Mon Sep 17 00:00:00 2001 From: Vexance Date: Thu, 9 Sep 2021 21:59:30 -0400 Subject: [PATCH 05/18] Edit template to permit session POSTs --- fire.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/fire.py b/fire.py index a1e9d7e..d449eef 100644 --- a/fire.py +++ b/fire.py @@ -180,6 +180,42 @@ def get_template(self): ], "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+}": { From 28ad3c68abc3ba79ee5e8d73884e15aeb98571df Mon Sep 17 00:00:00 2001 From: Vexance Date: Thu, 9 Sep 2021 22:00:03 -0400 Subject: [PATCH 06/18] Added IP rotation capability to dodge throttling --- o365enum.py | 56 +++++++++++++++++++++++------------------------------ 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/o365enum.py b/o365enum.py index 03881f0..527fdde 100644 --- a/o365enum.py +++ b/o365enum.py @@ -5,6 +5,7 @@ Author: Quentin Kaiser Author: Cameron Geehr @BarrelTit0r +Author: Vexance @vexance ''' import random import re @@ -12,13 +13,8 @@ import argparse import logging import requests -import xml.etree.ElementTree as ET from queue import Queue import threading -import datetime -import time -import sys -from bs4 import BeautifulSoup import fire import boto3, botocore @@ -51,23 +47,15 @@ def o365enum_office(usernames: list, fireprox_url: str) -> None: # 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 @@ -110,14 +98,10 @@ def o365enum_office(usernames: list, fireprox_url: str) -> None: # 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(fireprox_url, headers=headers, json=payload) - print(response.content) if response.status_code == 200: throttleStatus = int(response.json()['ThrottleStatus']) ifExistsResult = str(response.json()['IfExistsResult']) @@ -172,6 +156,7 @@ def delete_fireprox_apis(fp: fire.FireProx) -> list: 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('--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') @@ -203,6 +188,7 @@ def delete_fireprox_apis(fp: fire.FireProx) -> list: 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' @@ -211,18 +197,24 @@ def delete_fireprox_apis(fp: fire.FireProx) -> list: elif (args.command == 'delete'): delete_fireprox_apis(fp) elif (args.command == 'enum'): - fireprox_url = fp.create_api('https://login.microsoftonline.com/common/GetCredentialType?mkt=en-US') - users = load_usernames(args.users, args.domain) - #try: - o365enum_office(users, fireprox_url) - #except Exception as err: - # print(f'[x] {err}; Deleting Fireprox APIs') - # delete_fireprox_apis(fp) + # 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) + o365enum_office(users, endpoint) + except Exception as err: # Cleanup proxies after each run + print(f'[x] {err}; Deleting Fireprox APIs') + delete_fireprox_apis(fp) + except KeyboardInterrupt as err: + print('[+] Interrupt detected - deleting Fireprox APIs - CTRL-C again to force quit') + delete_fireprox_apis(fp) + else: print(f'[x] Invalid option \'{args.command}\' is not in [list,delete,enum]') parser.print_help() - exit() - - o365enum_office(load_usernames(args.userlist)) exit() From 17156bca8411abfd38434b7d5145ed7c14de68da Mon Sep 17 00:00:00 2001 From: Vexance Date: Thu, 9 Sep 2021 22:01:23 -0400 Subject: [PATCH 07/18] Shoutout to ustayready --- o365enum.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/o365enum.py b/o365enum.py index 527fdde..e66ae6e 100644 --- a/o365enum.py +++ b/o365enum.py @@ -6,6 +6,8 @@ Author: Quentin Kaiser Author: Cameron Geehr @BarrelTit0r Author: Vexance @vexance + +Shoutout to @ustayready for the implementation of Fireprox APIs (fire.py) ''' import random import re From 7a15451ea59669444d450eb6ffa54e4961b46972 Mon Sep 17 00:00:00 2001 From: Vexance Date: Thu, 9 Sep 2021 22:10:44 -0400 Subject: [PATCH 08/18] examples of new functionality --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 575c134..31fbb7f 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,69 @@ # 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. ## 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] [--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 + --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 run: - +Example O365 username enumeration ``` -./o365enum.py -u users.txt +./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) nonexistent@contoso.com INVALID_USER existing@contoso.com VALID_USER possible@federateddomain.com DOMAIN_NOT_SUPPORTED notreal@badomain.com UNKNOWN_DOMAIN ``` +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 +``` +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 **WARNING**: This method only works for organization that are subscribers of Exchange Online and that do not have on-premise or hybrid deployment of Exchange server. From 5cdceebc4ee2174c68cf6ac83fbe1086ea2e41e5 Mon Sep 17 00:00:00 2001 From: Vexance Date: Thu, 9 Sep 2021 22:11:03 -0400 Subject: [PATCH 09/18] automatically delete apis following completeion --- o365enum.py | 1 + 1 file changed, 1 insertion(+) diff --git a/o365enum.py b/o365enum.py index e66ae6e..dbfb2d7 100644 --- a/o365enum.py +++ b/o365enum.py @@ -208,6 +208,7 @@ def delete_fireprox_apis(fp: fire.FireProx) -> list: users = load_usernames(args.users, args.domain) o365enum_office(users, endpoint) + delete_fireprox_apis(fp) except Exception as err: # Cleanup proxies after each run print(f'[x] {err}; Deleting Fireprox APIs') delete_fireprox_apis(fp) From 617dd53baab0e6917bf2926c31e92592befb1f1e Mon Sep 17 00:00:00 2001 From: Vexance Date: Thu, 9 Sep 2021 22:18:16 -0400 Subject: [PATCH 10/18] editted to accomodat users w/o aws access keys --- o365enum.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/o365enum.py b/o365enum.py index dbfb2d7..b10c6c0 100644 --- a/o365enum.py +++ b/o365enum.py @@ -190,13 +190,13 @@ def delete_fireprox_apis(fp: fire.FireProx) -> list: 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') + if (not args.static): + fp = prep_proxy(args, 'https://login.microsoftonline.com/common/GetCredentialType?mkt=en-US') # Run 'module' - if (args.command == 'list'): + if (args.command == 'list' and (not args.static)): list_fireprox_apis(fp) - elif (args.command == 'delete'): + elif (args.command == 'delete' and (not args.static)): delete_fireprox_apis(fp) elif (args.command == 'enum'): # Select either a fireprox API to rotate IPs or use the client's actual public IP @@ -208,13 +208,18 @@ def delete_fireprox_apis(fp: fire.FireProx) -> list: users = load_usernames(args.users, args.domain) o365enum_office(users, endpoint) - delete_fireprox_apis(fp) + if (not args.static): + delete_fireprox_apis(fp) except Exception as err: # Cleanup proxies after each run - print(f'[x] {err}; Deleting Fireprox APIs') - delete_fireprox_apis(fp) + if (not args.static): + print(f'[x] {err}; Deleting Fireprox APIs') + delete_fireprox_apis(fp) + else: + print(f'[x] {err}') except KeyboardInterrupt as err: - print('[+] Interrupt detected - deleting Fireprox APIs - CTRL-C again to force quit') - delete_fireprox_apis(fp) + if (not args.static): + print('[+] Interrupt detected - deleting Fireprox APIs - CTRL-C again to force quit') + delete_fireprox_apis(fp) else: print(f'[x] Invalid option \'{args.command}\' is not in [list,delete,enum]') From 859cf07e594a319151e0c26e1a2a62cf311b25e2 Mon Sep 17 00:00:00 2001 From: Vexance Date: Thu, 9 Sep 2021 22:19:17 -0400 Subject: [PATCH 11/18] editted to accomodat users w/o aws access keys --- o365enum.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/o365enum.py b/o365enum.py index b10c6c0..bad005b 100644 --- a/o365enum.py +++ b/o365enum.py @@ -177,20 +177,20 @@ def delete_fireprox_apis(fp: fire.FireProx) -> list: requests_log.propagate = True # Fetch AWS access key info - 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: ') - 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' From 624af52e92d60b4a80305ee33d88204262ec6eab Mon Sep 17 00:00:00 2001 From: Vexance Date: Tue, 30 Nov 2021 10:46:13 -0500 Subject: [PATCH 12/18] Modifed output format; added outfile support --- o365enum.py | 86 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 59 insertions(+), 27 deletions(-) diff --git a/o365enum.py b/o365enum.py index bad005b..7525d80 100644 --- a/o365enum.py +++ b/o365enum.py @@ -3,34 +3,50 @@ Office365 User Enumeration script. Enumerate valid usernames from Office 365 using the office.com login page. -Author: Quentin Kaiser -Author: Cameron Geehr @BarrelTit0r -Author: Vexance @vexance +Author: Quentin Kaiser (original) +Author: Cameron Geehr @BarrelTit0r (refined and removed login attempt requirement) +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 io, logging, random, re, requests, string, termcolor, threading from queue import Queue -import threading +import argparse, botocore, boto3 import fire -import boto3, botocore mutex = threading.Lock() count_queue = Queue() search_results = set() - try: import http.client as http_client except ImportError: import httplib as http_client +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 == '[x]': + color = 'orange' + elif status_type == '[!]': + 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}') + + 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 = [] @@ -43,7 +59,7 @@ def load_usernames(usernames_file: str, domain: str = False) -> list: return user_list -def o365enum_office(usernames: list, fireprox_url: str) -> None: +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" } @@ -112,14 +128,21 @@ def o365enum_office(usernames: list, fireprox_url: str) -> None: 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: @@ -144,12 +167,13 @@ def list_fireprox_apis(fp: fire.FireProx) -> list: def delete_fireprox_apis(fp: fire.FireProx) -> list: """Removes all Fireprox APIs within an AWS account""" - print('[+] Listing Fireprox APIs prior to deletion') + 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(f'[+] Attempting to delete API \'{prox}\'') + print_status(f'+', f'Attempting to delete API \'{prox}\'') fp.delete_api(prox) - print('[+] Fireprox APIs following deletion:') + print('*', 'Fireprox APIs following deletion:') return list_fireprox_apis(fp) # Should be empty list [] @@ -160,6 +184,7 @@ def delete_fireprox_apis(fp: fire.FireProx) -> list: 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') @@ -193,12 +218,16 @@ def delete_fireprox_apis(fp: fire.FireProx) -> list: fp = prep_proxy(args, 'https://login.microsoftonline.com/common/GetCredentialType?mkt=en-US') - # Run 'module' + # 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 + if (args.outfile): + outfile_handle = open(args.outfile, 'w+') + # Select either a fireprox API to rotate IPs or use the client's actual public IP try: if (args.static): @@ -207,22 +236,25 @@ def delete_fireprox_apis(fp: fire.FireProx) -> list: endpoint = fp.create_api('https://login.microsoftonline.com/common/GetCredentialType?mkt=en-US') users = load_usernames(args.users, args.domain) - o365enum_office(users, endpoint) + o365enum_office(users, endpoint, outfile_handle) + outfile_handle.close() if (not args.static): delete_fireprox_apis(fp) except Exception as err: # Cleanup proxies after each run + outfile_handle.close() if (not args.static): - print(f'[x] {err}; Deleting Fireprox APIs') + print_status('x', f'{err}; Deleting Fireprox APIs') delete_fireprox_apis(fp) else: - print(f'[x] {err}') + print_status('x', f'{err}') except KeyboardInterrupt as err: + outfile_handle.close() if (not args.static): - print('[+] Interrupt detected - deleting Fireprox APIs - CTRL-C again to force quit') + print('!', 'Interrupt detected - deleting Fireprox APIs - CTRL-C again to force quit') delete_fireprox_apis(fp) else: - print(f'[x] Invalid option \'{args.command}\' is not in [list,delete,enum]') + print_status('x', f'Invalid option \'{args.command}\' is not in [list,delete,enum]') parser.print_help() exit() From 86a71f3b5beb438f52afacbc8bb45deb2a76ca53 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 1 Dec 2021 12:16:56 -0500 Subject: [PATCH 13/18] Adjusting example output for new functionality --- README.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 31fbb7f..3281163 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,18 @@ 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) -nonexistent@contoso.com INVALID_USER -existing@contoso.com VALID_USER -possible@federateddomain.com DOMAIN_NOT_SUPPORTED -notreal@badomain.com UNKNOWN_DOMAIN +[-] 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 Fireprox API Listing @@ -241,3 +249,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 From a1a84c360d3dd2ae6fe3b8c1c551044a086a5e45 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 1 Dec 2021 12:18:41 -0500 Subject: [PATCH 14/18] Adding -o flag in help setion --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3281163..c89cbfe 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ o365enum will read usernames from the file provided as first parameter. The file ``` python3 o365enum.py --help -usage: o365enum.py [-h] [-u USERS] [-d DOMAIN] [--static] [-v] [--profile PROFILE] [--access-key ACCESS_KEY] [--secret-key SECRET_KEY] [--session-token SESSION_TOKEN] [--region REGION] command +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 @@ -23,6 +23,7 @@ optional arguments: 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 From 0053e45d5eb653a791bd3a39b3fb72c1669fbcf9 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 1 Dec 2021 12:19:04 -0500 Subject: [PATCH 15/18] adjusting contribution notes --- o365enum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/o365enum.py b/o365enum.py index 7525d80..a1c8eb6 100644 --- a/o365enum.py +++ b/o365enum.py @@ -4,7 +4,7 @@ Enumerate valid usernames from Office 365 using the office.com login page. Author: Quentin Kaiser (original) -Author: Cameron Geehr @BarrelTit0r (refined and removed login attempt requirement) +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) From e2a2b4b4f8ad84cb0b9e798e934fe78c53a38749 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 1 Dec 2021 12:19:46 -0500 Subject: [PATCH 16/18] Adding termcolor to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index e897500..4f498cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ tzlocal bs4 lxml requests +termcolor From f39b9d6e992499e10783afe780e915b4d812acc0 Mon Sep 17 00:00:00 2001 From: Vexance Date: Thu, 2 Dec 2021 10:39:56 -0500 Subject: [PATCH 17/18] fix potential close of non-existing TextIOWrapper --- o365enum.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/o365enum.py b/o365enum.py index 7525d80..b506c89 100644 --- a/o365enum.py +++ b/o365enum.py @@ -30,9 +30,7 @@ def print_status(status_type: str, text: str, file_handle: io.TextIOWrapper = No status_type = f'[{status_type.lower()}]' color = 'white' - if status_type == '[x]': - color = 'orange' - elif status_type == '[!]': + if status_type == '[!]' or status_type == '[x]': color = 'yellow' elif status_type == '[-]': color = 'red' @@ -171,9 +169,9 @@ def delete_fireprox_apis(fp: fire.FireProx) -> list: #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}\'') + print_status(f'!', f'Attempting to delete API \'{prox}\'') fp.delete_api(prox) - print('*', 'Fireprox APIs following deletion:') + print_status('*', 'Fireprox APIs following deletion:') return list_fireprox_apis(fp) # Should be empty list [] @@ -225,9 +223,6 @@ def delete_fireprox_apis(fp: fire.FireProx) -> list: delete_fireprox_apis(fp) elif (args.command == 'enum'): outfile_handle = None - if (args.outfile): - outfile_handle = open(args.outfile, 'w+') - # Select either a fireprox API to rotate IPs or use the client's actual public IP try: if (args.static): @@ -236,19 +231,21 @@ def delete_fireprox_apis(fp: fire.FireProx) -> list: endpoint = fp.create_api('https://login.microsoftonline.com/common/GetCredentialType?mkt=en-US') users = load_usernames(args.users, args.domain) - o365enum_office(users, endpoint, outfile_handle) - outfile_handle.close() + + 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 - outfile_handle.close() 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: - outfile_handle.close() if (not args.static): print('!', 'Interrupt detected - deleting Fireprox APIs - CTRL-C again to force quit') delete_fireprox_apis(fp) From dedb052889c58a9d732010aae5a1c010debf714e Mon Sep 17 00:00:00 2001 From: Vexance Date: Tue, 5 Sep 2023 14:36:52 -0400 Subject: [PATCH 18/18] Deprecate repo in favor of Stratustryke --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index c89cbfe..65011fd 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ 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.