PHP代码审计 -- $$变量覆盖( unset() 与 extract() )

  • 知识点
  • $$变量覆盖漏洞利用
  • unset() 与 extract() 配合下的巧妙利用

结合例题解析

打开解题网址,目录扫描,得到一个 web1.zip,两个php源码:

在这里插入图片描述1、code.php:

<?php

class Pan
{
    public $hostname = '127.0.0.1';
    public $username = 'root';
    public $password = 'root';
    public $database = 'ctf';
    private $mysqli = null;

    public function __construct()
    {
        
        $this->mysqli = mysqli_connect(
            $this->hostname,
            $this->username,
            $this->password
        );
        mysqli_select_db($this->mysqli,$this->database);

        
    }

    public function filter($string) 
    {
        $safe = preg_match('/union|select|flag|in|or|on|where|like|\'/is', $string);    # s 匹配所有字符
        if($safe === 0){
            return $string;
        }else{
            return False;
        }
		    
    }

    public function getfile()
    {
        
        $code = $_POST['code'];

        if($code === False) return '非法提取码!';
        $file_code = array(114514,233333,666666);
        
        if(in_array($code,$file_code))
        {
            $sql = "select * from file where code='$code'";
            $result = mysqli_query($this->mysqli,$sql);
            $result = mysqli_fetch_object($result);
            return '下载直链为:'.$result->url;
        }else{
            return '提取码不存在!';
        }
        
    }

}

2、index.php 重要部分源码(注释部分请自动忽略~):

			<?php
			include 'code.php';		# 包含 code.php

			$pan = new Pan();		# 创建实例

			foreach(array('_GET', '_POST', '_COOKIE') as $key)		# 三种传值方式	$key = _POST
			{   
				if($$key) {		# $$key = $_GET
					foreach($$key as $key_2 => $value_2) { 			# 将封装在$_GET 等数组中的值拿出来 # $key_2 = $$key = $_POST($_POST 也是一个数组)  $value_2 = Array(cod = ...)
						if(isset($$key_2) and $$key_2 == $value_2)  # 存在 $$变量覆盖 				  #	 所以 $$key == $value_2 条件成立
							unset($$key_2); 		# 销毁变量
					}
				}
			}

			if(isset($_POST['code'])) $_POST['code'] = $pan->filter($_POST['code']);	# 如果POST 创建,进行过滤,需要绕过这个判断才能进行 sql 注入
			if($_GET) extract($_GET, EXTR_SKIP);	#  extract() ,EXTR_SKIP参数:已有变量不被覆盖	 不存在则创建
			if($_POST) extract($_POST, EXTR_SKIP);
			if(isset($_POST['code']))
			{
				$message = $pan->getfile();		# 方法调用
				echo <<<EOF
				<div class="alert alert-dismissable alert-info">
				 <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
				<h4>
					注意!
				</h4> <strong>注意!</strong> {$message}
				</div>
EOF;
			}
			?>
  • 代码逻辑分析:
    首先从网页那边的提取码登录框 利用 post 传参 code 到 index.php, index.php 会将 code 送入第一个 if , filter() 函数进行过滤,以防止 sql 注入,很显然,如果绕不过 filter 函数,则返回 False ;

观察filter 函数以及两个 php 代码之间的逻辑关系,不难发现,要解题有两种方式: 1,filter 没有过滤 if,sleep 等函数,但是禁用了单引号,所以使用盲注会非常难受 ; 2 ,利用 $$变量覆盖 和 unset() 与 extract() 漏洞 绕过 filter() 检测,使用联合注入;

  • 所以这里直接使用第二种方法,首先学习一下什么是 $$变量覆盖 以及 unset() 与 extract()

  • $$变量覆盖
    $$变量即可变变量,将一个变量的值加上 $ 来作为 另一个变量的名字;
    本地演示代码:

<?php
    $x = "hello";
    $$x = 666;
    echo $x;
    echo "<br>";
    echo $$x;
    echo "<br>";
    echo $$x === $hello;
?>

结果如下:显然,可变变量 $$x 将 $x 的值 hello 拿来拼接上 $ 变成了 $hello ,于是 $$x === $hello ;
在这里插入图片描述

$$变量覆盖问题经常在php代码审计中与 foreach() 遍历数组来出题,本地漏洞利用 代码示例:

<?php
    foreach(array('_GET','_POST') as $key){
        if($$key){
          #  var_dump($$key);     # 输出 GET 或 POST 方式提交的数据
            foreach($$key as $key2 => $_value){
                $$key2 = $_value;
            }
        }
    }
    if(isset($flag)){
        if($flag === 'hack'){
            echo "good job , this is flag{xxxxxxx}";
        }else{
            echo "nothing  here  ";
        }
    }else{
        echo "no no no";
    }
?>

