浅谈变量覆盖漏洞

本文首发于:安恒网络空间安全讲武堂

最近在跟师傅们讨论代码审计技巧的时候,好几个师傅都提到了变量覆盖漏洞,对于这一块的知识我并不是了解很多,网上的说明或多或少的都有一些粗略和不足,所以在这几天闲暇之余,我特意地将PHP变量覆盖漏洞进行了系统的总结,在此记录一下,个人难免会有疏漏和不足之处,非常欢迎各位师傅的补充与纠正

简介

我认为一个比较正确的定义是:在PHP代码中将自定义参数值替换为原有参数值的情况称为变量覆盖。

变量覆盖漏洞一般单体作用很小,并不能造成很大危害,但是在与其他应用代码或漏洞结合后,其造成的危害可能是无法估量的,最简单的例如购买商品的支付系统,某些爆出的0元支付下单的BUG就常常可以见到变量覆盖漏洞的身影。

常见的漏洞引发类型

由$$变量赋值引发的覆盖

$$是一种可变变量的写法,它可以使一个普通变量的值作为可变变量的名字,这种类型常常会使用遍历的方式来释放变量的代码,最常见的就是foreach的遍历,示例代码如下:

<?php
$yml = 10;
echo $yml;
echo "<br>";
foreach ($_POST as $k => $v){
    $$k = $v;
    echo $yml;
}
?>

无任何操作时的正常输出:

当post内容为yml=1000时:

很明显看到这里$yml的值变为了1000,我们成功的完成了一次变量覆盖。

再拿出一个我前几天给学弟们出的一个小题为例子:

代码:

<?php
include('flag.php');
$flag = 'flag{it_Is_Y0ur_flag}';
foreach ($_POST as $key => $value) {
    $a = $value;
    $$$key=$value;
    $ccut = $flag;
    $yml = $_GET["flag"];
    if ($yml == "iwantflag")
    {
        if ($ccut == "flag")
        {
            echo $fl4g;
        }
        else
        {
            echo "you will get it";
        }
    }
    else
    {
        echo "nonono";
    }    
    # code...
}
highlight_file(__FILE__);   
?>

题目的本质还是变量覆盖,题目中核心的部分就是需要将$flag的值由flag{it_Is_Y0ur_flag}覆盖为flag,仔细阅读代码流程再结合上面的例子就可以轻松解出,我这里直接给出payload:

extract()函数使用不当导致的变量覆盖

该函数可以将变量从数组中导入当前的符号表

我们看一下在w3school中函数的定义

这里我们要注意一下该函数的第二个参数,该参数的选择就确定了将变量导入符号表时的行为,在实际生产生活中,我们常常使用的值有EXTR_OVERWRITE和EXTR_SKIP。

当值设定为EXTR_SKIP时,在导入符号表的过程中,如果变量名发生冲突,则跳过该变量不进行覆盖,当值为EXTR_OVERWRITE时如果发生冲突,则覆盖已有变量,该函数在不指定第二个参数时默认使用EXTR_OVERWRITE,这就为我们提供了覆盖的可能。

示例代码:

<?php
$yml = 10;
echo 'out0:'.$yml;
extract($_POST);
echo '<br>';
echo "out1:".$yml;

?>

无post输入时

输入yml=199时:

我们成功的将$yml的值从10覆盖为了199

全局变量的覆盖

如果某些变量没有被初始化,并且黑客可以控制,将会是一件很危险的事情,在这种情况下,漏洞触发的前提是register_globals为ON(register_globals的值可以在php.ini中修改,我在个人的PHPstudy上发现在php5.2版本后该值默认是OFF)

示例代码:

<?php

echo (int)ini_get("register_globals");
echo '<br>';
echo "yml=".$yml;
?>

当register_globals为OFF时

可以我们无法将未初始化的变量进行注册,但是当register_globals的值为ON时,结果如下

可以看到我们成功注册了一个未初始化的变量

还有一种通过$GLOBALS获取的变量在使用不当时也会导致变量覆盖,同样漏洞触发的前提是register_globals为ON

还是用上面的示例代码:

我们成功通过注入GLOBALS[yml]来改变$yml的值

parse_str()函数使用不当导致的覆盖

该函数可以把查询的字符串解析到变量中,我们来看一下w3school中对该函数的定义

23.jpg

这里指的注意的是,如果未设置第二个参数的值,由该函数设置的变量将覆盖已存在的同名变量

所以当我们没有设置函数的第二个参数时,恶意攻击者很可能通过特定的输入来改变代码中已定义的变量的值

示例:

<?php
$yml = "cool";
echo "out0:".$yml;
echo "<br>";
$a = $_GET['a'];
parse_str($a);
echo "out1:".$yml;
?>

在这里我们没有设置parse_str()函数的第二个参数,现在我们来尝试构造同名变量

可以看到我们成功的使用构造同名变量的方法覆盖掉了$yml的原有值

import_request_variables所导致的变量覆盖

该函数可以将 GET/POST/Cookie 变量导入到全局作用域中,我们看一下该函数的定义(在PHP5.4之后的版本中,该函数将不再使用)

该函数的第二个参数用于设置注册变量的前缀,漏洞触发的原因是当第二个参数未进行设置时,将会出现覆盖全局变量的情况

示例:

<?php
$yml = "happy";
echo "out0:".$yml;
echo "<br>";
import_request_variables('P');
echo "out1:".$yml;
?>

无输入时:

代码没有设置import_request_variables的第二个参数,我们来设置同名变量输入看是否能够进行覆盖

在这里我们成功的注册了同名的全局变量将原有变量的值进行了覆盖。

漏洞防御

对于第一种情况,在审计的时候要注意$$的赋值语句,使用恰当的方式防止变量覆盖漏洞的发生

对于第二种情况,在使用extract()函数时,可以指定将第二个参数设置为EXTRSKIP,并且留意变量的获取顺序,控制好用户的输入。

对于第三种情况,强烈推荐将registerglobals设置为Off

对于第四种情况,我们应该在使用parse_str()时养成指定第二个参数的习惯,这样才能避免变量被覆盖

对于最后一种情况,我们同样要指定第二个函数参数来设置要注册的变量前缀,不过这个函数现在已经很少用了。

总结

变量覆盖漏洞触发的灵活性较高,但我们只要抓住根本问题,控制好用户输入并且规范代码的书写,还是可以进行防范的,该漏洞经常在ctf题目中作为一个考点出现,只要我们紧跟代码逻辑,还是很容易解出题目的。