6.1 变量覆盖漏洞

变量覆盖指的是可以用我们自定义的参数值替换程序原有的变量值,变量覆盖漏洞通常需要结合程序的其他功能来实现完整攻击,这个漏洞想象空间非常大,比如原本一个文件上传页面,限制的文件扩展名白名单列表写在配置文件中变量中,但是在上传的过程中有一个变量覆盖漏洞可以将任意扩展名覆盖掉原来的白名单列表,那我们就可以覆盖进去一个PHP的扩展名,从而上传一个PHP的shell。

变量覆盖漏洞大多由函数使用不当导致,经常引发变量覆盖漏洞的函数有:extract()函数和parse_str(),import_request_variables()函数则是用在没有开启全局变量注册的时候,调用了这个函数则相当于开启了全局变量注册,在PHP 5.4之后这个函数已经被取消。另外部分应用利用$$的方式注册变量没验证已有变量导致覆盖也是国内多套程序都犯过的一个问题,这些应用在使用外部传递进来的参数时不是用类似于$_GET['key']这样原始的数组变量,而是把里面的key注册成了一个变量$key,注册过程中由于没有验证该变量是否已经存在就直接赋值,所以导致已有的变量值会被覆盖掉。

6.1.1 挖掘经验

由于变量覆盖漏洞通常要结合应用其他功能代码来实现完整攻击,所以挖掘一个可用的变量覆盖漏洞不仅仅要考虑的是能够实现变量覆盖,还要考虑后面的代码能不能让这个漏洞利用起来。要挖可用的变量覆盖漏洞,一定要看漏洞代码行之前存在哪些变量可以覆盖并且后面有被使用到。

由函数导致的变量覆盖比较好挖掘,只要搜寻参数带有变量的extract()、parse_str()函数,然后去回溯变量是否可控,extract()还要考虑它的第二个参数,具体细节我们后面在详细介绍这个函数的时候再讲。import_request_variables()函数则相当于开了全局变量注册,这时候只要找哪些变量没有初始化并且操作之前没有赋值的,然后就大胆地去提交这个变量作为参数吧,另外只要写在import_request_variables()函数前面的变量,不管是否已经初始化都可以覆盖,不过这个函数在PHP 4~4.1.0和PHP 5~5.4.0的版本可用。

关于上面我们说到国内很多程序使用双$$符号注册变量会导致变量覆盖,我们可以通过搜“$$”这个关键字去挖掘,不过建议挖掘之前还是先把几个核心文件通读一遍,了解程序的大致框架。

6.1.1.1 函数使用不当

目前变量覆盖漏洞大多都是由于函数使用不正确导致的,这些函数有extract()、parse_str()以及import_request_variables(),而其中最常见的就是extract()这个函数了,使用频率最高,导致的漏洞数量也最多,下面我们分别来看看这几个函数导致的漏洞原理吧。

1.extract函数

extract()函数覆盖变量需要一定条件,它的官方功能说明为“从数组中将变量导入到当前的符号表”,通俗讲就是将数组中的键值对注册成变量,函数结构如下:


int extract array &$var_array [ int $extract_type = EXTR_OVERWRITE [

    string $prefix = NULL ]]

最多三个参数,我们来看看这三个参数的作用,参见表6-1。

表 6-1

从以上说明我们可以看到第一个参数是必须的,会不会导致变量覆盖漏洞由第二个参数决定,该函数有三种情况会覆盖掉已有变量,第一种是第二个参数为EXTR_OVERWRITE,它表示如果有冲突,则覆盖已有的变量。第二种情况是只传入第一个参数,这时候默认为EXTR_OVERWRITE模式,而第三种则是第二个参数为EXTR_IF_EXISTS,它表示仅在当前符号表中已有同名变量时,覆盖它们的值,其他的都不注册新变量。

为了更清楚地了解它的用法,我们来用代码来说明,测试代码如下:


< php

$b=3


$a=array
'b'=>'1' );

extract
$a );

print_r
$b );? >

测试结果如图6-1所示。

图 6-1

原本变量$b的值为3,经过extract()函数对变量$a处理后,变量$b的值被成功覆盖为了1。

2.parse_str函数

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


void parse_str string $str [ array &$arr ]

其中$str是必须的,代表要解析注册成变量的字符串,形式为“a=1”,经过parse_str()函数之后会注册变量$a并且赋值为1。第二个参数$arr是一个数组,当第二个参数存在时,注册的变量会放到这个数组里面,但是如这个数组原来就存在相同的键(key),则会覆盖掉原有的键值。

我们来测试一下,测试代码:


< php

$b=1


parse_str
'b=2' );

print_r
$b );? >

测试结果可以看到变量$b原有的值1被覆盖成了2,如图6-2所示。

图 6-2

3.import_request_variables函数

import_request_variables()函数作用是把GET、POST、COOKIE的参数注册成变量,用在register_globals被禁止的时候,需要PHP 4.1至5.4之间的版本,不过建议是不开启register_globals也不要使用import_request_variables()函数,这样容易导致变量覆盖。该函数的说明如下:


bool import_request_variables string $types [ string $prefix ]

