In this article I will show the techniques used to determine effective permissions for a user or computer account on a certificate template.
When you open certificate template in Certificate Templates MMC snap-in (certtmpl.msc) or in ADSI Editor (adsiedit.msc) and switch to Security tab, you will see the following:
When you click Advanced button, you won’t see Effective Permissions tab like in NTFS permission editor:
If you have complex group structure with multiple-level nesting, it is not very easy to track effective permissions for an account. Also, this information might be very helpful when troubleshooting enrollment issues.
Solution will consist of several steps:
Looks easy, however actual code will do a lot of non-trivial work. And here we go:
This step is fairly easy, we will use WindowsIdentity class:
PS C:\> New-Object Security.Principal.WindowsIdentity dc2@contoso.com AuthenticationType : Kerberos ImpersonationLevel : Identification IsAuthenticated : True IsGuest : False IsSystem : False IsAnonymous : False Name : CONTOSO\DC2$ Owner : S-1-5-21-3709200118-438321133-4282490648-1120 User : S-1-5-21-3709200118-438321133-4282490648-1120 Groups : {S-1-5-21-3709200118-438321133-4282490648-515, S-1-1-0, S-1-5-32-554, S-1-5-32-561...} Token : 3296 PS C:\> (New-Object Security.Principal.WindowsIdentity dc2@contoso.com).Groups BinaryLength AccountDomainSid Value ------------ ---------------- ----- 28 S-1-5-21-3709200118-438321133-428249... S-1-5-21-3709200118-438321133-428249... 12 S-1-1-0 16 S-1-5-32-554 16 S-1-5-32-561 16 S-1-5-32-545 12 S-1-5-2 12 S-1-5-11 12 S-1-5-15 28 S-1-5-21-3709200118-438321133-428249... S-1-5-21-3709200118-438321133-428249... 28 S-1-5-21-3709200118-438321133-428249... S-1-5-21-3709200118-438321133-428249... 28 S-1-5-21-3709200118-438321133-428249... S-1-5-21-3709200118-438321133-428249... 28 S-1-5-21-3709200118-438321133-428249... S-1-5-21-3709200118-438321133-428249... PS C:\>
Basically we use account’s user principal name (UPN) in the WindowsIdentity class constructor and then use Groups property to retrieve all groups account is member of. SID information doesn’t look very user-friendly. We can easily convert SID information to a NTAccount class:
PS C:\> $Groups = (New-Object Security.Principal.WindowsIdentity dc2@contoso.com).Groups | %{$_.translate([Security.Prin cipal.NTAccount])} PS C:\> $groups Value ----- CONTOSO\Domain Computers Everyone BUILTIN\Pre-Windows 2000 Compatible Access BUILTIN\Terminal Server License Servers BUILTIN\Users NT AUTHORITY\NETWORK NT AUTHORITY\Authenticated Users NT AUTHORITY\This Organization CONTOSO\CAs CONTOSO\Web Servers CONTOSO\CERTSVC_DCOM_ACCESS CONTOSO\Cert Publishers PS C:\>
Now it looks much better. If we look to the first figure and group list, we can tell (but not certainly) that specified computer account is a member of “Web Servers” group and this group has Read and Enroll permissions. However, there can be other groups with other permissions and even access type (Deny, instead of Allow) and what the hell, let PowerShell to do it’s work!
Just to note: although, it is a bad practice, some administrators assign permissions directly to a user/computer account, therefore, you may consider to add account name to the $Group variable:
$Groups += (New-Object Security.Principal.WindowsIdentity dc2@contoso.com).Name
This step is more interesting, because it requires to understand Active Directory permissions, which are much more complex, than NTFS or registry permissions. First, we need to retrieve certificate template object from Active Directory. Certificate templates are stored at:
CN=Certificate Templates, CN=Public Key Services, CN=Services, {Configuration naming context}
The following code is used to retrieve configuration naming context:
$ConfigContext = ([ADSI]"LDAP://RootDSE").configurationNamingContext
Prepend the path with certificate template container path and search for desired template:
$ConfigContext = ([ADSI]"LDAP://RootDSE").configurationNamingContext $ConfigContext = "CN=Certificate Templates,CN=Public Key Services,CN=Services,$ConfigContext" # use this form to find template by common name $filter = "(cn=WebServerV2)" # use this for to find template by display name $filter = "displayName=Web Server V2" $ds = New-object System.DirectoryServices.DirectorySearcher([ADSI]"LDAP://$ConfigContext",$filter) $Template = $ds.Findone().GetDirectoryEntry() | ForEach-Object {$_}
Now, retrieve current ACL:
PS C:\> $Access = $Template.ObjectSecurity.Access PS C:\> $access ActiveDirectoryRights : GenericRead InheritanceType : None ObjectType : 00000000-0000-0000-0000-000000000000 InheritedObjectType : 00000000-0000-0000-0000-000000000000 ObjectFlags : None AccessControlType : Allow IdentityReference : NT AUTHORITY\Authenticated Users IsInherited : False InheritanceFlags : None PropagationFlags : None ActiveDirectoryRights : CreateChild, DeleteChild, Self, WriteProperty, DeleteTree, Delete, GenericRead, WriteDacl, Writ eOwner InheritanceType : None ObjectType : 00000000-0000-0000-0000-000000000000 InheritedObjectType : 00000000-0000-0000-0000-000000000000 ObjectFlags : None AccessControlType : Allow IdentityReference : CONTOSO\Domain Admins IsInherited : False InheritanceFlags : None PropagationFlags : None <...>
and there we see a lot of ACEs (access control entry) there. Moreover, none of each ACE looks easy to understand. We need to take attention to the following permissions in the ActiveDirectoryRights property:
Look here:
ActiveDirectoryRights : ExtendedRight InheritanceType : None ObjectType : 0e10c968-78fb-11d2-90d4-00c04f79dc55 InheritedObjectType : 00000000-0000-0000-0000-000000000000 ObjectFlags : ObjectAceTypePresent AccessControlType : Allow IdentityReference : CONTOSO\Web Servers IsInherited : False InheritanceFlags : None PropagationFlags : None
Here we see an ACE with ExtendedRight. This right represents either Enroll or Autoenroll permissions depending on ObjectType value:
So, we will grab and convert all rights to a common right list:
$Accesses = @() $Accesses += $Template.ObjectSecurity.Access | %{ $current = $_ $Rights = @($current.ActiveDirectoryRights.ToString().Split(",",[StringSplitOptions]::RemoveEmptyEntries) | %{$_.trim()}) $GUID = $current.ObjectType.ToString() $current | Add-Member -Name Permission -MemberType NoteProperty -Value @() if ($Rights -contains "GenericRead") {$current.Permission += "Read"} if ($Rights -contains "WriteDacl") {$current.Permission += "Write"} if ($Rights -contains "GenericAll") {$current.Permission += "Full Control"} if ($Rights -contains "ExtendedRight") { if ($GUID -eq "a05b8cc2-17bc-4802-a710-e7c15ab866a2") {$current.Permission += "Autoenroll"} elseif ($GUID -eq "0e10c968-78fb-11d2-90d4-00c04f79dc55") {$current.Permission += "Enroll"} } $current }
In this code sample, we add a simple right name to each ACE, by translating native ACE to a simple right name. It will look like this:
Permission : {Read, Write} ActiveDirectoryRights : CreateChild, DeleteChild, Self, WriteProperty, DeleteTree, Delete, GenericRead, WriteDacl, Writ eOwner InheritanceType : None ObjectType : 00000000-0000-0000-0000-000000000000 InheritedObjectType : 00000000-0000-0000-0000-000000000000 ObjectFlags : None AccessControlType : Allow IdentityReference : CONTOSO\Enterprise Admins IsInherited : False InheritanceFlags : None PropagationFlags : None Permission : {Enroll} ActiveDirectoryRights : ReadProperty, WriteProperty, ExtendedRight InheritanceType : None ObjectType : 0e10c968-78fb-11d2-90d4-00c04f79dc55 InheritedObjectType : 00000000-0000-0000-0000-000000000000 ObjectFlags : ObjectAceTypePresent AccessControlType : Allow IdentityReference : CONTOSO\Domain Admins IsInherited : False InheritanceFlags : None PropagationFlags : None
I selected in bold custom added properties.
Filter ACL to get effective permissions
This step consist of several sub-steps:
Here we go:
# retrieve effective Deny permissions $EffectiveDeny = $Accesses | Where-Object {$_.AccessControlType -eq "Deny"} | ForEach-Object { if ($Groups -contains $_.IdentityReference.ToString()) { $_.Permission } } # retrieve effective Allow permissions $EffectiveAllow = $Accesses | Where-Object {$_.AccessControlType -eq "Allow"} | ForEach-Object { if ($Groups -contains $_.IdentityReference.ToString()) { $_.Permission } } # remove duplicates $EffectiveDeny = $EffectiveDeny | Select-Object -Unique $EffectiveAllow = $EffectiveAllow | Select-Object -Unique # compare effective Deny and Allow permissions and output only Allow permissions $EffectiveAllow | Where-Object {$EffectiveDeny -notcontains $_}
The last command will return effective permissions on a certificate template for a specified user or computer account:
PS C:\> $EffectiveDeny = $Accesses | Where-Object {$_.AccessControlType -eq "Deny"} | ForEach-Object { >> if ($Groups -contains $_.IdentityReference.ToString()) { >> $_.Permission >> } >> } >> # retrieve effective Allow permissions >> $EffectiveAllow = $Accesses | Where-Object {$_.AccessControlType -eq "Allow"} | ForEach-Object { >> if ($Groups -contains $_.IdentityReference.ToString()) { >> $_.Permission >> } >> } >> # remove duplicates >> $EffectiveDeny = $EffectiveDeny | Select-Object -Unique >> $EffectiveAllow = $EffectiveAllow | Select-Object -Unique >> # compare effective Deny and Allow permissions and output only Allow permissions >> $EffectiveAllow | Where-Object {$EffectiveDeny -notcontains $_} >> Read Enroll PS C:\>
Hell ya! We got all we need! Probably, you want to wrap all the code to a simple function:
function Get-TemplateEffectivePermission { [CmdletBinding(DefaultParameterSetName="Name")] param( [Parameter(Mandatory = $true, ParameterSetName = "Name")] [string]$Name, [Parameter(Mandatory = $true, ParameterSetName = "DisplayName")] [string]$DisplayName, [string]$UPN ) if ($UPN) { $User = New-Object Security.Principal.WindowsIdentity -ArgumentList $UPN } else { $User = [Security.Principal.WindowsIdentity]::GetCurrent() } $IDs = $User.Groups | %{$_.Translate([Security.Principal.NTAccount])} $IDs += $User.Name $filter = switch ($PsCmdlet.ParameterSetName) { "Name" {"(cn=$Name)"} "DisplayName" {"displayName=$DisplayName"} } $ConfigContext = ([ADSI]"LDAP://RootDSE").configurationNamingContext $ConfigContext = "CN=Certificate Templates,CN=Public Key Services,CN=Services," $ds = New-object System.DirectoryServices.DirectorySearcher([ADSI]"LDAP://$ConfigContext",$filter) $Template = $ds.Findone().GetDirectoryEntry() | %{$_} $Accesses = @() $Accesses += $Template.ObjectSecurity.Access | %{ $current = $_ $Rights = @($current.ActiveDirectoryRights.ToString().Split(",",[StringSplitOptions]::RemoveEmptyEntries) | %{$_.trim()}) $GUID = $current.ObjectType.ToString() $current | Add-Member -Name Permission -MemberType NoteProperty -Value @() if ($Rights -contains "GenericRead") {$current.Permission += "Read"} if ($Rights -contains "WriteDacl") {$current.Permission += "Write"} if ($Rights -contains "GenericAll") {$current.Permission += "Full Control"} if ($Rights -contains "ExtendedRight") { if ($GUID -eq "a05b8cc2-17bc-4802-a710-e7c15ab866a2") {$current.Permission += "Autoenroll"} elseif ($GUID -eq "0e10c968-78fb-11d2-90d4-00c04f79dc55") {$current.Permission += "Enroll"} } $current } $EffectiveDeny = $Accesses | Where-Object {$_.AccessControlType -eq "Deny"} | ForEach-Object { if ($IDs -contains $_.IdentityReference.ToString()) { $_.Permission } } $EffectiveAllow = $Accesses | Where-Object {$_.AccessControlType -eq "Allow"} | ForEach-Object { if ($IDs -contains $_.IdentityReference.ToString()) { $_.Permission } } $EffectiveDeny = $EffectiveDeny | Select-Object -Unique $EffectiveAllow = $EffectiveAllow | Select-Object -Unique $EffectiveAllow | Where-Object {$EffectiveDeny -notcontains $_} }
The function accepts two arguments: certificate template common or display name and account’s user principal name. If UPN is not specified, current user account is used instead:
PS C:\> Get-TemplateEffectivePermission -Name webserverv2 -UPN dc2@contoso.com Read Enroll PS C:\> Get-TemplateEffectivePermission -Name webserverv2 Read Write Enroll PS C:\>
the task is solved!
HTH
Vadims,
I needed to change these two lines to get it to work. here are the new lines:
$ConfigContext = "CN=Certificate Templates,CN=Public Key Services,CN=Services,$ConfigContext"
$ds = New-object System.DirectoryServices.DirectorySearcher([ADSI]"LDAP://$ConfigContext",$filter)
Let me know if the above changes improperly altered your script. -Tim.
To more accurately explain...I had to correct the full function code at the bottom of the article, but the lines were correct in your part by part explaination earlier in the article.
Post your comment:
Comments: