CVE-2025-53770 “ToolShell”: when patching wasn't enough
In July 2025, organizations that had dutifully installed Microsoft's Patch Tuesday updates were compromised anyway—by a bypass of the patch they had just applied. Then came the part that made this one genuinely different: the attackers were not after a shell. They were after a cryptographic key, and once they had it, patching the server did not remove them.
- CVE
- CVE-2025-53770 (“ToolShell”)
- Class
- Deserialization of untrusted data (CWE-502)
- Severity
- Critical — CVSS 9.8
- Affected
- On-premises only: SE < 16.0.18526.20508, 2019 < 16.0.10417.20037, 2016 < 16.0.5513.1001
- Not affected
- SharePoint Online / Microsoft 365
- Patched in
- KB5002768 (SE); KB5002754 + KB5002753 (2019); KB5002760 + KB5002759 (2016)
The bug in one sentence
A spoofed Referer header skipped SharePoint's authorization gate, giving an
anonymous attacker access to a page that deserializes attacker-controlled .NET
objects—and a type-filter that could be talked into approving them.
Part one: the gate that trusted a header
SharePoint's request pipeline includes a handler that decides whether to reject an unauthenticated request with a 401. It contains a sensible-looking exception: users in the middle of signing out need to reach the sign-out page without being challenged, so the handler checks whether the request's referring page was the sign-out URL, and if so, skips the challenge.
The referring page is read from the Referer header. That header is set by
the client. Sending Referer: /_layouts/SignOut.aspx with any request causes
the gate to step aside.
One precision point most coverage gets wrong, and it matters if you are writing detection logic: the request never becomes authenticated. The identity remains anonymous. What is bypassed is the module-level check that would have emitted the 401. The developer's mental model was "this user is mid-signout." The actual semantics were "the attacker said so."
The pattern to remember: trust flows from where a value originates, not from what it is named. Any conditional whose truth depends on a request header, and whose consequence is skipping an authorization check, is a bypass waiting to be found. Read auth code by asking of every guard flag: who ultimately sets this, and can they lie?
Part two: reaching a sink that was supposed to be unreachable
The target page, ToolPane.aspx, parses attacker-supplied web-part markup. It
was long considered safe from anonymous users because every page of its type performs a
rights check when it loads.
The original research contribution—demonstrated at Pwn2Own Berlin in May 2025 by Dinh Ho Anh Khoa of Viettel Cyber Security—was noticing that the dangerous parsing happens earlier in the ASP.NET page lifecycle than the rights check does. The markup is processed during page initialization; the check that would have stopped an anonymous user runs at load. Get your payload parsed during init, and the gate never arrives.
This is a useful thing to internalize as a reviewer. "This code path performs an authorization check" and "this code path performs an authorization check before the dangerous operation" are different statements, and only the second one is a security control.
Part three: the type filter that only looked at the outside
The parsed markup instantiates a control whose DataTable property getter
decompresses an attacker-supplied blob and hands it to .NET's
BinaryFormatter—a deserializer with a long history of turning object
graphs into command execution. (The setter merely stores the blob; it is reading the
property back that triggers deserialization, which is why the exploit markup forces the
getter to fire.) Microsoft knew the risk, so the path was guarded by a validator that
checks the types in the incoming XML against an allowlist.
The evasion is elegant: the validator inspected the type of each element, but not the type arguments nested inside it. Wrap the dangerous gadget type as a generic parameter of a permitted collection type, and the outer element that the validator examines looks entirely benign. The deserializer then resolves the full generic type, inner argument included, and instantiates the gadget.
The shape of the evasion// What the validator examines — the element's own type. Allowed.
System.Collections.Generic.List`1[[
// What it never inspects — the type argument carrying the gadget.
System.Data.Services.Internal.ExpandedWrapper`2[[...]]
]]
// The deserializer resolves the WHOLE type, inner argument included.
Two components read the same input and reached different conclusions about what it meant. That is a vulnerability class in its own right, and it is worth carrying as a review heuristic: a validator that inspects a value the consumer will reinterpret is not a security boundary—it is a second parser. The question is never "does this check reject bad input?" but "does the component that acts on this input resolve it identically to the component that checked it?" Nested, recursive, or generic structures are exactly where those two readings diverge, because a validator that stops at the first level is trivially cheaper to write than one that walks the whole tree.
The part that actually mattered: they stole the key
The file dropped by the mass-exploitation campaign was named
spinstall0.aspx, and it is widely described as a webshell. It is not one, and
the distinction changes your incident response.
It offers no command execution and no interactive access. It does one thing: it reads the
server's ASP.NET MachineKey configuration—the validation and decryption
keys—out of live process memory and returns them in an HTTP response. An attacker
retrieves them with an ordinary GET request. Reading them from the running process rather
than from disk sidesteps any encryption applied to the configuration file.
Those keys are what ASP.NET uses to sign __VIEWSTATE. With them, an attacker
can forge a ViewState payload that a fully patched, fully hardened server will
verify as authentic and deserialize on request. The resulting access is
unauthenticated, requires no file on disk, generates no logon event, works against every
.aspx page in the application, and—because the key is shared
farm-wide—works against every server in the farm.
So: patch the vulnerability, remove the dropped file, and a competent attacker is still inside, using nothing but legitimate framework behavior and correctly signed data. There is no exploit left to block and no artifact left to find.
Why this one mattered
- It defeated the patch victims had already applied. The July 8 updates addressed the original chain; this bypass was exploited from mid-July against servers whose owners believed they were protected.
- Compromise outlived remediation. Key theft converts a transient RCE into persistent access that patching does not revoke.
- The targeting was strategic. Public reporting documented hundreds of confirmed compromises across dozens of countries within days, weighted heavily toward government and education—including US federal agencies. Multiple distinct actors converged on it, including state-sponsored espionage groups and a ransomware operator.
- CISA gave federal agencies one day to remediate, rather than the usual two weeks. That compression is itself a severity signal.
What to do about it
The order of these steps is load-bearing. Rotating keys on a server that is still exploitable accomplishes nothing—the attacker simply steals the new key. This is the single most commonly botched item in ToolShell remediation writeups.
- Patch first. Subscription Edition to KB5002768; SharePoint 2019 to KB5002754 plus the language pack KB5002753; SharePoint 2016 to KB5002760 plus KB5002759. The language packs are separate, additionally required updates—skipping them is a common miss. SharePoint 2013 and earlier are end-of-life, will never be fixed, and should be disconnected.
- Then rotate the ASP.NET machine keys and restart IIS. This step is mandatory, not optional. If you patched but did not rotate, you fixed the vulnerability and kept the backdoor.
- Enable AMSI in Full Mode with a supported antivirus on every farm server. Treat it as risk reduction rather than a fix—at least one national CERT cautioned that it may not consistently block adapted attacks.
-
Hunt on the assumption you were hit. Look for the dropped file and its
variants in the LAYOUTS directory, POST requests to
ToolPane.aspxcarrying a sign-out referrer, and—most importantly—anomalous ViewState activity after remediation, which is what post-key-theft access looks like.
The lesson: patch the primitive, not the proof-of-concept
The July 8 fixes patched what the exploit did. One blocked a specific URL pattern; a trailing character defeated it. The other marked a single dangerous control unsafe in configuration—and, worse, did so through a change that only took effect if an administrator separately ran SharePoint's configuration wizard, which Microsoft's guidance never mentioned. A different route to the same deserialization sink defeated it. Both addressed the observed exploit rather than the reachable primitive.
When you triage a vulnerability report—yours or a vendor's—the question is not "what URL did the proof-of-concept use?" It is "what is the sink, and what is the complete set of paths that reach it?" The emergency updates that shipped from 19–21 July did that properly: an allowlist for the referrer exemption instead of a denylist, and stricter element-type validation that covers controls beyond the single one previously blocked.
Answering that question requires reading code rather than reproducing a PoC—the same skill that LearnCodeReview exists to teach, and the same one that separates a real assessment from a scan that confirms you applied the patch.