SharePoint deserialization RCE: why patching the server still leaves the attacker inside
A deserialization flaw in on-prem SharePoint (the ToolShell class) gives a network attacker code execution as the SharePoint service account. The non-obvious part: that code frequently reads the ASP.NET machineKey, and a stolen ValidationKey lets the attacker forge signed __VIEWSTATE payloads that survive the patch. Here is how it works, why patching is not enough, and how to detect and contain it.
On-prem Microsoft SharePoint Server sits on the boundary between your public web tier and your directory. It is internet-facing, it runs under a domain service account, and it holds an ASP.NET machine key on disk. A deserialization-of-untrusted-data flaw in that server (the ToolShell class of attack, now in the CISA Known Exploited Vulnerabilities catalog) lets a network attacker execute code on it. The part that catches responders out is what the attacker does with that code execution before you patch, and why the patch alone does not put them out.
How it works
The entry is a single crafted request. SharePoint deserializes an attacker-supplied object it should never have trusted, and the SharePoint worker process runs the attacker’s code as the SharePoint app-pool service account (T1190, Exploit Public-Facing Application). On the box, the signature is unmistakable once you look: w3wp.exe, the SharePoint worker process, spawns cmd.exe or powershell.exe. A web server process has no legitimate reason to launch an interactive shell.
A smash-and-grab attacker drops a web shell and moves on. A capable one reads the ASP.NET machineKey out of web.config first: the ValidationKey and the DecryptionKey. Those are the secrets SharePoint uses to sign and encrypt __VIEWSTATE, the serialized page state that travels with every ASP.NET request. With the ValidationKey in hand, the attacker can forge a __VIEWSTATE blob that SharePoint will deserialize as trusted, because it carries a valid signature (T1552, Unsecured Credentials, then T1550, Use Alternate Authentication Material). They usually also write a small ASPX handler to disk as a fallback (T1505.003, Server Software Component: Web Shell).
The last move is quieter and worse. The SharePoint service account is very often over-privileged in Active Directory, holding more directory rights than a web service ever needs. The attacker uses that valid account to enumerate privileged groups and hunt a path toward Tier 0 (T1078, Valid Accounts). A public web server has become a foothold in the domain.
Why patching is not enough
Here is the trap. You apply the vendor patch, the deserialization endpoint stops accepting the malicious object, and the entry is closed. The attacker still holds the machineKey.
A stolen ValidationKey is a patch-surviving key to the front door. The attacker sends a request carrying a __VIEWSTATE payload signed with your own key. SharePoint validates the signature, trusts the payload, and deserializes it, and the attacker has code execution again. The original bug is irrelevant now. Nothing about applying the update invalidates a key that was copied off the server before you patched.
So remediation is not patch-and-close. It is three things:
- Apply the patch, to close the deserialization entry.
- Rotate the ASP.NET machineKey across the whole farm, so any __VIEWSTATE signed with the old ValidationKey stops validating. This is what actually kills the forged-ViewState persistence.
- Reset the SharePoint service account whose credential ran attacker code, and cut its AD rights to least privilege, so the same foothold cannot reach the domain again.
Miss the machineKey rotation and you have a patched server that the attacker still owns.
How to detect it
The individual events look like a busy web server: HTTP 200s, request volume, CPU. None of those is the signal. The signal is the process lineage. Correlate three sources across the window:
- EDR process tree: w3wp.exe spawning cmd.exe or powershell.exe. A SharePoint worker process launching a shell is code execution, full stop.
- EDR file telemetry and SharePoint ULS logs: a read of the web.config machineKey, and any newly written ASPX handler.
- IIS logs: the initial crafted POST, and then later requests carrying a crafted __VIEWSTATE. Those later requests are how patch-surviving re-entry shows up after the fix.
If you see the worker process spawn a shell followed by a web.config read, treat it as machineKey theft until proven otherwise.
How to contain and shut it down
Code ran as the service account, and the server held the signing key, so both are burned. Restarting IIS clears the in-memory shell but not the stolen key or the on-disk web shell, and blocking one IP is trivially rotated. The real sequence is:
- Isolate the SharePoint server from the network so the shell cannot continue executing or beacon out.
- Treat the machineKey and the service account as compromised, pending rotation. Trust nothing they sign or authorize.
- Apply the patch, then rotate the machineKey farm-wide and reset the service account to least privilege.
- Reconstruct the timeline from IIS, ULS, and EDR together, so you can see both the initial exploit and any forged-__VIEWSTATE re-entry.
- Rebuild from a known-good state, confirm the new key is consistent across the farm, sweep for stray ASPX handlers, and prove that a payload signed with the old key now fails to validate.
Then scope honestly. An over-privileged service account was already probing the directory, so this is a potential domain-compromise event and a disclosure question over whatever SharePoint held, not a closed patch ticket. The decisions that follow are Tier 0 credential rotation, notification scoped to the data on the platform, and assurance that the machineKey persistence is genuinely severed.
Practice it
We built this as a scenario in GraphLattice Range: an internet-facing on-prem SharePoint server takes a deserialization request, the worker process spawns a shell, the attacker lifts the machineKey and forges __VIEWSTATE, and the over-privileged service account starts walking toward the domain. It forces the non-obvious call under time pressure: apply the patch and close the ticket, or treat the machineKey as stolen and rotate it along with the service account. Teams that have made that call in a drill make it faster when it is real.