PHP代码审计之变量覆盖攻击

0x01.$$导致的变量覆盖问题

$$介绍

$$这种写法称之为可变变量:一个可变变量获取了一个普通变量的值作为这个可变变量的变量名

demo

1
2
3
4
5
6
7
8
9
<?php
$a = "hello";
echo "$a<br />"; //输出hello
$$a= "world"; //等同于$hello= "world"
echo "$a<br />"; //输出hello
echo $$a."<br />"; //输出word
echo "$a ${$a}<br />"; //输出hello world
echo "$a $hello<br />"; //输出hello world
?>

漏洞产生


此时我们来看这串代码;此时我们看出$key为COOKIE、POST、GET中的参数,$$request是一个间接引用,它会根据$request的值(即请求类型的名称)获取相应的全局变量数组。例如,如果$request的值是”COOKIE”,那么$$request就等同于$COOKIE;如果$request的值是”POST”,那么$$request就等同于$POST;如果$request的值是”GET”,那么$$request就等同于$GET。然后,
通过foreach循环遍历选定的全局变量数组,将每个元素的键存储在变量$key中,将每个元素的值存储在变量$value中。
那么此时我们继续使用一个demo来实验以下变量覆盖

1
2
3
4
5
6
7
8
9
10
11
<?php

$a = 1;
foreach(array('_COOKIE', '_POST', '_GET') as $_request) {
foreach($$_request as $_key => $_value) {
echo $_key.'<br />';
$$_key = addslashes($_value);
}
}
echo $a;
?a=2

此时我们提交a=2;那么在这个代码实施过程中会使得$key=a;$value=2;接着$$key=$a;那么此时又$$key=$value;将value的值赋给了$a;此时便造成了变量覆盖攻击;将原来有的$a=1通过覆盖成了$a=2

题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php

include 'flag.php';

$yds = "dog";
$is = "cat";
$handsome = 'yds';

foreach($_POST as $x => $y){    
$$x = $y ;
}

foreach($_GET as $x => $y){   
$$x = $$y;
}

foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){ 
exit($handsome);
}
}

if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($yds);
}

if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
exit($is);
}



echo "the flag is: ".$flag;
}

此时我们就可以利用GET传入?yds=flag;然后经过第一次的foreach()后就会变成$x=yds;$y=flag;那么此时的$$x=$yds;然后$$y=$flag;即$yds=$flag;将原本的$yds = “dog”;进行覆盖成$yds=$flag;那么此时进入到第二个个if()中便会exit($flag)从而将flag显示出来

extract()函数

extract()

1
extract(array[,flag][,prefix])

array

一个关联数组(必需)。此函数会将键名当作变量名,值作为变量的值。 对每个键/值对都会在当前的符号表中建立变量,并受到 flags 和 prefix 参数的影响。

flags

必须使用关联数组,数字索引的数组将不会产生结果,除非用了 EXTR_PREFIX_ALL 或者 EXTR_PREFIX_INVALID。flags
对待非法/数字和冲突的键名的方法将根据取出标记 flags 参数决定。可以是以下值之一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
EXTR_OVERWRITE
如果有冲突,覆盖已有的变量。(默认)
EXTR_SKIP
如果有冲突,不覆盖已有的变量。
EXTR_PREFIX_SAME
如果有冲突,在变量名前加上前缀 prefix。
EXTR_PREFIX_ALL
给所有变量名加上前缀 prefix。
EXTR_PREFIX_INVALID
仅在非法/数字的变量名前加上前缀 prefix。
EXTR_IF_EXISTS
仅在当前符号表中已有同名变量时,覆盖它们的值。其它的都不处理。 举个例子,以下情况非常有用:定义一些有效变量,然后从 $_REQUEST 中仅导入这些已定义的变量。
EXTR_PREFIX_IF_EXISTS
仅在当前符号表中已有同名变量时,建立附加了前缀的变量名,其它的都不处理。
EXTR_REFS
将变量作为引用提取。这有力地表明了导入的变量仍然引用了 array 参数的值。可以单独使用这个标志或者在 flags 中用 OR 与其它任何标志结合使用。

prefix

注意 prefix 仅在 flags 的值是 EXTR_PREFIX_SAME,EXTR_PREFIX_ALL,EXTR_PREFIX_INVALID 或 EXTR_PREFIX_IF_EXISTS 时需要。 如果附加了前缀后的结果不是合法的变量名,将不会导入到符号表中。前缀和数组键名之间会自动加上一个下划线。

demo1

1
2
3
4
5
<?php
$a = array('name' => 'tom');
extract($a);
echo $name;
?>

demo2

原本变量$b的值为3,经过extract()函数对变量$a处理后,变量$b的值被成功覆盖为’hello’。
安全的做法是确定register_globals=OFF后,在调用extract()时使用EXTR_SKIP保证已有变量不会被覆盖

1
2
3
4
5
<?php

$a = 1;
extract($_GET, EXTR_SKIP);
echo $a;


但是此时若是变量未被定义或者未被初始化依然会出现变量覆盖的情况

题目

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
include('flag.php');
extract($_GET);
if(isset($bdctf)) {
$content=trim(file_get_contents($flag)); //trim 函数移除字符串两侧的空白字符或其他预定义字符。
if($bdctf==$content){
echo $flag;
} else {
echo'这里没有flag';
}
}

parse_str()函数

parse_str()函数的作用是解析字符串并且注册成变量,它在注册变量之前不会验证当前变量是否已经存在,所以会直接覆盖掉已有变量。
parse_str()

1
void parse_str ( string $str [, array &$arr] )

其中$str是必须的,代表要解析成变量的字符串,形式为”a=1”,经过parse_str()函数之后会注册变量$a并且赋值为1。第二个参数$arr是一个数组,当第二个参数存在时,注册的变量会放到这个数组里面,但是如这个数组原来就存在相同的键(key),则会覆盖掉原有的键值。
ps:php.ini 文件中的 magic_quotes_gpc 设置影响该函数的输出。如果已启用,那么在 parse_str() 解析之前,变量会被 addslashes() 转换。

tips

GPC会自动把我们提交上去的单引号等敏感字符转义掉,这样我们的攻击代码就没法执行了,GPC是PHP天生自带的功能。GPC是用来过滤request中提交的数据,将特殊字符进行转义来防止攻击,在PHP5之后用$SERVER取到的header字段不受GPC影响,所以当GPC开启的时候,它里面的特殊字符如单引号也不会被转义掉,另外一点是普通程序员很少会考虑这些字段被修改。而在header注入里面最常见的是user-agent、referer以及client-ip/x-forward-for,
因为大多的Web应用都会记录访问者的IP以及referer等信息。同样的$FILES变量也一样不受GPC保护。

demo1

1
2
3
4
<?php
echo 'GPC'.get_magic_quotes_gpc();
echo '<br /> client-ip = '.$_SERVER['HTTP_CLIENT_IP'];
echo '<br />$_GET[a] = '.$_GET['a'];

demo2

此时我们来测试一下

1
2
3
4
<?php
$b = 1;
parse_str('b=2');
print_r($b);

例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
error_reporting(0);
include('flag.php');
if(empty($_GET['id'])) {
show_source(__FILE__);
die();
}else{
$a = 'https://baidu-.com/';
$id = $_GET['id'];
@parse_str($id);
if ($a[0] != ‘QNKCDZO’ && md5($a[0]) == md5(‘QNKCDZO’)) {
echo $flag;
}else{
exit(‘其实很简单并不难!’);
}
}
?>

payload

1
?id=a[0]=240610708