Merge pull request #121 from BloodHoundAD/dev

Re-merge new PowerShell collection features
master
Andy Robbins 2017-09-06 13:15:37 -07:00 committed by GitHub
commit 5dc20a14ea
1 changed files with 402 additions and 130 deletions

View File

@ -4722,7 +4722,7 @@ function Invoke-BloodHound {
.PARAMETER CollectionMethod
The method to collect data. 'Group', 'ComputerOnly', 'LocalGroup', 'GPOLocalGroup', 'Session', 'LoggedOn', 'Trusts, 'Stealth', or 'Default'.
The method to collect data. 'Group', 'Containers', 'ComputerOnly', 'LocalGroup', 'GPOLocalGroup', 'Session', 'LoggedOn', 'Trusts, 'Stealth', or 'Default'.
'Stealth' uses 'Group' collection, stealth user hunting ('Session' on certain servers), 'GPOLocalGroup' enumeration, and trust enumeration.
'Default' uses 'Group' collection, regular user hunting with 'Session'/'LoggedOn', 'LocalGroup' enumeration, and 'Trusts' enumeration.
'ComputerOnly' only enumerates computers, not groups/trusts, and executes local admin/session/loggedon on each.
@ -4811,7 +4811,7 @@ function Invoke-BloodHound {
$DomainController,
[String]
[ValidateSet('Group', 'ACLs', 'ComputerOnly', 'LocalGroup', 'GPOLocalGroup', 'Session', 'LoggedOn', 'Stealth', 'Trusts', 'Default')]
[ValidateSet('Group', 'Containers', 'ACLs', 'ComputerOnly', 'LocalGroup', 'GPOLocalGroup', 'Session', 'LoggedOn', 'Stealth', 'Trusts', 'Default')]
$CollectionMethod = 'Default',
[Switch]
@ -4856,6 +4856,7 @@ function Invoke-BloodHound {
Switch ($CollectionMethod) {
'Group' { $UseGroup = $True; $SkipComputerEnumeration = $True; $SkipGCDeconfliction2 = $True }
'Containers' { $UseContainers = $True; $SkipComputerEnumeration = $True; $SkipGCDeconfliction2 = $True }
'ACLs' { $UseGroup = $False; $SkipComputerEnumeration = $True; $SkipGCDeconfliction2 = $True; $UseACLs = $True }
'ComputerOnly' { $UseGroup = $False; $UseLocalGroup = $True; $UseSession = $True; $UseLoggedOn = $True; $SkipGCDeconfliction2 = $False }
'LocalGroup' { $UseLocalGroup = $True; $SkipGCDeconfliction2 = $True }
@ -4865,6 +4866,7 @@ function Invoke-BloodHound {
'Trusts' { $UseDomainTrusts = $True; $SkipComputerEnumeration = $True; $SkipGCDeconfliction2 = $True }
'Stealth' {
$UseGroup = $True
$UseContainers = $True
$UseGPOGroup = $True
$UseSession = $True
$UseDomainTrusts = $True
@ -4872,6 +4874,7 @@ function Invoke-BloodHound {
}
'Default' {
$UseGroup = $True
$UseContainers = $True
$UseLocalGroup = $True
$UseSession = $True
$UseLoggedOn = $False
@ -4933,6 +4936,28 @@ function Invoke-BloodHound {
}
}
if($UseContainers) {
$ContainerPath = "$OutputFolder\$($CSVExportPrefix)container_structure.csv"
$Exists = [System.IO.File]::Exists($ContainerPath)
$ContainerFileStream = New-Object IO.FileStream($ContainerPath, [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write, [IO.FileShare]::Read)
$ContainerWriter = New-Object System.IO.StreamWriter($ContainerFileStream)
$ContainerWriter.AutoFlush = $True
if (-not $Exists) {
# add the header if the file doesn't already exist
$ContainerWriter.WriteLine('"ContainerType","ContainerName","ContainerGUID","ContainerBlocksInheritence","ObjectType","ObjectName","ObjectGUIDorSID"')
}
$GPLinkPath = "$OutputFolder\$($CSVExportPrefix)container_gplinks.csv"
$Exists = [System.IO.File]::Exists($GPLinkPath)
$GPLinkFileStream = New-Object IO.FileStream($GPLinkPath, [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write, [IO.FileShare]::Read)
$GPLinkWriter = New-Object System.IO.StreamWriter($GPLinkFileStream)
$GPLinkWriter.AutoFlush = $True
if (-not $Exists) {
# add the header if the file doesn't already exist
$GPLinkWriter.WriteLine('"ObjectType","ObjectName","ObjectGUID","GPODisplayName","GPOGUID","IsEnforced"')
}
}
if($UseACLs) {
$ACLPath = "$OutputFolder\$($CSVExportPrefix)acls.csv"
$Exists = [System.IO.File]::Exists($ACLPath)
@ -4941,7 +4966,7 @@ function Invoke-BloodHound {
$ACLWriter.AutoFlush = $True
if (-not $Exists) {
# add the header if the file doesn't already exist
$ACLWriter.WriteLine('"ObjectName","ObjectType","PrincipalName","PrincipalType","ActiveDirectoryRights","ACEType","AccessControlType","IsInherited"')
$ACLWriter.WriteLine('"ObjectName","ObjectType","ObjectGuid","PrincipalName","PrincipalType","ActiveDirectoryRights","ACEType","AccessControlType","IsInherited"')
}
}
@ -5244,6 +5269,159 @@ function Invoke-BloodHound {
[GC]::Collect()
}
if ($UseContainers -and $TargetDomains) {
ForEach ($TargetDomain in $TargetDomains) {
Write-Verbose "Enumerating container memberships and gpLinks for domain: $TargetDomain"
$OUs = New-Object System.Collections.Queue
# first get a cached listing of all GPO GUIDs -> display names
# GPODisplayName,GPOGUID,IsEnforced,ObjectType,ObjectName,ObjectGUID
$GPOSearcher = Get-DomainSearcher -Domain $TargetDomain -DomainController $DomainController
$GPOSearcher.filter="(&(objectCategory=groupPolicyContainer)(name=*)(gpcfilesyspath=*))"
$GPOSearcher.PropertiesToLoad.AddRange(('displayname', 'name'))
$GPOs = @{}
ForEach($GPOResult in $GPOSearcher.FindAll()) {
$GPOdisplayName = $GPOResult.Properties['displayname'][0]
$GPOname = $GPOResult.Properties['name'][0]
$GPOName = $GPOName.Substring(1, $GPOName.Length-2)
$GPOs[$GPOname] = $GPOdisplayName
}
# now get the base domain object and enumerate any GPLinks
$DomainSearcher = Get-DomainSearcher -Domain $TargetDomain -DomainController $DomainController
$DomainSearcher.SearchScope = 'Base'
$Null = $DomainSearcher.PropertiesToLoad.AddRange(('gplink', 'objectguid'))
$DomainObject = $DomainSearcher.FindOne()
$DomainGUID = (New-Object Guid (,$DomainObject.Properties['objectguid'][0])).Guid
if ($DomainObject.Properties['gplink']) {
$DomainObject.Properties['gplink'][0].split('][') | ForEach-Object {
if ($_.startswith('LDAP')) {
$Parts = $_.split(';')
$GPODN = $Parts[0]
if ($Parts[1] -eq 2) { $Enforced = $True }
else { $Enforced = $False }
$i = $GPODN.IndexOf("CN=")+4
$GPOName = $GPODN.subString($i, $i+25)
$GPODisplayName = $GPOs[$GPOname]
$GPLinkWriter.WriteLine("`"domain`",`"$TargetDomain`",`"$DomainGUID`",`"$GPODisplayName`",`"$GPOName`",`"$Enforced`"")
}
}
}
# find any non-ou containers and enumerate the users/computers contained in them
# example -> CN=Computers,DC=testlab,DC=local
$DomainSearcher.SearchScope = 'OneLevel'
$Null = $DomainSearcher.PropertiesToLoad.AddRange(('name'))
$DomainSearcher.Filter = "(objectClass=container)"
$DomainSearcher.FindAll() | ForEach-Object {
$ContainerName = ,$_.Properties['name'][0]
$ContainerPath = $_.Properties['adspath']
Write-Verbose "ContainerPath: $ContainerPath"
$ContainerSearcher = Get-DomainSearcher -ADSpath $ContainerPath
$Null = $ContainerSearcher.PropertiesToLoad.AddRange(('name', 'objectsid', 'samaccounttype'))
$ContainerSearcher.Filter = '(|(samAccountType=805306368)(samAccountType=805306369))'
$ContainerSearcher.SearchScope = 'SubTree'
$ContainerSearcher.FindAll() | ForEach-Object {
$ObjectName = ,$_.Properties['name'][0]
Write-Verbose "ObjectName: $ObjectName"
if ( (,$_.Properties['samaccounttype'][0]) -eq '805306368') {
$ObjectType = 'user'
}
else {
$ObjectType = 'computer'
}
$ObjectSID = (New-Object System.Security.Principal.SecurityIdentifier($_.Properties['objectsid'][0],0)).Value
$ContainerWriter.WriteLine("`"domain`",`"$TargetDomain`",`"$DomainGUID`",`"$False`",`"$ObjectType`",`"$ObjectName`",`"$ObjectSID`"")
}
$ContainerSearcher.Dispose()
}
# now enumerate all OUs that are on the "base" domain level
$DomainSearcher.SearchScope = 'OneLevel'
$Null = $DomainSearcher.PropertiesToLoad.AddRange(('name', 'objectguid', 'gplink'))
$DomainSearcher.Filter = "(objectCategory=organizationalUnit)"
$DomainSearcher.FindAll() | ForEach-Object {
$OUGuid = (New-Object Guid (,$_.Properties['objectguid'][0])).Guid
$OUName = ,$_.Properties['name'][0]
$ContainerWriter.WriteLine("`"domain`",`"$TargetDomain`",`"$DomainGUID`",`"$False`",`"ou`",`"$OUName`",`"$OUGuid`"")
$OUs.Enqueue($_.Properties['adspath'])
}
$DomainSearcher.Dispose()
while ($OUs.Count -gt 0) {
# pop a new OU ADSpath from the queue
$ADSPath = $OUs.Dequeue()
Write-Verbose "Enumerating OU: '$ADSPath'"
# grab the OU base object first to pull ContainerBlocksInheritence from gpoptions
$DomainSearcher = Get-DomainSearcher -ADSpath $ADSPath
$Null = $DomainSearcher.PropertiesToLoad.AddRange(('name', 'objectguid', 'gplink', 'gpoptions'))
$DomainSearcher.SearchScope = 'Base'
$OU = $DomainSearcher.FindOne()
$OUGuid = (New-Object Guid (,$OU.Properties['objectguid'][0])).Guid
$OUName = ,$OU.Properties['name'][0]
$ContainerBlocksInheritence = $False
if ($OU.Properties['gpoptions'] -and ($OU.Properties['gpoptions'] -eq 1)) {
$ContainerBlocksInheritence = $True
}
# parse any gpLinks if this OU currently has any
if ($OU.Properties['gplink'] -and $OU.Properties['gplink'][0]) {
$OU.Properties['gplink'][0].split('][') | ForEach-Object {
if ($_.startswith('LDAP')) {
$Parts = $_.split(';')
$GPODN = $Parts[0]
if ($Parts[1] -eq 2) { $Enforced = $True }
else { $Enforced = $False }
$i = $GPODN.IndexOf('CN=', [System.StringComparison]::CurrentCultureIgnoreCase)+4
$GPOName = $GPODN.SubString($i, $i+25)
$GPODisplayName = $GPOs[$GPOname]
$GPLinkWriter.WriteLine("`"ou`",`"$OUName`",`"$OUGuid`",`"$GPODisplayName`",`"$GPOName`",`"$Enforced`"")
}
}
}
# now enumerate all computers, users, and OUs in the next level
$Null = $DomainSearcher.PropertiesToLoad.AddRange(('name', 'objectsid', 'objectguid', 'gplink', 'gpoptions', 'objectclass'))
$DomainSearcher.Filter = '(|(samAccountType=805306368)(samAccountType=805306369)(objectclass=organizationalUnit))'
$DomainSearcher.SearchScope = 'OneLevel'
$DomainSearcher.FindAll() | ForEach-Object {
if ($_.Properties['objectclass'] -contains 'organizationalUnit') {
$SubOUName = ,$_.Properties['name'][0]
$SubOUGuid = (New-Object Guid (,$_.Properties['objectguid'][0])).Guid
$ContainerWriter.WriteLine("`"ou`",`"$OUName`",`"$OUGuid`",`"$ContainerBlocksInheritence`",`"ou`",`"$SubOUName`",`"$SubOUGuid`"")
$OUs.Enqueue($_.Properties['adspath'])
}
elseif ($_.Properties['objectclass'] -contains 'computer') {
$SubComputerName = ,$_.Properties['name'][0]
$SubComputerSID = (New-Object System.Security.Principal.SecurityIdentifier($_.Properties['objectsid'][0],0)).Value
$ContainerWriter.WriteLine("`"ou`",`"$OUName`",`"$OUGuid`",`"$ContainerBlocksInheritence`",`"computer`",`"$SubComputerName`",`"$SubComputerSID`"")
}
else {
$SubUserName = ,$_.Properties['name'][0]
$SubUserSID = (New-Object System.Security.Principal.SecurityIdentifier($_.Properties['objectsid'][0],0)).Value
$ContainerWriter.WriteLine("`"ou`",`"$OUName`",`"$OUGuid`",`"$ContainerBlocksInheritence`",`"user`",`"$SubUserName`",`"$SubUserSID`"")
}
}
$DomainSearcher.Dispose()
}
Write-Verbose "Done with container memberships and gpLink enumeration for domain: $TargetDomain"
}
[GC]::Collect()
}
if($UseACLs -and $TargetDomains) {
# $PrincipalMapping format -> @{ PrincipalSID : @(PrincipalSimpleName, PrincipalObjectClass) }
@ -5320,165 +5498,252 @@ function Invoke-BloodHound {
Write-Verbose "Enumerating ACLs for objects in domain: $TargetDomain"
$ObjectSearcher = Get-DomainSearcher -Domain $TargetDomain -DomainController $DomainController -ADSPath $UserADSpath
$ObjectSearcher.SecurityMasks = [System.DirectoryServices.SecurityMasks]::Dacl
$ObjectSearcher.SecurityMasks = [System.DirectoryServices.SecurityMasks]'Dacl,Owner'
# only enumerate user and group objects (for now)
# enumerate user, computer, group, and GPO objects
# 805306368 -> user
# 805306369 -> computer
# 268435456|268435457|536870912|536870913 -> groups
$ObjectSearcher.Filter = '(|(samAccountType=805306368)(samAccountType=805306369)(samAccountType=268435456)(samAccountType=268435457)(samAccountType=536870912)(samAccountType=536870913))'
$ObjectSearcher.PropertiesToLoad.AddRange(('distinguishedName','samaccountname','dnshostname','objectclass','objectsid','name', 'ntsecuritydescriptor'))
# (objectCategory=groupPolicyContainer) -> GPOs
$ObjectSearcher.Filter = '(|(samAccountType=805306368)(samAccountType=805306369)(samAccountType=268435456)(samAccountType=268435457)(samAccountType=536870912)(samAccountType=536870913)(objectCategory=groupPolicyContainer))'
$ObjectSearcher.PropertiesToLoad.AddRange(('distinguishedName','samaccountname','dnshostname','displayname','objectclass','objectsid','name','ntsecuritydescriptor'))
$ObjectSearcher.FindAll() | ForEach-Object {
$Object = $_.Properties
if($Object -and $Object.distinguishedname -and $Object.distinguishedname[0] -and $Object.objectsid -and $Object.objectsid[0]) {
if($Object -and $Object.distinguishedname -and $Object.distinguishedname[0]) {
$DN = $Object.distinguishedname[0]
$ObjectDomain = $DN.SubString($DN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
$ObjectName, $ObjectADType, $ObjectGuid = $Null
if ($Object.objectclass.contains('computer')) {
$ObjectADType = 'COMPUTER'
if ($Object.dnshostname) {
$ObjectName = $Object.dnshostname[0]
}
}
elseif ($Object.objectclass.contains('groupPolicyContainer')) {
$ObjectADType = 'GPO'
$ObjectGuid = $Object.name[0].trim('{}')
$ObjectDisplayName = $Object.displayname[0]
$ObjectName = "$ObjectDisplayName@$ObjectDomain"
}
else {
if($Object.samaccountname) {
$ObjectSamAccountName = $Object.samaccountname[0]
}
else {
$ObjectSamAccountName = $Object.name[0]
}
$ObjectName = "$ObjectSamAccountName@$ObjectDomain"
$ObjectSid = (New-Object System.Security.Principal.SecurityIdentifier($Object.objectsid[0],0)).Value
if ($Object.objectclass.contains('group')) {
$ObjectADType = 'GROUP'
}
elseif ($Object.objectclass.contains('user')) {
$ObjectADType = 'USER'
}
else {
$ObjectADType = 'OTHER'
}
}
try {
# parse the 'ntsecuritydescriptor' field returned
New-Object -TypeName Security.AccessControl.RawSecurityDescriptor -ArgumentList $Object['ntsecuritydescriptor'][0], 0 | Select-Object -Expand DiscretionaryAcl | ForEach-Object {
$Counter += 1
if($Counter % 10000 -eq 0) {
Write-Verbose "ACE counter: $Counter"
if($ACLWriter) {
$ACLWriter.Flush()
}
[GC]::Collect()
}
$RawActiveDirectoryRights = ([Enum]::ToObject([System.DirectoryServices.ActiveDirectoryRights], $_.AccessMask))
# check for the following rights:
# GenericAll - generic fully control of an object
# GenericWrite - write to any object properties
# WriteDacl - modify the permissions of the object
# WriteOwner - modify the owner of an object
# User-Force-Change-Password - extended attribute (00299570-246d-11d0-a768-00aa006e0529)
# WriteProperty/Self-Membership - modify group membership (bf9679c0-0de6-11d0-a285-00aa003049e2)
# WriteProperty/Script-Path - modify a user's script-path (bf9679a8-0de6-11d0-a285-00aa003049e2)
if (
( ($RawActiveDirectoryRights -match 'GenericAll|GenericWrite') -and (-not $_.ObjectAceType -or $_.ObjectAceType -eq '00000000-0000-0000-0000-000000000000') ) -or
($RawActiveDirectoryRights -match 'WriteDacl|WriteOwner') -or
( ($RawActiveDirectoryRights -match 'ExtendedRight') -and (-not $_.ObjectAceType -or $_.ObjectAceType -eq '00000000-0000-0000-0000-000000000000') ) -or
(($_.ObjectAceType -eq '00299570-246d-11d0-a768-00aa006e0529') -and ($RawActiveDirectoryRights -match 'ExtendedRight')) -or
(($_.ObjectAceType -eq 'bf9679c0-0de6-11d0-a285-00aa003049e2') -and ($RawActiveDirectoryRights -match 'WriteProperty')) -or
(($_.ObjectAceType -eq 'bf9679a8-0de6-11d0-a285-00aa003049e2') -and ($RawActiveDirectoryRights -match 'WriteProperty'))
) {
$PrincipalSid = $_.SecurityIdentifier.ToString()
$PrincipalSimpleName, $PrincipalObjectClass, $ACEType = $Null
# only grab the AD right names we care about
# 'GenericAll|GenericWrite|WriteOwner|WriteDacl'
$ActiveDirectoryRights = $ACLGeneralRightsRegex.Matches($RawActiveDirectoryRights) | Select-Object -ExpandProperty Value
if (-not $ActiveDirectoryRights) {
if ($RawActiveDirectoryRights -match 'ExtendedRight') {
$ActiveDirectoryRights = 'ExtendedRight'
}
else {
$ActiveDirectoryRights = 'WriteProperty'
}
# decode the ACE types here
$ACEType = Switch ($_.ObjectAceType) {
'00299570-246d-11d0-a768-00aa006e0529' {'User-Force-Change-Password'}
'bf9679c0-0de6-11d0-a285-00aa003049e2' {'Member'}
'bf9679a8-0de6-11d0-a285-00aa003049e2' {'Script-Path'}
Default {'All'}
if ($ObjectName -and $ObjectADType) {
try {
# parse the 'ntsecuritydescriptor' field returned
$SecDesc = New-Object -TypeName Security.AccessControl.RawSecurityDescriptor -ArgumentList $Object['ntsecuritydescriptor'][0], 0
$SecDesc| Select-Object -Expand DiscretionaryAcl | ForEach-Object {
$Counter += 1
if($Counter % 10000 -eq 0) {
Write-Verbose "ACE counter: $Counter"
if($ACLWriter) {
$ACLWriter.Flush()
}
[GC]::Collect()
}
if ($PrincipalMapping[$PrincipalSid]) {
# Write-Verbose "$PrincipalSid in cache!"
# $PrincipalMappings format -> @{ SID : @(PrincipalSimpleName, PrincipalObjectClass) }
$PrincipalSimpleName, $PrincipalObjectClass = $PrincipalMapping[$PrincipalSid]
}
elseif ($CommonSidMapping[$PrincipalSid]) {
# Write-Verbose "$PrincipalSid in common sids!"
$PrincipalName, $PrincipalObjectClass = $CommonSidMapping[$PrincipalSid]
$PrincipalSimpleName = "$PrincipalName@$TargetDomain"
$PrincipalMapping[$PrincipalSid] = $PrincipalSimpleName, $PrincipalObjectClass
}
else {
# Write-Verbose "$PrincipalSid NOT in cache!"
# first try querying the target domain for this SID
$SIDSearcher = Get-DomainSearcher -Domain $TargetDomain -DomainController $DomainController
$SIDSearcher.PropertiesToLoad.AddRange(('samaccountname','distinguishedname','dnshostname','objectclass'))
$SIDSearcher.Filter = "(objectsid=$PrincipalSid)"
$PrincipalObject = $SIDSearcher.FindOne()
$RawActiveDirectoryRights = ([Enum]::ToObject([System.DirectoryServices.ActiveDirectoryRights], $_.AccessMask))
if ((-not $PrincipalObject) -and ((-not $DomainController) -or (-not $DomainController.StartsWith('GC:')))) {
# if the object didn't resolve from the current domain, attempt to query the global catalog
$GCSearcher = Get-DomainSearcher -ADSpath $GCADSPath
$GCSearcher.PropertiesToLoad.AddRange(('samaccountname','distinguishedname','dnshostname','objectclass'))
$GCSearcher.Filter = "(objectsid=$PrincipalSid)"
$PrincipalObject = $GCSearcher.FindOne()
}
# check for the following rights:
# GenericAll - generic fully control of an object
# GenericWrite - write to any object properties
# WriteProperty/all - write to any object properties
# ExtendedRight/all - write to any object properties
# WriteDacl - modify the permissions of the object
# WriteOwner - modify the owner of an object
# ExtendedRight/User-Force-Change-Password - force reset a user's password (00299570-246d-11d0-a768-00aa006e0529)
# WriteProperty/Self-Membership - modify group membership (bf9679c0-0de6-11d0-a285-00aa003049e2)
# WriteProperty/Script-Path - modify a user's script-path (bf9679a8-0de6-11d0-a285-00aa003049e2)
# WriteProperty/GPC-File-Sys-Path - modify the files in a GPO's SYSVOL folder (f30e3bc1-9ff0-11d1-b603-0000f80367c1)
if (
( ($RawActiveDirectoryRights -match 'GenericAll|GenericWrite') -and (-not $_.ObjectAceType -or $_.ObjectAceType -eq '00000000-0000-0000-0000-000000000000') ) -or
( ($RawActiveDirectoryRights -match 'WriteProperty') -and (-not $_.ObjectAceType -or $_.ObjectAceType -eq '00000000-0000-0000-0000-000000000000') ) -or
( ($RawActiveDirectoryRights -match 'ExtendedRight') -and (-not $_.ObjectAceType -or $_.ObjectAceType -eq '00000000-0000-0000-0000-000000000000') ) -or
($RawActiveDirectoryRights -match 'WriteDacl|WriteOwner') -or
(($_.ObjectAceType -eq '00299570-246d-11d0-a768-00aa006e0529') -and ($RawActiveDirectoryRights -match 'ExtendedRight')) -or
(($_.ObjectAceType -eq 'bf9679c0-0de6-11d0-a285-00aa003049e2') -and ($RawActiveDirectoryRights -match 'WriteProperty')) -or
(($_.ObjectAceType -eq 'bf9679a8-0de6-11d0-a285-00aa003049e2') -and ($RawActiveDirectoryRights -match 'WriteProperty')) -or
(($_.ObjectAceType -eq 'f30e3bc1-9ff0-11d1-b603-0000f80367c1') -and ($RawActiveDirectoryRights -match 'WriteProperty'))
) {
if ($PrincipalObject) {
if ($PrincipalObject.Properties.objectclass.contains('computer')) {
$PrincipalObjectClass = 'COMPUTER'
$PrincipalSimpleName = $PrincipalObject.Properties.dnshostname[0]
$PrincipalSid = $_.SecurityIdentifier.ToString()
$PrincipalSimpleName, $PrincipalObjectClass, $ACEType = $Null
# only grab the AD right names we care about
# 'GenericAll|GenericWrite|WriteOwner|WriteDacl'
$ActiveDirectoryRights = $ACLGeneralRightsRegex.Matches($RawActiveDirectoryRights) | Select-Object -ExpandProperty Value
if (-not $ActiveDirectoryRights) {
if ($RawActiveDirectoryRights -match 'ExtendedRight') {
$ActiveDirectoryRights = 'ExtendedRight'
}
else {
$PrincipalSamAccountName = $PrincipalObject.Properties.samaccountname[0]
$PrincipalDN = $PrincipalObject.Properties.distinguishedname[0]
$PrincipalDomain = $PrincipalDN.SubString($PrincipalDN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
$PrincipalSimpleName = "$PrincipalSamAccountName@$PrincipalDomain"
$ActiveDirectoryRights = 'WriteProperty'
}
if ($PrincipalObject.Properties.objectclass.contains('group')) {
$PrincipalObjectClass = 'GROUP'
}
elseif ($PrincipalObject.Properties.objectclass.contains('user')) {
$PrincipalObjectClass = 'USER'
# decode the ACE types here
$ACEType = Switch ($_.ObjectAceType) {
'00299570-246d-11d0-a768-00aa006e0529' {'User-Force-Change-Password'}
'bf9679c0-0de6-11d0-a285-00aa003049e2' {'Member'}
'bf9679a8-0de6-11d0-a285-00aa003049e2' {'Script-Path'}
'f30e3bc1-9ff0-11d1-b603-0000f80367c1' {'GPC-File-Sys-Path'}
Default {'All'}
}
}
if ($PrincipalMapping[$PrincipalSid]) {
# $PrincipalMappings format -> @{ SID : @(PrincipalSimpleName, PrincipalObjectClass) }
$PrincipalSimpleName, $PrincipalObjectClass = $PrincipalMapping[$PrincipalSid]
}
elseif ($CommonSidMapping[$PrincipalSid]) {
$PrincipalName, $PrincipalObjectClass = $CommonSidMapping[$PrincipalSid]
$PrincipalSimpleName = "$PrincipalName@$TargetDomain"
$PrincipalMapping[$PrincipalSid] = $PrincipalSimpleName, $PrincipalObjectClass
}
else {
# first try querying the target domain for this SID
$SIDSearcher = Get-DomainSearcher -Domain $TargetDomain -DomainController $DomainController
$SIDSearcher.PropertiesToLoad.AddRange(('samaccountname','distinguishedname','dnshostname','objectclass'))
$SIDSearcher.Filter = "(objectsid=$PrincipalSid)"
$PrincipalObject = $SIDSearcher.FindOne()
if ((-not $PrincipalObject) -and ((-not $DomainController) -or (-not $DomainController.StartsWith('GC:')))) {
# if the object didn't resolve from the current domain, attempt to query the global catalog
$GCSearcher = Get-DomainSearcher -ADSpath $GCADSPath
$GCSearcher.PropertiesToLoad.AddRange(('samaccountname','distinguishedname','dnshostname','objectclass'))
$GCSearcher.Filter = "(objectsid=$PrincipalSid)"
$PrincipalObject = $GCSearcher.FindOne()
}
if ($PrincipalObject) {
if ($PrincipalObject.Properties.objectclass.contains('computer')) {
$PrincipalObjectClass = 'COMPUTER'
$PrincipalSimpleName = $PrincipalObject.Properties.dnshostname[0]
}
else {
$PrincipalObjectClass = 'OTHER'
$PrincipalSamAccountName = $PrincipalObject.Properties.samaccountname[0]
$PrincipalDN = $PrincipalObject.Properties.distinguishedname[0]
$PrincipalDomain = $PrincipalDN.SubString($PrincipalDN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
$PrincipalSimpleName = "$PrincipalSamAccountName@$PrincipalDomain"
if ($PrincipalObject.Properties.objectclass.contains('group')) {
$PrincipalObjectClass = 'GROUP'
}
elseif ($PrincipalObject.Properties.objectclass.contains('user')) {
$PrincipalObjectClass = 'USER'
}
else {
$PrincipalObjectClass = 'OTHER'
}
}
}
}
else {
Write-Verbose "SID not resolved: $PrincipalSid"
else {
Write-Verbose "SID not resolved: $PrincipalSid"
}
$PrincipalMapping[$PrincipalSid] = $PrincipalSimpleName, $PrincipalObjectClass
}
$PrincipalMapping[$PrincipalSid] = $PrincipalSimpleName, $PrincipalObjectClass
if ($PrincipalSimpleName -and $PrincipalObjectClass) {
if ($PSCmdlet.ParameterSetName -eq 'CSVExport') {
# "ObjectName","ObjectType","ObjectGuid","PrincipalName","PrincipalType","ActiveDirectoryRights","ACEType","AccessControlType","IsInherited"
$ACLWriter.WriteLine("`"$ObjectName`",`"$ObjectADType`",`"$ObjectGuid`",`"$PrincipalSimpleName`",`"$PrincipalObjectClass`",`"$ActiveDirectoryRights`",`"$ACEType`",`"$($_.AceQualifier)`",`"$($_.IsInherited)`"")
}
else {
Write-Warning 'TODO: implement neo4j RESTful API ingestion for ACLs!'
}
}
}
}
$SecDesc | Select-Object -Expand Owner | ForEach-Object {
# now extract out the object owner
$Counter += 1
if($Counter % 10000 -eq 0) {
Write-Verbose "ACE counter: $Counter"
if($ACLWriter) {
$ACLWriter.Flush()
}
[GC]::Collect()
}
if ($PrincipalSimpleName -and $PrincipalObjectClass) {
$ObjectName, $ObjectADType = $Null
if ($_ -and $_.Value) {
$PrincipalSid = $_.Value
$PrincipalSimpleName, $PrincipalObjectClass, $ACEType = $Null
if ($Object.objectclass.contains('computer')) {
$ObjectADType = 'COMPUTER'
if ($Object.dnshostname) {
$ObjectName = $Object.dnshostname[0]
}
if ($PrincipalMapping[$PrincipalSid]) {
# $PrincipalMappings format -> @{ SID : @(PrincipalSimpleName, PrincipalObjectClass) }
$PrincipalSimpleName, $PrincipalObjectClass = $PrincipalMapping[$PrincipalSid]
}
elseif ($CommonSidMapping[$PrincipalSid]) {
$PrincipalName, $PrincipalObjectClass = $CommonSidMapping[$PrincipalSid]
$PrincipalSimpleName = "$PrincipalName@$TargetDomain"
$PrincipalMapping[$PrincipalSid] = $PrincipalSimpleName, $PrincipalObjectClass
}
else {
if($Object.samaccountname) {
$ObjectSamAccountName = $Object.samaccountname[0]
}
else {
$ObjectSamAccountName = $Object.name[0]
}
$DN = $Object.distinguishedname[0]
$ObjectDomain = $DN.SubString($DN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
$ObjectName = "$ObjectSamAccountName@$ObjectDomain"
# first try querying the target domain for this SID
$SIDSearcher = Get-DomainSearcher -Domain $TargetDomain -DomainController $DomainController
$SIDSearcher.PropertiesToLoad.AddRange(('samaccountname','distinguishedname','dnshostname','objectclass'))
$SIDSearcher.Filter = "(objectsid=$PrincipalSid)"
$PrincipalObject = $SIDSearcher.FindOne()
if ($Object.objectclass.contains('group')) {
$ObjectADType = 'GROUP'
if ((-not $PrincipalObject) -and ((-not $DomainController) -or (-not $DomainController.StartsWith('GC:')))) {
# if the object didn't resolve from the current domain, attempt to query the global catalog
$GCSearcher = Get-DomainSearcher -ADSpath $GCADSPath
$GCSearcher.PropertiesToLoad.AddRange(('samaccountname','distinguishedname','dnshostname','objectclass'))
$GCSearcher.Filter = "(objectsid=$PrincipalSid)"
$PrincipalObject = $GCSearcher.FindOne()
}
elseif ($Object.objectclass.contains('user')) {
$ObjectADType = 'USER'
if ($PrincipalObject) {
if ($PrincipalObject.Properties.objectclass.contains('computer')) {
$PrincipalObjectClass = 'COMPUTER'
$PrincipalSimpleName = $PrincipalObject.Properties.dnshostname[0]
}
else {
$PrincipalSamAccountName = $PrincipalObject.Properties.samaccountname[0]
$PrincipalDN = $PrincipalObject.Properties.distinguishedname[0]
$PrincipalDomain = $PrincipalDN.SubString($PrincipalDN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
$PrincipalSimpleName = "$PrincipalSamAccountName@$PrincipalDomain"
if ($PrincipalObject.Properties.objectclass.contains('group')) {
$PrincipalObjectClass = 'GROUP'
}
elseif ($PrincipalObject.Properties.objectclass.contains('user')) {
$PrincipalObjectClass = 'USER'
}
else {
$PrincipalObjectClass = 'OTHER'
}
}
}
else {
$ObjectADType = 'OTHER'
Write-Verbose "SID not resolved: $PrincipalSid"
}
$PrincipalMapping[$PrincipalSid] = $PrincipalSimpleName, $PrincipalObjectClass
}
if ($ObjectName -and $ObjectADType) {
if ($PrincipalSimpleName -and $PrincipalObjectClass) {
if ($PSCmdlet.ParameterSetName -eq 'CSVExport') {
$ACLWriter.WriteLine("`"$ObjectName`",`"$ObjectADType`",`"$PrincipalSimpleName`",`"$PrincipalObjectClass`",`"$ActiveDirectoryRights`",`"$ACEType`",`"$($_.AceQualifier)`",`"$($_.IsInherited)`"")
# "ObjectName","ObjectType","ObjectGuid","PrincipalName","PrincipalType","ActiveDirectoryRights","ACEType","AccessControlType","IsInherited"
$ACLWriter.WriteLine("`"$ObjectName`",`"$ObjectADType`",`"$ObjectGuid`",`"$PrincipalSimpleName`",`"$PrincipalObjectClass`",`"Owner`",`"`",`"AccessAllowed`",`"False`"")
}
else {
Write-Warning 'TODO: implement neo4j RESTful API ingestion for ACLs!'
@ -5487,9 +5752,9 @@ function Invoke-BloodHound {
}
}
}
}
catch {
Write-Verbose "ACL ingestion error: $_"
catch {
Write-Verbose "ACL ingestion error: $_"
}
}
}
}
@ -5917,6 +6182,14 @@ function Invoke-BloodHound {
$GroupWriter.Dispose()
$GroupFileStream.Dispose()
}
if($ContainerWriter) {
$ContainerWriter.Dispose()
$ContainerFileStream.Dispose()
}
if($GPLinkWriter) {
$GPLinkWriter.Dispose()
$GPLinkFileStream.Dispose()
}
if($ACLWriter) {
$ACLWriter.Dispose()
$ACLFileStream.Dispose()
@ -5966,7 +6239,6 @@ $FunctionDefinitions = @(
(func advapi32 ConvertSidToStringSid ([Int]) @([IntPtr], [String].MakeByRefType()) -SetLastError)
)
# the NetWkstaUserEnum result structure
$WKSTA_USER_INFO_1 = struct $Mod WKSTA_USER_INFO_1 @{
wkui1_username = field 0 String -MarshalAs @('LPWStr')