漏洞概述
变量覆盖即通过外部输入将某个变量的值给覆盖掉。
通常将可以用自定义的参数值替换原有变量值的情况称为变量覆盖漏洞。
漏洞函数
register_globals
extract
parse_str
mb_parse_str
import_request_variables
$$
漏洞环境
$$ 介绍
$$这种写法称为可变变量
$与$\$区别
$var
#一个正常的变量,名称为:var,可以存储任何类型值:string、int、bool等。
$$var
#一个引用变量,用于存储$var的值。
1 2 3 4 5 6 7 8 9 10 11
| <?php header("Content-type:text/html;charset=utf-8"); $a = "abc"; $$a = 369; echo $a."<br/>"; #输出abc echo $$a."<br/>"; #输出369 echo $abc; #输出369
phpinfo(); ?> 如下所示
|

$$
导致的变量覆盖问题在CTF代码审计题目中经常在foreach
中出现,使用foreach
来遍历数组中的值,然后再将获取到的数组键名作为变量,数组中的键值作为变量的值。因此就产生了变量覆盖漏洞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php foreach (array('_COOKIE','_POST','_GET') as $_request) {s foreach ($$_request as $_key=>$_value) { $$_key= $_value; } } $id = isset($id) ? $id : "test"; if($id === "si1ent") { echo "flag{have_fun-ctf~}"; } else { echo "Nothing..."; } ?>
|
1
| http://dvwa.me:8888/var_ctf.php?id=1
|

1 2
| http://dvwa.me:8888/var_ctf.php?id=si1ent GET、POST或COOKIE请求方式均可,传入id=si1ent后,在foreach语句中,$_key为id,$_value为si1ent,进而$$_key为$id,从而实现了变量覆盖获得flag值。
|

import_request_variables
支持版本:PHP 4 >= 4.1.0 PHP 5 < 5.4.0
import_request_variables()函数将GET、POST、Cookies中的变量导入到全局。
PHP Version 5.3.29
1 2 3 4 5 6 7 8 9 10 11 12
| <?php $auth = '0'; import_request_variables('P');
if ($auth === '0') { echo "private"; #私有 }else{ echo "public"; #全局 } phpinfo(); ?> 执行结果如下
|

POST提交
1 2 3 4
| http://dvwa.me:8888/import_request_test.php POST提交 auth=123 如下存在变量覆盖
|

PHP Version 5.6.40
访问后直接报错无法解析
1
| http://dvwa.me:8888/import_request_test.php
|

parse_str
1 2
| parse_str() 函数用于把查询字符串解析到变量中,如果没有array参数,则由该函数设置的变量将覆盖已存在的同名变量。 PHP官方建议:极度不建议 在没有 result 参数的情况下使用此函数,并且在 PHP 7.2 中将废弃不设置参数的行为。
|
函数介绍
1 2 3 4 5 6 7
| (PHP 4, PHP 5, PHP 7) parse_str — 将字符串解析成多个变量 parse_str ( string $encoded_string [, array &$result ] ) : void
如果 encoded_string 是 URL 传递入的查询字符串(query string),则将它解析为变量并设置到当前作用域(如果提供了 result 则会设置到该数组里 )。 encoded_string result
|
演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php /* $auth = 'test'; import_request_variables('P');
if ($auth === '0') { echo "private"; #私有 }else{ echo "public"; #全局 } */ $b = 0; parse_str('b=2'); print_r($b); # 此时$b变量值已经修改了
phpinfo(); ?>
|

CTF试题
以下CTF试题参考网上;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php error_reporting(0); $flag = 'flag{V4ri4ble_M4y_Be_C0verEd}'; if (empty($_GET['b'])) { show_source(__FILE__); die(); }else{ $a = "www.sqlsec.com"; $b = $_GET['b']; @parse_str($b); if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) { echo $flag; }else{ exit('your answer is wrong~'); } } ?>
|
1 2 3 4
| @parse_str($b); 这里使用了parse_str函数来传递b的变量值 if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) 利用PHP针对hash值比较,产生的安全隐患;密码经过hash后,值将都是以0E开头;那么PHP会认为它们的值都是0.
|
因为这里用到了parse_str
函数来传递b
,if语句的条件是拿$a[0]
来比较的,因为这里的变量a的值已经是固定的了。
整体代码乍看起来又不可能,但是利用变量覆盖函数的缺陷这里可以对a
的变量进行重新赋值,后面的的if语句再利用本文前面提到的md5()
比较缺陷进行绕过:

