Public/Export-BloodHoundData.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 Export-BloodHoundData { <# .SYNOPSIS Exports PSGuerrilla's collected Active Directory graph to a BloodHound OpenGraph file. .DESCRIPTION Turns the privileged-group membership and dangerous-ACL data PSGuerrilla already collects into a BloodHound CE OpenGraph import (nodes + edges) so an operator can pathfind the full attack surface in BloodHound. Edges use BloodHound's NATIVE kinds (GenericAll, WriteDacl, WriteOwner, GenericWrite, AllExtendedRights, GetChanges, GetChangesAll, MemberOf), and nodes carry their SID as objectid, so the import overlays cleanly with native SharpHound data and BloodHound's built-in path queries work over it. Unlike the in-report attack-path engine, this export includes the FULL graph (it does NOT drop by-design / default principals) — BloodHound performs its own reachability analysis, so it needs the complete edge set. Output is the OpenGraph shape: { "metadata": { "source_kind": "PSGuerrilla" }, "graph": { "nodes": [ { id, kinds, properties } ], "edges": [ { start:{value}, end:{value}, kind, properties } ] } } Note: depth/coverage tracks PSGuerrilla's ACL collection — today the six critical Tier-0 objects plus privileged-group membership. The full-domain ACL collector (roadmap) widens the exported edge set; this exporter consumes it unchanged. .PARAMETER AuditData The collected reconnaissance data (hashtable with ACLs.DangerousACEs and PrivilegedAccounts.PrivilegedGroups). .PARAMETER OutputPath Destination .json path. Default: ./PSGuerrilla-BloodHound.json. .EXAMPLE Export-BloodHoundData -AuditData $reconData -OutputPath .\corp-bh.json # Then in BloodHound CE: Administration > File Ingest > upload corp-bh.json #> [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$AuditData, [string]$OutputPath = (Join-Path (Get-Location) 'PSGuerrilla-BloodHound.json') ) $nodes = @{} # id -> node object (dedup by id) $edges = [System.Collections.Generic.List[object]]::new() $edgeSeen = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) $idFor = { param($name, $sid) if ($sid) { "$sid".ToUpper() } else { ("NAME:" + ("$name").Trim().ToUpper()) } } $kindForObject = { param($objClass, $objName) $oc = "$objClass".ToLower(); $on = "$objName".ToLower() if ($oc -match 'group') { return 'Group' } if ($oc -match 'computer') { return 'Computer' } if ($oc -match 'user') { return 'User' } if ($oc -match 'organizationalunit' -or $on -match ' ou$|controllers ou') { return 'OU' } if ($oc -match 'grouppolicy' -or $on -match 'gpo|policies') { return 'GPO' } if ($on -eq 'domain root' -or $oc -match 'domaindns') { return 'Domain' } return 'Base' } $kindForMember = { param($isGroup, $sam) if ($isGroup) { 'Group' } elseif ("$sam" -match '\$$') { 'Computer' } else { 'User' } } $addNode = { param($id, $name, $kind, $sid) if (-not $id) { return } if (-not $nodes.ContainsKey($id)) { $props = @{ name = ("$name").ToUpper() } if ($sid) { $props['objectid'] = "$sid".ToUpper() } $nodes[$id] = [PSCustomObject]@{ id = $id; kinds = @($kind, 'Base'); properties = $props } } } $addEdge = { param($startId, $endId, $kind) if (-not $startId -or -not $endId -or $startId -eq $endId) { return } $k = "$startId|$endId|$kind" if (-not $edgeSeen.Add($k)) { return } $edges.Add([PSCustomObject]@{ start = @{ value = $startId } end = @{ value = $endId } kind = $kind properties = @{ source = 'PSGuerrilla' } }) } # ── Membership edges (member -> group, kind MemberOf) ── if ($AuditData.PrivilegedAccounts -and $AuditData.PrivilegedAccounts.PrivilegedGroups) { foreach ($entry in $AuditData.PrivilegedAccounts.PrivilegedGroups.GetEnumerator()) { $gName = "$($entry.Key)" $gId = (& $idFor $gName $null) # group SID usually not on the key; key by name & $addNode $gId $gName 'Group' $null foreach ($m in @($entry.Value)) { $sam = "$($m.SamAccountName)" if (-not $sam) { continue } $mSid = "$($m.SID)" $mId = (& $idFor $sam $mSid) & $addNode $mId $sam (& $kindForMember $m.IsGroup $sam) $mSid & $addEdge $mId $gId 'MemberOf' } } } # ── Control edges from dangerous ACEs (principal -> object) ── $acl = $AuditData.ACLs $haveAcl = [bool]($acl -and (-not ($acl -is [System.Collections.IDictionary]) -or $acl.Contains('DangerousACEs'))) if ($haveAcl) { foreach ($ace in @($acl.DangerousACEs)) { $principal = "$($ace.IdentityReference)" $pSid = "$($ace.IdentitySID)" if (-not $principal -and -not $pSid) { continue } $pName = ($principal -split '\\')[-1] $pId = (& $idFor $pName $pSid) & $addNode $pId $pName 'Base' $pSid $objName = "$($ace.ObjectName)" $objSid = "$($ace.ObjectSID)" $oId = (& $idFor $objName $objSid) & $addNode $oId $objName (& $kindForObject $ace.ObjectClass $objName) $objSid # Map the AD right to a native BloodHound edge kind. $r = "$($ace.ActiveDirectoryRights)"; $ot = "$($ace.ObjectType)" $kind = if ($ot -match '1131f6ad' -or $r -match 'Get-Changes-All|GetChangesAll') { 'GetChangesAll' } elseif ($ot -match '1131f6aa' -or $r -match 'Get-Changes|GetChanges') { 'GetChanges' } elseif ($r -match '(?i)GenericAll') { 'GenericAll' } elseif ($r -match '(?i)WriteDacl') { 'WriteDacl' } elseif ($r -match '(?i)WriteOwner') { 'WriteOwner' } elseif ($r -match '(?i)GenericWrite') { 'GenericWrite' } elseif ($r -match '(?i)ExtendedRight') { 'AllExtendedRights' } elseif ($r -match '(?i)WriteProperty') { 'GenericWrite' } else { 'GenericAll' } & $addEdge $pId $oId $kind } } $payload = [PSCustomObject]@{ metadata = @{ source_kind = 'PSGuerrilla'; generated_by = 'PSGuerrilla'; version = 1 } graph = [PSCustomObject]@{ nodes = @($nodes.Values) edges = @($edges) } } $dir = Split-Path -Parent $OutputPath if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } $payload | ConvertTo-Json -Depth 8 | Set-Content -Path $OutputPath -Encoding UTF8 return [PSCustomObject]@{ PSTypeName = 'PSGuerrilla.BloodHoundExport' Path = $OutputPath NodeCount = @($nodes.Values).Count EdgeCount = @($edges).Count Format = 'BloodHound OpenGraph' Message = "BloodHound OpenGraph written to $OutputPath ($(@($nodes.Values).Count) nodes, $(@($edges).Count) edges). Import via BloodHound CE > Administration > File Ingest." } } |