原文链接:https://www.mi1k7ea.com/2019/06/20/PHP变量覆盖漏洞/

0x01 概述

变量覆盖即通过外部输入将某个变量的值给覆盖掉。
通常将可以用自定义的参数值替换原有变量值的情况称为变量覆盖漏洞。

0x02 register_globals全局变量覆盖

php.ini中有一项为register_globals,即注册全局变量,当register_globals=On时,传递过来的值会被直接的注册为全局变量直接使用,而register_globals=Off时,我们需要到特定的数组里去得到它。

PHP变量覆盖漏洞小结 (转载) - 图1

注意:register_globals已自 PHP 5.3.0 起废弃并将自 PHP 5.4.0 起移除。

当register_globals=On,变量未被初始化且能够用户所控制时,就会存在变量覆盖漏洞:

  1. <?php
  2. echo "Register_globals: " . (int)ini_get("register_globals") . "<br/>";
  3. if ($a) {
  4. echo "Hacked!";
  5. }
  6. ?>

通过GET和POST方式输入变量a的值:

PHP变量覆盖漏洞小结 (转载) - 图2

当然,也可以从COOKIE中输入:

PHP变量覆盖漏洞小结 (转载) - 图3

0x03 extract()变量覆盖

extract()函数从数组中将变量导入到当前的符号表。该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量。
函数定义如下:

  1. int extract ( array $var_array [, int $extract_type [, string $prefix ]] )

其中,第二个参数指定函数将变量导入符号表时的行为,最常见的两个值是EXTR_OVERWRITE和EXTR_SKIP。
当值为EXTR_OVERWRITE时,在将变量导入符号表的过程中,如果变量名发生冲突,则覆盖所有变量;值为EXTR_SKIP则表示跳过不覆盖。若第二个参数未指定,则在默认情况下使用EXTR_OVERWRITE。
当extract()函数从用户可以控制的数组中导出变量且第二个参数未设置或设置为EXTR_OVERWRITE时,就存在变量覆盖漏洞:

  1. <?php
  2. $a = "0";
  3. extract($_GET);
  4. if ($a == 1) {
  5. echo "Hacked!";
  6. } else {
  7. echo "Hello!";
  8. }
  9. ?>

以上示例是以GET为例:
PHP变量覆盖漏洞小结 (转载) - 图4
防御方法:在调用extract()时使用EXTR_SKIP保证已有变量不会被覆盖

  1. extract($_GET,EXTR_SKIP);

0x04 parse_str()变量覆盖

parse_str()函数通常用于解析URL中的querystring,把查询字符串解析到变量中,如果没有array参数,则由该函数设置的变量将覆盖已存在的同名变量。
函数定义如下:

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

当parse_str()函数的参数值可以被用户控制时,则存在变量覆盖漏洞:

  1. <?php
  2. $a = 'oop';
  3. parse_str($_SERVER["QUERY_STRING"]);
  4. if ($a == 'mi1k7ea') {
  5. echo "Hacked!";
  6. } else {
  7. echo "Hello!";
  8. }
  9. ?>

PHP变量覆盖漏洞小结 (转载) - 图5

再来看到小题目:

  1. <?php
  2. error_reporting(0);
  3. if(empty($_GET['id'])) {
  4. show_source(__FILE__);
  5. die();
  6. } else {
  7. include ('flag.php');
  8. $a = "www.mi1k7ea.com";
  9. $id = $_GET['id'];
  10. @parse_str($id);
  11. if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
  12. echo $flag;
  13. } else {
  14. exit('so easy!');
  15. }
  16. }
  17. ?>

是弱类型和变量覆盖的结合:

PHP变量覆盖漏洞小结 (转载) - 图6

0x05 mb_parse_str()变量覆盖

mb_parse_str()函数用于解析GET/POST/COOKIE数据并设置全局变量,和parse_str()类似:

  1. <?php
  2. $a = 'oop';
  3. mb_parse_str($_SERVER["QUERY_STRING"]);
  4. if ($a == 'mi1k7ea') {
  5. echo "Hacked!";
  6. } else {
  7. echo "Hello!";
  8. }
  9. ?>

0x06 import_request_variables()变量覆盖

支持版本:PHP 4 >= 4.1.0, PHP 5 < 5.4.0

import_request_variables()函数将GET、POST、Cookies中的变量导入到全局。

函数定义如下:

  1. bool import_request_variables (string $types [, string $prefix])

