欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-05-14 23:46:27
...
  • 知识点
  • $$变量覆盖漏洞利用
  • unset() 与 extract() 配合下的巧妙利用

结合例题解析

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

PHP代码审计 -- $$变量覆盖( unset() 与 extract() )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代码审计 -- $$变量覆盖( unset() 与 extract() )

$$变量覆盖问题经常在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’ 形成漏洞;
PHP代码审计 -- $$变量覆盖( unset() 与 extract() )

  • 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 传参的处理,以及一些漏洞的利用;

相关标签: PHP随笔