Detectarea atacurilor de tip Password Spraying în Active Directory cu PowerShell și rezumat AI

Configurare noua (How To)

Situatie

Un atac de password spraying (MITRE ATT&CK T1110.003) este opusul unui brute-force clasic. Atacatorul nu încearcă mii de parole pe un singur cont, ci una sau două parole comune (Iarna2026!, Parola123!, NumeFirma2026) pe sute de conturi, cu pauze lungi între runde.

De ce trece neobservat:

  • fiecare cont are doar 1–2 încercări eșuate, deci nu se declanșează politica de lockout;
  • încercările sunt împrăștiate pe ore, deci nu apar ca vârf în monitorizare;
  • în log-uri, câteva sute de eșecuri se pierd printre zecile de mii de evenimente normale.

Semnătura atacului este clară doar dacă privești pe sursă: o singură adresă IP sau stație care eșuează autentificarea pe multe conturi diferite, cu foarte puține încercări pe fiecare cont.

Soluția de mai jos:

  1. colectează evenimentele de autentificare eșuată de pe toate Domain Controllerele;
  2. le grupează pe sursă și pe fereastră de timp și aplică reguli care separă spraying-ul de brute-force și de greșelile normale ale utilizatorilor;
  3. trimite incidentele detectate către un LLM local (Ollama), care scrie rezumatul pentru echipa de securitate;
  4. rulează periodic și trimite alertă pe email.

Evenimentele relevante

Event ID Unde apare Ce înseamnă Cod relevant
4771 Domain Controller Kerberos pre-authentication eșuată Status 0x18 = parolă greșită
4776 Domain Controller Validare credențiale NTLM Status 0xC000006A = parolă greșită
4625 DC / servere membre Logon eșuat SubStatus 0xC000006A = parolă greșită, 0xC0000064 = utilizator inexistent

0xC0000064 (utilizator inexistent) în număr mare de la aceeași sursă este un indiciu în plus: atacatorul folosește o listă de nume ghicite.

Pre-condiție: auditarea activată

Prin GPO aplicat pe OU-ul Domain Controllers:

Computer Configuration → Policies → Windows Settings → Security Settings → Advanced Audit Policy Configuration → Audit Policies → Account Logon

  • Audit Kerberos Authentication Service → Failure
  • Audit Credential Validation → Failure

Și la Logon/Logoff → Audit Logon → Failure.

Măriți și dimensiunea log-ului Security pe DC-uri (de exemplu 1–4 GB), altfel evenimentele se suprascriu înainte de colectare.

Backup

Exportați setările GPO de audit existente (Backup-GPO) înainte de modificare și notați dimensiunea actuală a log-ului Security pe DC-uri.

Solutie

Pasi de urmat

Pasul 1 – Colectarea evenimentelor de pe toate DC-urile

Parsăm evenimentele prin XML, după numele câmpurilor, nu după index. Astfel scriptul rămâne stabil între versiuni de Windows Server.

