反序列化
一般的反序列化入口
| 方法名 | 触发时机 / 定义 | 触发方式极简示例 |
|---|---|---|
__construct |
创建对象时初始化 | new User(); |
__destruct |
销毁对象时 (如脚本结束) | $obj = null; 或 脚本执行完毕 |
__sleep |
序列化前 | serialize($obj); |
__wakeup |
反序列化后 | unserialize($str); |
__serialize |
序列化时 (PHP 7.4+) | serialize($obj); |
__unserialize |
反序列化时 (PHP 7.4+) | unserialize($str); |
__set_state |
使用 var_export 导出时 |
var_export($obj); |
__clone |
使用 clone 复制对象时 |
$copy = clone $obj; |
| 一般跳板(pop链) |
| 方法名 | 触发时机 / 定义 | 触发方式极简示例 |
|---|---|---|
__toString |
对象当字符串用时 | echo $obj; 或 $str = 'Text' . $obj; |
__invoke |
对象当函数用时 | $obj(); 或 call_user_func($obj); |
__call |
调用不存在的方法时 | $obj->noMethod(); |
__callStatic |
静态调用不存在的方法时 | User::noMethod(); |
__get |
读取不存在的属性时 | $val = $obj->noProp; |
__set |
设置不存在的属性时 | $obj->noProp = 'value'; |
__isset |
检测不存在的属性时 | isset($obj->noProp); 或 empty($obj->noProp); |
__unset |
删除不存在的属性时 | unset($obj->noProp); |
__debugInfo |
使用 var_dump 打印时 |
var_dump($obj); |
一边做题一边补充知识点吧。
- 这一关先定义了一个类,里面的函数是_destruct,也就是对象销毁之后自动执行,含有//和/都直接die,然后还有一个wakeup,是在反序列化的时候自动执行,会将查看的文件改为index.php,那么我们需要做到的是,绕过wakeup,在网上学习的方法,只要将序列化后的属性值改大,就可以绕过wakeup,这里问了ivory学长,最好不要手打序列化的结果,而是php里面自动输出,并且套一层urlencode,这里在下一关会埋下伏笔。 然后贴上我的源码,
1 | class SoFun{ |
,但是我发现转码过后我有点找不到属性值在哪了,太混乱了,所以我先没有转码了,而是先得到序列化之后的东西,然后就是把1改成2,然后再转码。
2. 这里是wakeup之后,就可以响应,我们看这个正则匹配,不能是o或者c,然后加一个冒号,然后加一个数字,这就刚好限制了O:2(举个例子,就这种的都不行),而且后面还有这个不分大小写,那么大小写绕过就被淘汰了。这里在网上看到的,在数字面前加一个+,就可以绕过,就是这里,我太蠢了,我居然在url里面直接打了一个+,我都忘了之前sql注入的时候,+会被自动当作空格,然后就去找对应的编码,是%2b
1 | class funny{ |
得到O:5:”funny”:0:{},然后在5前面加上%2b
3. 看第三关,定义了两个属性,然后如果wakeup的话,就会把一个不知道的东西赋给password,如果password严格等于verity的话,就得到flag。这个确实是没有头绪,然后去问了ai,说在类的内部就直接是他们共享一个值。
1 |
|
=&是引用符号,这样就可以让这两个属性持续相等。
4. ini_set() 设置 PHP 的会话序列化处理器为php_serialize,会话数据将以 PHP 的默认序列化格式进行存储 GET 变量 tryhackme 的值存储到 $_SESSION 数组中,这个数组最终被序列化 Session:服务器用来存储用户会话状态(比如登录状态、用户 ID、购物车信息)的机制,避免每次请求都重新验证用户
PHP 中 session 序列化的关键细节
- PHP 默认使用
php序列化处理器(对应serialize()函数),也支持php_serialize、php_binary等格式。它对$_SESSION的序列化规则非常特殊:
对于$_SESSION['键名'] = 数据,最终存储的字符串格式是:键名|序列化后的数值 - session 数据默认存储在服务器的临时文件中(比如
/tmp/sess_xxxxxx,xxxxxx是 session_id)。
典型的 session 反序列化漏洞,即两个⻚面设置的的session 序列化方式不同
这里42是能通过直接创建一个funny的实例 销毁了过后直接就flag了 4这里是储存到会话 然后serialize() 负责 “写入 session 数据”(用php_serialize格式)php处理器解析 session 数据时,会按第一个 | 把字符串拆成「键名」和「要反序列化的值」|左边被当成空键名,|右边的funny对象序列化字符串会被当成 “值”,触发反序列化。
class funny{
public $a;
}
$x = new funny;
echo urlencode(“|” . serialize($x));
全过程 php处理器解析 session 数据:
把a:1:{s:8:”tryhackme”;s:31:”|O:5:”funny”:1:{s:1:”a”;N;}”;}当成普通字符串(因为php处理器不认识php_serialize的数组格式);
按第一个 | 分割:左边是a:1:{s:8:”tryhackme”;s:31:”(被当成键名),右边是O:5:”funny”:1:{s:1:”a”;N;}(被当成 “值”);
PHP 自动对右边的funny对象序列化字符串执行unserialize() → 生成funny对象
- 这里其实并不是在考pop链,我们看条件很简单,只需要创建一个对象然后就出发类里面的第一个函数construct,之后等结束又出发destruct,就自动出flag了,那么重要的就是看他是怎么反序列化的,发现这里对序列化的结果有要求,ascii,这个之前做sql的时候学过,反正注意这里的属性是私有化的,那么在序列化之后就会在类名和属性名前都加上空格\0,这个的ascii码是0,直接死了,可以用16进制过了。
- $a() 意味着
- PHP会尝试调用
$a这个变量 - 如果
$a是函数名,就调用该函数 - 如果
$a是数组[对象, 方法名],就调用该对象的方法,
1 | <?php |
这样子就可以做到调用new funny->pyflag,然后响应flag,这道题完全没有用到魔术方法,只用类本身的方式来做。
7. 先看到有个__destruct,在对象被销毁的时候自动响应flag,然后又看到下面,先得到a传参的值 b还传一个 内层的 if 检查文件是否已存在,else if 拼接文件路径 $filename = “./upload/<
随机数>.txt” 。然后获取 GET 参数 data,将 base64 解码内容写入到文件 $filename
这里学到的新知识 结合ai还有文章
先明确:phar 是什么?
phar 是 PHP 的归档文件格式(类似 ZIP),用于打包多个 PHP 文件 / 资源成一个文件,方便分发和使用。
它的核心结构包含 4 部分:
Stub(标识头):必须以<?php __HALT_COMPILER(); ?>结尾,是 PHP 识别 phar 文件的标记(可自定义前缀,比如伪装成图片:GIF89a<?php __HALT_COMPILER(); ?>)。
Manifest(清单):存储 phar 包含的文件信息 +元数据(metadata)—— 而元数据是用serialize()序列化后存储的(这是漏洞的关键!)。
文件内容:归档的实际文件数据。
签名:可选的完整性校验信息
PHP 在解析 phar 文件时,会自动将其元数据(序列化后的字符串)进行反序列化操作—— 无论你用什么函数读取 phar 文件,只要触发了 phar 的解析,元数据就会被反序列化。
而 PHP 的很多文件操作函数(如file_exists()、file_get_contents()、fopen()、unlink()等)支持phar://协议(类似file://),只要给这些函数传入phar://文件路径,就会触发 phar 文件的解析,进而触发元数据的反序列化。
如果元数据中包含恶意序列化对象(比如带有__wakeup()、__destruct()等魔术方法的类,这些方法会在反序列化时自动执行),就会触发代码执行,形成漏洞。
然后读取文件内容,进行编码
$content = file_get_contents(‘a.phar’);
echo urlencode(base64_encode($content)) . “
“;
我贴一下源码
1 | <?php |
上传过后 会给一个路径 这就是文件所在的路径 去反序列化它 也就是php解析它就可以了
8. 这道题有很多个类,每个类里面都有不止一个函数,那么我们肯定想的是构造pop链,先确定要怎么触发,我们先来确定开始和终点,这里源码的末尾,是把发送的东西反序列化了,我们就可以先想到开头,可能会是c类里面的construct。a类里面的resolve函数应该是终点,因为里面就可以响应到flag,假设一下 比如起点是最好触发的destruct开始 这里会调用一个add方法 是不存在的方法call 这里这个函数 拼接起来就是调用到addme方法嘛 然后会返回字符串 就到了tostring 这里访问了string属性 并不存在于a里面 那么触发get get($name) 里的 $name,
永远等于你写在 -> 后面的那个名字 前面属性名是 字面量 string
插入规则 规则原文级别的结论
当访问
obj->prop 时,如果该属性在当前作用域中不可访问,PHP 会调用 get(“prop”) 也就是说$this->string["string"]();调用resolve
看了半天wp 也没有看懂这个为啥能用到 这里补充知识点
array_walk:这个函数接受一个数组和一个回调函数,将数组中的每个元素作为参数
传递给回调函数
此外,将一个对象传递给需要数组的函数时,PHP 会将对象转换为对象的公共属性
数组来处理,所以第一个参数可以传 $this
对象传入 array_walk 时,它会遍历对象的每个公共属性,并将每个属性值和属性名
传递给回调函数,回调函数两个参数分别为 属性值 和 属性名
原来如此 那么我先尝试自己口述一次
先创造个a1实例 销毁后调用des 为了触发call a1的this->object作为b 这里用到addme 会出字符串
当 PHP 执行:
“字符串” . $x
$x是字符串 → 直接用$x是数字 → 转字符串$x是对象 → 尝试调用$x->__toString()
触发tostring
然后call c = new c(array(“string”=>array($a2, “resolve”)));
创建一个对象c,
它的私有属性$string是一个数组,
当别人访问$c->string时,
就自动调用$a2->resolve()。
私有属性不知道怎么搞了
看了文章的反射
1 | $refc = new ReflectionClass($c); |
内部结构对象 此时 $refc 里包含:
- 类名
- 所有属性(包括 private)
- 所有方法
- 可见性信息
1 | $propString = $refc->getProperty('string'); |
在类 c 里,找到那个叫 string 的属性
1 | $propString->setAccessible(true); |
强行关闭可见性检查
1 | $propString->setValue($c, ["string" => [$a2, "resolve"]]); |
直接往对象内存里写值
1 | <?php |

