Public/Computer/Reset-ADComputerAccountSecurity.ps1
|
<#
.SYNOPSIS Restores the default owner and/or the default permissions of one or more computer accounts. .DESCRIPTION When a computer account is created through a delegation, the object owner becomes the account that created it, and specific ACE are granted to that same account. The mS-DS-CreatorSID attribute is also populated with the creator SID when ms-DS-MachineAccountQuota is not 0. Depending on the Scope parameter, this function resets the owner to the Domain Admins group, replaces the discretionary access control list with the default one defined in the schema for the computer class, or both. Resetting the permissions removes every explicit ACE from the object, including tier model delegations applied directly on the computer account. ACE inherited from the parent organizational unit are not stored on the object and are therefore preserved, unless inheritance is blocked on that object. Use Get-ADComputerJoinedByUser to find the computers that need to be remediated, its output can be piped directly to this function. .PARAMETER Identity Name, SamAccountName, distinguishedName, GUID or SID of the computer accounts to clean up. .PARAMETER Scope Part of the security descriptor to reset: Owner Only the object owner. Permissions Only the discretionary access control list. All Both, the permissions first, then the owner. This is the default value. .PARAMETER Owner Account or group to set as the new owner, in DOMAIN\Name or Name format. Defaults to the Domain Admins group of the target domain. Ignored when Scope is Permissions. .PARAMETER Server Domain controller or domain to query. Defaults to the one selected by the ActiveDirectory module. .PARAMETER EventLogSource Name of the event log source used to trace the operations in the Application log. Defaults to 'ComputerAccountCleanup'. Set it to an empty string to disable event logging. .PARAMETER Force Applies the reset without comparing the current state to the expected one. By default, the owner is compared by SID and the explicit ACE are compared to the schema default ones, and the write is skipped when the object is already compliant. .PARAMETER Simulation Runs in read-only mode: targets are resolved and every action is reported, but no change is written to Active Directory and no entry is written to the event log. .INPUTS System.String. Computer identities can be piped to this function. .OUTPUTS System.Management.Automation.PSCustomObject. One result object per processed computer, with a cumulative ResultCode property: 0 Success, or nothing to do 1 Owner could not be modified 10 Computer could not be read 100 Permissions could not be reset .EXAMPLE Reset-ADComputerAccountSecurity -Identity 'SRV-APP01' -Scope Owner Resets only the owner of the SRV-APP01 computer account, leaving the permissions untouched. .EXAMPLE Reset-ADComputerAccountSecurity -Identity 'SRV-APP01' -Scope Permissions -Simulation -Verbose Reports the permission changes that would be applied, without modifying anything. .EXAMPLE Get-ADComputer -Filter { Name -like 'SRV-*' } | Reset-ADComputerAccountSecurity -Scope All | Where-Object -FilterScript { $_.ResultCode -ne 0 } Processes several computers and returns only the failures. .EXAMPLE $domainSID = (Get-ADDomain).DomainSID.Value Get-ADComputerJoinedByUser -SearchBy Owner | Where-Object { $_.OwnerSID.Value -notin @("$domainSID-512", 'S-1-5-32-544') } | Reset-ADComputerAccountSecurity -Scope Owner Audits the domain and resets the owner of every computer still owned by the account that created it. .NOTES Credits: https://blog.piservices.fr/post/2021/03/29/powershell-who-s-owner-of-my-ad-object https://blog.piservices.fr/post/2021/04/12/powershell-change-the-owner-of-my-ad-objects #> function Reset-ADComputerAccountSecurity { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] Param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('Name', 'SamAccountName', 'DistinguishedName')] [String[]] $Identity, [Parameter(Mandatory = $false, Position = 1)] [ValidateSet('All', 'Owner', 'Permissions')] [String] $Scope = 'All', [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String] $Owner, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String] $Server, [Parameter(Mandatory = $false)] [AllowEmptyString()] [String] $EventLogSource = 'ComputerAccountCleanup', [Parameter(Mandatory = $false)] [Switch] $Force, [Parameter(Mandatory = $false)] [Alias('DryRun')] [Switch] $Simulation ) begin { Set-StrictMode -Version Latest # Common parameters forwarded to every ActiveDirectory cmdlet call. $adParameters = @{ ErrorAction = 'Stop' } if ($PSBoundParameters.ContainsKey('Server')) { $adParameters.Add('Server', $Server) } $isSimulation = $Simulation.IsPresent $isForced = $Force.IsPresent $isLogEnabled = -not [String]::IsNullOrWhiteSpace($EventLogSource) $isOwnerInScope = $Scope -in @('All', 'Owner') $arePermsInScope = $Scope -in @('All', 'Permissions') function Get-DaclFingerprint { <# .SYNOPSIS Builds a comparable signature of the explicit ACE of a security descriptor. .DESCRIPTION Inherited ACE are excluded, identities are expressed as SID and entries are sorted, so that two descriptors carrying the same explicit permissions in a different order produce the same signature. #> [CmdletBinding()] [OutputType([String])] Param( [Parameter(Mandatory = $true)] [System.DirectoryServices.ActiveDirectorySecurity] $SecurityDescriptor ) $accessRules = $SecurityDescriptor.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) $entries = foreach ($accessRule in $accessRules) { '{0}|{1}|{2}|{3}|{4}|{5}|{6}' -f $accessRule.IdentityReference.Value, $accessRule.AccessControlType, [Int]$accessRule.ActiveDirectoryRights, $accessRule.ObjectType, $accessRule.InheritedObjectType, $accessRule.InheritanceType, $accessRule.PropagationFlags } return (($entries | Sort-Object) -join "`n") } function Write-OperationLog { <# .SYNOPSIS Writes an entry to the Application event log, ignoring any logging failure. #> [CmdletBinding()] Param( [Parameter(Mandatory = $true)] [String] $Message, [Parameter(Mandatory = $false)] [System.Diagnostics.EventLogEntryType] $EntryType = [System.Diagnostics.EventLogEntryType]::SuccessAudit, [Parameter(Mandatory = $false)] [Int] $EventId = 0 ) if (-not $isLogEnabled) { return } if ($isSimulation) { Write-Verbose -Message "[SIMULATION] Event log entry skipped: $Message" return } try { Write-EventLog -LogName 'Application' -Source $EventLogSource -EntryType $EntryType -EventId $EventId -Category 0 -Message $Message } catch { Write-Warning -Message "Unable to write to the event log: $($_.Exception.Message)" } } if ($isSimulation) { Write-Warning -Message 'Simulation mode enabled: no change will be applied.' } # Register the event log source when missing (requires local administrative rights). if ($isLogEnabled) { try { if (-not [System.Diagnostics.EventLog]::SourceExists($EventLogSource)) { if ($isSimulation) { Write-Verbose -Message "[SIMULATION] Event log source '$EventLogSource' would be created in the Application log." } else { New-EventLog -LogName 'Application' -Source $EventLogSource -ErrorAction Stop Write-Verbose -Message "Event log source '$EventLogSource' created in the Application log." } } } catch { Write-Warning -Message "Unable to register the event log source '$EventLogSource': $($_.Exception.Message)" } } $domain = Get-ADDomain @adParameters # Resolve the target owner: Domain Admins of the target domain unless overridden. if ($isOwnerInScope) { if ($PSBoundParameters.ContainsKey('Owner')) { $targetOwner = $Owner } else { $targetOwner = '{0}\{1}' -f $domain.NetBIOSName, (Get-ADGroup -Identity ('{0}-512' -f $domain.DomainSID.Value) @adParameters).Name } # Fail fast when the owner cannot be translated into a SID. $targetOwnerAccount = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList $targetOwner $targetOwnerSid = $targetOwnerAccount.Translate([System.Security.Principal.SecurityIdentifier]) Write-Verbose -Message "New owner resolved to '$targetOwner' ($($targetOwnerSid.Value))." } else { $targetOwner = $null } # Retrieve the default security descriptor of the computer class from the schema. if ($arePermsInScope) { $schemaNamingContext = (Get-ADRootDSE @adParameters).schemaNamingContext $defaultSecurityDescriptor = Get-ADObject -Identity "CN=Computer,$schemaNamingContext" -Properties 'defaultSecurityDescriptor' @adParameters | Select-Object -ExpandProperty 'defaultSecurityDescriptor' # Reference signature used to detect objects already carrying the default permissions. $referenceDescriptor = New-Object -TypeName System.DirectoryServices.ActiveDirectorySecurity $referenceDescriptor.SetSecurityDescriptorSddlForm($defaultSecurityDescriptor, [System.Security.AccessControl.AccessControlSections]::Access) $referenceFingerprint = Get-DaclFingerprint -SecurityDescriptor $referenceDescriptor Write-Warning -Message 'Permissions are in scope: every explicit ACE set on the target objects will be removed.' } } process { foreach ($computerIdentity in $Identity) { # Retrieve the target computer account. try { $computerAccount = Get-ADComputer -Identity $computerIdentity -Properties 'nTSecurityDescriptor' @adParameters } catch { Write-OperationLog -Message "The computer '$computerIdentity' could not be retrieved from Active Directory." -EntryType Error -EventId 10 Write-Error -Message "The computer '$computerIdentity' could not be retrieved: $($_.Exception.Message)" -ErrorAction Continue [PSCustomObject]@{ PSTypeName = 'ComputerAccountSecurityResult' Identity = $computerIdentity DistinguishedName = $null Scope = $Scope PreviousOwner = $null NewOwner = $targetOwner OwnerCompliant = $null PermsCompliant = $null OwnerReset = $false PermissionsReset = $false Simulation = $isSimulation ResultCode = 10 } continue } $resultCode = 0 $isOwnerReset = $false $arePermsReset = $false $isOwnerCompliant = $null $arePermsCompliant = $null $samAccountName = $computerAccount.SamAccountName $distinguishedName = $computerAccount.DistinguishedName # Read the current owner once, as a SID and as an account name. $currentOwner = Resolve-ADObjectOwner -SecurityDescriptor $computerAccount.nTSecurityDescriptor $currentOwnerSid = if ($null -ne $currentOwner) { $currentOwner.OwnerSID } else { $null } $previousOwner = if ($null -ne $currentOwner) { $currentOwner.OwnerName } else { $null } # Reset the permissions first, so that the owner is applied last and cannot be overwritten. if ($arePermsInScope) { $currentFingerprint = Get-DaclFingerprint -SecurityDescriptor $computerAccount.nTSecurityDescriptor $arePermsCompliant = $currentFingerprint -eq $referenceFingerprint if ($arePermsCompliant -and -not $isForced) { Write-Verbose -Message "Permissions of '$samAccountName' already match the schema default value, nothing to do." } elseif ($isSimulation) { Write-Verbose -Message "[SIMULATION] '$distinguishedName': permissions would be replaced by the schema default value." Write-Verbose -Message "[SIMULATION] Current DACL: $($computerAccount.nTSecurityDescriptor.GetSecurityDescriptorSddlForm('Access'))" Write-Verbose -Message "[SIMULATION] Default SDDL: $defaultSecurityDescriptor" } elseif ($PSCmdlet.ShouldProcess($distinguishedName, 'Reset permissions to the schema default value')) { try { # Only the Access section is replaced, the owner and the audit entries are left as is. $computerAccount.nTSecurityDescriptor.SetSecurityDescriptorSddlForm($defaultSecurityDescriptor, [System.Security.AccessControl.AccessControlSections]::Access) Set-ADObject -Identity $distinguishedName -Replace @{ nTSecurityDescriptor = $computerAccount.nTSecurityDescriptor } -Confirm:$false @adParameters $arePermsReset = $true Write-OperationLog -Message "The computer '$samAccountName' permissions have been reset to their default value." Write-Verbose -Message "Permissions of '$samAccountName' reset to their default value." } catch { $resultCode += 100 Write-OperationLog -Message "The computer '$samAccountName' permissions could not be reset." -EntryType FailureAudit -EventId 100 Write-Error -Message "Permissions of '$samAccountName' could not be reset: $($_.Exception.Message)" -ErrorAction Continue } } } # Reset the object owner. if ($isOwnerInScope) { # The owner is compared by SID, the name form being unreliable for unresolvable principals. $isOwnerCompliant = ($null -ne $currentOwnerSid) -and ($currentOwnerSid.Value -eq $targetOwnerSid.Value) if ($isOwnerCompliant -and -not $isForced) { Write-Verbose -Message "Owner of '$samAccountName' is already '$targetOwner', nothing to do." } elseif ($isSimulation) { Write-Verbose -Message "[SIMULATION] '$distinguishedName': owner would be changed from '$previousOwner' to '$targetOwner'." } elseif ($PSCmdlet.ShouldProcess($distinguishedName, "Set owner to '$targetOwner'")) { $directoryEntry = $null try { $directoryEntry = [ADSI]"LDAP://$distinguishedName" $directoryEntry.PSBase.ObjectSecurity.SetOwner($targetOwnerAccount) $directoryEntry.PSBase.CommitChanges() $isOwnerReset = $true Write-OperationLog -Message "The computer '$samAccountName' owner has been changed from '$previousOwner' to '$targetOwner'." Write-Verbose -Message "Owner of '$samAccountName' set to '$targetOwner'." } catch { $resultCode += 1 Write-OperationLog -Message "The computer '$samAccountName' owner could not be modified to '$targetOwner'." -EntryType FailureAudit -EventId 1 Write-Error -Message "Owner of '$samAccountName' could not be modified: $($_.Exception.Message)" -ErrorAction Continue } finally { if ($null -ne $directoryEntry) { $directoryEntry.Dispose() } } } } [PSCustomObject]@{ PSTypeName = 'ComputerAccountSecurityResult' Identity = $samAccountName DistinguishedName = $distinguishedName Scope = $Scope PreviousOwner = $previousOwner NewOwner = $targetOwner OwnerCompliant = $isOwnerCompliant PermsCompliant = $arePermsCompliant OwnerReset = $isOwnerReset PermissionsReset = $arePermsReset Simulation = $isSimulation ResultCode = $resultCode } } } end { } } |