function Get-FailedAuthEvents {
    param([int]$Minutes = 60)

    $start = (Get-Date).AddMinutes(-$Minutes)
    $dcs   = (Get-ADDomainController -Filter *).HostName

    foreach ($dc in $dcs) {
        try {
            $events = Get-WinEvent -ComputerName $dc -FilterHashtable @{
                LogName = 'Security'; Id = 4771, 4776, 4625; StartTime = $start
            } -ErrorAction Stop
        } catch {
            Write-Warning "Nu pot citi de pe $dc : $($_.Exception.Message)"; continue
        }

        foreach ($e in $events) {
            $d = @{}
            ([xml]$e.ToXml()).Event.EventData.Data | ForEach-Object { $d[$_.Name] = $_.'#text' }

            # Atenție: în PowerShell, 'continue' în interiorul unui switch nu sare la următorul
            # element din foreach, de aceea folosim variabila $cod ca indicator.
            $src = $null; $cod = $null
            switch ($e.Id) {
                4771 {
                    if ($d.Status -eq '0x18') {
                        $src = ($d.IpAddress -replace '^::ffff:', '')
                        $cod = 'BAD_PASSWORD'
                    }
                }
                4776 {
                    if ($d.Status -in '0xc000006a', '0xc0000064') {
                        $src = $d.Workstation
                        $cod = if ($d.Status -eq '0xc0000064') { 'NO_SUCH_USER' } else { 'BAD_PASSWORD' }
                    }
                }
                4625 {
                    if ($d.SubStatus -in '0xc000006a', '0xc0000064') {
                        $src = if ($d.IpAddress -and $d.IpAddress -ne '-') { $d.IpAddress } else { $d.WorkstationName }
                        $cod = if ($d.SubStatus -eq '0xc0000064') { 'NO_SUCH_USER' } else { 'BAD_PASSWORD' }
                    }
                }
            }
            if (-not $cod) { continue }

            [pscustomobject]@{
                Time    = $e.TimeCreated
                DC      = $dc
                EventId = $e.Id
                User    = $d.TargetUserName.ToLower()
                Source  = $src
                Result  = $cod
            }
        }
    }
}

Pasul 2 – Logica de detecție

Pentru fiecare sursă calculăm, pe fereastra analizată:

  • numărul de conturi distincte încercate
  • media încercărilor per cont
  • numărul de conturi inexistente
  • durata atacului (prima și ultima încercare).

Regulile de clasificare:

Tip Condiție
Password Spraying ≥ 10 conturi distincte și ≤ 3 încercări în medie per cont
Brute-force ≤ 3 conturi distincte și ≥ 15 încercări pe cel puțin un cont
Normal restul (utilizatori care greșesc parola, parole expirate salvate pe telefoane etc.)
function Find-SprayingIncidents {
    param(
        [Parameter(Mandatory)] $Events,
        [int]$MinDistinctUsers = 10,
        [double]$MaxAvgPerUser = 3,
        [string[]]$ExcludedSources = @('127.0.0.1', '::1', '-', '')
    )

    $Events | Where-Object { $_.Source -notin $ExcludedSources } |
    Group-Object Source | ForEach-Object {
        $perUser  = $_.Group | Group-Object User
        $distinct = $perUser.Count
        $avg      = [math]::Round($_.Count / [math]::Max($distinct,1), 2)
        $noUser   = ($_.Group | Where-Object Result -eq 'NO_SUCH_USER').Count
        $sorted   = $_.Group | Sort-Object Time

        $tip = if ($distinct -ge $MinDistinctUsers -and $avg -le $MaxAvgPerUser) { 'PASSWORD_SPRAYING' }
               elseif ($distinct -le 3 -and ($perUser | Measure-Object Count -Maximum).Maximum -ge 15) { 'BRUTE_FORCE' }
               else { $null }

        if ($tip) {
            [pscustomobject]@{
                Tip               = $tip
                Sursa             = $_.Name
                ConturiDistincte  = $distinct
                TotalIncercari    = $_.Count
                MediePerCont      = $avg
                ConturiInexistente= $noUser
                Inceput           = $sorted[0].Time
                Sfarsit           = $sorted[-1].Time
                DCuri             = ($_.Group.DC | Sort-Object -Unique) -join ', '
                ExempleConturi    = ($perUser.Name | Select-Object -First 15) -join ', '
                ConturiPrivilegiate = ($perUser.Name | Where-Object { $_ -in $script:PrivilegedUsers }) -join ', '
            }
        }
    }
}

Lista conturilor privilegiate se încarcă o singură dată, la pornire. Dacă atacul atinge conturi de admin, severitatea crește:

$script:PrivilegedUsers = @('Domain Admins','Enterprise Admins','Administrators') |
    ForEach-Object { Get-ADGroupMember $_ -Recursive } |
    Select-Object -ExpandProperty SamAccountName -Unique |
    ForEach-Object { $_.ToLower() }

Pasul 3 – Verificare: a reușit atacatorul?

Cel mai important pas. Dacă după eșecuri apare un logon reușit de la aceeași sursă pe unul dintre conturile încercate, contul este probabil compromis.