其中$type代表要注册的变量,G代表GET,P代表POST,C代表COOKIE,所以当$type为GPC的时候,则会注册GET、POST、COOKIE参数为变量。第二个参数$prefix为要注册的变量前缀,这里我们不细说,来看看它是怎么覆盖变量的,测试代码如下:


< php

$b=1


import_request_variables
'GP' );

print_r
$b );? >

从测试结果我们可以看到变量$b的值1被覆盖成了2,如图6-3所示。

图 6-3

6.1.1.2 $$变量覆盖

曾经有一段很经典的$$注册变量导致变量覆盖的代码,在很多应用上面都出现过这个问题,这段代码如下:


foreach array '_COOKIE' '_POST' '_GET' as $_request {

    foreach
$$_request as $_key => $_value {

        $$_key = addslashes
$_value );

    }

}

为什么它会导致变量覆盖呢,重点在$$符号,从代码中我们可以看出$_key为COOKIE、POST、GET中的参数,比如提交?a=1,则$key的值为a,而还有一个$在a的前面,结合起来则是$a=addslashes($_value);所以这样会覆盖已有的变量$a的值,我们用代码来解释会更清楚,代码如下:


< php

$a=1


foreach
array '_COOKIE' '_POST' '_GET' as $_request {

    foreach
$$_request as $_key => $_value {

        echo $_key.'<br />'


        $$_key = addslashes
$_value );

    }

}

echo $a
;? >

这段代码的执行结果如图6-4所示。从执行结果可以看出我们成功把变量$a的值覆盖成了“2”。

图 6-4

6.1.1.3 Metinfo变量覆盖漏洞分析

由于之前笔者挖到的这类漏洞没有记录,所以这里的案例是笔者临时看了一下metinfo的代码找的,我们尝试用它的变量覆盖漏洞进行SQL注入,在metinfo的include/common.inc.php文件中代码如下:


< php

/****
省略 ******/

$db_settings = parse_ini_file
ROOTPATH.'config/config_db.php' );

@extract
$db_settings );

//require_once ROOTPATH.'config/tablepre.php'


require_once ROOTPATH.'include/mysql_class.php'


$db = new dbmysql
();

$db->dbconn
$con_db_host $con_db_id $con_db_pass $con_db_name );

define
'MAGIC_QUOTES_GPC' get_magic_quotes_gpc ());

isset
$_REQUEST['GLOBALS'] && exit 'Access Error' );

require_once ROOTPATH.'include/global.func.php'


foreach
array '_COOKIE' '_POST' '_GET' as $_request {  foreach $$_request as $_key => $_value {           $_key{0} = '_' && $$_key = daddslashes $_value );   }

}

$query="select * from {$tablepre}config where name='met_tablename' and lang='metinfo'"


$mettable=$db->get_one
$query );

$mettables=explode
'|' $mettable[value] );

foreach
$mettables as $key=>$val {  $tablename='met_'.$val         $$tablename=$tablepre.$val

}

变量覆盖核心的代码如下:


foreach array '_COOKIE' '_POST' '_GET' as $_request {

    foreach
$$_request as $_key => $_value {

           $_key{0}
= '_' && $$_key = daddslashes $_value );

       }

}

这就是上面我们据介绍过的$$变量覆盖的经典代码,在这段代码之前的变量,我们都可以覆盖掉,包括数据库配置,这样就能搭建远程数据库服务以登录后台,不过我们只是为了说明这个漏洞,所以不搞那么复杂,可以看到下面有一个SQL语句中使用了$tablepre变量:


$query="select * from {$tablepre}config where name='met_tablename' and lang='metinfo'"

这里我们只要覆盖这个变量即可进行SQL注入。举例一个exp为:


/include/common.inc.php tablepre=mysql.user limit 1 %23

则执行的SQL语句为:


select * from mysql.user limit 1 #config where name='met_tablename' and lang='metinfo'

我们在以上代码的最后加上:


echo $tablepre.'<br/>'

print_r
$mettable );

exit
();

输出的执行结果已确认覆盖掉并且注入了SQL语句,请求结果证实确实成功利用,如图6-5所示。

图 6-5

6.1.2 漏洞防范

变量覆盖漏洞最常见漏洞点是在做变量注册时没有验证变量是否存在,以及在赋值给变量的时候,所以我们推荐使用原始的变量数组,如$_GET、$_POST,或者在注册变量前一定要验证变量是否存在。

6.1.2.1 使用原始变量

以上我们说的变量覆盖漏洞都是因为在进行变量注册而导致,所以要解决变量覆盖的问题,最直接的方法就是不进行变量注册,建议直接用原生的$_GET、$_POST等数组变量进行操作,如果考虑程序可读性等原因,需要注册个别变量,可以直接在代码中定义变量,然后再把请求中的值赋值给它。

6.1.2.2 验证变量存在

如果一定要使用前面几种方式注册变量,为了解决变量覆盖的问题,可以在注册变量前先判断变量是否存在,如使用extract()函数则可以配置第二个参数为EXTR_SKIP。使用parse_str()函数注册变量前需要先自行通过代码判断变量是否存在。笔者不建议使用import_request_variables()函数注册全局变量,会导致变量不可控。最重要的一点,自行申明的变量一定要初始化,不然即使注册变量代码在执行流程最前面也能覆盖掉这些未初始化的变量。