漏洞原理
变量覆盖(Dynamic Variable Evaluation) 是指变量未被初始化,
而我们自定义的变量可以替换程序原有的变量值。
相关函数
$$ , extract , parse_str , import_request_variables 等等
这里涉及到一个安全函数: register_globals
register_globals 的意思就是注册为全局变量,
当为 On 的时候,传递过来的值会被直接注册为全局变量直接使用,
当为 Off 的时候,则需要到特定的数组里去得到它。
注:此选项已经被 PHP5.3.0 起废弃,并将自 PHP 5.4.0 起移除
1. $$可变变量
双 $$
这种写法称为可变变量,
为了更好的理解,举个例子:
<?php $a = "hello"; echo "$a".'<hr>'; // 输出 hello $a = "world"; echo "$a".'<hr>'; // 输出 world echo "$$a".'<hr>'; // 输出 $world ?>
解释:一个可变变量($$a)
获取了一个普通变量($a)
的值(world)
作为这个可变变量的变量名($world)
。
再来一个常见的例子
<?php foreach ($_GET as $key => $value) { $$key = $value; echo $$key.'<hr>'; } ?>
这里的意思是变量GET的内容,然后把值($value) 当作 $$key 的变量名,
当我们输入 flag=aaa
的时候,来看看结果
这个时候 $$key 把 值(flag) 当作变量名来使用了。
再看个ctf题
<?php include "flag.php"; // 包含并运行指定文件 $_403 = "Access Denied"; // 设置初始变量 $_200 = "Welcome Admin"; if ($_SERVER["REQUEST_METHOD"] != "POST") // 返回post的值 { die("BugsBunnyCTF is here :p..."); } if ( !isset($_POST["flag"]) ) // 检查是否设置,没有设置直接pass { die($_403); } foreach ($_GET as $key => $value) // 遍历 GET数据 { $$key = $$value; } foreach ($_POST as $key => $value) // 遍历 POST数据 { $$key = $value; } if ( $_POST["flag"] !== $flag ) { die($_403); } echo "This is your flag : ". $flag . "\n"; die($_200); ?>
die()
函数输出一条消息,并退出当前脚本。
REQUEST_METHOD 函数 ,超全局变量,可以接收 GET,POST,COOKIE的值
来分析下代码,
要满足三个if条件才能访问到flag,
第一个if判断有没有POST数据传递过来,
第二个if判断传递过来的POST数据有没有参数flag,
第三个if判断POST参数flag的值是不是等于$flag变量的值,
如果不是很熟悉代码的话,这三个if判断估计会有点懵逼,
我们先一步一步来测试吧。
首先从这段代码开始
第一个遍历语句
foreach ($_GET as $key => $value){ // 遍历 POST数据 $$key = $$value; }
value 的意思是 把键和值分别当作变量名来使用,
因为两个都是可变变量,所有不存在谁使用谁的值,
我们GET输入 flag=aaa
看看结果,
经过 $$key = $$value
处理后讲会变成了 $flag = $aaa
再来看第二个遍历语句
foreach ($_POST as $key => $value){ // 遍历 GET数据 $$key = $value; }
当输入 flag=aaa
时 ,将会变成 $flag=aaa
既然我们知道了这些条件,那么将如何构造payload呢???
经过上面分析,我们知道必须要满足 $_POST["flag"] !== $flag
,
那么也就是说,$_GE
的 value值 要等于 $_POST
的 key值
同事 $_POST
不能为空
2. extract()函数
介绍:extract()
函数从数组中将变量导入到当前的符号表。
该函数使用数组键名作为变量名,使用数组键值作为变量值。
语法:extract(array,extract_rules,prefix)
extract_rules 可能值:
- EXTR_OVERWRITE - 默认。如果有冲突,则覆盖已有的变量。
- EXTR_SKIP - 如果有冲突,不覆盖已有的变量。
- EXTR_PREFIX_SAME - 如果有冲突,在变量名前加上前缀 prefix。
- EXTR_PREFIX_ALL - 给所有变量名加上前缀 prefix。
- EXTR_PREFIX_INVALID - 仅在不合法或数字变量名前加上前缀 prefix。
- EXTR_IF_EXISTS - 仅在当前符号表中已有同名变量时,覆盖它们的值。其它的都不处理。
- EXTR_PREFIX_IF_EXISTS - 仅在当前符号表中已有同名变量时,建立附加了前缀的变量名,其它的都不处理。
- EXTR_REFS - 将变量作为引用提取。导入的变量仍然引用了数组参数的值。
从以上说明我们可以看到第一个参数是必须的,
会不会导致变量覆盖漏洞由第二个参数决定,
该函数有三种情况会导致变量覆盖,
第一种:当第二个参数为 EXTR_OVERWRITE 时,他表示如果有冲突,则覆盖已有的变量,
第二种:当只传入第一个参数,这个时候默认为 EXTR_OVERWRITE模式,
第三种:当第二个参数为 EXTR_PREFIX_IF_EXISTS,它表示仅在当前符号表中已有的同名变量时,覆盖他们的值
举个粟子
<?php $a = 1; // 初始值为1 $b = array('a' => '2'); extract($b); // 经过extract()函数对$b处理后 echo $a; //输出结果为2 ?>
这种满足第一种情况,本来 $a已经有初始值了,
然后又将 a 的值指定为 2 , =>
定义数组键对值,
当经过 extract()函数处理后,两个值就冲突了,
所以后面的值会覆盖掉前面的值
举个CTF粟子
<?php if ($_SERVER["REQUEST_METHOD"] == "POST") { ?> <?php extract($_POST); if ($pass == $thepassword_123) { ?> <div class="alert alert-success"> <code><?php echo $theflag; ?></code> </div> <?php } ?> <?php } ?>
$_SERVER
是一个包含了诸如头信息(header)、路径(path)、以及脚本位置(script locations)等等信息的数组。
REQUEST_METHOD
访问页面使用的请求方法;例如,“GET”, “HEAD”,“POST”,“PUT”。
代码首先判断是不是POST提交,然后通过 extract()函数来处理POST参数,
如果 $pass == $thepassword_123
那么就输出flag
我们知道,这个函数判断变量有没有冲突,有冲突就覆盖,
那么我们可以让他不冲突,同时又满足了 $pass == $thepassword_123
那么payload就应该是这样子的: $pass=233&$thepassword_123=233
3. parse_str()函数
parse_str() 函数把查询字符串解析到变量中,如果没有array 参数,则由该函数设置的变量将覆盖已存在的同名变量。
语法: parse_str(string,array)
参数:
- string 必需。规定要解析的字符串。
- array 可选。规定存储变量的数组的名称。该参数指示变量将被存储到数组中。
例:
<?php $b = 1; parse_str('b=2'); echo $b; ?>
经过 parse_str() 函数处理后,注册的变量会放到这个数组里面,
如果这个数组原来就存在相同的键(key) ,也就是 b 的时候,
则会覆盖掉原有的键值(value) , 然后输出 2
来看道题
<?php error_reporting(0); if(empty($_GET['id'])) { // empty()检查是否为空 show_source(__FILE__); // highlight_file 代码高亮 die(); // 等同于exit 输出一个消息并且退出当前脚本 } else { include ('flag.php'); $a = "www.OPENCTF.com"; $id = $_GET['id']; @parse_str($id); if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('NKCDZO')) { echo $flag; } else { exit('其实很简单其实并不难!'); } } ?>
首先检查GET参数id是否为空,如果为空直接退出当前脚本 ,
给 $a 设置初始值,用变量$id来接收GET的数据 ,
然后使用 parse_str()
函数来处理 $id ,
然后再判断 $a[0]
不能等于 QNKCDZO
,并且要求 md5加密后的 $a[0]
要等于 md5加密后的 NKCDZO
我们知道 PHP在处理哈希字符串时,会利用 "!="
或 "=="
来对哈希值进行比较,
它把每一个以 "0E"
开头的哈希值都解释为0,
所以如果两个不同的密码经过哈希以后,其哈希值都是以 "0E"
开头的,
那么PHP将会认为他们相同,都是 0 。
详情请参考:
https://www.freebuf.com/news/67007.html
然后我们发现,NKCDZO 经过 md5加密(16位) 后是以 0e
开头,
所以只要 a[0] 的值加密之后能以 0e
开头,条件就成立,
然后payload:
?id=a[0]=s1836677006a
或者
?id=a[0]=240610708
s1836677006a
md5加密(32位),也是以 0e
开头,
$a已经有初始值了,所以当我们输入payload后,就会替换掉原来的 $a的值,
最后 0=0
条件成立
4. import_request_variables()函数
mport_request_variables()函数就是把GET、POST、COOKIE的参数注册成变量,
在register_globals被禁止的时候使用
语法: bool import_request_variables ( string $types [, string $prefix ] )
例:
<?php $b = 1; import_request_variables('GP'); echo $b; ?>
其中 G表示 GET , P表示POST ,C表示 COOKIE
当我们输入 ?b=2 的时候,
$b 的值 1
会被覆盖为 2