diff --git a/PowerShell/BloodHound.ps1 b/PowerShell/BloodHound.ps1 index fec36902c..4232752e4 100644 --- a/PowerShell/BloodHound.ps1 +++ b/PowerShell/BloodHound.ps1 @@ -2588,32 +2588,37 @@ function Get-NetUser { $UserSearcher.filter="(&(samAccountType=805306368)$Filter)" } - $Results = $UserSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - # convert/process the LDAP fields for each result - $User = Convert-LDAPProperty -Properties $_.Properties + try { + $Results = $UserSearcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + # convert/process the LDAP fields for each result + $User = Convert-LDAPProperty -Properties $_.Properties - $User | Add-Member NoteProperty 'Domain' $TargetDomain + $User | Add-Member NoteProperty 'Domain' $TargetDomain - $DomainSID = $User.objectsid.Substring(0, $User.objectsid.LastIndexOf('-')) - $PrimaryGroupSID = "$DomainSID-$($User.primarygroupid)" + $DomainSID = $User.objectsid.Substring(0, $User.objectsid.LastIndexOf('-')) + $PrimaryGroupSID = "$DomainSID-$($User.primarygroupid)" - $User | Add-Member NoteProperty 'PrimaryGroupSID' $PrimaryGroupSID + $User | Add-Member NoteProperty 'PrimaryGroupSID' $PrimaryGroupSID - if($PrimaryGroups[$PrimaryGroupSID]) { - $PrimaryGroupName = $PrimaryGroups[$PrimaryGroupSID] - } - else { - $PrimaryGroupName = Get-ADObject -Domain $Domain -SID $PrimaryGroupSID | Select-Object -ExpandProperty samaccountname - $PrimaryGroups[$PrimaryGroupSID] = $PrimaryGroupName - } + if($PrimaryGroups[$PrimaryGroupSID]) { + $PrimaryGroupName = $PrimaryGroups[$PrimaryGroupSID] + } + else { + $PrimaryGroupName = Get-ADObject -Domain $Domain -SID $PrimaryGroupSID | Select-Object -ExpandProperty samaccountname + $PrimaryGroups[$PrimaryGroupSID] = $PrimaryGroupName + } - $User | Add-Member NoteProperty 'PrimaryGroupName' $PrimaryGroupName + $User | Add-Member NoteProperty 'PrimaryGroupName' $PrimaryGroupName - $User.PSObject.TypeNames.Add('PowerView.User') - $User + $User.PSObject.TypeNames.Add('PowerView.User') + $User + } + $Results.dispose() + } + catch { + Write-Verbose "Error building the UserSearcher searcher object!" } - $Results.dispose() $UserSearcher.dispose() } } @@ -4192,17 +4197,22 @@ function Get-ADObject { $ObjectSearcher.filter = "(&(samAccountName=$SamAccountName)$Filter)" } - $Results = $ObjectSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - if($ReturnRaw) { - $_ - } - else { - # convert/process the LDAP fields for each result - Convert-LDAPProperty -Properties $_.Properties + try { + $Results = $ObjectSearcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + if($ReturnRaw) { + $_ + } + else { + # convert/process the LDAP fields for each result + Convert-LDAPProperty -Properties $_.Properties + } } + $Results.dispose() + } + catch { + Write-Verbose "Error building the searcher object!" } - $Results.dispose() $ObjectSearcher.dispose() } } @@ -13236,526 +13246,395 @@ function Invoke-MapDomainTrust { # ######################################################## -function Export-BloodHoundData { +function Get-BloodHoundData { <# .SYNOPSIS - Exports PowerView object data to a BloodHound server instance. + This function automates the collection of the data needed for BloodHound. Author: @harmj0y License: BSD 3-Clause Required Dependencies: PowerView.ps1 Optional Dependencies: None - .PARAMETER Object + .DESCRIPTION - The PowerView PSObject to insert into BloodHound. + This function collects the information needed to populate the BloodHound graph + database. It offers a varity of targeting and collection options. + By default, it will map all domain trusts, enumerate all groups and associated memberships, + enumerate all computers on the domain and execute session/loggedon/local admin enumeration + queries against each. Targeting options are modifiable with -CollectionMethod. The + -SearchForest searches all domains in the forest instead of just the current domain. - .PARAMETER URI + .PARAMETER ComputerName - The BloodHound neo4j URL location (http://host:port/). + Host array to enumerate, passable on the pipeline. - .PARAMETER UserPass + .PARAMETER Domain - The "user:password" for the BloodHound neo4j instance. + Domain to query for machines, defaults to the current domain. - .PARAMETER SkipGCDeconfliction + .PARAMETER DomainController - Switch. Don't resolve user domain memberships for session information using a global catalog. + Domain controller to reflect LDAP queries through. - .PARAMETER GlobalCatalog + .PARAMETER CollectionMethod - The global catalog location to resole user memberships from, form of GC://global.catalog. + The method to collect data. 'Group', 'LocalGroup', 'GPOLocalGroup', 'Sesssion', 'LoggedOn', 'Trusts, 'TrustsLDAP', 'Stealth', or 'Default'. + 'TrustsLDAP' uses LDAP enumeration for trusts, while 'Trusts' using .NET methods. + 'Stealth' uses 'Group' collection, stealth user hunting ('Session' on certain servers), 'GPOLocalGroup' enumeration, and LDAP trust enumeration. + 'Default' uses 'Group' collection, regular user hunting with 'Session'/'LoggedOn', 'LocalGroup' enumeration, and 'Trusts' enumeration. - .PARAMETER Credential + .PARAMETER SearchForest - A [Management.Automation.PSCredential] object that stores a BloodHound username - and password for the neo4j connection. + Switch. Search all domains in the forest for target users instead of just + a single domain. - .PARAMETER Throttle + .PARAMETER Threads - The number of object insertion queries to run in each batch, defaults to 100. + The maximum concurrent threads to execute. .EXAMPLE - PS C:\> Get-NetGroupMember | Export-BloodHoundData -URI http://host:80/ -UserPass "user:pass" + PS C:\> Get-BloodHoundData | Export-BloodHoundData -URI http://SERVER:7474/ -UserPass "user:pass" - Export the Domain Admins group members to the given BloodHound database. + Executes default collection options and exports the data to a BloodHound neo4j RESTful API endpoint. - .LINK + .EXAMPLE - http://neo4j.com/docs/stable/rest-api-batch-ops.html - http://stackoverflow.com/questions/19839469/optimizing-high-volume-batch-inserts-into-neo4j-using-rest + PS C:\> Get-BloodHoundData | Export-BloodHoundData -URI http://SERVER:7474/ -UserPass "user:pass" -Threads 20 + + Executes default collection options and exports the data to a BloodHound neo4j RESTful API endpoint, + and use 20 threads for collection operations. + .EXAMPLE + + PS C:\> Get-BloodHoundData | Export-BloodHoundCSV + + Executes default collection options and exports the data to a CSVs in the current directory. + + .EXAMPLE + + PS C:\> Get-BloodHoundData -CollectionMethod 'Stealth' | Export-BloodHoundData -URI http://SERVER:7474/ -UserPass "user:pass" + + Executes 'stealth' collection options and exports the data to a BloodHound neo4j RESTful API endpoint. + This includes 'stealth' user hunting and GPO object correlation for local admin membership. + This is significantly faster but the information is not as complete as the default options. #> + [CmdletBinding(DefaultParameterSetName = 'None')] param( - [Parameter(Position=0, ValueFromPipeline=$True, Mandatory = $True)] - [PSObject] - $Object, + [Parameter(Position=0, ValueFromPipeline=$True)] + [Alias('Hosts')] + [String[]] + $ComputerName, - [Parameter(Position=1, Mandatory = $True)] - [URI] - $URI, + [String] + $Domain, - [Parameter(Position=2, Mandatory = $True, ParameterSetName = 'PlaintextPW')] [String] - [ValidatePattern('.*:.*')] - $UserPass, + $DomainController, - [Parameter(Position=2, Mandatory = $True, ParameterSetName = 'PSCredential')] - [Management.Automation.PSCredential] - $Credential, + [String] + [ValidateSet('Group', 'LocalGroup', 'GPOLocalGroup', 'Session', 'LoggedOn', 'Stealth', 'Trusts', 'TrustsLDAP', 'Default')] + $CollectionMethod = 'Default', [Switch] - $SkipGCDeconfliction, - - [ValidatePattern('^GC://')] - [String] - $GlobalCatalog, + $SearchForest, + [ValidateRange(1,100)] [Int] - $Throttle = 1000 + $Threads ) begin { - $WebClient = New-Object System.Net.WebClient - if($PSBoundParameters['Credential']) { - $BloodHoundUserName = $Credential.UserName - $BloodHoundPassword = $Credential.GetNetworkCredential().Password - $Base64UserPass = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($BloodHoundUserName + ':' + $BloodHoundPassword)) + Switch ($CollectionMethod) { + 'Group' { $UseGroup = $True; $SkipComputerEnumeration = $True } + 'LocalGroup' { $UseLocalGroup = $True } + 'GPOLocalGroup' { $UseGPOGroup = $True; $SkipComputerEnumeration = $True } + 'Session' { $UseSession = $True } + 'LoggedOn' { $UseLoggedOn = $True } + 'TrustsLDAP' { $UseDomainTrustsLDAP = $True; $SkipComputerEnumeration = $True } + 'Trusts' { $UseDomainTrusts = $True; $SkipComputerEnumeration = $True } + 'Stealth' { + $UseGroup = $True + $UseGPOGroup = $True + $UseSession = $True + $UseDomainTrustsLDAP = $True + } + 'Default' { + $UseGroup = $True + $UseLocalGroup = $True + $UseSession = $True + $UseLoggedOn = $True + $UseDomainTrusts = $True + } + } + + if($Domain) { + $TargetDomains = @($Domain) + } + elseif($SearchForest) { + # get ALL the domains in the forest to search + $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } } else { - $Base64UserPass = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($UserPass)) + # use the local domain + $TargetDomains = @( (Get-NetDomain).name ) } - # add the auth headers - $WebClient.Headers.Add('Accept','application/json; charset=UTF-8') - $WebClient.Headers.Add('Authorization',"Basic $Base64UserPass") + if($UseGroup) { + ForEach ($TargetDomain in $TargetDomains) { + # enumerate all groups and all members of each group + Get-NetGroup -Domain $Domain -DomainController $DomainController | Get-NetGroupMember -Domain $Domain -DomainController $DomainController -FullData - # check auth to the BloodHound neo4j server - try { - $Null = $WebClient.DownloadString($URI.AbsoluteUri + "user/neo4j") - $Authorized = $True - } - catch [Net.WebException] { - $Authorized = $False - throw "Error connecting to Neo4j rest REST server at '$(URI.AbsoluteUri)' : $($_.Exception)" + # enumerate all user objects so we can extract out the primary group for each + Get-NetUser -Domain $Domain -DomainController $DomainController + } } - Add-Type -Assembly System.Web.Extensions + if($UseDomainTrusts) { + Invoke-MapDomainTrust + } - # from http://stackoverflow.com/questions/28077854/powershell-2-0-convertfrom-json-and-convertto-json-implementation - function ConvertTo-Json20([object] $Item){ - $ps_js = New-Object System.Web.Script.Serialization.javascriptSerializer - return $ps_js.Serialize($item) + if($UseDomainTrustsLDAP) { + Invoke-MapDomainTrust -LDAP -DomainController $DomainController } - $Authorized = $True - $ObjectBuffer = New-Object System.Collections.ArrayList + if (-not $SkipComputerEnumeration) { + if(-not $ComputerName) { + [Array]$TargetComputers = @() - $UserDomainMappings = @{} - if(-not $SkipGCDeconfliction) { - # if we're doing session enumeration, create a {user : @(domain,..)} from a global catalog - # in order to do user domain deconfliction for sessions - if(-not $PSBoundParameters['GlobalCatalog']) { - $ForestRoot = Get-NetForest | Select-Object -ExpandProperty RootDomain - $ADSPath = "GC://$ForestRoot" - Write-Verbose "Global catalog string from enumerated forest root: $ADSPath" - } - else { - $ADSpath = $GlobalCatalog - } + ForEach ($Domain2 in $TargetDomains) { + if($CollectionMethod -eq 'Stealth') { + Write-Verbose "Querying domain $Domain2 for File Servers..." + $TargetComputers += Get-NetFileServer -Domain $Domain2 -DomainController $DomainController - Get-NetUser -ADSPath $ADSpath | ForEach-Object { - $UserName = $_.samaccountname.ToUpper() - $UserDN = $_.distinguishedname + Write-Verbose "Querying domain $Domain2 for DFS Servers..." + $TargetComputers += Get-DFSshare -Domain $Domain2 -DomainController $DomainController | ForEach-Object {$_.RemoteServerName} - if (($UserDN -match 'ForeignSecurityPrincipals') -and ($UserDN -match 'S-1-5-21')) { - try { - if(-not $MemberSID) { - $MemberSID = $_.cn[0] - } - $MemberSimpleName = Convert-SidToName -SID $_.objectsid | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' - if($MemberSimpleName) { - $UserDomain = $MemberSimpleName.Split('/')[0] - } - else { - Write-Verbose "Error converting $UserDN" - $UserDomain = $Null - } + Write-Verbose "Querying domain $Domain2 for Domain Controllers..." + $TargetComputers += Get-NetDomainController -LDAP -Domain $Domain2 -DomainController $DomainController | ForEach-Object { $_.dnshostname} } - catch { - Write-Verbose "Error converting $UserDN" - $UserDomain = $Null + else { + Write-Verbose "Querying domain $Domain2 for hosts" + + $TargetComputers += Get-NetComputer -Domain $Domain2 -DomainController $DomainController } - } - else { - # extract the FQDN from the Distinguished Name - $UserDomain = ($UserDN.subString($UserDN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.').ToUpper() - } - if(-not $UserDomainMappings[$UserName]) { - $UserDomainMappings[$UserName] = @($UserDomain) + if($UseGPOGroup) { + Write-Verbose "Enumerating GPO local group memberships for domain $Domain2" + Find-GPOLocation -Domain $Domain2 -DomainController $DomainController + + # add in a local administrator relationship for "Domain Admins" -> every machine + $DomainSID = Get-DomainSID -Domain $Domain2 -DomainController $DomainController + $DomainAdminsSid = "$DomainSID-512" + $Temp = Convert-SidToName -SID $DomainAdminsSid + $DomainAdminsName = $Temp.Split('\')[1] + + Get-NetComputer -Domain $Domain2 -DomainController $DomainController | ForEach-Object { + $LocalUser = New-Object PSObject + $LocalUser | Add-Member Noteproperty 'MemberName' $DomainAdminsName + $LocalUser | Add-Member Noteproperty 'MemberDomain' $Domain2 + $LocalUser | Add-Member Noteproperty 'IsGroup' $True + $LocalUser | Add-Member Noteproperty 'ComputerName' $_ + $LocalUser.PSObject.TypeNames.Add('PowerView.LocalUserSpecified') + $LocalUser + } + } } - elseif($UserDomainMappings[$UserName] -notcontains $UserDomain) { - $UserDomainMappings[$UserName] += $UserDomain + + # remove any null target hosts, uniquify the list and shuffle it + $TargetComputers = $TargetComputers | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } + if($($TargetComputers.Count) -eq 0) { + Write-Warning "No hosts found!" } } - } - } + else { + $TargetComputers = $ComputerName + } + } - process { - if($Authorized) { + # get the current user so we can ignore it in the results + $CurrentUser = ([Environment]::UserName).toLower() - $Queries = @() + # script block that enumerates a server + $HostEnumBlock = { + param($ComputerName, $Ping, $CurrentUser2, $UseLocalGroup2, $UseSession2, $UseLoggedon2) - if($Object.PSObject.TypeNames -contains 'PowerView.UserSession') { - if($Object.SessionFromName) { - try { - # $SessionFromDomain = $Object.SessionFromName.SubString($Object.SessionFromName.IndexOf('.')+1) - $UserName = $Object.UserName.ToUpper() - $SessionFromName = $Object.SessionFromName + $Up = $True + if($Ping) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName + } + if($Up) { - if($UserDomainMappings) { - $UserDomain = $Null - if($UserDomainMappings[$UserName]) { - if($UserDomainMappings[$UserName].Count -eq 1) { - $UserDomain = $UserDomainMappings[$UserName] - $LoggedOnUser = "$UserName@$UserDomain" + if($UseLocalGroup2) { + # grab the users for the local admins on this server + Get-NetLocalGroup -ComputerName $ComputerName -API | Where-Object {$_.IsDomain} + } - $Queries += "MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '1'}]->(user)" - } - else { - $ComputerDomain = $Object.SessionFromName.SubString($Object.SessionFromName.IndexOf('.')+1).ToUpper() + $IPAddress = @(Get-IPAddress -ComputerName $ComputerName)[0].IPAddress - $UserDomainMappings[$UserName] | ForEach-Object { - if($_ -eq $ComputerDomain) { - $UserDomain = $_ - $LoggedOnUser = "$UserName@$UserDomain" + if($UseSession2) { + $Sessions = Get-NetSession -ComputerName $ComputerName + ForEach ($Session in $Sessions) { + $UserName = $Session.sesi10_username + $CName = $Session.sesi10_cname - $Queries += "MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '1'}]->(user)" - } - else { - $UserDomain = $_ - $LoggedOnUser = "$UserName@$UserDomain" + if($CName -and $CName.StartsWith("\\")) { + $CName = $CName.TrimStart("\") + } - $Queries += "MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '2'}]->(user)" - } - } - } + # make sure we have a result + if (($UserName) -and ($UserName.trim() -ne '') -and ($UserName -notmatch '\$') -and ($UserName -notmatch $CurrentUser2)) { + + $FoundUser = New-Object PSObject + $FoundUser | Add-Member Noteproperty 'UserDomain' $Null + $FoundUser | Add-Member Noteproperty 'UserName' $UserName + $FoundUser | Add-Member Noteproperty 'ComputerName' $ComputerName + $FoundUser | Add-Member Noteproperty 'IPAddress' $IPAddress + $FoundUser | Add-Member Noteproperty 'SessionFrom' $CName + + # Try to resolve the DNS hostname of $Cname + try { + $CNameDNSName = [System.Net.Dns]::GetHostEntry($CName) | Select-Object -ExpandProperty HostName } - else { - # no user object in the GC with this username - $LoggedOnUser = "$UserName@UNKNOWN" - $Queries += "MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '2'}]->(user)" + catch { + $CNameDNSName = $CName } + $FoundUser | Add-Member NoteProperty 'SessionFromName' $CNameDNSName + $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Null + $FoundUser.PSObject.TypeNames.Add('PowerView.UserSession') + $FoundUser } - else { - $LoggedOnUser = "$UserName@UNKNOWN" - $Queries += "MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '2'}]->(user)" - } - } - catch { - Write-Warning "Error extracting domain from $($Object.SessionFromName)" } } - elseif($Object.SessionFrom) { - $Queries += "MERGE (user:User { name: UPPER(`"$($Object.UserName)`") }) MERGE (computer:Computer { name: UPPER(`"$($Object.SessionFrom)`") }) MERGE (computer)-[:HasSession {Weight: '2'}]->(user)" - } - else { - # assume Get-NetLoggedOn result - try { - $MemberSimpleName = "$($Object.UserDomain)\$($Object.UserName)" | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' - if($MemberSimpleName) { - $MemberDomain = $MemberSimpleName.Split('/')[0] - $AccountName = "$($Object.UserName)@$MemberDomain" - } - else { - $AccountName = "$($Object.UserName)@UNKNOWN" - } + if($UseLoggedon2) { + $LoggedOn = Get-NetLoggedon -ComputerName $ComputerName + ForEach ($User in $LoggedOn) { + $UserName = $User.wkui1_username + $UserDomain = $User.wkui1_logon_domain - $Queries += "MERGE (user:User { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$($Object.ComputerName)') }) MERGE (computer)-[:HasSession {Weight: '1'}]->(user)" + # ignore local account logons + # TODO: better way to determine if network logon or not + if($ComputerName -notmatch "^$UserDomain") { + if (($UserName) -and ($UserName.trim() -ne '') -and ($UserName -notmatch '\$')) { + $FoundUser = New-Object PSObject + $FoundUser | Add-Member Noteproperty 'UserDomain' $UserDomain + $FoundUser | Add-Member Noteproperty 'UserName' $UserName + $FoundUser | Add-Member Noteproperty 'ComputerName' $ComputerName + $FoundUser | Add-Member Noteproperty 'IPAddress' $IPAddress + $FoundUser | Add-Member Noteproperty 'SessionFrom' $Null + $FoundUser | Add-Member Noteproperty 'SessionFromName' $Null + $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Null + $FoundUser.PSObject.TypeNames.Add('PowerView.UserSession') + $FoundUser + } + } } - catch { - Write-Verbose "Error converting $($Object.UserDomain)\$($Object.UserName)" + + $LocalLoggedOn = Get-LoggedOnLocal -ComputerName $ComputerName + ForEach ($User in $LocalLoggedOn) { + $UserName = $User.UserName + $UserDomain = $User.UserDomain + + # ignore local account logons ? + if($ComputerName -notmatch "^$UserDomain") { + $FoundUser = New-Object PSObject + $FoundUser | Add-Member Noteproperty 'UserDomain' $UserDomain + $FoundUser | Add-Member Noteproperty 'UserName' $UserName + $FoundUser | Add-Member Noteproperty 'ComputerName' $ComputerName + $FoundUser | Add-Member Noteproperty 'IPAddress' $IPAddress + $FoundUser | Add-Member Noteproperty 'SessionFrom' $Null + $FoundUser | Add-Member Noteproperty 'SessionFromName' $Null + $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Null + $FoundUser.PSObject.TypeNames.Add('PowerView.UserSession') + $FoundUser + } } } } - elseif($Object.PSObject.TypeNames -contains 'PowerView.GroupMember') { - $AccountName = "$($Object.MemberName)@$($Object.MemberDomain)" + } + } - if ($Object.MemberName -Match "\\") { - # if the membername itself contains a backslash, get the trailing section - # TODO: later preserve this once BloodHound can properly display these characters - $AccountName = $($Object.Membername).split('\')[1] + '@' + $($Object.MemberDomain) + process { + if (-not $SkipComputerEnumeration) { + if($Threads) { + Write-Verbose "Using threading with threads = $Threads" + + # if we're using threading, kick off the script block with Invoke-ThreadedFunction + $ScriptParams = @{ + 'Ping' = $True + 'CurrentUser2' = $CurrentUser + 'UseLocalGroup2' = $UseLocalGroup + 'UseSession2' = $UseSession + 'UseLoggedon2' = $UseLoggedon } - $GroupName = "$($Object.GroupName)@$($Object.GroupDomain)" + Invoke-ThreadedFunction -ComputerName $TargetComputers -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads + } - if($Object.IsGroup) { - $Queries += "MERGE (group1:Group { name: UPPER('$AccountName') }) MERGE (group2:Group { name: UPPER('$GroupName') }) MERGE (group1)-[:MemberOf]->(group2)" + else { + if($TargetComputers.Count -ne 1) { + # ping all hosts in parallel + $Ping = {param($ComputerName2) if(Test-Connection -ComputerName $ComputerName2 -Count 1 -Quiet -ErrorAction Stop) { $ComputerName2 }} + $TargetComputers2 = Invoke-ThreadedFunction -NoImports -ComputerName $TargetComputers -ScriptBlock $Ping -Threads 100 } else { - # check if -FullData objects are returned, and if so check if the group member is a computer object - if($Object.ObjectClass -and ($Object.ObjectClass -contains 'computer')) { - $Queries += "MERGE (computer:Computer { name: UPPER('$($Object.dnshostname)') }) MERGE (group:Group { name: UPPER('$GroupName') }) MERGE (computer)-[:MemberOf]->(group)" - } - else { - # otherwise there's no way to determine if this is a computer object or not - $Queries += "MERGE (user:User { name: UPPER('$AccountName') }) MERGE (group:Group { name: UPPER('$GroupName') }) MERGE (user)-[:MemberOf]->(group)" - } + $TargetComputers2 = $TargetComputers } - } - elseif($Object.PSObject.TypeNames -Contains 'PowerView.User') { - $AccountDomain = $Object.Domain - $AccountName = "$($Object.SamAccountName)@$AccountDomain" - if($Object.PrimaryGroupName -and ($Object.PrimaryGroupName -ne '')) { - $PrimaryGroupName = "$($Object.PrimaryGroupName)@$AccountDomain" - $Queries += "MERGE (user:User { name: UPPER('$AccountName') }) MERGE (group:Group { name: UPPER('$PrimaryGroupName') }) MERGE (user)-[:MemberOf]->(group)" + Write-Verbose "[*] Total number of active hosts: $($TargetComputers2.count)" + $Counter = 0 + + $TargetComputers2 | ForEach-Object { + $Counter = $Counter + 1 + Write-Verbose "[*] Enumerating server $($_) ($Counter of $($TargetComputers2.count))" + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList @($_, $False, $CurrentUser, $UseLocalGroup, $UseSession, $UseLoggedon) } - # TODO: extract pwdlastset/etc. and ingest } - elseif(($Object.PSObject.TypeNames -contains 'PowerView.LocalUserAPI') -or ($Object.PSObject.TypeNames -contains 'PowerView.LocalUser')) { - $AccountName = $($Object.AccountName.replace('/', '\')).split('\')[-1] + } + } +} - $MemberSimpleName = Convert-SidToName -SID $Object.SID | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' - if($MemberSimpleName) { - $MemberDomain = $MemberSimpleName.Split('/')[0] - } - else { - $MemberDomain = "UNKNOWN" - } +function Export-BloodHoundData { +<# + .SYNOPSIS - $AccountName = "$AccountName@$MemberDomain" + Takes custom objects from Get-BloodHound data and exports everything to a BloodHound + neo4j RESTful API batch ingestion interface. - if($Object.IsGroup) { - $Queries += "MERGE (group:Group { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$($Object.ComputerName)') }) MERGE (group)-[:AdminTo]->(computer)" - } - else { - $Queries += "MERGE (user:User { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$($Object.ComputerName)') }) MERGE (user)-[:AdminTo]->(computer)" - } - } - elseif($Object.PSObject.TypeNames -contains 'PowerView.LocalUserSpecified') { - # manually specified localgroup membership where resolution happens by the callee + Author: @harmj0y + License: BSD 3-Clause + Required Dependencies: PowerView.ps1 + Optional Dependencies: None - $AccountName = "$($Object.MemberName)@$($Object.MemberDomain)" + .DESCRIPTION - if($Object.IsGroup) { - $Queries += "MERGE (group:Group { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$($Object.ComputerName)') }) MERGE (group)-[:AdminTo]->(computer)" - } - else { - $Queries += "MERGE (user:User { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$($Object.ComputerName)') }) MERGE (user)-[:AdminTo]->(computer)" - } - } - elseif($Object.PSObject.TypeNames -contains 'PowerView.GPOLocalGroup') { - $MemberSimpleName = Convert-SidToName -SID $Object.ObjectSID | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' + This function takes custom tagged PowerView objects types from Get-BloodHoundData and packages/ingests + them into a neo4j RESTful API batch ingestion interface. For user session data without a logon + domain, by default the global catalog is used to attempt to deconflict what domain the user may be + located in. If the user exists in more than one domain in the forest, a series of weights is used to + modify the attack path likelihood. - if($MemberSimpleName) { - $MemberDomain = $MemberSimpleName.Split('/')[0] - $AccountName = "$($Object.ObjectName)@$MemberDomain" - } - else { - $AccountName = $Object.ObjectName - } - - ForEach($Computer in $Object.ComputerName) { - if($Object.IsGroup) { - $Queries += "MERGE (group:Group { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$Computer') }) MERGE (group)-[:AdminTo]->(computer)" - } - else { - $Queries += "MERGE (user:User { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$Computer') }) MERGE (user)-[:AdminTo]->(computer)" - } - } - } - elseif($Object.PSObject.TypeNames -contains 'PowerView.DomainTrustLDAP') { - # [uint32]'0x00000001' = 'non_transitive' - # [uint32]'0x00000002' = 'uplevel_only' - # [uint32]'0x00000004' = 'quarantined_domain' - # [uint32]'0x00000008' = 'forest_transitive' - # [uint32]'0x00000010' = 'cross_organization' - # [uint32]'0x00000020' = 'within_forest' - # [uint32]'0x00000040' = 'treat_as_external' - # [uint32]'0x00000080' = 'trust_uses_rc4_encryption' - # [uint32]'0x00000100' = 'trust_uses_aes_keys' - # [uint32]'0x00000200' = 'cross_organization_no_tgt_delegation' - # [uint32]'0x00000400' = 'pim_trust' - if($Object.SourceDomain) { - $SourceDomain = $Object.SourceDomain - } - else { - $SourceDomain = $Object.SourceName - } - if($Object.TargetDomain) { - $TargetDomain = $Object.TargetDomain - } - else { - $TargetDomain = $Object.TargetName - } - - $Query = "MERGE (SourceDomain:Domain { name: UPPER('$SourceDomain') }) MERGE (TargetDomain:Domain { name: UPPER('$TargetDomain') })" - - if($Object.TrustType -match 'cross_organization') { - $TrustType = 'CrossLink' - } - elseif ($Object.TrustType -match 'within_forest') { - $TrustType = 'ParentChild' - } - elseif ($Object.TrustType -match 'forest_transitive') { - $TrustType = 'Forest' - } - elseif ($Object.TrustType -match 'treat_as_external') { - $TrustType = 'External' - } - else { - Write-Verbose "Trust type unhandled/unknown: $($Object.TrustType)" - $TrustType = 'Unknown' - } - - if ($Object.TrustType -match 'non_transitive') { - $Transitive = $False - } - else { - $Transitive = $True - } - - Switch ($Object.TrustDirection) { - 'Inbound' { - $Query += " MERGE (TargetDomain)-[:TrustedBy{ TrustType: UPPER('$TrustType'), Transitive: UPPER('$Transitive')}]->(SourceDomain)" - } - 'Outbound' { - $Query += " MERGE (SourceDomain)-[:TrustedBy{ TrustType: UPPER('$TrustType'), Transitive: UPPER('$Transitive')}]->(TargetDomain)" - } - 'Bidirectional' { - $Query += " MERGE (TargetDomain)-[:TrustedBy{ TrustType: UPPER('$TrustType'), Transitive: UPPER('$Transitive')}]->(SourceDomain) MERGE (SourceDomain)-[:TrustedBy{ TrustType: UPPER('$TrustType'), Transitive: UPPER('$Transitive')}]->(TargetDomain)" - } - } - $Queries += $Query - } - elseif($Object.PSObject.TypeNames -contains 'PowerView.DomainTrust') { - if($Object.SourceDomain) { - $SourceDomain = $Object.SourceDomain - } - else { - $SourceDomain = $Object.SourceName - } - if($Object.TargetDomain) { - $TargetDomain = $Object.TargetDomain - } - else { - $TargetDomain = $Object.TargetName - } - - $Query = "MERGE (SourceDomain:Domain { name: UPPER('$SourceDomain') }) MERGE (TargetDomain:Domain { name: UPPER('$TargetDomain') })" - - $TrustType = $Object.TrustType - $Transitive = $True - - Switch ($Object.TrustDirection) { - 'Inbound' { - $Query += " MERGE (SourceDomain)-[:TrustedBy{ TrustType: UPPER('$TrustType'), Transitive: UPPER('$Transitive')}]->(TargetDomain)" - } - 'Outbound' { - $Query += " MERGE (TargetDomain)-[:TrustedBy{ TrustType: UPPER('$TrustType'), Transitive: UPPER('$Transitive')}]->(SourceDomain)" - } - 'Bidirectional' { - $Query += " MERGE (TargetDomain)-[:TrustedBy{ TrustType: UPPER('$TrustType'), Transitive: UPPER('$Transitive')}]->(SourceDomain) MERGE (SourceDomain)-[:TrustedBy{ TrustType: UPPER('$TrustType'), Transitive: UPPER('$Transitive')}]->(TargetDomain)" - } - } - $Queries += $Query - } - elseif($Object.PSObject.TypeNames -contains 'PowerView.ForestTrust') { - if($Object.SourceDomain) { - $SourceDomain = $Object.SourceDomain - } - else { - $SourceDomain = $Object.SourceName - } - if($Object.TargetDomain) { - $TargetDomain = $Object.TargetDomain - } - else { - $TargetDomain = $Object.TargetName - } - - $Query = "MERGE (SourceDomain:Domain { name: UPPER('$SourceDomain') }) MERGE (TargetDomain:Domain { name: UPPER('$TargetDomain') })" - - $TrustType = 'Forest' - $Transitive = $True - - Switch ($Object.TrustDirection) { - 'Inbound' { - $Query += " MERGE (SourceDomain)-[:TrustedBy{ TrustType: UPPER('FOREST'), Transitive: UPPER('$Transitive')}]->(TargetDomain)" - } - 'Outbound' { - $Query += " MERGE (TargetDomain)-[:TrustedBy{ TrustType: UPPER('FOREST'), Transitive: UPPER('$Transitive')}]->(SourceDomain)" - } - 'Bidirectional' { - $Query += " MERGE (TargetDomain)-[:TrustedBy{ TrustType: UPPER('FOREST'), Transitive: UPPER('$Transitive')}]->(SourceDomain) MERGE (SourceDomain)-[:TrustedBy{ TrustType: UPPER('FOREST'), Transitive: UPPER('$Transitive')}]->(TargetDomain)" - } - } - $Queries += $Query - } - else { - Write-Verbose "No matching type name" - } - - # built the batch object submission object for each query - ForEach($Query in $Queries) { - $BatchObject = @{ - "method" = "POST"; - "to" = "/cypher"; - "body" = @{"query"=$Query}; - } - $Null = $ObjectBuffer.Add($BatchObject) - } - - if ($ObjectBuffer.Count -ge $Throttle) { - $JsonRequest = ConvertTo-Json20 $ObjectBuffer - $Null = $WebClient.UploadString($URI.AbsoluteUri + "db/data/batch", $JsonRequest) - $ObjectBuffer.Clear() - } - } - else { - throw 'Not authorized' - } - } - end { - if($Authorized) { - $JsonRequest = ConvertTo-Json20 $ObjectBuffer - $Null = $WebClient.UploadString($URI.AbsoluteUri + "db/data/batch", $JsonRequest) - $ObjectBuffer.Clear() - } - } -} - - -function Export-BloodHoundCSV { -<# - .SYNOPSIS - - Exports PowerView object data to a BloodHound server instance. - - Author: @harmj0y - License: BSD 3-Clause - Required Dependencies: PowerView.ps1 - Optional Dependencies: None + Cypher queries are built for each appropriate relationship to ingest, and the set of queries is 'batched' + so '-Throttle X' queries are sent at a time in each batch request. All of the Cypher queries are + jsonified using System.Web.Script.Serialization.javascriptSerializer. .PARAMETER Object - The PowerView PSObject to export to csv. + The PowerView PSObject to export to the RESTful API interface. - .PARAMETER CSVFolder + .PARAMETER URI - The folder to output all CSVs to, defaults to the current working directory. + The BloodHound neo4j URL location (http://host:port/). - .PARAMETER CSVPrefix + .PARAMETER UserPass - A prefix to append to each CSV file. + The "user:password" for the BloodHound neo4j instance. .PARAMETER SkipGCDeconfliction @@ -13765,69 +13644,101 @@ function Export-BloodHoundCSV { The global catalog location to resole user memberships from, form of GC://global.catalog. + .PARAMETER Credential + + A [Management.Automation.PSCredential] object that stores a BloodHound username + and password for the neo4j connection. + + .PARAMETER Throttle + + The number of object insertion queries to run in each batch, defaults to 100. + .EXAMPLE - PS C:\> Get-NetGroupMember | ... + PS C:\> Get-BloodHoundData | Export-BloodHoundData -URI http://SERVER:7474/ -UserPass "user:pass" - .NOTES + Executes default collection options and exports the data to a BloodHound neo4j RESTful API endpoint. + + .EXAMPLE - PowerView.UserSession - UserName,ComputerName,Weight - "john@domain.local","computer2.domain.local",1 + PS C:\> Get-BloodHoundData | Export-BloodHoundData -URI http://SERVER:7474/ -UserPass "user:pass" -SkipGCDeconfliction - PowerView.GroupMember/PowerView.User - AccountName,AccountType,GroupName - "john@domain.local","user","GROUP1" - "computer3.testlab.local","computer","GROUP1" + Executes default collection options and exports the data to a BloodHound neo4j RESTful API endpoint, + and skip the global catalog deconfliction process. - PowerView.LocalUserAPI/PowerView.GPOLocalGroup - AccountName,AccountType,ComputerName - "john@domain.local","user","computer2.domain.local" + .LINK - PowerView.DomainTrustLDAP/PowerView.DomainTrust/PowerView.ForestTrust (direction ->) - SourceDomain,TargetDomain,TrustDirection,TrustType,Transitive - "domain.local","dev.domain.local","Bidirectional","ParentChild","True" -#> + http://neo4j.com/docs/stable/rest-api-batch-ops.html + http://stackoverflow.com/questions/19839469/optimizing-high-volume-batch-inserts-into-neo4j-using-rest - [CmdletBinding()] +#> + [CmdletBinding(DefaultParameterSetName = 'PlaintextPW')] param( - [Parameter(Position = 0, ValueFromPipeline = $True, Mandatory = $True)] + [Parameter(Position=0, ValueFromPipeline=$True, Mandatory = $True)] [PSObject] $Object, - [Parameter()] - [ValidateScript({ Test-Path -Path $_ })] - [String] - $CSVFolder = $(Get-Location), + [Parameter(Position=1, Mandatory = $True)] + [URI] + $URI, - [Parameter()] - [ValidateNotNullOrEmpty()] + [Parameter(Position=2, Mandatory = $True, ParameterSetName = 'PlaintextPW')] [String] - $CSVPrefix, + [ValidatePattern('.*:.*')] + $UserPass, + + [Parameter(Position=2, Mandatory = $True, ParameterSetName = 'PSCredential')] + [Management.Automation.PSCredential] + $Credential, [Switch] $SkipGCDeconfliction, [ValidatePattern('^GC://')] [String] - $GlobalCatalog + $GlobalCatalog, + + [Int] + $Throttle = 1000 ) - BEGIN { + begin { + $WebClient = New-Object System.Net.WebClient + + if($PSBoundParameters['Credential']) { + $BloodHoundUserName = $Credential.UserName + $BloodHoundPassword = $Credential.GetNetworkCredential().Password + $Base64UserPass = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($BloodHoundUserName + ':' + $BloodHoundPassword)) + } + else { + $Base64UserPass = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($UserPass)) + } + + # add the auth headers + $WebClient.Headers.Add('Accept','application/json; charset=UTF-8') + $WebClient.Headers.Add('Authorization',"Basic $Base64UserPass") + + # check auth to the BloodHound neo4j server try { - $OutputFolder = $CSVFolder | Resolve-Path -ErrorAction Stop | Select-Object -ExpandProperty Path + $Null = $WebClient.DownloadString($URI.AbsoluteUri + 'user/neo4j') + $Authorized = $True } catch { - throw "Error: $_" + $Authorized = $False + throw "Error connecting to Neo4j rest REST server at '$($URI.AbsoluteUri)'" } - if($CSVPrefix) { - $CSVExportPrefix = "$($CSVPrefix)_" - } - else { - $CSVExportPrefix = '' + Add-Type -Assembly System.Web.Extensions + + # from http://stackoverflow.com/questions/28077854/powershell-2-0-convertfrom-json-and-convertto-json-implementation + function ConvertTo-Json20([object] $Item){ + $ps_js = New-Object System.Web.Script.Serialization.javascriptSerializer + return $ps_js.Serialize($item) } + $Authorized = $True + $ObjectBuffer = New-Object System.Collections.ArrayList + $UserDomainMappings = @{} if(-not $SkipGCDeconfliction) { # if we're doing session enumeration, create a {user : @(domain,..)} from a global catalog @@ -13844,7 +13755,7 @@ function Export-BloodHoundCSV { Get-NetUser -ADSPath $ADSpath | ForEach-Object { $UserName = $_.samaccountname.ToUpper() $UserDN = $_.distinguishedname - + if (($UserDN -match 'ForeignSecurityPrincipals') -and ($UserDN -match 'S-1-5-21')) { try { if(-not $MemberSID) { @@ -13879,681 +13790,856 @@ function Export-BloodHoundCSV { } } - PROCESS { + process { + if($Authorized) { - if($Object.PSObject.TypeNames -contains 'PowerView.UserSession') { + $Queries = @() - if($Object.SessionFromName) { - try { - # $SessionFromDomain = $Object.SessionFromName.SubString($Object.SessionFromName.IndexOf('.')+1) - $UserName = $Object.UserName.ToUpper() - $SessionFromName = $Object.SessionFromName + if($Object.PSObject.TypeNames -contains 'PowerView.UserSession') { + if($Object.SessionFromName) { + try { + # $SessionFromDomain = $Object.SessionFromName.SubString($Object.SessionFromName.IndexOf('.')+1) + $UserName = $Object.UserName.ToUpper() + $SessionFromName = $Object.SessionFromName - if($UserDomainMappings) { - $UserDomain = $Null - if($UserDomainMappings[$UserName]) { - if($UserDomainMappings[$UserName].Count -eq 1) { - $UserDomain = $UserDomainMappings[$UserName] - $LoggedOnUser = "$UserName@$UserDomain" + if($UserDomainMappings) { + $UserDomain = $Null + if($UserDomainMappings[$UserName]) { + if($UserDomainMappings[$UserName].Count -eq 1) { + $UserDomain = $UserDomainMappings[$UserName] + $LoggedOnUser = "$UserName@$UserDomain" - $Properties = @{ - UserName = $LoggedOnUser - ComputerName = $SessionFromName - Weight = 1 + $Queries += "MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '1'}]->(user)" } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)user_sessions.csv" - } - else { - $ComputerDomain = $Object.SessionFromName.SubString($Object.SessionFromName.IndexOf('.')+1).ToUpper() + else { + $ComputerDomain = $Object.SessionFromName.SubString($Object.SessionFromName.IndexOf('.')+1).ToUpper() - $UserDomainMappings[$UserName] | ForEach-Object { - if($_ -eq $ComputerDomain) { - $UserDomain = $_ - $LoggedOnUser = "$UserName@$UserDomain" + $UserDomainMappings[$UserName] | ForEach-Object { + if($_ -eq $ComputerDomain) { + $UserDomain = $_ + $LoggedOnUser = "$UserName@$UserDomain" - $Properties = @{ - UserName = $LoggedOnUser - ComputerName = $SessionFromName - Weight = 1 + $Queries += "MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '1'}]->(user)" } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)user_sessions.csv" - } - else { - $UserDomain = $_ - $LoggedOnUser = "$UserName@$UserDomain" + else { + $UserDomain = $_ + $LoggedOnUser = "$UserName@$UserDomain" - $Properties = @{ - UserName = $LoggedOnUser - ComputerName = $SessionFromName - Weight = 2 + $Queries += "MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '2'}]->(user)" } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)user_sessions.csv" } } } + else { + # no user object in the GC with this username + $LoggedOnUser = "$UserName@UNKNOWN" + $Queries += "MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '2'}]->(user)" + } } else { - # no user object in the GC with this username $LoggedOnUser = "$UserName@UNKNOWN" - - $Properties = @{ - UserName = $LoggedOnUser - ComputerName = $SessionFromName - Weight = 2 - } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)user_sessions.csv" + $Queries += "MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '2'}]->(user)" } } - else { - $LoggedOnUser = "$UserName@UNKNOWN" - # $Queries += "MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '2'}]->(user)" - $Properties = @{ - UserName = $LoggedOnUser - ComputerName = $SessionFromName - Weight = 2 - } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)user_sessions.csv" + catch { + Write-Warning "Error extracting domain from $($Object.SessionFromName)" } } - catch { - Write-Warning "Error extracting domain from $($Object.SessionFromName)" - } - } - elseif($Object.SessionFrom) { - $Properties = @{ - UserName = "$($Object.UserName)@UNKNOWN" - ComputerName = $Object.SessionFrom - Weight = 2 + elseif($Object.SessionFrom) { + $Queries += "MERGE (user:User { name: UPPER(`"$($Object.UserName)`") }) MERGE (computer:Computer { name: UPPER(`"$($Object.SessionFrom)`") }) MERGE (computer)-[:HasSession {Weight: '2'}]->(user)" } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)user_sessions.csv" - } - else { - # assume Get-NetLoggedOn result - try { - $MemberSimpleName = "$($Object.UserDomain)\$($Object.UserName)" | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' + else { + # assume Get-NetLoggedOn result + try { + $MemberSimpleName = "$($Object.UserDomain)\$($Object.UserName)" | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' - if($MemberSimpleName) { - $MemberDomain = $MemberSimpleName.Split('/')[0] - $AccountName = "$($Object.UserName)@$MemberDomain" - } - else { - $AccountName = "$($Object.UserName)@UNKNOWN" - } + if($MemberSimpleName) { + $MemberDomain = $MemberSimpleName.Split('/')[0] + $AccountName = "$($Object.UserName)@$MemberDomain" + } + else { + $AccountName = "$($Object.UserName)@UNKNOWN" + } - $Properties = @{ - UserName = $AccountName - ComputerName = $Object.ComputerName - Weight = 1 + $Queries += "MERGE (user:User { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$($Object.ComputerName)') }) MERGE (computer)-[:HasSession {Weight: '1'}]->(user)" + } + catch { + Write-Verbose "Error converting $($Object.UserDomain)\$($Object.UserName)" } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)user_sessions.csv" - } - catch { - Write-Verbose "Error converting $($Object.UserDomain)\$($Object.UserName)" } } - } - elseif($Object.PSObject.TypeNames -contains 'PowerView.GroupMember') { - $AccountName = "$($Object.MemberName)@$($Object.MemberDomain)" + elseif($Object.PSObject.TypeNames -contains 'PowerView.GroupMember') { + $AccountName = "$($Object.MemberName)@$($Object.MemberDomain)" - if ($Object.MemberName -Match "\\") { - # if the membername itself contains a backslash, get the trailing section - # TODO: later preserve this once BloodHound can properly display these characters - $AccountName = $($Object.Membername).split('\')[1] + '@' + $($Object.MemberDomain) - } + if ($Object.MemberName -Match "\\") { + # if the membername itself contains a backslash, get the trailing section + # TODO: later preserve this once BloodHound can properly display these characters + $AccountName = $($Object.Membername).split('\')[1] + '@' + $($Object.MemberDomain) + } - $GroupName = "$($Object.GroupName)@$($Object.GroupDomain)" + $GroupName = "$($Object.GroupName)@$($Object.GroupDomain)" - if($Object.IsGroup) { - $Properties = @{ - AccountName = $AccountName - AccountType = 'group' - GroupName = $GroupName - } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)group_memberships.csv" - } - else { - # check if -FullData objects are returned, and if so check if the group member is a computer object - if($Object.ObjectClass -and ($Object.ObjectClass -contains 'computer')) { - $Properties = @{ - AccountName = $Object.dnshostname - AccountType = 'computer' - GroupName = $GroupName - } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)group_memberships.csv" + if($Object.IsGroup) { + $Queries += "MERGE (group1:Group { name: UPPER('$AccountName') }) MERGE (group2:Group { name: UPPER('$GroupName') }) MERGE (group1)-[:MemberOf]->(group2)" } else { - # otherwise there's no way to determine if this is a computer object or not - $Properties = @{ - AccountName = $AccountName - AccountType = 'user' - GroupName = $GroupName + # check if -FullData objects are returned, and if so check if the group member is a computer object + if($Object.ObjectClass -and ($Object.ObjectClass -contains 'computer')) { + $Queries += "MERGE (computer:Computer { name: UPPER('$($Object.dnshostname)') }) MERGE (group:Group { name: UPPER('$GroupName') }) MERGE (computer)-[:MemberOf]->(group)" + } + else { + # otherwise there's no way to determine if this is a computer object or not + $Queries += "MERGE (user:User { name: UPPER('$AccountName') }) MERGE (group:Group { name: UPPER('$GroupName') }) MERGE (user)-[:MemberOf]->(group)" } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)group_memberships.csv" } } - } - elseif($Object.PSObject.TypeNames -Contains 'PowerView.User') { - $AccountDomain = $Object.Domain - $AccountName = "$($Object.SamAccountName)@$AccountDomain" - - if($Object.PrimaryGroupName -and ($Object.PrimaryGroupName -ne '')) { - $PrimaryGroupName = "$($Object.PrimaryGroupName)@$AccountDomain" + elseif($Object.PSObject.TypeNames -Contains 'PowerView.User') { + $AccountDomain = $Object.Domain + $AccountName = "$($Object.SamAccountName)@$AccountDomain" - $Properties = @{ - AccountName = $AccountName - AccountType = 'user' - GroupName = $PrimaryGroupName + if($Object.PrimaryGroupName -and ($Object.PrimaryGroupName -ne '')) { + $PrimaryGroupName = "$($Object.PrimaryGroupName)@$AccountDomain" + $Queries += "MERGE (user:User { name: UPPER('$AccountName') }) MERGE (group:Group { name: UPPER('$PrimaryGroupName') }) MERGE (user)-[:MemberOf]->(group)" } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)group_memberships.csv" + # TODO: extract pwdlastset/etc. and ingest } - # TODO: extract pwdlastset/etc. and ingest - } - elseif(($Object.PSObject.TypeNames -contains 'PowerView.LocalUserAPI') -or ($Object.PSObject.TypeNames -contains 'PowerView.LocalUser')) { - $AccountName = $($Object.AccountName.replace('/', '\')).split('\')[-1] + elseif(($Object.PSObject.TypeNames -contains 'PowerView.LocalUserAPI') -or ($Object.PSObject.TypeNames -contains 'PowerView.LocalUser')) { + $AccountName = $($Object.AccountName.replace('/', '\')).split('\')[-1] - $MemberSimpleName = Convert-SidToName -SID $Object.SID | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' + $MemberSimpleName = Convert-SidToName -SID $Object.SID | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' - if($MemberSimpleName) { - $MemberDomain = $MemberSimpleName.Split('/')[0] - } - else { - $MemberDomain = "UNKNOWN" - } + if($MemberSimpleName) { + $MemberDomain = $MemberSimpleName.Split('/')[0] + } + else { + $MemberDomain = "UNKNOWN" + } - $AccountName = "$AccountName@$MemberDomain" + $AccountName = "$AccountName@$MemberDomain" - if($Object.IsGroup) { - $Properties = @{ - AccountName = $AccountName - AccountType = 'group' - ComputerName = $Object.ComputerName + if($Object.IsGroup) { + $Queries += "MERGE (group:Group { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$($Object.ComputerName)') }) MERGE (group)-[:AdminTo]->(computer)" } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)local_admin.csv" - } - else { - $Properties = @{ - AccountName = $AccountName - AccountType = 'user' - ComputerName = $Object.ComputerName + else { + $Queries += "MERGE (user:User { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$($Object.ComputerName)') }) MERGE (user)-[:AdminTo]->(computer)" } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)local_admin.csv" } - } - elseif($Object.PSObject.TypeNames -contains 'PowerView.LocalUserSpecified') { - # manually specified localgroup membership where resolution happens by the callee + elseif($Object.PSObject.TypeNames -contains 'PowerView.LocalUserSpecified') { + # manually specified localgroup membership where resolution happens by the callee - $AccountName = "$($Object.MemberName)@$($Object.MemberDomain)" + $AccountName = "$($Object.MemberName)@$($Object.MemberDomain)" - if($Object.IsGroup) { - $Properties = @{ - AccountName = $AccountName - AccountType = 'group' - ComputerName = $Object.ComputerName + if($Object.IsGroup) { + $Queries += "MERGE (group:Group { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$($Object.ComputerName)') }) MERGE (group)-[:AdminTo]->(computer)" } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)local_admin.csv" - } - else { - $Properties = @{ - AccountName = $AccountName - AccountType = 'user' - ComputerName = $Object.ComputerName + else { + $Queries += "MERGE (user:User { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$($Object.ComputerName)') }) MERGE (user)-[:AdminTo]->(computer)" } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)local_admin.csv" } - } - elseif($Object.PSObject.TypeNames -contains 'PowerView.GPOLocalGroup') { - $MemberSimpleName = Convert-SidToName -SID $Object.ObjectSID | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' + elseif($Object.PSObject.TypeNames -contains 'PowerView.GPOLocalGroup') { + $MemberSimpleName = Convert-SidToName -SID $Object.ObjectSID | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' - if($MemberSimpleName) { - $MemberDomain = $MemberSimpleName.Split('/')[0] - $AccountName = "$($Object.ObjectName)@$MemberDomain" - } - else { - $AccountName = $Object.ObjectName - } + if($MemberSimpleName) { + $MemberDomain = $MemberSimpleName.Split('/')[0] + $AccountName = "$($Object.ObjectName)@$MemberDomain" + } + else { + $AccountName = $Object.ObjectName + } - ForEach($Computer in $Object.ComputerName) { - if($Object.IsGroup) { - $Properties = @{ - AccountName = $AccountName - AccountType = 'group' - ComputerName = $Computer + ForEach($Computer in $Object.ComputerName) { + if($Object.IsGroup) { + $Queries += "MERGE (group:Group { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$Computer') }) MERGE (group)-[:AdminTo]->(computer)" } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)local_admin.csv" + else { + $Queries += "MERGE (user:User { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$Computer') }) MERGE (user)-[:AdminTo]->(computer)" + } + } + } + elseif($Object.PSObject.TypeNames -contains 'PowerView.DomainTrustLDAP') { + # [uint32]'0x00000001' = 'non_transitive' + # [uint32]'0x00000002' = 'uplevel_only' + # [uint32]'0x00000004' = 'quarantined_domain' + # [uint32]'0x00000008' = 'forest_transitive' + # [uint32]'0x00000010' = 'cross_organization' + # [uint32]'0x00000020' = 'within_forest' + # [uint32]'0x00000040' = 'treat_as_external' + # [uint32]'0x00000080' = 'trust_uses_rc4_encryption' + # [uint32]'0x00000100' = 'trust_uses_aes_keys' + # [uint32]'0x00000200' = 'cross_organization_no_tgt_delegation' + # [uint32]'0x00000400' = 'pim_trust' + if($Object.SourceDomain) { + $SourceDomain = $Object.SourceDomain } else { - $Properties = @{ - AccountName = $AccountName - AccountType = 'user' - ComputerName = $Computer + $SourceDomain = $Object.SourceName + } + if($Object.TargetDomain) { + $TargetDomain = $Object.TargetDomain + } + else { + $TargetDomain = $Object.TargetName + } + + $Query = "MERGE (SourceDomain:Domain { name: UPPER('$SourceDomain') }) MERGE (TargetDomain:Domain { name: UPPER('$TargetDomain') })" + + if($Object.TrustType -match 'cross_organization') { + $TrustType = 'CrossLink' + } + elseif ($Object.TrustType -match 'within_forest') { + $TrustType = 'ParentChild' + } + elseif ($Object.TrustType -match 'forest_transitive') { + $TrustType = 'Forest' + } + elseif ($Object.TrustType -match 'treat_as_external') { + $TrustType = 'External' + } + else { + Write-Verbose "Trust type unhandled/unknown: $($Object.TrustType)" + $TrustType = 'Unknown' + } + + if ($Object.TrustType -match 'non_transitive') { + $Transitive = $False + } + else { + $Transitive = $True + } + + Switch ($Object.TrustDirection) { + 'Inbound' { + $Query += " MERGE (TargetDomain)-[:TrustedBy{ TrustType: UPPER('$TrustType'), Transitive: UPPER('$Transitive')}]->(SourceDomain)" + } + 'Outbound' { + $Query += " MERGE (SourceDomain)-[:TrustedBy{ TrustType: UPPER('$TrustType'), Transitive: UPPER('$Transitive')}]->(TargetDomain)" + } + 'Bidirectional' { + $Query += " MERGE (TargetDomain)-[:TrustedBy{ TrustType: UPPER('$TrustType'), Transitive: UPPER('$Transitive')}]->(SourceDomain) MERGE (SourceDomain)-[:TrustedBy{ TrustType: UPPER('$TrustType'), Transitive: UPPER('$Transitive')}]->(TargetDomain)" } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)local_admin.csv" } + $Queries += $Query } - } - elseif($Object.PSObject.TypeNames -contains 'PowerView.DomainTrustLDAP') { - # [uint32]'0x00000001' = 'non_transitive' - # [uint32]'0x00000002' = 'uplevel_only' - # [uint32]'0x00000004' = 'quarantined_domain' - # [uint32]'0x00000008' = 'forest_transitive' - # [uint32]'0x00000010' = 'cross_organization' - # [uint32]'0x00000020' = 'within_forest' - # [uint32]'0x00000040' = 'treat_as_external' - # [uint32]'0x00000080' = 'trust_uses_rc4_encryption' - # [uint32]'0x00000100' = 'trust_uses_aes_keys' - # [uint32]'0x00000200' = 'cross_organization_no_tgt_delegation' - # [uint32]'0x00000400' = 'pim_trust' - if($Object.SourceDomain) { - $SourceDomain = $Object.SourceDomain - } - else { - $SourceDomain = $Object.SourceName - } - if($Object.TargetDomain) { - $TargetDomain = $Object.TargetDomain - } - else { - $TargetDomain = $Object.TargetName - } + elseif($Object.PSObject.TypeNames -contains 'PowerView.DomainTrust') { + if($Object.SourceDomain) { + $SourceDomain = $Object.SourceDomain + } + else { + $SourceDomain = $Object.SourceName + } + if($Object.TargetDomain) { + $TargetDomain = $Object.TargetDomain + } + else { + $TargetDomain = $Object.TargetName + } - $TrustType = Switch ($Object.TrustType) { - 'cross_organization' { 'CrossLink' } - 'within_forest' { 'ParentChild' } - 'forest_transitive' { 'Forest' } - 'treat_as_external' { 'External' } - 'Default' { 'Unknown' } - } + $Query = "MERGE (SourceDomain:Domain { name: UPPER('$SourceDomain') }) MERGE (TargetDomain:Domain { name: UPPER('$TargetDomain') })" - if ($Object.TrustType -match 'non_transitive') { - $Transitive = $False - } - else { + $TrustType = $Object.TrustType $Transitive = $True - } - $Properties = @{ - SourceDomain = $SourceDomain - TargetDomain = $TargetDomain - TrustDirection = $Object.TrustDirection - TrustType = $TrustType - Transitive = "$Transitive" + Switch ($Object.TrustDirection) { + 'Inbound' { + $Query += " MERGE (SourceDomain)-[:TrustedBy{ TrustType: UPPER('$TrustType'), Transitive: UPPER('$Transitive')}]->(TargetDomain)" + } + 'Outbound' { + $Query += " MERGE (TargetDomain)-[:TrustedBy{ TrustType: UPPER('$TrustType'), Transitive: UPPER('$Transitive')}]->(SourceDomain)" + } + 'Bidirectional' { + $Query += " MERGE (TargetDomain)-[:TrustedBy{ TrustType: UPPER('$TrustType'), Transitive: UPPER('$Transitive')}]->(SourceDomain) MERGE (SourceDomain)-[:TrustedBy{ TrustType: UPPER('$TrustType'), Transitive: UPPER('$Transitive')}]->(TargetDomain)" + } + } + $Queries += $Query } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)trusts.csv" - } - elseif($Object.PSObject.TypeNames -contains 'PowerView.DomainTrust') { - if($Object.SourceDomain) { - $SourceDomain = $Object.SourceDomain + elseif($Object.PSObject.TypeNames -contains 'PowerView.ForestTrust') { + if($Object.SourceDomain) { + $SourceDomain = $Object.SourceDomain + } + else { + $SourceDomain = $Object.SourceName + } + if($Object.TargetDomain) { + $TargetDomain = $Object.TargetDomain + } + else { + $TargetDomain = $Object.TargetName + } + + $Query = "MERGE (SourceDomain:Domain { name: UPPER('$SourceDomain') }) MERGE (TargetDomain:Domain { name: UPPER('$TargetDomain') })" + + $TrustType = 'Forest' + $Transitive = $True + + Switch ($Object.TrustDirection) { + 'Inbound' { + $Query += " MERGE (SourceDomain)-[:TrustedBy{ TrustType: UPPER('FOREST'), Transitive: UPPER('$Transitive')}]->(TargetDomain)" + } + 'Outbound' { + $Query += " MERGE (TargetDomain)-[:TrustedBy{ TrustType: UPPER('FOREST'), Transitive: UPPER('$Transitive')}]->(SourceDomain)" + } + 'Bidirectional' { + $Query += " MERGE (TargetDomain)-[:TrustedBy{ TrustType: UPPER('FOREST'), Transitive: UPPER('$Transitive')}]->(SourceDomain) MERGE (SourceDomain)-[:TrustedBy{ TrustType: UPPER('FOREST'), Transitive: UPPER('$Transitive')}]->(TargetDomain)" + } + } + $Queries += $Query } else { - $SourceDomain = $Object.SourceName - } - if($Object.TargetDomain) { - $TargetDomain = $Object.TargetDomain + Write-Verbose "No matching type name" } - else { - $TargetDomain = $Object.TargetName + + # built the batch object submission object for each query + ForEach($Query in $Queries) { + $BatchObject = @{ + "method" = "POST"; + "to" = "/cypher"; + "body" = @{"query"=$Query}; + } + $Null = $ObjectBuffer.Add($BatchObject) } - $Properties = @{ - SourceDomain = $SourceDomain - TargetDomain = $TargetDomain - TrustDirection = $Object.TrustDirection - TrustType = $Object.TrustType - Transitive = "$True" + if ($ObjectBuffer.Count -ge $Throttle) { + $JsonRequest = ConvertTo-Json20 $ObjectBuffer + $Null = $WebClient.UploadString($URI.AbsoluteUri + "db/data/batch", $JsonRequest) + $ObjectBuffer.Clear() } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)trusts.csv" - } - elseif($Object.PSObject.TypeNames -contains 'PowerView.ForestTrust') { - if($Object.SourceDomain) { - $SourceDomain = $Object.SourceDomain - } - else { - $SourceDomain = $Object.SourceName - } - if($Object.TargetDomain) { - $TargetDomain = $Object.TargetDomain - } - else { - $TargetDomain = $Object.TargetName - } - - $Properties = @{ - SourceDomain = $SourceDomain - TargetDomain = $TargetDomain - TrustDirection = $Object.TrustDirection - TrustType = 'Forest' - Transitive = "$True" - } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)trusts.csv" } else { - Write-Verbose "No matching type name" + throw 'Not authorized' + } + } + end { + if($Authorized) { + $JsonRequest = ConvertTo-Json20 $ObjectBuffer + $Null = $WebClient.UploadString($URI.AbsoluteUri + "db/data/batch", $JsonRequest) + $ObjectBuffer.Clear() } } } -function Get-BloodHoundData { +function Export-BloodHoundCSV { <# .SYNOPSIS - This function queries the domain for all active machines with - Get-NetComputer, then for each server it queries the local - Administrators with Get-NetLocalGroup and the users/sessions with - Get-NetSession/Get-NetLoggedOn. It will return only domain localgroup - data. + Takes input from Get-BloodHound data and exports the objects to one custom CSV file + per object type (sessions, local admin, domain trusts, etc.). Author: @harmj0y License: BSD 3-Clause Required Dependencies: PowerView.ps1 Optional Dependencies: None - .PARAMETER ComputerName + .DESCRIPTION - Host array to enumerate, passable on the pipeline. + This function takes custom tagged PowerView objects types from Get-BloodHoundData and exports + the data to one custom CSV file per object type (sessions, local admin, domain trusts, etc.). + For user session data without a logon domain, by default the global catalog is used to attempt to + deconflict what domain the user may be located in. If the user exists in more than one domain in + the forest, a series of weights is used to modify the attack path likelihood. - .PARAMETER Domain + .PARAMETER Object - Domain to query for machines, defaults to the current domain. + The PowerView PSObject to export to csv. - .PARAMETER DomainController + .PARAMETER CSVFolder - Domain controller to reflect LDAP queries through. + The folder to output all CSVs to, defaults to the current working directory. - .PARAMETER CollectionMethod + .PARAMETER CSVPrefix - The method to collect data. 'Group', 'LocalGroup', 'GPOLocalGroup', 'Sesssion', 'LoggedOn', 'Trusts, 'TrustsLDAP', 'Stealth', or 'Default'. - 'TrustsLDAP' uses LDAP enumeration for trusts, while 'Trusts' using .NET methods. - 'Stealth' uses 'Group' collection, stealth user hunting ('Session' on certain servers), 'GPOLocalGroup' enumeration, and LDAP trust enumeration. - 'Default' uses 'Group' collection, regular user hunting with 'Session'/'LoggedOn', 'LocalGroup' enumeration, and 'Trusts' enumeration. + A prefix to append to each CSV file. - .PARAMETER SearchForest + .PARAMETER SkipGCDeconfliction - Switch. Search all domains in the forest for target users instead of just - a single domain. + Switch. Don't resolve user domain memberships for session information using a global catalog. - .PARAMETER Threads + .PARAMETER GlobalCatalog - The maximum concurrent threads to execute. + The global catalog location to resole user memberships from, form of GC://global.catalog. + + .EXAMPLE + + PS C:\> Get-BloodHoundData | Export-BloodHoundCSV + Executes default collection options and exports the data to user_sessions.csv, group_memberships.csv, + local_admin.csv, and trusts.csv in the current directory. + .EXAMPLE - PS C:\> Get-BloodHoundData | Export-BloodHoundData -URI http://host:80/ -UserPass "user:pass" + PS C:\> Get-BloodHoundData | Export-BloodHoundCSV -SkipGCDeconfliction + + Executes default collection options, skips the global catalog deconfliction, and exports the data + to user_sessions.csv, group_memberships.csv, local_admin.csv, and trusts.csv in the current directory. + + .EXAMPLE + + PS C:\> Get-BloodHoundData | Export-BloodHoundCSV -CSVFolder C:\Temp\ -CSVPrefix "domainX" + + Executes default collection options and exports the data to domainX_user_sessions.csv, domainX_group_memberships.csv, + domainX_local_admin.csv, and tdomainX_rusts.csv in C:\Temp\. + + .NOTES + + CSV file types: + + PowerView.UserSession -> $($CSVExportPrefix)user_sessions.csv + UserName,ComputerName,Weight + "john@domain.local","computer2.domain.local",1 + + PowerView.GroupMember/PowerView.User -> $($CSVExportPrefix)group_memberships.csv + AccountName,AccountType,GroupName + "john@domain.local","user","GROUP1" + "computer3.testlab.local","computer","GROUP1" + + PowerView.LocalUserAPI/PowerView.GPOLocalGroup -> $($CSVExportPrefix)local_admin.csv + AccountName,AccountType,ComputerName + "john@domain.local","user","computer2.domain.local" + + PowerView.DomainTrustLDAP/PowerView.DomainTrust/PowerView.ForestTrust -> $($CSVExportPrefix)trusts.csv + SourceDomain,TargetDomain,TrustDirection,TrustType,Transitive + "domain.local","dev.domain.local","Bidirectional","ParentChild","True" #> - [CmdletBinding(DefaultParameterSetName = 'None')] + [CmdletBinding()] param( - [Parameter(Position=0, ValueFromPipeline=$True)] - [Alias('Hosts')] - [String[]] - $ComputerName, - - [String] - $Domain, + [Parameter(Position = 0, ValueFromPipeline = $True, Mandatory = $True)] + [PSObject] + $Object, + [Parameter()] + [ValidateScript({ Test-Path -Path $_ })] [String] - $DomainController, + $CSVFolder = $(Get-Location), + [Parameter()] + [ValidateNotNullOrEmpty()] [String] - [ValidateSet('Group', 'LocalGroup', 'GPOLocalGroup', 'Session', 'LoggedOn', 'Stealth', 'Trusts', 'TrustsLDAP', 'Default')] - $CollectionMethod = 'Default', + $CSVPrefix, [Switch] - $SearchForest, + $SkipGCDeconfliction, - [ValidateRange(1,100)] - [Int] - $Threads + [ValidatePattern('^GC://')] + [String] + $GlobalCatalog ) - begin { - - Switch ($CollectionMethod) { - 'Group' { $UseGroup = $True; $SkipComputerEnumeration = $True } - 'LocalGroup' { $UseLocalGroup = $True } - 'GPOLocalGroup' { $UseGPOGroup = $True; $SkipComputerEnumeration = $True } - 'Session' { $UseSession = $True } - 'LoggedOn' { $UseLoggedOn = $True } - 'TrustsLDAP' { $UseDomainTrustsLDAP = $True; $SkipComputerEnumeration = $True } - 'Trusts' { $UseDomainTrusts = $True; $SkipComputerEnumeration = $True } - 'Stealth' { - $UseGroup = $True - $UseGPOGroup = $True - $UseSession = $True - $UseDomainTrustsLDAP = $True - } - 'Default' { - $UseGroup = $True - $UseLocalGroup = $True - $UseSession = $True - $UseLoggedOn = $True - $UseDomainTrusts = $True - } + BEGIN { + try { + $OutputFolder = $CSVFolder | Resolve-Path -ErrorAction Stop | Select-Object -ExpandProperty Path } - - if($Domain) { - $TargetDomains = @($Domain) + catch { + throw "Error: $_" } - elseif($SearchForest) { - # get ALL the domains in the forest to search - $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } + + if($CSVPrefix) { + $CSVExportPrefix = "$($CSVPrefix)_" } else { - # use the local domain - $TargetDomains = @( (Get-NetDomain).name ) + $CSVExportPrefix = '' } - if($UseGroup) { - ForEach ($TargetDomain in $TargetDomains) { - # enumerate all groups and all members of each group - Get-NetGroup -Domain $Domain -DomainController $DomainController | Get-NetGroupMember -Domain $Domain -DomainController $DomainController -FullData + $UserDomainMappings = @{} + if(-not $SkipGCDeconfliction) { + # if we're doing session enumeration, create a {user : @(domain,..)} from a global catalog + # in order to do user domain deconfliction for sessions + if(-not $PSBoundParameters['GlobalCatalog']) { + $ForestRoot = Get-NetForest | Select-Object -ExpandProperty RootDomain + $ADSPath = "GC://$ForestRoot" + Write-Verbose "Global catalog string from enumerated forest root: $ADSPath" + } + else { + $ADSpath = $GlobalCatalog + } - # enumerate all user objects so we can extract out the primary group for each - Get-NetUser -Domain $Domain -DomainController $DomainController + Get-NetUser -ADSPath $ADSpath | ForEach-Object { + $UserName = $_.samaccountname.ToUpper() + $UserDN = $_.distinguishedname + + if (($UserDN -match 'ForeignSecurityPrincipals') -and ($UserDN -match 'S-1-5-21')) { + try { + if(-not $MemberSID) { + $MemberSID = $_.cn[0] + } + $MemberSimpleName = Convert-SidToName -SID $_.objectsid | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' + if($MemberSimpleName) { + $UserDomain = $MemberSimpleName.Split('/')[0] + } + else { + Write-Verbose "Error converting $UserDN" + $UserDomain = $Null + } + } + catch { + Write-Verbose "Error converting $UserDN" + $UserDomain = $Null + } + } + else { + # extract the FQDN from the Distinguished Name + $UserDomain = ($UserDN.subString($UserDN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.').ToUpper() + } + + if(-not $UserDomainMappings[$UserName]) { + $UserDomainMappings[$UserName] = @($UserDomain) + } + elseif($UserDomainMappings[$UserName] -notcontains $UserDomain) { + $UserDomainMappings[$UserName] += $UserDomain + } } } + } - if($UseDomainTrusts) { - Invoke-MapDomainTrust - } + PROCESS { - if($UseDomainTrustsLDAP) { - Invoke-MapDomainTrust -LDAP -DomainController $DomainController - } + if($Object.PSObject.TypeNames -contains 'PowerView.UserSession') { - if (-not $SkipComputerEnumeration) { - if(-not $ComputerName) { - [Array]$TargetComputers = @() + if($Object.SessionFromName) { + try { + # $SessionFromDomain = $Object.SessionFromName.SubString($Object.SessionFromName.IndexOf('.')+1) + $UserName = $Object.UserName.ToUpper() + $SessionFromName = $Object.SessionFromName - ForEach ($Domain2 in $TargetDomains) { - if($CollectionMethod -eq 'Stealth') { - Write-Verbose "Querying domain $Domain2 for File Servers..." - $TargetComputers += Get-NetFileServer -Domain $Domain2 -DomainController $DomainController + if($UserDomainMappings) { + $UserDomain = $Null + if($UserDomainMappings[$UserName]) { + if($UserDomainMappings[$UserName].Count -eq 1) { + $UserDomain = $UserDomainMappings[$UserName] + $LoggedOnUser = "$UserName@$UserDomain" - Write-Verbose "Querying domain $Domain2 for DFS Servers..." - $TargetComputers += Get-DFSshare -Domain $Domain2 -DomainController $DomainController | ForEach-Object {$_.RemoteServerName} + $Properties = @{ + UserName = $LoggedOnUser + ComputerName = $SessionFromName + Weight = 1 + } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)user_sessions.csv" + } + else { + $ComputerDomain = $Object.SessionFromName.SubString($Object.SessionFromName.IndexOf('.')+1).ToUpper() - Write-Verbose "Querying domain $Domain2 for Domain Controllers..." - $TargetComputers += Get-NetDomainController -LDAP -Domain $Domain2 -DomainController $DomainController | ForEach-Object { $_.dnshostname} + $UserDomainMappings[$UserName] | ForEach-Object { + if($_ -eq $ComputerDomain) { + $UserDomain = $_ + $LoggedOnUser = "$UserName@$UserDomain" + + $Properties = @{ + UserName = $LoggedOnUser + ComputerName = $SessionFromName + Weight = 1 + } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)user_sessions.csv" + } + else { + $UserDomain = $_ + $LoggedOnUser = "$UserName@$UserDomain" + + $Properties = @{ + UserName = $LoggedOnUser + ComputerName = $SessionFromName + Weight = 2 + } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)user_sessions.csv" + } + } + } + } + else { + # no user object in the GC with this username + $LoggedOnUser = "$UserName@UNKNOWN" + + $Properties = @{ + UserName = $LoggedOnUser + ComputerName = $SessionFromName + Weight = 2 + } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)user_sessions.csv" + } } else { - Write-Verbose "Querying domain $Domain2 for hosts" + $LoggedOnUser = "$UserName@UNKNOWN" + # $Queries += "MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '2'}]->(user)" + $Properties = @{ + UserName = $LoggedOnUser + ComputerName = $SessionFromName + Weight = 2 + } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)user_sessions.csv" + } + } + catch { + Write-Warning "Error extracting domain from $($Object.SessionFromName)" + } + } + elseif($Object.SessionFrom) { + $Properties = @{ + UserName = "$($Object.UserName)@UNKNOWN" + ComputerName = $Object.SessionFrom + Weight = 2 + } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)user_sessions.csv" + } + else { + # assume Get-NetLoggedOn result + try { + $MemberSimpleName = "$($Object.UserDomain)\$($Object.UserName)" | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' - $TargetComputers += Get-NetComputer -Domain $Domain2 -DomainController $DomainController + if($MemberSimpleName) { + $MemberDomain = $MemberSimpleName.Split('/')[0] + $AccountName = "$($Object.UserName)@$MemberDomain" + } + else { + $AccountName = "$($Object.UserName)@UNKNOWN" } - if($UseGPOGroup) { - Write-Verbose "Enumerating GPO local group memberships for domain $Domain2" - Find-GPOLocation -Domain $Domain2 -DomainController $DomainController + $Properties = @{ + UserName = $AccountName + ComputerName = $Object.ComputerName + Weight = 1 + } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)user_sessions.csv" + } + catch { + Write-Verbose "Error converting $($Object.UserDomain)\$($Object.UserName)" + } + } + } + elseif($Object.PSObject.TypeNames -contains 'PowerView.GroupMember') { + $AccountName = "$($Object.MemberName)@$($Object.MemberDomain)" - # add in a local administrator relationship for "Domain Admins" -> every machine - $DomainSID = Get-DomainSID -Domain $Domain2 -DomainController $DomainController - $DomainAdminsSid = "$DomainSID-512" - $Temp = Convert-SidToName -SID $DomainAdminsSid - $DomainAdminsName = $Temp.Split('\')[1] + if ($Object.MemberName -Match "\\") { + # if the membername itself contains a backslash, get the trailing section + # TODO: later preserve this once BloodHound can properly display these characters + $AccountName = $($Object.Membername).split('\')[1] + '@' + $($Object.MemberDomain) + } - Get-NetComputer -Domain $Domain2 -DomainController $DomainController | ForEach-Object { - $LocalUser = New-Object PSObject - $LocalUser | Add-Member Noteproperty 'MemberName' $DomainAdminsName - $LocalUser | Add-Member Noteproperty 'MemberDomain' $Domain2 - $LocalUser | Add-Member Noteproperty 'IsGroup' $True - $LocalUser | Add-Member Noteproperty 'ComputerName' $_ - $LocalUser.PSObject.TypeNames.Add('PowerView.LocalUserSpecified') - $LocalUser - } + $GroupName = "$($Object.GroupName)@$($Object.GroupDomain)" + + if($Object.IsGroup) { + $Properties = @{ + AccountName = $AccountName + AccountType = 'group' + GroupName = $GroupName + } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)group_memberships.csv" + } + else { + # check if -FullData objects are returned, and if so check if the group member is a computer object + if($Object.ObjectClass -and ($Object.ObjectClass -contains 'computer')) { + $Properties = @{ + AccountName = $Object.dnshostname + AccountType = 'computer' + GroupName = $GroupName } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)group_memberships.csv" } + else { + # otherwise there's no way to determine if this is a computer object or not + $Properties = @{ + AccountName = $AccountName + AccountType = 'user' + GroupName = $GroupName + } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)group_memberships.csv" + } + } + } + elseif($Object.PSObject.TypeNames -Contains 'PowerView.User') { + $AccountDomain = $Object.Domain + $AccountName = "$($Object.SamAccountName)@$AccountDomain" - # remove any null target hosts, uniquify the list and shuffle it - $TargetComputers = $TargetComputers | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } - if($($TargetComputers.Count) -eq 0) { - Write-Warning "No hosts found!" + if($Object.PrimaryGroupName -and ($Object.PrimaryGroupName -ne '')) { + $PrimaryGroupName = "$($Object.PrimaryGroupName)@$AccountDomain" + + $Properties = @{ + AccountName = $AccountName + AccountType = 'user' + GroupName = $PrimaryGroupName } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)group_memberships.csv" + } + # TODO: extract pwdlastset/etc. and ingest + } + elseif(($Object.PSObject.TypeNames -contains 'PowerView.LocalUserAPI') -or ($Object.PSObject.TypeNames -contains 'PowerView.LocalUser')) { + $AccountName = $($Object.AccountName.replace('/', '\')).split('\')[-1] + + $MemberSimpleName = Convert-SidToName -SID $Object.SID | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' + + if($MemberSimpleName) { + $MemberDomain = $MemberSimpleName.Split('/')[0] + } + else { + $MemberDomain = "UNKNOWN" + } + + $AccountName = "$AccountName@$MemberDomain" + + if($Object.IsGroup) { + $Properties = @{ + AccountName = $AccountName + AccountType = 'group' + ComputerName = $Object.ComputerName + } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)local_admin.csv" } else { - $TargetComputers = $ComputerName + $Properties = @{ + AccountName = $AccountName + AccountType = 'user' + ComputerName = $Object.ComputerName + } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)local_admin.csv" } } + elseif($Object.PSObject.TypeNames -contains 'PowerView.LocalUserSpecified') { + # manually specified localgroup membership where resolution happens by the callee - # get the current user so we can ignore it in the results - $CurrentUser = ([Environment]::UserName).toLower() - - # script block that enumerates a server - $HostEnumBlock = { - param($ComputerName, $Ping, $CurrentUser2, $UseLocalGroup2, $UseSession2, $UseLoggedon2) + $AccountName = "$($Object.MemberName)@$($Object.MemberDomain)" - $Up = $True - if($Ping) { - $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName + if($Object.IsGroup) { + $Properties = @{ + AccountName = $AccountName + AccountType = 'group' + ComputerName = $Object.ComputerName + } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)local_admin.csv" } - if($Up) { - - if($UseLocalGroup2) { - # grab the users for the local admins on this server - Get-NetLocalGroup -ComputerName $ComputerName -API | Where-Object {$_.IsDomain} + else { + $Properties = @{ + AccountName = $AccountName + AccountType = 'user' + ComputerName = $Object.ComputerName } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)local_admin.csv" + } + } + elseif($Object.PSObject.TypeNames -contains 'PowerView.GPOLocalGroup') { + $MemberSimpleName = Convert-SidToName -SID $Object.ObjectSID | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' - $IPAddress = @(Get-IPAddress -ComputerName $ComputerName)[0].IPAddress - - if($UseSession2) { - $Sessions = Get-NetSession -ComputerName $ComputerName - ForEach ($Session in $Sessions) { - $UserName = $Session.sesi10_username - $CName = $Session.sesi10_cname - - if($CName -and $CName.StartsWith("\\")) { - $CName = $CName.TrimStart("\") - } - - # make sure we have a result - if (($UserName) -and ($UserName.trim() -ne '') -and ($UserName -notmatch '\$') -and ($UserName -notmatch $CurrentUser2)) { - - $FoundUser = New-Object PSObject - $FoundUser | Add-Member Noteproperty 'UserDomain' $Null - $FoundUser | Add-Member Noteproperty 'UserName' $UserName - $FoundUser | Add-Member Noteproperty 'ComputerName' $ComputerName - $FoundUser | Add-Member Noteproperty 'IPAddress' $IPAddress - $FoundUser | Add-Member Noteproperty 'SessionFrom' $CName + if($MemberSimpleName) { + $MemberDomain = $MemberSimpleName.Split('/')[0] + $AccountName = "$($Object.ObjectName)@$MemberDomain" + } + else { + $AccountName = $Object.ObjectName + } - # Try to resolve the DNS hostname of $Cname - try { - $CNameDNSName = [System.Net.Dns]::GetHostEntry($CName) | Select-Object -ExpandProperty HostName - } - catch { - $CNameDNSName = $CName - } - $FoundUser | Add-Member NoteProperty 'SessionFromName' $CNameDNSName - $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Null - $FoundUser.PSObject.TypeNames.Add('PowerView.UserSession') - $FoundUser - } + ForEach($Computer in $Object.ComputerName) { + if($Object.IsGroup) { + $Properties = @{ + AccountName = $AccountName + AccountType = 'group' + ComputerName = $Computer } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)local_admin.csv" } - - if($UseLoggedon2) { - $LoggedOn = Get-NetLoggedon -ComputerName $ComputerName - ForEach ($User in $LoggedOn) { - $UserName = $User.wkui1_username - $UserDomain = $User.wkui1_logon_domain - - # ignore local account logons - # TODO: better way to determine if network logon or not - if($ComputerName -notmatch "^$UserDomain") { - if (($UserName) -and ($UserName.trim() -ne '') -and ($UserName -notmatch '\$')) { - $FoundUser = New-Object PSObject - $FoundUser | Add-Member Noteproperty 'UserDomain' $UserDomain - $FoundUser | Add-Member Noteproperty 'UserName' $UserName - $FoundUser | Add-Member Noteproperty 'ComputerName' $ComputerName - $FoundUser | Add-Member Noteproperty 'IPAddress' $IPAddress - $FoundUser | Add-Member Noteproperty 'SessionFrom' $Null - $FoundUser | Add-Member Noteproperty 'SessionFromName' $Null - $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Null - $FoundUser.PSObject.TypeNames.Add('PowerView.UserSession') - $FoundUser - } - } - } - - $LocalLoggedOn = Get-LoggedOnLocal -ComputerName $ComputerName - ForEach ($User in $LocalLoggedOn) { - $UserName = $User.UserName - $UserDomain = $User.UserDomain - - # ignore local account logons ? - if($ComputerName -notmatch "^$UserDomain") { - $FoundUser = New-Object PSObject - $FoundUser | Add-Member Noteproperty 'UserDomain' $UserDomain - $FoundUser | Add-Member Noteproperty 'UserName' $UserName - $FoundUser | Add-Member Noteproperty 'ComputerName' $ComputerName - $FoundUser | Add-Member Noteproperty 'IPAddress' $IPAddress - $FoundUser | Add-Member Noteproperty 'SessionFrom' $Null - $FoundUser | Add-Member Noteproperty 'SessionFromName' $Null - $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Null - $FoundUser.PSObject.TypeNames.Add('PowerView.UserSession') - $FoundUser - } + else { + $Properties = @{ + AccountName = $AccountName + AccountType = 'user' + ComputerName = $Computer } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)local_admin.csv" } } } - } - - process { - if (-not $SkipComputerEnumeration) { - if($Threads) { - Write-Verbose "Using threading with threads = $Threads" + elseif($Object.PSObject.TypeNames -contains 'PowerView.DomainTrustLDAP') { + # [uint32]'0x00000001' = 'non_transitive' + # [uint32]'0x00000002' = 'uplevel_only' + # [uint32]'0x00000004' = 'quarantined_domain' + # [uint32]'0x00000008' = 'forest_transitive' + # [uint32]'0x00000010' = 'cross_organization' + # [uint32]'0x00000020' = 'within_forest' + # [uint32]'0x00000040' = 'treat_as_external' + # [uint32]'0x00000080' = 'trust_uses_rc4_encryption' + # [uint32]'0x00000100' = 'trust_uses_aes_keys' + # [uint32]'0x00000200' = 'cross_organization_no_tgt_delegation' + # [uint32]'0x00000400' = 'pim_trust' + if($Object.SourceDomain) { + $SourceDomain = $Object.SourceDomain + } + else { + $SourceDomain = $Object.SourceName + } + if($Object.TargetDomain) { + $TargetDomain = $Object.TargetDomain + } + else { + $TargetDomain = $Object.TargetName + } - # if we're using threading, kick off the script block with Invoke-ThreadedFunction - $ScriptParams = @{ - 'Ping' = $True - 'CurrentUser2' = $CurrentUser - 'UseLocalGroup2' = $UseLocalGroup - 'UseSession2' = $UseSession - 'UseLoggedon2' = $UseLoggedon - } + $TrustType = Switch ($Object.TrustType) { + 'cross_organization' { 'CrossLink' } + 'within_forest' { 'ParentChild' } + 'forest_transitive' { 'Forest' } + 'treat_as_external' { 'External' } + 'Default' { 'Unknown' } + } - Invoke-ThreadedFunction -ComputerName $TargetComputers -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads + if ($Object.TrustType -match 'non_transitive') { + $Transitive = $False + } + else { + $Transitive = $True } + $Properties = @{ + SourceDomain = $SourceDomain + TargetDomain = $TargetDomain + TrustDirection = $Object.TrustDirection + TrustType = $TrustType + Transitive = "$Transitive" + } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)trusts.csv" + } + elseif($Object.PSObject.TypeNames -contains 'PowerView.DomainTrust') { + if($Object.SourceDomain) { + $SourceDomain = $Object.SourceDomain + } else { - if($TargetComputers.Count -ne 1) { - # ping all hosts in parallel - $Ping = {param($ComputerName2) if(Test-Connection -ComputerName $ComputerName2 -Count 1 -Quiet -ErrorAction Stop) { $ComputerName2 }} - $TargetComputers2 = Invoke-ThreadedFunction -NoImports -ComputerName $TargetComputers -ScriptBlock $Ping -Threads 100 - } - else { - $TargetComputers2 = $TargetComputers - } + $SourceDomain = $Object.SourceName + } + if($Object.TargetDomain) { + $TargetDomain = $Object.TargetDomain + } + else { + $TargetDomain = $Object.TargetName + } - Write-Verbose "[*] Total number of active hosts: $($TargetComputers2.count)" - $Counter = 0 + $Properties = @{ + SourceDomain = $SourceDomain + TargetDomain = $TargetDomain + TrustDirection = $Object.TrustDirection + TrustType = $Object.TrustType + Transitive = "$True" + } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)trusts.csv" + } + elseif($Object.PSObject.TypeNames -contains 'PowerView.ForestTrust') { + if($Object.SourceDomain) { + $SourceDomain = $Object.SourceDomain + } + else { + $SourceDomain = $Object.SourceName + } + if($Object.TargetDomain) { + $TargetDomain = $Object.TargetDomain + } + else { + $TargetDomain = $Object.TargetName + } - $TargetComputers2 | ForEach-Object { - $Counter = $Counter + 1 - Write-Verbose "[*] Enumerating server $($_) ($Counter of $($TargetComputers2.count))" - Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList @($_, $False, $CurrentUser, $UseLocalGroup, $UseSession, $UseLoggedon) - } + $Properties = @{ + SourceDomain = $SourceDomain + TargetDomain = $TargetDomain + TrustDirection = $Object.TrustDirection + TrustType = 'Forest' + Transitive = "$True" } + New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)trusts.csv" + } + else { + Write-Verbose "No matching type name" } } }