Private/AD/Core/Get-ReconnaissanceData.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-ReconnaissanceData { [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$Connection, [string[]]$Categories = @('All'), [int]$InactiveDays = 90, [int]$PasswordAgeDays = 365, [string]$NtdsPath, [string]$WeakPasswordList, # Opt-in: sweep ACLs across every domain object (not just the six critical Tier-0 objects). # Heaviest read PSGuerrilla performs; unlocks deep transitive chains + a richer BloodHound export. [switch]$FullDomainAcl, [switch]$Quiet ) # ── Category-to-data-source mapping ────────────────────────────────── $categoryDataNeeds = @{ DomainForest = @('DomainInfo', 'DomainControllers') Trusts = @('TrustRelationships') PrivilegedAccounts = @('PrivilegedMembers', 'DomainInfo') PasswordPolicy = @('PasswordPolicies', 'DomainInfo') Kerberos = @('KerberosConfig') ACLDelegation = @('ObjectACLs', 'PrivilegedMembers') GroupPolicy = @('GroupPolicyObjects') LogonScripts = @('LogonScripts') CertificateServices = @('CertificateServices') StaleObjects = @('StaleObjects') Network = @('NetworkConfig') TierZero = @('PrivilegedMembers', 'TierZeroSignals') Logging = @('NetworkConfig') Tradecraft = @('DomainControllers', 'TradecraftSignals') AttackPath = @('ObjectACLs', 'PrivilegedMembers') } # Resolve which data sources are required $requiredSources = [System.Collections.Generic.HashSet[string]]::new( [StringComparer]::OrdinalIgnoreCase ) if ($Categories -contains 'All') { foreach ($sources in $categoryDataNeeds.Values) { foreach ($s in $sources) { [void]$requiredSources.Add($s) } } } else { foreach ($cat in $Categories) { if ($categoryDataNeeds.ContainsKey($cat)) { foreach ($s in $categoryDataNeeds[$cat]) { [void]$requiredSources.Add($s) } } } } # Always collect module availability [void]$requiredSources.Add('ModuleAvailability') # ── Initialize result hashtable ────────────────────────────────────── $data = @{ Domain = $null DomainControllers = $null Trusts = $null PrivilegedAccounts = $null PasswordPolicies = $null Kerberos = $null ACLs = $null GroupPolicies = $null LogonScripts = $null CertificateServices = $null StaleObjects = $null Network = $null TierZero = $null Tradecraft = $null ModuleAvailability = $null Connection = $Connection Errors = @{} } # Helper: determine whether a data source is needed $needsSource = { param([string]$Name) $requiredSources.Contains($Name) } # ── 1. Module Availability ─────────────────────────────────────────── if (& $needsSource 'ModuleAvailability') { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Checking module availability' } try { $data.ModuleAvailability = Test-ADModuleAvailability } catch { $data.Errors['ModuleAvailability'] = $_.Exception.Message $data.ModuleAvailability = @{ ActiveDirectory = $false GroupPolicy = $false DSInternals = $false PSPKI = $false } } # Single pre-flight note for the DSInternals-gated password-hash checks # (ADPWD-010..014), instead of five identical per-check SKIP lines in the report run. if (-not $Quiet -and $data.ModuleAvailability -and -not $data.ModuleAvailability.DSInternals) { Write-ProgressLine -Phase INFO -Message 'DSInternals not installed — the 5 password-hash checks (ADPWD-010..014) will SKIP. Install-Module DSInternals (and run on a DC / with replication rights) to enable NT-hash analysis.' } } # ── 2. Domain Information ──────────────────────────────────────────── if (& $needsSource 'DomainInfo') { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Collecting domain information' } try { $data.Domain = Get-ADDomainInfo -Connection $Connection -Quiet:$Quiet } catch { Write-Warning "Failed to collect domain information: $_" $data.Errors['DomainInfo'] = $_.Exception.Message } } # ── 3. Domain Controllers ──────────────────────────────────────────── if (& $needsSource 'DomainControllers') { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Enumerating domain controllers' } try { $data.DomainControllers = Get-ADDomainControllers -Connection $Connection -Quiet:$Quiet } catch { Write-Warning "Failed to enumerate domain controllers: $_" $data.Errors['DomainControllers'] = $_.Exception.Message } } # ── 4. Trust Relationships ─────────────────────────────────────────── if (& $needsSource 'TrustRelationships') { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Collecting trust relationships' } try { $data.Trusts = Get-ADTrustRelationships -Connection $Connection -Quiet:$Quiet } catch { Write-Warning "Failed to collect trust relationships: $_" $data.Errors['TrustRelationships'] = $_.Exception.Message } } # ── 5. Privileged Members ──────────────────────────────────────────── if (& $needsSource 'PrivilegedMembers') { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Enumerating privileged group members' } try { $data.PrivilegedAccounts = Get-ADPrivilegedMembers -Connection $Connection -Quiet:$Quiet } catch { Write-Warning "Failed to enumerate privileged members: $_" $data.Errors['PrivilegedMembers'] = $_.Exception.Message } } # ── 6. Password Policies ──────────────────────────────────────────── if (& $needsSource 'PasswordPolicies') { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Collecting password policies' } try { $data.PasswordPolicies = Get-ADPasswordPolicies -Connection $Connection -Quiet:$Quiet } catch { Write-Warning "Failed to collect password policies: $_" $data.Errors['PasswordPolicies'] = $_.Exception.Message } } # ── 7. Kerberos Configuration ──────────────────────────────────────── if (& $needsSource 'KerberosConfig') { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Analyzing Kerberos configuration' } try { $data.Kerberos = Get-ADKerberosConfig -Connection $Connection -Quiet:$Quiet } catch { Write-Warning "Failed to analyze Kerberos configuration: $_" $data.Errors['KerberosConfig'] = $_.Exception.Message } } # ── 8. Object ACLs / Delegation ────────────────────────────────────── if (& $needsSource 'ObjectACLs') { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Auditing object ACLs and delegation' } try { $data.ACLs = Get-ADObjectACLs -Connection $Connection -Quiet:$Quiet } catch { Write-Warning "Failed to audit object ACLs: $_" $data.Errors['ObjectACLs'] = $_.Exception.Message } # ── Full-domain ACL sweep (opt-in) — merges domain-wide control ACEs into DangerousACEs ── # so the transitive engine and BloodHound export see the whole control graph, not just the # six critical objects. Only runs when ACL collection succeeded. if ($FullDomainAcl -and $data.ACLs -and $null -ne $data.ACLs.DangerousACEs) { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Full-domain ACL sweep (this can take a while on large domains)' } try { $fd = Get-ADFullDomainAcl -Connection $Connection -Quiet:$Quiet if ($fd.Error) { $data.Errors['FullDomainAcl'] = $fd.Error } else { $data.ACLs.DangerousACEs = @($data.ACLs.DangerousACEs) + @($fd.DangerousACEs) $data.ACLs.FullDomainScanned = $true $data.ACLs.FullDomainObjectsScanned = $fd.ObjectsScanned $data.ACLs.FullDomainTruncated = $fd.Truncated if (-not $Quiet) { $detail = "$($fd.ObjectsScanned) objects, $(@($fd.DangerousACEs).Count) dangerous ACE(s)$(if ($fd.Truncated) { ' (TRUNCATED at cap — coverage incomplete)' })" Write-ProgressLine -Phase RECON -Message 'Full-domain ACL sweep complete' -Detail $detail } } } catch { Write-Warning "Full-domain ACL sweep failed: $_" $data.Errors['FullDomainAcl'] = $_.Exception.Message } } # Derive DCSync principals from the domain-root DACL so ADPRIV-028 (DCSync rights) # lights up — the data is in ACLs.DangerousACEs but ADPRIV-028 reads a top-level # DCSyncAccounts field. Only set it when ACL data was actually collected, so a # failed ACL collection leaves it unset and ADPRIV-028 SKIPs (not a false PASS). if ($data.ACLs -and $null -ne $data.ACLs.DangerousACEs) { $dcSyncGuids = @( '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2' # DS-Replication-Get-Changes '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2' # DS-Replication-Get-Changes-All '89e95b76-444d-4c62-991a-0facbeda640c' # DS-Replication-Get-Changes-In-Filtered-Set ) $dcSyncAces = @($data.ACLs.DangerousACEs | Where-Object { ($_.ObjectTypeGUID -and $_.ObjectTypeGUID -in $dcSyncGuids) -or ($_.ObjectType -and $_.ObjectType -match 'DS-Replication-Get-Changes') } | Where-Object { -not (Test-SafeAdminSid -Sid $_.IdentitySID -IdentityReference $_.IdentityReference) }) $byIdentity = @{} foreach ($ace in $dcSyncAces) { $id = $ace.IdentityReference ?? $ace.IdentitySID if (-not $id) { continue } if (-not $byIdentity.ContainsKey($id)) { $byIdentity[$id] = [System.Collections.Generic.List[string]]::new() } $rn = $ace.ObjectType ?? $ace.ObjectTypeGUID ?? 'ExtendedRight' if (-not $byIdentity[$id].Contains($rn)) { [void]$byIdentity[$id].Add($rn) } } $data.DCSyncAccounts = @($byIdentity.Keys | ForEach-Object { @{ SamAccountName = $_; Rights = @($byIdentity[$_]) } }) } } # ── 9. Group Policy Objects ────────────────────────────────────────── if (& $needsSource 'GroupPolicyObjects') { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Collecting Group Policy Objects' } try { $data.GroupPolicies = Get-ADGroupPolicyObjects -Connection $Connection -Quiet:$Quiet } catch { Write-Warning "Failed to collect Group Policy Objects: $_" $data.Errors['GroupPolicyObjects'] = $_.Exception.Message } } # ── 10. Logon Scripts ──────────────────────────────────────────────── if (& $needsSource 'LogonScripts') { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Analyzing logon scripts' } try { $data.LogonScripts = Get-ADLogonScripts -Connection $Connection -Quiet:$Quiet } catch { Write-Warning "Failed to analyze logon scripts: $_" $data.Errors['LogonScripts'] = $_.Exception.Message } } # ── 11. Certificate Services (AD CS) ───────────────────────────────── if (& $needsSource 'CertificateServices') { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Enumerating AD Certificate Services' } try { $data.CertificateServices = Get-ADCertificateServices -Connection $Connection -Quiet:$Quiet } catch { Write-Warning "Failed to enumerate Certificate Services: $_" $data.Errors['CertificateServices'] = $_.Exception.Message } } # ── 12. Network policy (relay-precondition surface) ────────────────── if (& $needsSource 'NetworkConfig') { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Reading network-layer policy from SYSVOL' } try { $data.Network = Get-ADNetworkConfig -Connection $Connection -Quiet:$Quiet } catch { Write-Warning "Failed to read network policy from SYSVOL: $_" $data.Errors['NetworkConfig'] = $_.Exception.Message } } # ── 13. Tier-Zero signals (MSOL_ accounts, hybrid identity surface) ── if (& $needsSource 'TierZeroSignals') { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Scanning for Tier-0 hybrid-identity signals' } try { $data.TierZero = Get-ADTierZeroSignals -Connection $Connection -Quiet:$Quiet } catch { Write-Warning "Tier-Zero signal collection failed: $_" $data.Errors['TierZeroSignals'] = $_.Exception.Message } } # ── 14. Tradecraft signals (cpassword, DCShadow, BitLocker, RODC) ──── if (& $needsSource 'TradecraftSignals') { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Scanning for adversary-tradecraft signals' } try { $data.Tradecraft = Get-ADTradecraftSignals -Connection $Connection -Quiet:$Quiet } catch { Write-Warning "Tradecraft signal collection failed: $_" $data.Errors['TradecraftSignals'] = $_.Exception.Message } } # ── 15. Stale Objects ──────────────────────────────────────────────── if (& $needsSource 'StaleObjects') { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Identifying stale and abandoned objects' } try { $data.StaleObjects = Get-ADStaleObjects ` -Connection $Connection ` -InactiveDays $InactiveDays ` -PasswordAgeDays $PasswordAgeDays ` -Quiet:$Quiet } catch { Write-Warning "Failed to identify stale objects: $_" $data.Errors['StaleObjects'] = $_.Exception.Message } } # ── 16. Replication health (ADDOM-007) ─────────────────────────────── # Only attempt if domain info was collected (we merge into $data.Domain). if ((& $needsSource 'DomainInfo') -and $data.Domain) { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Assessing AD replication health' } try { $dcCount = if ($null -ne $data.DomainControllers) { @($data.DomainControllers).Count } else { 0 } $replHealth = Get-ADReplicationHealth -Connection $Connection -DomainControllerCount $dcCount -Quiet:$Quiet # $null means "not assessable" — leave it so ADDOM-007 SKIPs honestly. if ($null -ne $replHealth) { $data.Domain.ReplicationHealth = $replHealth } } catch { Write-Warning "Replication health collection failed: $_" $data.Errors['ReplicationHealth'] = $_.Exception.Message } } # ── 17. User Rights Assignment on DCs (ADPRIV-026/027) ──────────────── # Parses the Domain Controllers OU GPO security template (GptTmpl.inf). if (& $needsSource 'PrivilegedMembers') { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Parsing DC-OU User Rights Assignment from SYSVOL' } try { # If the GroupPolicies collector found GPOs linked to the DC OU, # pass their GUIDs so additional templates get parsed. $uraConn = $Connection.Clone() if ($data.GroupPolicies -and $data.GroupPolicies.ContainsKey('DCOULinkedGpoGuids')) { $uraConn['DCOULinkedGpoGuids'] = @($data.GroupPolicies.DCOULinkedGpoGuids) } $ura = Get-ADUserRightsAssignment -Connection $uraConn -Quiet:$Quiet if ($null -ne $ura) { if (-not $data.PrivilegedAccounts) { $data.PrivilegedAccounts = @{} } $data.PrivilegedAccounts['UserRightsAssignment'] = $ura } } catch { Write-Warning "User Rights Assignment collection failed: $_" $data.Errors['UserRightsAssignment'] = $_.Exception.Message } } # ── 18. NT-hash quality via DSInternals (ADPWD-010/011, ADPRIV-016) ─── # Only when the password-hash analysis is in scope (PasswordPolicies or # PrivilegedMembers) AND DSInternals is available. On any failure the # collector returns null fields so the dependent checks SKIP. $hashScope = (& $needsSource 'PasswordPolicies') -or (& $needsSource 'PrivilegedMembers') if ($hashScope -and $data.ModuleAvailability -and $data.ModuleAvailability.DSInternals) { if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Analyzing NT password-hash quality (DSInternals)' } try { # Build the privileged SamAccountName allow-set so the collector can # compute the ADPRIV-016 privileged subset (names only — no hashes). $privSamNames = @() if ($data.PrivilegedAccounts -and $data.PrivilegedAccounts.AllPrivilegedUsers) { $privSamNames = @($data.PrivilegedAccounts.AllPrivilegedUsers | ForEach-Object { $_.SamAccountName } | Where-Object { $_ }) } $hashConn = $Connection.Clone() $hashConn['PrivilegedSamNames'] = $privSamNames $hashParams = @{ Connection = $hashConn; Quiet = $Quiet } if ($WeakPasswordList) { $hashParams['WeakPasswordHashesFile'] = $WeakPasswordList } $hq = Get-ADPasswordHashQuality @hashParams if ($null -ne $hq -and $hq.Performed) { if (-not $data.PasswordPolicies) { $data.PasswordPolicies = @{} } # Only the no-dataset-required fields are always set; the rest stay # $null (Not Assessed) unless a dataset was supplied. $data.PasswordPolicies['BlankPasswordUsers'] = $hq.BlankPasswordUsers $data.PasswordPolicies['DuplicateHashGroups'] = $hq.DuplicateHashGroups if ($null -ne $hq.HIBPCompromisedUsers) { $data.PasswordPolicies['HIBPCompromisedUsers'] = $hq.HIBPCompromisedUsers } if ($null -ne $hq.DictionaryMatchUsers) { $data.PasswordPolicies['DictionaryMatchUsers'] = $hq.DictionaryMatchUsers } if ($null -ne $hq.CommonPasswordUsers) { $data.PasswordPolicies['CommonPasswordUsers'] = $hq.CommonPasswordUsers } # Privileged weak-password subset for ADPRIV-016. if ($null -ne $hq.PasswordAnalysis) { if (-not $data.PrivilegedAccounts) { $data.PrivilegedAccounts = @{} } $data.PrivilegedAccounts['PasswordAnalysis'] = $hq.PasswordAnalysis $data.PasswordAnalysis = $hq.PasswordAnalysis } } elseif ($null -ne $hq -and $hq.Error) { # Record why analysis did not run; fields stay $null → checks SKIP. $data.Errors['PasswordHashQuality'] = $hq.Error } } catch { Write-Warning "NT password-hash analysis failed: $_" $data.Errors['PasswordHashQuality'] = $_.Exception.Message } } # ── Summary ────────────────────────────────────────────────────────── if (-not $Quiet) { $collectedCount = 0 $nullKeys = [System.Collections.Generic.List[string]]::new() foreach ($key in @('Domain', 'DomainControllers', 'Trusts', 'PrivilegedAccounts', 'PasswordPolicies', 'Kerberos', 'ACLs', 'GroupPolicies', 'LogonScripts', 'CertificateServices', 'StaleObjects', 'Network', 'TierZero', 'Tradecraft')) { if ($null -ne $data[$key]) { $collectedCount++ } elseif ($requiredSources.Count -gt 0) { # Only track as missing if it was actually requested $sourceMapping = @{ Domain = 'DomainInfo' DomainControllers = 'DomainControllers' Trusts = 'TrustRelationships' PrivilegedAccounts = 'PrivilegedMembers' PasswordPolicies = 'PasswordPolicies' Kerberos = 'KerberosConfig' ACLs = 'ObjectACLs' GroupPolicies = 'GroupPolicyObjects' LogonScripts = 'LogonScripts' CertificateServices = 'CertificateServices' StaleObjects = 'StaleObjects' Network = 'NetworkConfig' TierZero = 'TierZeroSignals' Tradecraft = 'TradecraftSignals' } if ($sourceMapping.ContainsKey($key) -and $requiredSources.Contains($sourceMapping[$key])) { $nullKeys.Add($key) } } } $errorCount = $data.Errors.Count $domainName = if ($data.Domain) { $data.Domain.DomainName } else { 'unknown' } $summary = "Reconnaissance complete for $domainName`: $collectedCount data source(s) collected" if ($errorCount -gt 0) { $summary += ", $errorCount error(s)" } if ($nullKeys.Count -gt 0) { $summary += " (missing: $($nullKeys -join ', '))" } Write-ProgressLine -Phase RECON -Message $summary } return $data } |