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