ADACLScanner is a PowerShell tool, created by Robin Granberg (formerly a PFE at Microsoft and now at Semperis). The tool can be accessed on https://github.com/canix1/ADACLScanner

It's designed to audit Active Directory permissions, export them in CSV/HTML formats, compare them to a previous export or another domain, or to default permissions. The tool also provides features to know the permissions of a user or group, identify the owner of an object, the date of the last rights modification, inherited rights, and much more.

It can be used via a graphical interface (GUI) or command line. The project is ongoing, and Robin is committed to fixing errors reported via Github.

Command Line Usage

-RecursiveFind This parameter searches all nested groups to display all security principals having access.

-RecursiveObjectType This parameter filters nested groups to only display users who have access.

-FilterTrustee ACL filter for matching strings in Trustee.

-Owner To obtain the owners.

Search examples

💡
To search only in specific locations, use the Get-AD* CMDlets. The PowerShell module must be installed on the PC or server you are using.

Custom ACLs on all OUs and containers

$res = .\ADACLScan.ps1 -Base (Get-ADRootDSE).defaultNamingContext -Scope subtree -Filter "(|(objectClass=organizationalUnit)(objectClass=container))" -SkipDefaults -ShowCriticalityColor -Output HTML -Show
# To view the result and exlucde groupPolicyContainer
$res | Where-Object ObjectClass -ne 'groupPolicyContainer'

AdminSDHolder ACL and last modified date

.\ADACLScan.ps1 -Base "CN=AdminSDHolder,CN=System,$((Get-ADRootDSE).defaultNamingContext)" -SDDate -ShowCriticalityColor -Show -Output HTML

Obtain rights for a specific user/group

.\ADACLScan.ps1 -Base (Get-ADRootDSE).defaultNamingContext -Scope subtree -Filter "(|(objectClass=organizationalUnit)(objectClass=container))" -SkipDefaults -SkipBuiltIn -ShowCriticalityColor -Output HTML -Show -EffectiveRightsPrincipal domain\user

GPO rights

.\ADACLScan.ps1 -GPO -Scope subtree -ShowCriticalityColor -Output HTML -Show -RecursiveFind

Get owners

.\ADACLScan.ps1 -Base (Get-ADRootDSE).defaultNamingContext -Owner -Scope subtree -Filter '(objectClass=*)' | Where-Object {$_.Access -eq 'Owner'}

Get Deny permissions

Search for permissions Deny (can be useful for checking ManagedBy or other useful hidden attributes).

.\ADACLScan.ps1 -Base (Get-ADRootDSE).defaultNamingContext -Scope subtree -Filter '(objectClass=*)' | Where-Object {$_.Access -eq 'Deny'}

Unresolved SIDs - all naming context and proprietary naming

⚠️ Execution time depends on number of objects⚠️

[System.Collections.Generic.List[PSObject]]$unresolvedSIDArray = @()
foreach($nc in (Get-ADRootDSE).namingcontexts){
    $unresolvedSID = .\ADACLScan.ps1 -Base $nc -Scope subtree -Filter '(objectClass=*)' -Owner -FilterTrustee 'S-1-5*'
    $unresolvedSID | ForEach-Object {
        $unresolvedSIDArray.add($_)
    }
}

Unresolved SIDs - all objects in the domain partition

⚠️ Execution time depends on number of objects⚠️

$unresolvedSIDArray = .\ADACLScan.ps1 -Base (Get-ADRootDSE).defaultNamingContext -Scope subtree -Filter '(objectClass=*)' -Owner -FilterTrustee 'S-1-5*'

Unresolved SIDs on OUs and containers only

⚠️ Execution time depends on number of objects⚠️

$unresolvedSIDArray= .\ADACLScan.ps1 -Base (Get-ADRootDSE).defaultNamingContext -Scope subtree -Filter '(|(objectClass=organizationalUnit)(objectClass=container))' -Owner -FilterTrustee 'S-1-5*'

Unresolved SIDs on GPO only

⚠️ Execution time depends on number of objects⚠️

[System.Collections.Generic.List[PSObject]]$unresolvedSIDArray = @()
$unresolvedSID = .\ADACLScan.ps1 -GPO -Scope subtree -Owner -FilterTrustee 'S-1-5*'
$unresolvedSID | ForEach-Object {
    $unresolvedSIDArray.add($_)
}

Unresolved SIDs on AD-integrated DNS partitions