$type代表要注册的变量,G代表GET,P代表POST,C代表COOKIE,第二个参数为要注册变量的前缀。

使用这个函数只用简单地指定类型即可,这里G指定导入GET请求中的变量:

  1. <?php
  2. $a = "0";
  3. import_request_variables("G");
  4. if ($a == 1) {
  5. echo "Fucked!";
  6. } else {
  7. echo "Nothing!";
  8. }
  9. ?>

PHP变量覆盖漏洞小结 (转载) - 图7

0x07 $$导致的变量覆盖

$$即可变变量,一个可变变量获取了一个普通变量的值作为这个可变变量的变量名。

$与$$的区别

$var是一个正常变量,名称为:var,存储任何值,如:string,integer,float等。

$$var是一个引用变量,用于存储$var的值。

看个Demo就清楚了:

  1. <?php
  2. $x = "mi1k7ea";
  3. $$x = 666;
  4. echo $x."<br/>";
  5. echo $$x."<br/>";
  6. echo $mi1k7ea;
  7. //也可以写到双引号中解析,输出结果一样
  8. // echo "$x<br/>";
  9. // echo "${$x}<br/>";
  10. // echo "$mi1k7ea";
  11. ?>

PHP变量覆盖漏洞小结 (转载) - 图8

变量覆盖漏洞

$$导致的变量覆盖问题在CTF代码审计题目中经常在foreach中出现,如以下的示例代码,使用foreach来遍历数组中的值,然后再将获取到的数组键名作为变量,数组中的键值作为变量的值。因此就产生了变量覆盖漏洞。:

  1. <?php
  2. foreach (array('_COOKIE','_POST','_GET') as $_request)
  3. {
  4. foreach ($$_request as $_key=>$_value)
  5. {
  6. $$_key= $_value;
  7. }
  8. }
  9. $id = isset($id) ? $id : "test";
  10. if($id === "mi1k7ea") {
  11. echo "flag{xxxxxxxxxx}";
  12. } else {
  13. echo "Nothing...";
  14. }
  15. ?>

这里以GET、POST或COOKIE都能触发,传入id=mi1k7ea后,在foreach语句中,$_key为id,$_value为mi1k7ea,进而$$_key为$id,从而实现了变量覆盖:

PHP变量覆盖漏洞小结 (转载) - 图9

一道CTF题目

  1. <?php
  2. include "flag.php";
  3. $_403 = "Access Denied";
  4. $_200 = "Welcome Admin";
  5. if ($_SERVER["REQUEST_METHOD"] != "POST"){
  6. die("BugsBunnyCTF is here :p…");
  7. }
  8. if ( !isset($_POST["flag"]) ){
  9. die($_403);
  10. }
  11. foreach ($_GET as $key => $value){
  12. $$key = $$value;
  13. }
  14. foreach ($_POST as $key => $value){
  15. $$key = $value;
  16. }
  17. if ( $_POST["flag"] !== $flag ) {
  18. die($_403);
  19. } else {
  20. echo "This is your flag : ". $flag . "\n";
  21. die($_200);
  22. }
  23. ?>

可以看到,有3个if语句和2个foreach语句。

在if语句中,第一个需要你是通过POST方式进行请求,第二个是需要POST一个flag参数过去,第三个是比较flag参数和包含进来的真正的flag是否相等。

在foreach语句中,第一个是可以将GET的参数进行变量覆盖,第二个是将POST的参数进行变量覆盖,但两个语句的处理是有点区别的即一个键值为$$value另一个为$value。

这里整理一下思路:因为POST的参数必须为flag,则第二个foreach语句的$key为flag,进而$$key为$flag,从而得到$flag的值为POST传递的flag参数的值;这里因为第二个foreach语句修改了$flag原来的值为POST传递的flag参数的值,因而最后一个if语句的条件是恒不成立的,在其后的else代码块逻辑中echo输出出来的只能是修改了的$flag的值即POST传递的flag参数的值而非原本的$flag的值、接着输出$_200变量的值;要想输出原本的$flag的值,我们需要将原本的$flag覆盖$_200变量,因此在第一个foreach语句中通过GET输入_200=flag,从而得到的$$key为$_200、$$value为$flag,从而实现在修改$flag的值之前将其覆盖到$_200变量中。

验证一下:

PHP变量覆盖漏洞小结 (转载) - 图10

0x08 参考

CTF之php变量覆盖漏洞