KerberLoss: patch the DCs, then hunt the invisible ink
The SPN uniqueness bypass that uses invisible Unicode to force a Kerberos-to-NTLM downgrade. Patch the DCs, sweep the directory for invisible ink, and make the downgrade pointless.
01 The Big PictureFor leadership · no jargon
Okay, so KerberLoss, the Active Directory bug with the cheekiest mechanism of the year: invisible ink. This guide walks through what it actually is, how to check your directory isn't already carrying it, and the tidy-up that closes the door for good.
Every service in a Windows domain hangs a name tag in Active Directory (a Service Principal Name, or SPN) so Kerberos knows which account owns which service. Since 2021 the domain checks that no two tags are identical before a new one goes up. KerberLoss, presented at Black Hat earlier this month, is the discovery that the bouncer and the librarian read those tags differently. The uniqueness check compares every byte, but the lookup Kerberos actually uses quietly ignores certain invisible Unicode characters, like the zero-width space. So an attacker hangs a tag that reads cifs/fileserver to the librarian but cifs/fileserver plus an invisible squiggle to the bouncer: the new tag goes up, and now two accounts appear to own the same service.
Faced with two owners, Kerberos refuses to pick, and the Windows client silently falls back to NTLM, the older authentication protocol whose sessions can be relayed or cracked offline. The attacker needs no admin rights, just write access to some account's name-tag field, which is exactly the sort of stale delegation that accumulates in a mature directory. The same trick can also simply break services (two owners, no valid tickets) or hijack a delegation target. Microsoft patched it in the March 2026 updates, five months before the public disclosure, so on a maintained estate the hole itself is closed. The job now: confirm the patch, sweep the directory for invisible ink and near-duplicate tags, tighten who may write those tags, and harden NTLM so the downgrade buys nothing. A normal patch cycle plus an audit; no data loss, no downtime beyond the usual reboot.
02 Technical BreakdownFor engineers
Right, let's get into the nitty-gritty. The root cause is a disagreement between two AD components over what "the same name" means. The SPN/UPN uniqueness checks, added in November 2021 for CVE-2021-42282 (KB5008382), compare strings byte-for-byte. The LDAP filter evaluation the KDC uses to resolve an SPN to an account does not: it silently drops certain characters entirely. Shai Laron of Semperis, who found the bug and named it, tested 385 invisible Unicode characters (the disclosure writeup): only 106 are filterable in LDAP queries at all, some are treated as whitespace, and some are completely ignored by the DC, even with RFC 4515 escaping. A directory that stores cifs/serverb{U+200C} answers a query for plain cifs/serverb with it.
The tl;dr of the three weaponised scenarios, all of which need only write access to some account's servicePrincipalName (GenericWrite, WriteProperty or GenericAll, no Domain Admin):
- Kerberos-to-NTLM downgrade. Duplicate an explicit SPN. The KDC sees two owners, returns
KDC_ERR_S_PRINCIPAL_UNKNOWN, and the Windows client falls back to NTLM without telling anyone, exposing the session to relay or offline cracking. Microsoft's own advisory FAQ confirms this verbatim. - Denial of service. Plant a shadowed SPN colliding with a HOST-mapped service. Explicit SPNs beat
sPNMappingsaliases in KDC lookup, so tickets come back encrypted with the wrong key and the real server rejects them (KRB_AP_ERR_MODIFIED). - SPN-jacking for constrained delegation. Shadow a delegation target's SPN onto an attacker-controlled account, then S4U2Self/S4U2Proxy your way to a privileged ticket for the target.
Two details worth knowing before you build detections. First, the exploitable character set varies by OS build: the public PoC (felixbillieres/pyKerberloss) notes the zero-width space is unreachable on Server 2019 and defaults to the zero-width non-joiner. Second, a MachineAccountQuota-created machine account is not enough, because Validated-Write-SPN rejects foreign SPNs with constraintViolation; the attacker needs genuine write permission on a victim account. The vulnerability is CVE-2026-25177, rated Important by Microsoft with a CVSS of 8.8, patched on the March 2026 Patch Tuesday. MSRC says it was not publicly disclosed before the patch and is not known to be exploited, and it's not in CISA's KEV catalog, as far as I've verified today. It was disclosed alongside a sibling bug, ResetNightmare (CVE-2026-27912), which is a story for another post.
03 Affected
Every supported Windows Server version before the March 2026 updates: Server 2012 through Server 2025, all domain functional levels (it's a DC build-level fix, the functional level is irrelevant). MSRC frames the bug as covering UPNs as well as SPNs, so user accounts are in scope too, not just service accounts.
Fixed builds, per NVD's CPE data (cross-checked against the pyKerberloss README, which lists the same):
- Server 2012:
6.2.9200.25973 - Server 2012 R2:
6.3.9600.23074 - Server 2016:
10.0.14393.8957 - Server 2019:
10.0.17763.8511 - Server 2022:
10.0.20348.4893 - Server 2022 23H2:
10.0.25398.2207 - Server 2025:
10.0.26100.32522
The priority is every domain controller, then anywhere NTLM fallback would be juicy: file servers, certificate authorities, anything Tier-0.
04 Detection
Before we change anything, let's see what we're dealing with. One honest caveat up front, because the detection angle doing the rounds on social media is too narrow: hunting setspn.exe command lines only catches the version of this attack someone simulates with setspn. The public PoC writes SPNs over raw LDAP from Linux, which produces no process-creation telemetry on any Windows host at all, and the Semperis research used direct directory writes too. SPN writes are LDAP modify operations; the client tool is arbitrary. So treat the process-based rule below as one layer, and the directory-side signals as the durable ones.
First, are the DCs patched?
Get-ComputerInfo | Select-Object OsBuildNumber,OsUBRCompare against the fixed-build table above; anything at or above is patched.
The directory sweep, the single highest-value check (adapted from Forestall's writeup):
Get-ADObject -LDAPFilter '(servicePrincipalName=*)' -Properties servicePrincipalName |
ForEach-Object { $_.servicePrincipalName } | Sort-Object -Unique |
Where-Object { $_ -cmatch '[^ -~]' }Any SPN containing a character outside printable ASCII. Zero-width characters are not printable, so a healthy directory returns nothing here. There is almost never a business reason for a non-ASCII SPN.
The near-duplicate hunt, which is the real tell (byte-identical duplicate SPNs are a common hygiene problem; raw-vs-normalised mismatches are this attack):
$invisible = [char]0x200B,[char]0x200C,[char]0x200D,[char]0x200E,[char]0x200F,[char]0x2060,[char]0xFEFF
Get-ADObject -LDAPFilter '(servicePrincipalName=*)' -Properties servicePrincipalName |
ForEach-Object { $_.servicePrincipalName } |
Group-Object { ($_.ToCharArray() | Where-Object { $invisible -notcontains $_ }) -join '' } |
Where-Object Count -gt 1 | Select-Object Name,CountStrips the usual invisible suspects, then groups: any group of two or more is a collision worth staring at. Sanity-check in a lab before you point it at production.
Directory-change auditing, if you have it on (it is off by default, per TrustedSec's DACL-detections guide):
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=5136} -MaxEvents 500 |
Where-Object Message -match 'servicePrincipalName'Event 5136 records directory object changes; filter for servicePrincipalName writes, especially from accounts that have no business writing SPNs.
And the process-creation layer, for completeness. If you simulate the attack with PowerShell and setspn, you get two 4688 events: the parent powershell.exe shows [char]0x200B as visible text, the child setspn.exe receives the real, invisible character. So search the child for the Unicode codepoints, not the literal string. A Sigma rule for exactly that:
title: KerberLoss Detection - Zero-Width SPN Injection (CVE-2026-25177)
id: 5a8a1234-abcd-4e67-8901-23456789abcd
status: production
description: Detects setspn.exe execution containing invisible zero-width Unicode characters used to bypass SPN uniqueness checks.
author: Marko Mladenovic
date: 2026/08/26
tags:
- attack.privilege_escalation
- attack.t1098
- attack.defense_evasion
- attack.t1558
logsource:
category: process_creation
product: windows
detection:
selection:
Image|endswith: '\setspn.exe'
CommandLine|re: '.*[\u200B\u200C\u200D\u200E\u200F\u2060\uFEFF].*'
condition: selection
falsepositives:
- Highly unlikely. Legitimate SPN configuration does not use zero-width Unicode.
level: high❗ Three gotchas before you deploy it. First, the 4688 CommandLine field is empty by default; it needs the GPO "Audit Process Creation → Include command line in process creation events" (Microsoft's Event 4688 doc). Second, regex syntax differs per SIEM. QRadar's rule engine is Java regex, so \u200B works verbatim — this one is confirmed, not theoretical: Marko Mladenovic (co-author of this guide) built and tested the QRadar version of this rule, a custom rule over 4688 process-creation events with the image ending in \setspn.exe and the command line matching the same character class, and it fires. KQL/Sentinel uses RE2, where \u200B is not supported, so use [\x{200B}\x{200C}\x{200D}\x{200E}\x{200F}\x{2060}\x{FEFF}] or simply \p{Cf} (the Unicode format-character category, which covers all of them); if your tenant behaves differently, test both \u200B and \x{200B} against your own events. Splunk is PCRE-based and takes \x{200B}. Third, if your pipeline strips non-printable characters at ingest, no regex will save you: verify against raw events before trusting any of this.
Worth watching on the Kerberos side too: spikes of KDC_ERR_S_PRINCIPAL_UNKNOWN on previously healthy SPNs, and RC4 (etype 23) tickets appearing for services that normally do AES, which is the downgrade fingerprint the pyKerberloss README calls out when a user account holds the shadowed SPN. And if you want a ready-made scanner, danaug23/detect_CVE-2026-25177 is a read-only community tool that flags non-printable characters in SPNs, duplicates and recent SPN modifications. One caveat from the research itself: because the DC ignores the worst offenders, server-side LDAP filters can't match them, which is why all of this hunts in event logs and offline sweeps rather than LDAP queries.
05 Remediation
01 Patch the domain controllers
Any cumulative update from March 2026 onward closes the bug; on a maintained estate this step is already done (go on, check; I'll wait). On frozen or legacy systems, apply the latest cumulative update through your normal rings.
Install-Module PSWindowsUpdate; Get-WindowsUpdate -Install -AcceptAll Or push via WSUS/SCCM rings. The March 2026 updates per build: Server 2012 KB5078775 (Monthly Rollup), 2012 R2 KB5078774, 2016 KB5078938, 2019 KB5078752, 2022 KB5078766, 2022 23H2 KB5078734, 2025 KB5078740 — that last one lands you on exactly the 26100.32522 fixed build from the table above.
02 Sweep the directory and evict the invisible ink
Run the Detection sweeps above and remove anything they surface. A poisoned SPN you found with the non-ASCII sweep gets removed with a targeted Set-ADObject -Remove; investigate how it got there before you clean, because the write permission that allowed it is the real problem.
Set-ADObject <victim-DN> -Remove @{servicePrincipalName='<poisoned-SPN-from-the-sweep>'}Paste the exact value the sweep returned, invisible character included. Then keep reading: removing the tag without fixing the write access just means it comes back.
03 Tighten who can write SPNs
This is the precondition, and the part a patch can't fix for you. Audit which principals hold GenericWrite, GenericAll or WriteProperty on servicePrincipalName over user and computer accounts, and trim stale delegations; BloodHound or your AD auditing tooling of choice will map these edges. While you're there, restrict the "Validated write to service principal name" right to accounts that genuinely need it. I won't pretend there's a one-liner for this bit, sorry: it's a review, not a command.
04 Turn on directory-change auditing
The durable detection depends on it, and it's off by default.
auditpol /set /subcategory:"Directory Service Changes" /success:enableEnable it on DCs and forward event 5136 to your SIEM; the command takes effect immediately.
05 Enable command-line logging
Only needed for the process-based Sigma layer, but it's broadly useful telemetry anyway.
GPO: Administrative Templates → System → Audit Process Creation → "Include command line in process creation events". No command here, it's a policy setting.
06 Make the downgrade pointless
KerberLoss only pays out because NTLM fallback is useful to an attacker. Enforcing SMB signing kills the classic relay path, and evicting NTLMv1 removes the worst cracking surface:
Set-SmbServerConfiguration -RequireSecuritySignature $true -EnableSecuritySignature $true -Confirm:$falseRelay dies when the target demands signed sessions. Pilot first: unsigned legacy clients will break, loudly.
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -Name LmCompatibilityLevel -Value 5Value 5 means NTLMv2 only, refuse LM and NTLM. Ancient clients will notice; that is rather the point, but test before you roll it fleet-wide.
06 Verification
Patched, swept and hardened? Let's prove it rather than assume it.
Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 5 HotFixID,InstalledOnThe newest cumulative update on every DC must be March 2026 or later.
Get-ADObject -LDAPFilter '(servicePrincipalName=*)' -Properties servicePrincipalName |
ForEach-Object { $_.servicePrincipalName } | Where-Object { $_ -cmatch '[^ -~]' }The sweep from Detection, rerun: expect silence.
auditpol /get /subcategory:"Directory Service Changes"Expect Success (ideally Success and Failure); without it there is no 5136 and no alarm.
For a functional check, the PoC doubles as the verifier: pyKerberloss's --check action plants and removes a colliding SPN, and per its README it fails cleanly against a patched DC, while --audit gives a read-only sweep. I've not run it against every build in the table, so treat that as the README's claim and verify in a lab first.
07 Rollback
The realistic rollback is the hardening, not the patch. If SMB signing breaks a legacy client that can't be fixed quickly, relax it on that scope while you remediate properly:
Set-SmbServerConfiguration -RequireSecuritySignature $false -Confirm:$falseRe-enable only where a legacy client genuinely needs it, and track the exception.
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -Name LmCompatibilityLevel -Value 3Value 3 restores the common previous behaviour (NTLMv2 responses, NTLMv1 still negotiated); use whatever value you started from.
Rolling back the security update itself is almost never justified: it reopens a publicly documented privilege escalation with a working PoC. If a cumulative update breaks something line-of-business, fix forward, or uninstall that specific KB while keeping the directory sweep and the write-permission review in place.
wusa /uninstall /kb:NNNNNNN /norestartSubstitute the KB you actually deployed — the March 2026 per-build numbers are listed in the first Remediation step (e.g. KB5078752 for Server 2019, KB5078740 for Server 2025).
08 References
- NVD, CVE-2026-25177
- Microsoft Security Update Guide, CVE-2026-25177
- Semperis, Identity Crisis (Shai Laron's disclosure writeup)
- Forestall, KerberLoss deep-dive, with detection queries
- felixbillieres/pyKerberloss, public PoC and audit tool
- danaug23/detect_CVE-2026-25177, read-only scanner
- Microsoft KB5008382, the 2021 SPN/UPN uniqueness checks
- Microsoft, Event 4688 (command-line auditing)
- TrustedSec, DACL-based detections (5136 is off by default)
If I've got anything wrong here, or you've hit an edge case I haven't covered, do say, corrections and questions are always welcome :)
exit(0);
Ship the fix. Prove it’s closed.
If this runbook doesn’t fit your stack, send the CVE — an operator writes a verified path, usually within a business day. Or contribute your own and get co-authored credit.