$adIntegratedDNSNC = (Get-ADRootDSE).namingContexts | Where-Object {$_ -like 'DC=DomainDNSZones*' -or $_ -like 'DC=ForestDNSZones*'}
[System.Collections.Generic.List[PSObject]]$unresolvedSIDArray = @()
foreach($nc in $adIntegratedDNSNC){
    $unresolvedSID = .\ADACLScan.ps1 -Base $nc -Scope subtree -Filter '(objectClass=*)' -Owner  -FilterTrustee 'S-1-5*'
     $unresolvedSID | ForEach-Object {
        $unresolvedSIDArray.add($_)
    }
}

DCsync permission

The RecursiveFind parameter searches recursively for group members.

.\ADACLScan.ps1 -Base (Get-ADRootDSE).defaultNamingContext -RecursiveFind | Where-Object {$_.Permission -eq 'ExtendedRight Replicating Directory Changes' -or $_.Permission -eq 'ExtendedRight Replicating Directory Changes All' -or $_.Permission -eq 'ExtendedRight Replicating Directory Changes in Filtered Set'}

Advanced use of results

If you wish to delete unknown SIDs or modify the owner, you can use the following commands.

Delete unknown SIDs

Warning: only use the following commands if you know what you're doing! These commands are *not* ADACLScanner commands!

Some useful information:

  • Import-Module ActiveDirectory is required to access the AD:\ drive.
  • You need to use RemoveAccessRuleSpecific() instead of RemoveAccessRule().
    The reason is that with the initial method, the algorithm cannot always accurately identify the ACE to remove.
    RemoveAccessRuleSpecific() scans the ACL to find the exact ACE and removes only that one.
    Detailed explanation here:
    https://forums.powershell.org/t/weird-issue-when-removing-permissions/5066
Import-Module ActiveDirectory

$DNWithUnresolvedSID = ($unresolvedSIDArray | Where-Object {$_.Access -ne 'Owner'}).Object | Select-Object -Unique

foreach ($dn in $DNWithUnresolvedSID){
    $acl = Get-Acl "AD:\$dn"
    
    $aclsToDelete = $acl.Access | Where-Object {$_.IdentityReference -like 'S-1-5*'}

    foreach($aclToDelete in $aclsToDelete){
        $acl.RemoveAccessRuleSpecific($aclToDelete)
    }

    Set-Acl -Path "AD:\$dn" -AclObject $acl
}

For reference, here is the method using RemoveAccessRule, but as mentioned earlier, it does not always work. I am keeping it for historical purposes.

Import-Module ActiveDirectory
$DNWithUnresolvedSID = ($unresolvedSIDArray | Where-Object {$_.Access -ne 'Owner'}  | Select-Object -Unique).Object

foreach ($dn in $DNWithUnresolvedSID){
    $acl = Get-Acl "AD:\$dn"
    
    $aclsToDelete = $acl.Access | Where-Object {$_.IdentityReference -like 'S-1-5*'}

    foreach($aclToDelete in $aclsToDelete){
        $acl.RemoveAccessRule($aclToDelete)
    }

    Set-Acl -Path "AD:\$dn" -AclObject $acl
}

$DNWithUnresolvedSID = ($unresolvedSIDArray | Where-Object {$_.Access -ne 'Owner'}).Object
foreach ($dn in $DNWithUnresolvedSID){
    $acl = Get-Acl "AD:\$dn"
    $aclsToDelete = $acl.Access | Where-Object {$_.IdentityReference -like 'S-1-5*'}
    foreach($aclToDelete in $aclsToDelete){
        $acl.RemoveAccessRule($aclToDelete)
    }
    Set-Acl -Path "AD:\$dn" -AclObject $acl
}

Change owner

Warning: only use the following commands if you know what you're doing! These commands are *not* ADACLScanner commands!
$DNWithUnresolvedSIDOwner = ($unresolvedSIDArray | Where-Object {$_.Access -eq 'Owner'}).Object
$newOwner = New-Object System.Security.Principal.Ntaccount("Domain Admins")

foreach ($dn in $DNWithUnresolvedSIDOwner){
    $acl = Get-Acl "AD:\$dn"
    $acl.SetOwner($newOwner)
    Set-Acl -Path "AD:\$dn" -AclObject $acl
}

Comments

banner-Bastien Perez
Bastien Perez's avatar

Freelance Microsoft 365 - Active Directory - Modern Workplace

France