Private/AD/Core/Get-ADTradecraftSignals.ps1
|
# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0 # https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/ # AI/LLM use: see AI-USAGE.md for required attribution function Get-ADTradecraftSignals { <# .SYNOPSIS Collects signals for advanced-adversary tradecraft detection. .DESCRIPTION Gathers: * GPP cpassword matches across SYSVOL Policies\**\*.xml * Server objects under CN=Sites,CN=Configuration (DCShadow surface) * msFVE-RecoveryInformation objects + parent computer staleness * RODC inventory (so the PRP check can short-circuit if none) #> [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$Connection, [switch]$Quiet ) $result = @{ CpasswordHits = @() ConfigPartitionServers = @() BitLockerKeys = @() Rodcs = @() SysvolReadable = $false Errors = @{} } $domainDns = ($Connection.DomainDN -replace '^DC=', '' -replace ',DC=', '.').ToLower() # ── 1. GPP cpassword scan ──────────────────────────────────────────── $policiesRoot = "\\$domainDns\SYSVOL\$domainDns\Policies" try { if (Test-Path -LiteralPath $policiesRoot -ErrorAction Stop) { $result.SysvolReadable = $true $xmlFiles = @(Get-ChildItem -LiteralPath $policiesRoot -Recurse -Filter '*.xml' -ErrorAction SilentlyContinue) foreach ($f in $xmlFiles) { try { $content = Get-Content -LiteralPath $f.FullName -Raw -ErrorAction Stop if ($content -match 'cpassword="([^"]+)"') { # Capture surrounding userName/runAs if present for human-readable output $userMatch = if ($content -match 'userName="([^"]+)"') { $Matches[1] } elseif ($content -match 'runAs="([^"]+)"') { $Matches[1] } else { '(unknown)' } $result.CpasswordHits += [PSCustomObject]@{ FilePath = $f.FullName ExposedUser = $userMatch CpasswordLength = $Matches[1].Length } } } catch { # Don't fail the whole scan on a single unreadable XML } } } } catch { $result.Errors['CpasswordScan'] = $_.Exception.Message } # ── 2. Configuration-partition server objects ─────────────────────── try { $configDN = $Connection.ConfigDN if ($configDN) { $sitesDN = "CN=Sites,$configDN" $sitesRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $sitesDN $servers = @(Invoke-LdapQuery -SearchRoot $sitesRoot ` -Filter '(objectClass=server)' ` -Properties @('cn', 'dNSHostName', 'distinguishedName', 'whenCreated', 'serverReference')) foreach ($s in $servers) { $result.ConfigPartitionServers += [PSCustomObject]@{ CN = $s['cn'] ?? '' DNSHostName = $s['dnshostname'] ?? '' DistinguishedName = $s['distinguishedname'] ?? '' WhenCreated = $s['whencreated'] ServerReference = $s['serverreference'] ?? '' } } } } catch { $result.Errors['ConfigPartitionServers'] = $_.Exception.Message } # ── 3. BitLocker recovery information ─────────────────────────────── try { $blRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $Connection.DomainDN $blKeys = @(Invoke-LdapQuery -SearchRoot $blRoot ` -Filter '(objectClass=msFVE-RecoveryInformation)' ` -Properties @('distinguishedName', 'whenCreated')) foreach ($k in $blKeys) { # Parent computer DN = drop the leftmost CN= component $dn = $k['distinguishedname'] $parentDN = if ($dn -match '^[^,]+,(.+)$') { $Matches[1] } else { $null } $result.BitLockerKeys += [PSCustomObject]@{ DistinguishedName = $dn ParentComputer = $parentDN WhenCreated = $k['whencreated'] } } } catch { $result.Errors['BitLockerKeys'] = $_.Exception.Message } # ── 4. RODC inventory ─────────────────────────────────────────────── try { $rodcRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $Connection.DomainDN # RODCs have userAccountControl bit 0x4000000 (PARTIAL_SECRETS_ACCOUNT, 67108864) set on their computer object. $rodcs = @(Invoke-LdapQuery -SearchRoot $rodcRoot ` -Filter '(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=67108864))' ` -Properties @('cn', 'dNSHostName', 'distinguishedName')) foreach ($r in $rodcs) { $result.Rodcs += [PSCustomObject]@{ CN = $r['cn'] ?? '' DNSHostName = $r['dnshostname'] ?? '' DistinguishedName = $r['distinguishedname'] ?? '' } } } catch { $result.Errors['Rodcs'] = $_.Exception.Message } return $result } |