function Test-SuccessfulLogonAfterSpray {
    param($Incident)

    $dcs = (Get-ADDomainController -Filter *).HostName
    foreach ($dc in $dcs) {
        Get-WinEvent -ComputerName $dc -FilterHashtable @{
            LogName = 'Security'; Id = 4768, 4624; StartTime = $Incident.Inceput
        } -ErrorAction SilentlyContinue | ForEach-Object {
            $d = @{}
            ([xml]$_.ToXml()).Event.EventData.Data | ForEach-Object { $d[$_.Name] = $_.'#text' }
            $ip = ($d.IpAddress -replace '^::ffff:', '')
            if ($ip -eq $Incident.Sursa -or $d.WorkstationName -eq $Incident.Sursa) {
                [pscustomobject]@{ Time = $_.TimeCreated; User = $d.TargetUserName; EventId = $_.Id; DC = $dc }
            }
        }
    }
}

4768 este emiterea unui TGT Kerberos, adică autentificare reușită pe DC.

Pasul 4 – Rezumatul incidentului cu LLM local

Analistul de serviciu nu are nevoie de un tabel brut la 3 dimineața, ci de un rezumat clar: ce s-a întâmplat, cât de grav e și ce face acum. Folosim un model local, ca datele despre conturi și infrastructură să nu părăsească rețeaua.

