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