diff options
Diffstat (limited to 'Recon/PowerView.ps1')
-rwxr-xr-x | Recon/PowerView.ps1 | 1802 |
1 files changed, 1703 insertions, 99 deletions
diff --git a/Recon/PowerView.ps1 b/Recon/PowerView.ps1 index 487ed09..89557ee 100755 --- a/Recon/PowerView.ps1 +++ b/Recon/PowerView.ps1 @@ -731,6 +731,363 @@ New-Struct. :P # ######################################################## +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 @@ -752,6 +1109,10 @@ is parsed, and then the connection is destroyed with Remove-RemoteConnection. 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 @@ -763,6 +1124,12 @@ 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 @@ -800,7 +1167,10 @@ https://blogs.technet.microsoft.com/heyscriptingguy/2011/08/20/use-powershell-to [Management.Automation.PSCredential] [Management.Automation.CredentialAttribute()] - $Credential = [Management.Automation.PSCredential]::Empty + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $OutputObject ) BEGIN { @@ -819,12 +1189,24 @@ https://blogs.technet.microsoft.com/heyscriptingguy/2011/08/20/use-powershell-to } if (Test-Path -Path $TargetPath) { - $IniObject = @{} + if ($PSBoundParameters['OutputObject']) { + $IniObject = New-Object PSObject + } + else { + $IniObject = @{} + } Switch -Regex -File $TargetPath { "^\[(.+)\]" # Section { $Section = $matches[1].Trim() - $IniObject[$Section] = @{} + if ($PSBoundParameters['OutputObject']) { + $Section = $Section.Replace(' ', '') + $SectionObject = New-Object PSObject + $IniObject | Add-Member Noteproperty $Section $SectionObject + } + else { + $IniObject[$Section] = @{} + } $CommentCount = 0 } "^(;.*)$" # Comment @@ -832,15 +1214,29 @@ https://blogs.technet.microsoft.com/heyscriptingguy/2011/08/20/use-powershell-to $Value = $matches[1].Trim() $CommentCount = $CommentCount + 1 $Name = 'Comment' + $CommentCount - $IniObject[$Section][$Name] = $Value + 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) } - $IniObject[$Section][$Name] = $Values + + # 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 @@ -2751,13 +3147,22 @@ A custom PSObject with LDAP hashtable properties translated. $Properties.PropertyNames | ForEach-Object { if ($_ -ne 'adspath') { if (($_ -eq 'objectsid') -or ($_ -eq 'sidhistory')) { - # convert the SID to a string - $ObjectProperties[$_] = (New-Object System.Security.Principal.SecurityIdentifier($Properties[$_][0], 0)).Value + # 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 @@ -2774,6 +3179,14 @@ A custom PSObject with LDAP hashtable properties translated. $ObjectProperties['SystemAcl'] = $Descriptor.SystemAcl } } + elseif ($_ -eq 'accountexpires') { + if ($Properties[$_][0] -gt [DateTime]::MaxValue.Ticks) { + $ObjectProperties[$_] = "NEVER" + } + else { + $ObjectProperties[$_] = [datetime]::fromfiletime($Properties[$_][0]) + } + } elseif ( ($_ -eq 'lastlogon') -or ($_ -eq 'lastlogontimestamp') -or ($_ -eq 'pwdlastset') -or ($_ -eq 'lastlogoff') -or ($_ -eq 'badPasswordTime') ) { # convert timestamps if ($Properties[$_][0] -is [System.MarshalByRefObject]) { @@ -4178,19 +4591,16 @@ Finds user/group/computer objects in AD that have 'outlier' properties set. Author: Will Schroeder (@harmj0y), Matthew Graeber (@mattifestation) License: BSD 3-Clause -Required Dependencies: Get-Domain, Get-DomainUser, Get-DomainGroup, Get-DomainComputer, Get-ForestSchemaClass +Required Dependencies: Get-Domain, Get-DomainUser, Get-DomainGroup, Get-DomainComputer .DESCRIPTION -Enumerates the schema for the specified -ClassName (if passed) by using Get-ForestSchemaClass. -If a -ReferenceObject is passed, the class is extracted from the passed object. -A 'reference' set of property names is then calculated, either from a standard set preserved +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. These property names are substracted -from the master schema propertyu name list to retrieve a set of 'non-standard' properties. -Every user/group/computer object (depending on determined class) are enumerated, and for each -object, if the object has a 'non-standard' property set, the object samAccountName, property -name, and property value are output to the pipeline. +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 @@ -4246,13 +4656,13 @@ for connection to the target domain. .EXAMPLE -Find-DomainObjectPropertyOutlier -User +Find-DomainObjectPropertyOutlier -ClassName 'User' Enumerates users in the current domain with 'outlier' properties filled in. .EXAMPLE -Find-DomainObjectPropertyOutlier -Group -Domain external.local +Find-DomainObjectPropertyOutlier -ClassName 'Group' -Domain external.local Enumerates groups in the external.local forest/domain with 'outlier' properties filled in. @@ -4372,7 +4782,7 @@ Custom PSObject with translated object property outliers. Write-Verbose "[Find-DomainObjectPropertyOutlier] Extracting property names from -ReferenceObject to use as the reference property set" $ReferenceObjectProperties = Get-Member -InputObject $ReferenceObject -MemberType NoteProperty | Select-Object -Expand Name $ReferenceObjectClass = $ReferenceObject.objectclass | Select-Object -Last 1 - Write-Verbose "[Find-DomainObjectPropertyOutlier] Caldulated ReferenceObjectClass : $ReferenceObjectClass" + Write-Verbose "[Find-DomainObjectPropertyOutlier] Calculated ReferenceObjectClass : $ReferenceObjectClass" } else { Write-Verbose "[Find-DomainObjectPropertyOutlier] Using the default reference property set for the object class '$ClassName'" @@ -4380,34 +4790,30 @@ Custom PSObject with translated object property outliers. if (($ClassName -eq 'User') -or ($ReferenceObjectClass -eq 'User')) { $Objects = Get-DomainUser @SearcherArguments - $SchemaClass = Get-ForestSchemaClass @SchemaArguments -ClassName 'User' - $ReferenceObjectProperties = $UserReferencePropertySet + if (-not $ReferenceObjectProperties) { + $ReferenceObjectProperties = $UserReferencePropertySet + } } elseif (($ClassName -eq 'Group') -or ($ReferenceObjectClass -eq 'Group')) { $Objects = Get-DomainGroup @SearcherArguments - $SchemaClass = Get-ForestSchemaClass @SchemaArguments -ClassName 'Group' - $ReferenceObjectProperties = $GroupReferencePropertySet + if (-not $ReferenceObjectProperties) { + $ReferenceObjectProperties = $GroupReferencePropertySet + } } elseif (($ClassName -eq 'Computer') -or ($ReferenceObjectClass -eq 'Computer')) { - Write-Verbose "COMPUTER!" $Objects = Get-DomainComputer @SearcherArguments - $SchemaClass = Get-ForestSchemaClass @SchemaArguments -ClassName 'Computer' - $ReferenceObjectProperties = $ComputerReferencePropertySet + if (-not $ReferenceObjectProperties) { + $ReferenceObjectProperties = $ComputerReferencePropertySet + } } else { throw "[Find-DomainObjectPropertyOutlier] Invalid class: $ClassName" } - $SchemaProperties = $SchemaClass | Select-Object -ExpandProperty OptionalProperties | Select-Object -ExpandProperty name - $SchemaProperties += $SchemaClass | Select-Object -ExpandProperty MandatoryProperties | Select-Object -ExpandProperty name - - # find the schema properties that are NOT in the first returned reference property set - $NonstandardProperties = Compare-Object -ReferenceObject $ReferenceObjectProperties -DifferenceObject $SchemaProperties -PassThru - ForEach ($Object in $Objects) { $ObjectProperties = Get-Member -InputObject $Object -MemberType NoteProperty | Select-Object -Expand Name ForEach($ObjectProperty in $ObjectProperties) { - if ($NonstandardProperties -Contains $ObjectProperty) { + if ($ReferenceObjectProperties -NotContains $ObjectProperty) { $Out = New-Object PSObject $Out | Add-Member Noteproperty 'SamAccountName' $Object.SamAccountName $Out | Add-Member Noteproperty 'Property' $ObjectProperty @@ -4455,6 +4861,11 @@ Wildcards accepted. Also accepts DOMAIN\user format. 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). @@ -4567,6 +4978,12 @@ Search for users with a primary group ID other than 513 ('domain users') and onl .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 @@ -4686,6 +5103,14 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. $Raw ) + 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 } @@ -4702,6 +5127,11 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. } PROCESS { + #bind dynamic parameter to a friendly variable + if ($PSBoundParameters -and ($PSBoundParameters.Count -ne 0)) { + New-DynamicParameter -CreateVariables -BoundParameters $PSBoundParameters + } + if ($UserSearcher) { $IdentityFilter = '' $Filter = '' @@ -4712,6 +5142,17 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. } 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 + $UserSearcher = Get-DomainSearcher @SearcherArguments + if (-not $UserSearcher) { + Write-Warning "[Get-DomainUser] 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 '' @@ -4767,6 +5208,19 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. $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)" + } + } + $UserSearcher.filter = "(&(samAccountType=805306368)$Filter)" Write-Verbose "[Get-DomainUser] filter string: $($UserSearcher.filter)" @@ -5509,6 +5963,11 @@ A SamAccountName (e.g. WINDOWS10$), DistinguishedName (e.g. CN=WINDOWS10,CN=Comp 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. @@ -5610,6 +6069,12 @@ 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. @@ -5723,6 +6188,14 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. $Raw ) + 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 } @@ -5739,8 +6212,12 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. } PROCESS { - if ($CompSearcher) { + #bind dynamic parameter to a friendly variable + if ($PSBoundParameters -and ($PSBoundParameters.Count -ne 0)) { + New-DynamicParameter -CreateVariables -BoundParameters $PSBoundParameters + } + if ($CompSearcher) { $IdentityFilter = '' $Filter = '' $Identity | Where-Object {$_} | ForEach-Object { @@ -5750,6 +6227,17 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. } 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))" @@ -5798,6 +6286,18 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. 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)" + } + } $CompSearcher.filter = "(&(samAccountType=805306369)$Filter)" Write-Verbose "[Get-DomainComputer] Get-DomainComputer filter string: $($CompSearcher.filter)" @@ -5858,6 +6358,11 @@ A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC= 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. @@ -6030,6 +6535,14 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. $Raw ) + 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 } @@ -6046,6 +6559,10 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. } PROCESS { + #bind dynamic parameter to a friendly variable + if ($PSBoundParameters -and ($PSBoundParameters.Count -ne 0)) { + New-DynamicParameter -CreateVariables -BoundParameters $PSBoundParameters + } if ($ObjectSearcher) { $IdentityFilter = '' $Filter = '' @@ -6056,6 +6573,17 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. } 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 '' @@ -6088,6 +6616,19 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. $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 '') { $ObjectSearcher.filter = "(&$Filter)" } @@ -6119,6 +6660,485 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. } +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 @@ -6272,13 +7292,13 @@ scriptpath [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] [CmdletBinding()] Param( - [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] [Alias('DistinguishedName', 'SamAccountName', 'Name')] [String[]] $Identity, [ValidateNotNullOrEmpty()] - [Alias('Reaplce')] + [Alias('Replace')] [Hashtable] $Set, @@ -6601,12 +7621,12 @@ System.Security.AccessControl.AuthorizationRule [Alias('DistinguishedName', 'SamAccountName', 'Name')] [String] $PrincipalIdentity, - - [ValidateNotNullOrEmpty()] + + [ValidateNotNullOrEmpty()] [String] $PrincipalDomain, - [ValidateNotNullOrEmpty()] + [ValidateNotNullOrEmpty()] [Alias('DomainController')] [String] $Server, @@ -6625,8 +7645,8 @@ System.Security.AccessControl.AuthorizationRule [Switch] $Tombstone, - - [Management.Automation.PSCredential] + + [Management.Automation.PSCredential] [Management.Automation.CredentialAttribute()] $Credential = [Management.Automation.PSCredential]::Empty, @@ -6688,7 +7708,7 @@ System.Security.AccessControl.AuthorizationRule 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) { @@ -6703,8 +7723,9 @@ System.Security.AccessControl.AuthorizationRule New-Object System.DirectoryServices.ActiveDirectoryAuditRule -ArgumentList $Identity, $ADRight, $AuditFlag, $ObjectType, $InheritanceType, $InheritedObjectType } - } else { - + } + 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) { @@ -6909,7 +7930,8 @@ function Get-DomainObjectAcl { <# .SYNOPSIS -Returns the ACLs associated with a specific active directory object. +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 @@ -6921,6 +7943,10 @@ A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC= 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. @@ -6982,6 +8008,12 @@ 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 @@ -7003,6 +8035,9 @@ Custom PSObject with ACL entries. $Identity, [Switch] + $Sacl, + + [Switch] $ResolveGUIDs, [String] @@ -7051,9 +8086,15 @@ Custom PSObject with ACL entries. BEGIN { $SearcherArguments = @{ - 'SecurityMasks' = 'Dacl' '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 } @@ -7088,6 +8129,17 @@ Custom PSObject with ACL entries. } 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 '' @@ -7126,8 +8178,7 @@ Custom PSObject with ACL entries. } try { - New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Object['ntsecuritydescriptor'][0], 0 | Select-Object -Expand DiscretionaryAcl | ForEach-Object { - + New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Object['ntsecuritydescriptor'][0], 0 | ForEach-Object { if ($PSBoundParameters['Sacl']) {$_.SystemAcl} else {$_.DiscretionaryAcl} } | ForEach-Object { if ($PSBoundParameters['RightsFilter']) { $GuidFilter = Switch ($RightsFilter) { 'ResetPassword' { '00299570-246d-11d0-a768-00aa006e0529' } @@ -7148,7 +8199,6 @@ Custom PSObject with ACL entries. if ($Continue) { $_ | Add-Member NoteProperty 'ActiveDirectoryRights' ([Enum]::ToObject([System.DirectoryServices.ActiveDirectoryRights], $_.AccessMask)) - if ($GUIDs) { # if we're resolving GUIDs, map them them to the resolved hash table $AclProperties = @{} @@ -7991,6 +9041,17 @@ Custom PSObject with translated OU property fields. $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 { @@ -8250,6 +9311,17 @@ Custom PSObject with translated site property fields. $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 { @@ -8508,6 +9580,17 @@ Custom PSObject with translated subnet property fields. $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 { @@ -8691,6 +9774,16 @@ specifying the user/group member to query for group membership. 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. @@ -8853,6 +9946,15 @@ Custom PSObject with translated group property fields. [Switch] $AdminCount, + [ValidateSet('DomainLocal', 'NotDomainLocal', 'Global', 'NotGlobal', 'Universal', 'NotUniversal')] + [Alias('Scope')] + [String] + $GroupScope, + + [ValidateSet('Security', 'Distribution', 'CreatedBySystem', 'NotCreatedBySystem')] + [String] + $GroupProperty, + [ValidateNotNullOrEmpty()] [String] $Domain, @@ -8968,6 +10070,17 @@ Custom PSObject with translated group property fields. } 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 '' @@ -8997,6 +10110,28 @@ Custom PSObject with translated group property fields. 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" @@ -9687,6 +10822,17 @@ http://www.powershellmagazine.com/2013/05/23/pstip-retrieve-group-membership-of- } 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 '' @@ -9890,6 +11036,211 @@ http://www.powershellmagazine.com/2013/05/23/pstip-retrieve-group-membership-of- } +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 @@ -10024,6 +11375,128 @@ http://richardspowershellblog.wordpress.com/2008/05/25/system-directoryservices- } } +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 { <# @@ -10699,6 +12172,10 @@ the files are parsed, and the connection is destroyed later with Remove-RemoteCo 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 @@ -10740,6 +12217,9 @@ Ouputs a hashtable representing the parsed GptTmpl.inf file. [String] $GptTmplPath, + [Switch] + $OutputObject, + [Management.Automation.PSCredential] [Management.Automation.CredentialAttribute()] $Credential = [Management.Automation.PSCredential]::Empty @@ -10766,9 +12246,21 @@ Ouputs a hashtable representing the parsed GptTmpl.inf file. } Write-Verbose "[Get-GptTmpl] Parsing GptTmplPath: $TargetGptTmplPath" - $Contents = Get-IniContent -Path $TargetGptTmplPath -ErrorAction Stop - $Contents['Path'] = $TargetGptTmplPath - $Contents + + 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 : $_" @@ -11269,6 +12761,17 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. $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)" @@ -11733,21 +13236,14 @@ for connection to the target domain. .EXAMPLE -Find-GPOLocation +Get-DomainGPOUserLocalGroupMapping Find all user/group -> machine relationships where the user/group is a member of the local administrators group on target machines. .EXAMPLE -Find-GPOLocation -UserName dfm -Domain dev.testlab.local - -Find all computers that dfm user has local administrator rights to in -the dev.testlab.local domain. - -.EXAMPLE - -Find-GPOLocation -UserName dfm -Domain dev.testlab.local +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. @@ -12216,8 +13712,8 @@ The domain to query for default policies, defaults to the current domain. .PARAMETER Policy -Extract 'Domain' or 'DC' (domain controller) policies, otherwise queries for the particular -GPO name or GUID. +Extract 'Domain', 'DC' (domain controller) policies, or 'All' for all policies. +Otherwise queries for the particular GPO name or GUID. .PARAMETER Server @@ -12227,10 +13723,6 @@ Specifies an Active Directory server (domain controller) to bind to. Specifies the maximum amount of time the server spends searching. Default of 120 seconds. -.PARAMETER ResolveSids - -Switch. Resolve Sids from a DC policy to object names. - .PARAMETER Credential A [Management.Automation.PSCredential] object of alternate credentials @@ -12252,7 +13744,7 @@ Returns the default domain policy for the dev.testlab.local domain. Get-DomainGPO | Get-DomainPolicy -Parses any GptTmpl.infs found for any policies. +Parses any GptTmpl.infs found for any policies in the current domain. .EXAMPLE @@ -12295,9 +13787,6 @@ Ouputs a hashtable representing the parsed GptTmpl.inf file. [Int] $ServerTimeLimit, - [Switch] - $ResolveSids, - [Management.Automation.PSCredential] [Management.Automation.CredentialAttribute()] $Credential = [Management.Automation.PSCredential]::Empty @@ -12320,7 +13809,10 @@ Ouputs a hashtable representing the parsed GptTmpl.inf file. $ConvertArguments['Domain'] = $Domain } - if ($Policy -eq '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')) { @@ -12330,39 +13822,23 @@ Ouputs a hashtable representing the parsed GptTmpl.inf file. $SearcherArguments['Identity'] = $Policy } - $GPO = Get-DomainGPO @SearcherArguments + $GPOResults = Get-DomainGPO @SearcherArguments - if ($GPO) { + 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} + $ParseArgs = @{ + 'GptTmplPath' = $GptTmplPath + 'OutputObject' = $True + } if ($PSBoundParameters['Credential']) { $ParseArgs['Credential'] = $Credential } # parse the GptTmpl.inf Get-GptTmpl @ParseArgs | ForEach-Object { - if ($PSBoundParameters['ResolveSids']) { - $Root = $_ - $PrivilegeRightsResovled = @{} - # if we're resolving sids in PrivilegeRights to names - if ($Root.'Privilege Rights') { - $PrivilegeRights = $Root.'Privilege Rights' - ForEach ($PrivilegeRight in $PrivilegeRights.Keys) { - $PrivilegeRightsResovled[$PrivilegeRight] = $PrivilegeRights."$PrivilegeRight" | ForEach-Object { - try { - $_ | ForEach-Object { ConvertFrom-SID -ObjectSid ($_.Trim('*')) @ConvertArguments } - } - catch { - Write-Verbose "[Get-DomainPolicy] Error resolving SID : $_" - $_ - } - } - } - } - $Root.'Privilege Rights' = $PrivilegeRightsResovled - $Root - } - else { $_ } + $_ | Add-Member Noteproperty 'GPOName' $GPO.name + $_ | Add-Member Noteproperty 'GPODisplayName' $GPO.displayname + $_ } } } @@ -17557,6 +19033,12 @@ Custom PSObject with translated group property fields from WinNT results. $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 @@ -18765,6 +20247,76 @@ Custom PSObject with translated domain API trust result fields. } +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 + } + } + } +} + + ######################################################## # # Expose the Win32API functions and datastructures below @@ -18778,6 +20330,58 @@ $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 + PARTIAL_SECRETS_ACCOUNT = 67108864 +} -Bitfield + # enum used by $WTS_SESSION_INFO_1 below $WTSConnectState = psenum $Mod WTS_CONNECTSTATE_CLASS UInt16 @{ Active = 0 @@ -18973,7 +20577,7 @@ Set-Alias Get-DFSshare Get-DomainDFSShare Set-Alias Get-NetGPO Get-DomainGPO Set-Alias Get-NetGPOGroup Get-DomainGPOLocalGroup Set-Alias Find-GPOLocation Get-DomainGPOUserLocalGroupMapping -Set-Alias Find-GPOComputerAdmin Get-DomainGPOComputerLocalGroupMappin +Set-Alias Find-GPOComputerAdmin Get-DomainGPOComputerLocalGroupMapping Set-Alias Get-LoggedOnLocal Get-RegLoggedOn Set-Alias Invoke-CheckLocalAdminAccess Test-AdminAccess Set-Alias Get-SiteName Get-NetComputerSiteName |