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
这里先对代码进行一波解读
首先是定义一个类 FLAG
这个先去看看什么叫做类
在 PHP 中,类(Class)是面向对象编程(OOP)的核心概念,它像一个蓝图或模板,用于创建具有相同属性和行为的对象。类定义了数据的结构和操作数据的方法,是封装代码的基础单元。
这里简单看来就是把同样之后需要用到的行为封装在一个容器里面,class就是这个容器,之后需要使用的时候直接调动这个容器
然后public是一个修饰符,是公共,表面这个属性在类的内部或者外部都可以访问
定义了一个变量$flag_string存储flag
构建的这个函数表明,在进行实例化的时候会自己输出flag
因为这个函数是一个魔术方法
在类实例化的时候会自动调用__construct()函数
也就会自己输出flag
至于那个$this->flag_string
$this是一个特殊变量,可以访问对象的属性
这里的思路就很清晰了
我们需要去调用到__construct函数
所以我们需要进行类的实例化
new xxx();
来进行类的实例化
new 是一个创建对象实例的关键字,它触发类的实例化过程
直接传参
Level2
一样的先分析代码
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');
}
方法一(直接输出)
因为flag_string是直接定义在类外的
可以直接输出
方法二(变量传递)
可以利用题目所说的变量传递
code=$target->free_flag=$flag_string;
这里来详细解释一下这个payload
首先$target是一个对象变量,这里是指向他的属性free_flag
然后后面就是将$flag_string赋值给了free_flag
之所以可以直接赋值,因为其是public公有所以可以直接赋值
之后会调用方法输出
方法三(暴力输出所有变量)
这个方法是在看妥师傅wp的时候发现的
既然这个flag是储存在变量里的
那我们可以直接输出所有的变量
code=var_dump(get_defined_vars());
解释一下函数
get_defined_vars()
这个是一个php的内置函数,他会返回一个包含当前作用域内所有已定义变量的关联数组。数组的键是变量名,值是变量的内容。
var_dump()
用于输出变量的详细信息,包括类型和值。对数组会递归输出每个元素。
综合来说,这一个payload会输出所以已经定义过的变量,不管你是局部变量还是全局变量
只要是定义过的都会输出
看一下
Level3
依然先分析代码
这里显示定义一个类FLAG
公共属性变量public_flag里面是flag
这里又有两个属性声明
解释一下
protected(受保护)是只能在类和继承这个属性子类里可以访问
private(私有的)这个只有这个类里可以访问
这里给一个表格方便比较
修饰符 | 类内部 | 子类 | 外部 |
---|---|---|---|
private | ✅ | ❌ | ❌ |
protected | ✅ | ✅ | ❌ |
public | ✅ | ✅ | ✅ |
受保护的属性protected_flag
私有的属性private_flag
然后定义一个方法get_protected_flag()
获取变量$protected_flag的属性
然后定义一个方法get_private_flag()
获取变量$private_flag的属性
再看下一段
定义一个类这里有个新东西
extends这个是子类继承父类属性
所以这里就是定义一个类SubFLAG继承FLAG的属性
这里定义一个方法show_protected_flag()
返回protected_flag的属性
然后再定义一个方法show_private_flag()
返回private_flag的属性
再看下一段
这里是定义两个变量
target 和 sub_target
里面的内容是分别对两个对象进行实例化
POST获取code的值
下面如果code的值不是空
eval运行
如果是空的
输出四句话
就是我们进入界面的四句话
这里已经还是比较明显的就是剩下的那几个flag在protected_flag和private_flag这两个变量里面
那就可以用上面的方法
方法一(暴力输出所有变量)
code=var_dump(get_defined_vars());
这里自己拼接一下
HelloCTF{se3_me_4nd_g3t_mmmme}
方法二(调用方法)
eval(code=$target->get_protected_flag();)
是个啥玩意儿
这里要加个echo来输出
code=echo $target->get_protected_flag();
这里就一起输出
code=echo $target->public_flag.$target->get_protected_flag().$target->get_private_flag();
Level4
依然先分析代码
这里是定义了FLAG3的类
里面有个私有变量flag3_object_array里面是一个数组
定义一个FLAG类,又是三个私有变量
然后定义了一个__construct()方法
指向flag3_object进行一个实例化FLAG3
$flag_is_here实例化FLAG这个类
然后post获取code的post值
看code是不是空,不是就使用eval执行
方法一(暴力输出所有变量)
code=var_dump(get_defined_vars());
自己拼接一下看看
ser4l1ze me好像就只有这个
方法二(序列化)
这里先来介绍一手
序列化,可以将其内容进行序列化
这里呢会将对象的所有属性进行序列化,然后变成字符串
这样就可以打印出来了
这里就需要对$flag_is_here进行序列化然后输出
这里来解释一下为什么
因为他创建了一个FLAG对象
然后因为在进行实例化的时候会自动调用魔法方法__construct()
然后又会进行实例化FLAG3这个类
等于所有的私有属性都可以输出出来
这里直接输出看一下
code=echo serialize($flag_is_here);
这里就可以看见序列化后的内容了
这里自己拼接一下就知道了
ser4l1ze me
方法三(使用ReflectionClass)
来自妥师傅的高端操作
这里我们就先来了解一下什么叫ReflectionClass
首先这个是在php5之后才有的一个内置类
他是一个反射类
我们可以用来
- 分析类结构
获取类名、命名空间、文件路径、接口、父类、修饰符(是否抽象/最终)、注释等。 - 访问成员
检索类的方法、属性、常量等信息(返回 ReflectionMethod、ReflectionProperty 等对象)。 - 实例化对象
动态创建类的实例(支持绕过构造函数)。 - 操作访问控制
绕过 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);
再读取另外一个
这里因为要读取的是一个数组,不能直接使用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);
Level5
依然先分析一下代码
定义一个类a_class
这里是一个公有变量
这里定义了六个变量
第一个是实例化这个类
第二个是定义了一个关联数组
第三个是赋值给a_string为HelloCTF
第四个赋值为678470
第五个赋值为true
第六个赋值为null
这里又是定义六个变量
反序列化这些变量
然后是一个条件函数
第一个为真
如果第二个是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;
运行一下
传参传进去
Level6
依然先分析一下代码
定义一个类protectedKEY
有个受保护的变量
还定义了一个方法get_key()来获取$protected_key的值
这里也是定义一个类
有一个私有变量
定义一个方法来获取这私有变量的值
这里反序列化这俩变量的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
直接传参
Level7
老套路,先分析代码
这里是先定义一个类FLAG
有个公有变量
然后定义了一个方法后门函数,会eval执行变量
这里先定义了三个变量
第一个赋值一段序列化的字符串
四二个实例化FLAG
第三个反序列化第一个变量的值
然后后两个变量再调用backdoor()方法
这里就是反序列化传入的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
然后就可以启动了
以上操作都是在终端进行
你再访问就可以了
然后直接传参
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();
}
依然先解析一下代码
这里就是两个全局变量
然后给这俩赋值为0
这里是定义了一个类FLAG
里面有已给公有变量$class_name
定义了一个方法__construct
将参数 $class_name 的值赋给当前对象的 class_name 属性
然后又是一个全局变量
然后是将construct_flag累计加一
输出一串东西
又定义了一个方法__destruct()
定义一个全局变量
又是累加
然后输出
这里就是一个GC的演示
首先进行实例化后面那个demo是传给构造函数的参数
然后是序列化demo然后又是反序列化s
然后unset()是一个结构不是函数
用来删除变量及其值
这里就是删除n和demo
先定义了一个类