1
| http://dvwa.me:8888/import_request_test.php?b=a[0]=a
|

1
| http://dvwa.me:8888/import_request_test.php?b=a[0]=240610708
|

mb_parse_str
函数介绍
(PHP 4 >= 4.0.6, PHP 5, PHP 7)
mb_parse_str — 解析 GET/POST/COOKIE 数据并设置全局变量。
和parse_str
函数基本一致
1 2 3
| mb_parse_str ( string $encoded_string [, array &$result ] ) : bool encoded_string # URL 编码过的数据。 result # 一个 array,包含解码过的、编码转换后的值。
|
解析 GET/POST/COOKIE 数据并设置全局变量。 由于 PHP 不提供原始 POST/COOKIE 数据,目前它仅能够用于 GET 数据。 它解析了 URL 编码过的数据,检测其编码,并转换编码为内部编码,然后设置其值为 array 的 result
或者全局变量。
1 2 3 4 5
| <?php $b = '0'; mb_parse_str('b=123123'); print_r($b); ?>
|

函数介绍
(PHP 4, PHP 5, PHP 7)
extract — 从数组中将变量导入到当前的符号表
1 2 3 4 5 6 7 8 9
| extract ( array &$array [, int $flags = EXTR_OVERWRITE [, string $prefix = NULL ]] ) : int array 一个关联数组。此函数会将键名当作变量名,值作为变量的值。 对每个键/值对都会在当前的符号表中建立变量,并受到 flags 和 prefix 参数的影响。 flags 对待非法/数字和冲突的键名的方法将根据取出标记 flags 参数决定。可以是以下值之一: EXTR_OVERWRITE 如果有冲突,覆盖已有的变量。 EXTR_SKIP 如果有冲突,不覆盖已有的变量。(常采用的防御方式)
|
本函数用来将变量从数组中导入到当前的符号表中。
检查每个键名看是否可以作为一个合法的变量名,同时也检查和符号表中已有的变量名的冲突。
1 2 3 4 5 6 7 8 9
| <?php $b = '1'; extract($_GET); if ($b == 2){ echo "123"; # 当参数值给a=2时会输出"123" }else{ echo "321"; } ?>
|

全局变量
介绍
php.ini
中有一项为register_globals
,即注册全局变量,当register_globals=On
时,传递过来的值会被直接的注册为全局变量直接使用,而register_globals=Off
时,我们需要到特定的数组里去得到它。
注意:register_globals已自 PHP 5.3.0 起废弃并将自 PHP 5.4.0 起移除。

1 2 3 4 5 6
| <?php echo "Register_globals: " . (int)ini_get("register_globals") . "<br/>"; if ($a) { echo "si1ent!"; } ?>
|
1
| http://dvwa.me:8888/import_request_test.php?a=123
|

全局变量关闭
在访问时将不会输出任何值

漏洞修复
1、以上产生漏洞PHP 7版本部分已经修复
2、部分修复方式可参考php.net
官方修复
3、参考网络检索
Referer
1 2 3 4
| https://blog.51cto.com/12332766/2121012 https://www.mi1k7ea.com/2019/06/20/PHP%E5%8F%98%E9%87%8F%E8%A6%86%E7%9B%96%E6%BC%8F%E6%B4%9E/#0x02-register-globals%E5%85%A8%E5%B1%80%E5%8F%98%E9%87%8F%E8%A6%86%E7%9B%96 PHP Hash比较Bug http://www.freebuf.com/news/67007.html
|