blubiu

My Love

上网不网恋,简直浪费电.


代码审计--变量覆盖


前言

变量覆盖一点点小笔记


目录:


漏洞原理

变量覆盖(Dynamic Variable Evaluation) 是指变量未被初始化,

而我们自定义的变量可以替换程序原有的变量值。


相关函数

$$extractparse_strimport_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) 当作变量名来使用了。

images


再看个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 函数 ,超全局变量,可以接收 GETPOSTCOOKIE的值

来分析下代码,

要满足三个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

images


再来看第二个遍历语句

foreach ($_POST as $key => $value){  // 遍历 GET数据
  $$key = $value;
}

当输入 flag=aaa 时 ,将会变成 $flag=aaa

images


既然我们知道了这些条件,那么将如何构造payload呢???

经过上面分析,我们知道必须要满足 $_POST["flag"] !== $flag

那么也就是说,$_GEvalue值 要等于 $_POST 的 key值

同事 $_POST 不能为空

images


2. extract()函数

介绍:
extract() 函数从数组中将变量导入到当前的符号表。

该函数使用数组键名作为变量名,使用数组键值作为变量值。

语法:extract(array,extract_rules,prefix)

extract_rules 可能值:

  1. EXTR_OVERWRITE -     默认。如果有冲突,则覆盖已有的变量。
  2. EXTR_SKIP -     如果有冲突,不覆盖已有的变量。
  3. EXTR_PREFIX_SAME -     如果有冲突,在变量名前加上前缀 prefix。
  4. EXTR_PREFIX_ALL -     给所有变量名加上前缀 prefix。
  5. EXTR_PREFIX_INVALID -     仅在不合法或数字变量名前加上前缀 prefix。
  6. EXTR_IF_EXISTS -     仅在当前符号表中已有同名变量时,覆盖它们的值。其它的都不处理。
  7. EXTR_PREFIX_IF_EXISTS -    仅在当前符号表中已有同名变量时,建立附加了前缀的变量名,其它的都不处理。
  8. 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)

参数:

  1. string   必需。规定要解析的字符串。
  2. 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()函数就是把GETPOSTCOOKIE的参数注册成变量,

register_globals被禁止的时候使用

语法: bool import_request_variables ( string $types [, string $prefix ] )

例:

<?php 

  $b = 1;
  import_request_variables('GP');
  echo $b;

?>

其中 G表示 GETP表示POSTC表示 COOKIE

当我们输入 ?b=2 的时候,

$b 的值 1 会被覆盖为 2


总结

在CTF比赛中,出现最多的是 $$extract()

总的来说还是要多看,多练


参考文献

https://www.jianshu.com/p/a4d782e91852

https://www.freebuf.com/column/150731.html