si1ent

PHP变量覆盖漏洞学习

2020-10-31

漏洞概述

变量覆盖即通过外部输入将某个变量的值给覆盖掉。

通常将可以用自定义的参数值替换原有变量值的情况称为变量覆盖漏洞。

漏洞函数

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();
?>
如下所示

image-20201021200033539

$$导致的变量覆盖问题在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

image-20201021200537872

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

image-20201021200559430

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();
?>
执行结果如下

image-20201031141343648

POST提交

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

image-20201031142415111

PHP Version 5.6.40

访问后直接报错无法解析

1
http://dvwa.me:8888/import_request_test.php

image-20201031143301288

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 # 如果设置了第二个变量 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();
?>

image-20201031155535112

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()比较缺陷进行绕过:

image-20201031160328223

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

image-20201031160405044

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

image-20201031160454609

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);
?>

image-20201031165954876

extract
函数介绍

(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";
}
?>

image-20201031170729698

全局变量
介绍

php.ini中有一项为register_globals,即注册全局变量,当register_globals=On时,传递过来的值会被直接的注册为全局变量直接使用,而register_globals=Off时,我们需要到特定的数组里去得到它。

注意:register_globals已自 PHP 5.3.0 起废弃并将自 PHP 5.4.0 起移除。

image-20201031171309810

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

image-20201031173147773

全局变量关闭

在访问时将不会输出任何值

image-20201031173259646

漏洞修复

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
使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章