function Get-AISummary {
    param($Incident, $Successes, [string]$Model = 'llama3.1:8b')

    $facts = [ordered]@{
        incident = $Incident
        autentificari_reusite_dupa_atac = @($Successes | Select-Object Time, User, EventId)
    } | ConvertTo-Json -Depth 4

    $prompt = @"
Ești analist SOC. Scrie în limba română un rezumat de incident pentru echipa IT, pe baza datelor JSON de mai jos.
Structură obligatorie:
1. REZUMAT (2-3 propoziții)
2. SEVERITATE: Critică / Ridicată / Medie, cu justificare (conturi privilegiate sau logon reușit = Critică)
3. MITRE ATT&CK: tehnica și sub-tehnica
4. CONTURI POSIBIL COMPROMISE (doar dacă există autentificări reușite)
5. PAȘI DE RĂSPUNS imediați (maxim 6, concreți, pentru Active Directory)
Nu inventa date care nu apar în JSON.

$facts
"@

    $body = @{ model = $Model; prompt = $prompt; stream = $false; options = @{ temperature = 0.2 } } | ConvertTo-Json -Depth 5
    (Invoke-RestMethod 'http://localhost:11434/api/generate' -Method Post -Body $body `
        -ContentType 'application/json; charset=utf-8').response
}

Pasul 5 – Scriptul complet și alertarea

Detect-PasswordSpraying.ps1:

#Requires -Modules ActiveDirectory
param([int]$WindowMinutes = 60)

. "$PSScriptRoot\SprayFunctions.ps1"   # funcțiile din pașii 1-4

$script:PrivilegedUsers = @('Domain Admins','Enterprise Admins','Administrators') |
    ForEach-Object { Get-ADGroupMember $_ -Recursive } |
    Select-Object -ExpandProperty SamAccountName -Unique | ForEach-Object { $_.ToLower() }

$events    = @(Get-FailedAuthEvents -Minutes $WindowMinutes)
$incidents = @(Find-SprayingIncidents -Events $events)

if ($incidents.Count -eq 0) { Write-Host "Niciun incident în ultimele $WindowMinutes minute."; return }

foreach ($inc in $incidents) {
    $succ    = @(Test-SuccessfulLogonAfterSpray -Incident $inc)
    $summary = Get-AISummary -Incident $inc -Successes $succ

    $subject = "[ALERTĂ AD] $($inc.Tip) de la $($inc.Sursa) - $($inc.ConturiDistincte) conturi" +
               $(if ($succ.Count) { " - LOGON REUȘIT DETECTAT" } else { "" })

    $html = "<h2>$subject</h2><pre style='font-family:Segoe UI'>$([System.Net.WebUtility]::HtmlEncode($summary))</pre>" +
            ($inc | ConvertTo-Html -Fragment -As List) +
            $(if ($succ.Count) { "<h3>Autentificări reușite după atac</h3>" + ($succ | ConvertTo-Html -Fragment) })

    Send-MailMessage -SmtpServer 'smtp.firma.local' -From 'ad-monitor@firma.ro' -To 'securitate@firma.ro' `
        -Subject $subject -Body $html -BodyAsHtml -Encoding UTF8 `
        -Priority $(if ($succ.Count -or $inc.ConturiPrivilegiate) { 'High' } else { 'Normal' })

    # Jurnal local pentru istoric
    $inc | Add-Member Logon_Reusit ($succ.Count -gt 0) -PassThru |
        Export-Csv 'C:\ADMonitor\incidente.csv' -Append -NoTypeInformation -Encoding UTF8
}

Send-MailMessage este marcat ca învechit de Microsoft, dar funcționează în continuare cu un relay SMTP intern. Pentru Exchange Online folosiți Microsoft Graph (Send-MgUserMail).

Pasul 6 – Rulare programată

Rulează la fiecare 30 de minute, cu fereastră de 60 de minute, ca atacurile lente să fie prinse între rulări:

$action  = New-ScheduledTaskAction -Execute 'powershell.exe' `
  -Argument '-NoProfile -ExecutionPolicy Bypass -File C:\ADMonitor\Detect-PasswordSpraying.ps1 -WindowMinutes 60'
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 30)
# gMSA: fără parolă, LogonType Password indică folosirea parolei gestionate de AD
$principal = New-ScheduledTaskPrincipal -UserId 'DOMENIU\svc-admonitor$' -LogonType Password -RunLevel Highest
Register-ScheduledTask -TaskName 'Detectie Password Spraying' -Action $action -Trigger $trigger -Principal $principal

Recomandat: rulați cu un gMSA (svc-admonitor$) membru în grupul Event Log Readers pe DC-uri, nu cu un cont Domain Admin.

Răspuns la incident – ce faceți când primiți alerta

  1. Blocați sursa la firewall sau VPN. Dacă e o stație internă, izolați-o din EDR, pentru că poate fi compromisă.
  2. Pentru conturile cu logon reușit după atac: resetare parolă, revocare sesiuni, verificare MFA și verificarea activității recente.
  3. Dacă e implicat un cont privilegiat: tratați incidentul ca critic și verificați modificările recente din AD (grupuri, GPO-uri, conturi noi).
  4. Pe termen lung: activați Microsoft Entra Password Protection on-premise cu listă de parole interzise (numele firmei, anotimpuri, ani), astfel parolele folosite de obicei la spraying nu mai pot fi setate.

În primele zile rulați scriptul doar cu jurnalizare, fără email, și studiați incidente.csv. Surse legitime care pot declanșa alerta:

  • servere RADIUS/NPS, proxy-uri, gateway-uri VPN sau Exchange, care centralizează autentificările multor utilizatori;
  • scanere de vulnerabilități autorizate.

Pentru acestea, nu le excludeți complet. Folosiți un prag mai mare în -MinDistinctUsers, pentru că un atac real poate trece tocmai prin VPN.

Tip solutie

Permanent

Impact colateral

Interogarea log-ului Security de pe toate DC-urile la fiecare 30 de minute adaugă o încărcare mică pe controllere. Mărirea log-ului Security consumă spațiu pe disc. Pot apărea alerte fals-pozitive de la servere RADIUS/VPN până la ajustarea pragurilor.

Plan de restaurare in caz de nefunctionare

Ștergeți task-ul programat (Unregister-ScheduledTask -TaskName "Detectie Password Spraying"), restaurați GPO-ul de audit din backup (Restore-GPO) și readuceți dimensiunea log-ului Security la valoarea inițială.

Voteaza

(2 din 4 persoane apreciaza acest articol)

Despre Autor

Leave A Comment?