变量覆盖漏洞的基础概念:
变量覆盖漏洞是指自定义的参数替换原有变量值的情况,如$$使用不当,extract函数使用不当,parse_str() 函数使用不当,import_request_variables() 使用不当,开启了全局变量注册等。
漏洞简介:
当register_globals全局变量设置开启时,传递过来的值会被直接注册为全局变量而使用,这会造成全局变量覆盖 。
register_globals全局变量设置, 在PHP5.3之前默认开启, PHP5.3默认关闭 ,PHP5.6以后已经被移除 。
漏洞示例:
$num=0;
if ($num){
echo "flag{this is flag}";
}
?>
//payload: http://xx.xx.xx.xx/test.php?num=1
//result: flag{this is flag}
//解析:由于我们使用的是低版本的PHP,默认开启了全局变量,当我们传入num的时候,PHP会直接将传入的参数定义为全局变量,导致变量覆盖。
基本概念:
PHP动态变量是指一个变量名的变量名可以动态的设置和使用,一个变量获取另一个变量的值作为这个变量的变量名。
示例代码:
$bar= "a";
$Foo="Bar";
$World="Foo";
$Hello="world";
$a="Hello";
echo $a; //hello
echo $$a; //world
echo $$$a; //foo
echo $$$$$a; //Bar
echo $$$$$$a; //a
echo $$$$$$$a; //hello
echo $$$$$$$$a; //world
?>
函数详解:
定义:extract() 函数从数组中将变量导入到当前的符号表。该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量,返回成功设置的变量数目。
语法:extract(array,extract_rules,prefix)
//array:必须,规定用于准换的数组
//extract_rules:可选,函数将检查每个键名是否为合法的变量名,同时也检查和符号表中已存在的变量名是否冲突。对不合法和冲突的键名的处理将根据此参数决定。默认设置EXTR_OVERWRITE,有冲突则覆盖。
//prefix:该参数规定了前缀。前缀和数组键名之间会自动加上一个下划线,可选。
示例代码:
$a = "Original";
$my_array = array("a" => "Cat", "b" => "Dog", "c" => "Horse");
extract($my_array);
echo "\$a = $a; \$b = $b; \$c = $c; ";
?>
//result: $a = Cat; $b = Dog; $c = Horse;
函数详解:
语法:parse_str(string,array)
定义:parse_str() 函数把查询字符串解析到变量中。如果未设置 array 参数,由该函数设置的变量将覆盖已存在的同名变量。
注释:php.ini 文件中的 magic_quotes_gpc 设置影响该函数的输出。如果已启用,那么在 parse_str() 解析之前,变量会被 addslashes() 转换。
示例代码:
parse_str("a=this&b=is&c=flag");
echo $a." ";
echo $b." ";
echo $c;
?>
//this is flag
函数解析:
使用范围:PHP 4 >= 4.1.0, PHP 5 < 5.4.0
函数语法:import_request_variables ( string $types [, string $prefix ] )
作用:将 GET/POST/Cookie 变量导入到全局作用域中。如果你禁止了 register_globals,但又想用到一些全局变量,那么此函数就很有用。
参数说明:
$types:指定需要导入的变量,可以用字母 G、P 和 C 分别表示 GET、POST 和 Cookie,这些字母不区分大小写
$prefix:变量名的前缀,置于所有被导入到全局作用域的变量之前。所以如果你有个名为 userid 的 GET 变量,同时提供了 pref_ 作为前缀,那么你将获得一个名为 $pref_userid 的全局变量。虽然 prefix 参数是可选的,但如果不指定前缀,或者指定一个空字符串作为前缀,你将获得一个 E_NOTICE 级别的错误。
示例代码:
$num=0;
$flag="this is flag";
import_request_variables('gp'); //导入get和post中变量
if($num){
echo $flag;
}else{
echo "NO!";
}
?>
//payload:http://127.0.0.1/test.php?num=xiaohua
//result: this is flag
源码:
$flag="flag{34nt_8tuhg_INF_49nfe}";
$yds = "dog";
$is = "cat";
$handsome = 'yds';
foreach($_POST as $x => $y){ //遍历POST中的数据,然后将$value赋值给$$key。
$$x = $y;
}
foreach($_GET as $x => $y){ //循环遍历GET中的数据,将$$value赋值给$$key
$$x = $$y;
}
foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){ //循环遍历GET参数,如果设置flag参数,并且参数值不等于flag,输出$handsome
exit($handsome);
}
}
if(!isset($_GET['flag']) && !isset($_POST['flag'])){ //POST和GET都不设置flag参数,输出$yds
exit($yds);
}
if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){ //post或者get设置了flag参数,输出$is
exit($is);
}
echo "the flag is: ".$flag;
//payload: test.php?yds=flag
//result: flag{34nt_8tuhg_INF_49nfe}
思路分析:
这里发现存在 $$x = $$y;
所以肯定是需要进行变量覆盖利用的,如果我们想要在最后echo的时候,输出flag,就需要绕过前面的三个检测。
首先如果设置了flag参数,则会在第2个foreach中,使我们的$flag=$$value
,这里如果构造$value=flag
,那么在第三个foreach中,进入if判断,输出$handsome
。所以无法利用。
如果我们在POST或者GET都不设置flag参数,在第二个foreach中,就会将$$key
赋值为$$value
,然后进入if(!isset($_GET['flag']) && !isset($_POST['flag']))
输出$yds.
如果此时我们传入?yds=flag
,就会让$yds=$falg
,成功在exit处输出。
如果我们试图进入最后一个if,进行判断,首先设置GET参数flag=flag的话,在第二个foreach中会设置$flag=$flag
,并没有什么影响,所以需要加上POST参数,这里如果设置POST参数is=$flag
的话,会输出$falg
,导致flag变量变成了$flag
字符串,无法获取正常的flag。
如果在POST中设置flag=flag的话,就会导致$flag=flag
,也不可行,所以就只有上面一个方法能行得通。
源码:
<meta charset="utf-8">
<?php
error_reporting(0);
if (empty($_GET['b'])) {
show_source(__FILE__);
die();
}else{
$flag = "flag{This is flag!!!}";
$a = "www.XMAN.com";
$b = $_GET['b'];
parse_str($b);
echo $b,"
";
var_dump($a);
echo "
";
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
echo $flag;
}else{
exit('你的答案不对0.0');
}
}
?>
可以发现,我们需要满足$a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')
才能输出flag。可以知道这里需要使用PHP弱类型绕过md5检测,但是$a
已经国定,所以就需要想办法改变$a的值。
由于上面使用了 parse_str()函数,所以就可以尝试使用变量覆盖来改变$a的值。
绕过MD5检测,可以让$a[0]
等于任意的弱碰撞即可,比如s1502113478a
。然后利用 parse_str()进行变量覆盖的话,我们可以传入?b=a[]=s1502113478a
,这样就能成功的覆盖$a.
所以这里的paylaod就是:test.php?b=a[]=s1502113478a
测试结果如下:
源码:
$flag='dog.txt';
extract($_GET);
if(isset($shiyan)){
$content=trim(file_get_contents($flag));
if($shiyan==$content){
echo 'flag{this is flag,your are right!}';
}else {
echo'Oh.no';
}
}
?>
简单分析:
首先使用了extrct()来导入GET中的变量。
如果设置了$shiyan参数
,则进入if语句。
使用file_get_contents()
获取$flag对应的文件的内容,并去除前后的空格。
如果$shiyan的值等于文件内容,则输出flag。
所以这里需要解决的啷个问题就是:
isset($shiyan) //true
$shiyan==$content //true
对于第一个问题,我们只需要传入一个名为shiyan的参数即可,对于第二个问题,由于这里使用了==
,所以存在弱类型的问题,当我们的文件不存在或读取失败的时候,会返回空,所以,只要我们传入的$shiyan
为空即可绕过。
测试结果: