diff options
Diffstat (limited to 'Recon')
-rwxr-xr-x | Recon/PowerView.ps1 | 1351 |
1 files changed, 1304 insertions, 47 deletions
diff --git a/Recon/PowerView.ps1 b/Recon/PowerView.ps1 index 0a4e000..40b060c 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 @@ -2790,8 +3147,8 @@ 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 @@ -4234,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 @@ -4302,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. @@ -4428,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'" @@ -4436,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 @@ -4511,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). @@ -4623,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 @@ -4742,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 } @@ -4758,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 = '' @@ -4768,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 '' @@ -4823,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)" @@ -5565,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. @@ -5666,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. @@ -5779,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 } @@ -5795,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 { @@ -5806,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))" @@ -5854,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)" @@ -5914,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. @@ -6086,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 } @@ -6102,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 = '' @@ -6112,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 '' @@ -6144,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)" } @@ -6175,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 @@ -6328,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, @@ -6966,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 @@ -6978,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. @@ -7039,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 @@ -7060,6 +8035,9 @@ Custom PSObject with ACL entries. $Identity, [Switch] + $Sacl, + + [Switch] $ResolveGUIDs, [String] @@ -7108,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 } @@ -7145,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 '' @@ -7183,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' } @@ -7205,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 = @{} @@ -8048,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 { @@ -8307,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 { @@ -8565,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 { @@ -9044,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 '' @@ -9785,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 '' @@ -9988,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 @@ -11386,6 +12639,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)" @@ -11850,21 +13114,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. @@ -18946,7 +20203,7 @@ $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.GroupTypeEnum UInt32 @{ +$SamAccountTypeEnum = psenum $Mod PowerView.SamAccountTypeEnum UInt32 @{ DOMAIN_OBJECT = '0x00000000' GROUP_OBJECT = '0x10000000' NON_SECURITY_GROUP_OBJECT = '0x10000001' @@ -18961,7 +20218,7 @@ $SamAccountTypeEnum = psenum $Mod PowerView.GroupTypeEnum UInt32 @{ } # used to parse the 'grouptype' property for groups -$GroupTypeEnum = psenum $Mod PowerView.SamAccountTypeEnum UInt32 @{ +$GroupTypeEnum = psenum $Mod PowerView.GroupTypeEnum UInt32 @{ CREATED_BY_SYSTEM = '0x00000001' GLOBAL_SCOPE = '0x00000002' DOMAIN_LOCAL_SCOPE = '0x00000004' |