这里,使用 GET 或者 POST 方式传参,就能触发 $$ 变量覆盖, 显然, 当我们传入 ?flag=hack 时,php 首先将 get 传参变成数组赋给全局变量 $_GET = array(‘flag’ => 'hack’) ,经过第一次 foreach 之后, $$key 就是$_GET , 而 $key2 = flag , $_vlaue = hack ;再经过第二次 foreach 之后,$$key2 = $flag 。
此时便意外创建了一个变量为 $flag ,并且被赋值后 $flag === ‘hack’ 形成漏洞;
在这里插入图片描述

  • unset(): 销毁或者删除一个变量

  • extract(array,[EXTR_SKIP/EXTR_OVERWRITE]) : 第一个参数是 数组,该函数的功能是将一个数组中的键名拿出来当做 变量名,将键值拿出来当做变量的值,即 创建变量 , 第二参数, EXTR_SKIP :如果创建的变量已存在,则不进行创建,EXTR_OVERWRITE:如果创建的变量已存在,则覆盖原有变量;

  • 继续解题
    核心代码再分析:

			foreach(array('_GET', '_POST', '_COOKIE') as $key)		# 三种传值方式	$key = _POST
			{   
				if($$key) {		# $$key = $_GET
					foreach($$key as $key_2 => $value_2) { 			# 将封装在$_GET 等数组中的值拿出来 # $key_2 = $$key = $_POST($_POST 也是一个数组)  $value_2 = Array(cod = ...)
						if(isset($$key_2) and $$key_2 == $value_2)  # 存在 $$变量覆盖 				  #	 所以 $$key == $value_2 条件成立
							unset($$key_2); 		# 销毁变量
					}
				}
			}

			if(isset($_POST['code'])) $_POST['code'] = $pan->filter($_POST['code']);	# 如果POST 创建,进行过滤,需要绕过这个判断才能进行 sql 注入
			if($_GET) extract($_GET, EXTR_SKIP);	#  extract() ,EXTR_SKIP参数:已有变量不被覆盖	 不存在则创建
			if($_POST) extract($_POST, EXTR_SKIP);
			if(isset($_POST['code']))
			{
				$message = $pan->getfile();		# 方法调用

审计代码发现,要绕过 filter() 函数,则要是 $_POST[‘code’] 不存在,而输入框是通过 post方式提交数据,便要从GET方式提交数据入手,而整个核心代码中,下面这段代码便是绕过 filter 的关键点:

			foreach(array('_GET', '_POST', '_COOKIE') as $key)		# 三种传值方式	$key = _POST
			{   
				if($$key) {		# $$key = $_GET
					foreach($$key as $key_2 => $value_2) { 			# 将封装在$_GET 等数组中的值拿出来 # $key_2 = $$key = $_POST($_POST 也是一个数组)  $value_2 = Array(cod = ...)
						if(isset($$key_2) and $$key_2 == $value_2)  # 存在 $$变量覆盖 				  #	 所以 $$key == $value_2 条件成立
							unset($$key_2); 		# 销毁变量
					}
				}
			}

我们可以利用 GET 方式传 payload 结合 $$变量覆盖,并且同时使用 POST传一样的数据来使得第二个 if判断成立来调用 unset 销毁 $_POST ,从而绕过 filter;
payload : GET传参:url?_POST[code]=114514' %23
同时POST传参:code=114514' %23 (%23 经过 url 解码后变成 #)

对这个payload 进行 深入分析它在代码中的作用:
首先我们页面 GET 方式传参,使得全局$_GET存在,第一个 if 判断成立,进入第二个foreach 来遍历 $_GET 这个数组,$key_2 = _POST , $value_2 = array(‘code’ => ‘114514’ #’) , 进入第二个if判断,此时,$$key_2 = $_POST ,而我们使用了 POST方式提交了 code=114514’ 的数据,所以 $_POST 是存在的,并且为 $_POST = array(‘code’ => ‘114514’ #’) ,isset()返回 True, 并且$$key_2 == $value_2 ,判断条件成立, 执行 unset() 将 $$key_2 也就是 $_POST 销毁;

至此,便成功绕过了 filter 的检测; 而此时,往后看代码发现:后面仍有一个地方是需要使用到 $_POST[‘code’]的;

if(isset($_POST['code']))
			{
				$message = $pan->getfile();		# 方法调用

因此,这里 extract() 函数就为我们提供了便利:

			if($_GET) extract($_GET, EXTR_SKIP);	#  extract() ,EXTR_SKIP参数:已有变量不被覆盖	 不存在则创建
			if($_POST) extract($_POST, EXTR_SKIP);

分析:执行第一个if,$_GET = array(’_POST’ => array(‘code’ => ‘114514’ #’) ) ,条件成立,执行 extract() ,extract 创建变量,将$_GET 数组中的 键名 _POST 拿出来作为变量名变成$_POST, 将键值赋给变量,最终创建一个变量:$_POST=array(‘code’ => ‘114514’ #’) , 而 前面我们刚好 unset掉了一个 $_POST 来绕过 filter,所以这里第二个 EXTR_SKIP参数便如同虚设,最终创建了一个 $_POST 变量使得后面需要用到该变量的地方成立;

至此,整个关于绕过 filter的过程结束,后面便是构造 payload 并同时 使用 POST,GET 传参,进行一个常规的SQL联合注入便可解题;

最后建议去搭建本地环境来观察php 对于 post ,get ,cookie 传参的处理,以及一些漏洞的利用;

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值