PHP 反序列化漏洞解析
序列化
所谓序列化就是将原数据对象转换为具有一定格式的数据
举一个最简单的例子,在C
中,若要开发一个数据库,那么一定涉及到数据的存储,要将内存中的数据持久化的保存在磁盘中,这就要对数据的存储格式进行优化,比如使用结构体保存一个数据对象,结构体是存在默认对齐机制的,所以实际上结构体的大小会大于其中实际数据的大小,如果直接将结构体对象写在内存一定会造成内存空间的泄露,因此为了优化,可以将结构体存储的每个数据memcpy
出,并紧凑排列并写入内存,这就是最简单的一种序列化。
所以实际上序列化就是将数据按照更易存储、传输等,改变其原有格式进行保存的一种方式,上面所说的只是最简单的一种序列化方式,实际上还可以在其序列化后的数据中加上对数据的描述控制信息(比如说长度等),方便后期更快的还原出原数据。
也许有人会认为加上描述控制信息会让新数据的内容相对于原数据更加冗杂,但这是不影响的,因为描述控制信息的添加是在序列化的过程中所添加,而序列化的目的是方便与更快的存储与传输等而服务的,二者并不处于同一阶段。控制信息的添加也确实可以加快存储、传输过程中对数据的识别等过程。
①网络安全学习路线
②20 份渗透测试电子书
③安全攻防 357 页笔记
④50 份安全攻防面试指南
⑤安全红队渗透工具包
⑥网络安全必备书籍
⑦100 个漏洞实战案例
⑧安全大厂内部教程
PHP 序列化/反序列化
数据转换
将 对象Object
、字符串String
、数组Array
、变量 转换为具有一定格式的字符串(其中不会保留函数方法),方便保持稳定的格式在文件中传输。
相关函数:
由返回值确认其返回字符串,该字符串中包含了表示value
的字节流,可存储与任何地方。( 统一格式,易于存储)
对上述的Deu
对象进行序列化,并观察其序列化后的结果。
为了方便理解该字符串的含义,将其分为两大部分:
数据对象类型:数据名称长度:数据名称:对象个数O:3:"Deu":4
其中每一个对象的结构(以;
分割) **数据类型:数据名称长度(可选):数据名称
序列化的各种结构
根据数据类型的不同,其序列化后的字符串有以下几种情况。
访问控制符不同对序列化后结构的影响
public
序列化后没有变化
protected
序列化后会变成
%00*%00属性名
eg:
s:6:"*sex";s:4:"male"
注意其中的长度,长度 =
6
,但后面字符串中只有4
个字符,所以0
是被省略掉的但是在拿着该序列化后的字符串去提交,反序列化的时候是要带上
0
的,否则会出错private
序列化后会变成
%00类名%00属性名
eg:
s:8:"Deuage";i:66
同上
反序列化
相关函数
示例代码
魔法方法
PHP 中常见的魔法方法
构造函数/析构函数
与
C++
中的 构造函数 与 析构函数 基本一致,PHP
也提供构造函数与析构函数
构造函数
类中会默认存在一个没有参数列表并且内容为空的构造函数。如果显式地声明构造函数则类中的默认构造方法将不会存在,并且在实例化对象时调用该方法。
析构函数
析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。
示例代码
可以看到在实例化对象时,调用了构造函数,并在结束的时候调用了析构函数销毁了该实例( 注意输出结果已经换行了,说明是在最后结束的时候调用了析构函数 )。
__sleep()
和 __wakeup()
__sleep()
当调用 serialize()
函数序列化一个实例时,会首先检查该实例是否存在 __sleep()
方法,如果该方法存在,则该方法会先被调用,然后才执行序列化操作。否则使用默认的序列化方式。
此功能可以用于清理对象,并**返回一个包含对象中所有应被序列化的变量名称的数组,**如果该方法未返回任何内容,则 **null
**被序列化,并产生一个 **E_NOTICE
**级别的错误。
__wakeup()
与之相反,unserialize()
会检查是否存在一个 __wakeup()
方法。如果存在,则会先调用 __wakeup
方法,预先准备对象需要的资源。
示例代码
__toString()
__toString()
方法用于一个类被当成字符串时应怎样回应。例如 echo $obj;
应该显示些什么
从 PHP 8.0.0 起,返回值遵循标准的 PHP 类型语义, 这意味着如果禁用 严格类型,它将会强制转换为字符串。
示例代码
__invoke()
类似于C++
中的仿函数,但实现机制不同。
当尝试以调用函数的方式调用一个对象时,__invoke()
方法会被自动调用。
示例代码
属性重载
读取不可访问(protected
或private
)或不存在的属性的值时,__get()
会被调用
在给不可访问(protected
或private
)或不存在的属性赋值时,__set()
会被调用
当对不可访问(protected
或private
)或不存在的属性调用 isset()
或 empty()
时,__isset()
会被调用
当对不可访问(protected
或private
)或不存在的属性调用 unset()
时,__unset()
会被调用
示例代码
PHP 反序列化漏洞
反序列化漏洞最根本的成因在于 反序列化函数unserialize()
的参数是可控的:也就是说可以传入我们特殊构造的一个序列化后的对象。
但若仅有这一点是很难形成攻击的,这时我们想到了魔法函数的调用一般都是由某些事件发生所自动触发的,所以自动联想出 可控的参数再配合上PHP
特殊的魔法方法 也许就可以形成攻击,这也就是一个最基本的反序列化漏洞的利用。
魔法函数的利用
在上面介绍的魔法函数中,先着重看这俩用的面比较广的:
__destruct()
__weakup()
为什么这两个魔法函数出现概率高:因为其被调用的面广,类的销毁会调用__destruct()
反序列化函数unserialize()
调用时会先调用__weakup()
。(若其存在)
主体的利用思路很简单:**根据魔法函数中提供的功能构造合适的反序列化对象,在对象的销毁或反序列化时,调用了魔法函数并传入我们构造的恶意参数造成攻击,**这里以Burp
中的 靶场为例。
Lab: Arbitrary object injection in PHP
实验地址:Lab: Arbitrary object injection in PHP | Web Security Academy
题目:
该实验室使用基于序列化的会话机制,因此容易受到任意对象注入的影响。为了解决实验室问题,创建并注入恶意序列化对象将morale.txt
从Carlos
的家目录中删除文件。您需要获得源代码访问权限才能解决此实验。您可以使用以下凭据登录到您自己的帐户:wiener:peter
。
确认存在序列化
首先根据提供的账户,登录到用户的界面,之后开启 Burp 进行抓包,并随便点一个功能,比如点击My-Account
得到的数据包为:
通过对其Cookie
中的session
字段解Base64
编码,可以发现Cookie
传递的是一个序列化的PHP
类对象
此时我们可以确认该网站使用序列化的对象传递用户身份数据(传递什么不是重点),那么如上所述,下一步的目的就是找到可以利用的 魔术方法,这也是最难的一步。
寻找可利用的魔术方法
首先打开Burp``Target
模块中的Site map
看看整个过程中出现过哪些文件,在libs
中,看到了一个CustomTemplate.php
文件,很不错!!!该文件很可能存在着某些魔术函数
之后点到他后,发现文件查看不了,遂将该请求报文发至Repeater
做进一步的处理。
直接发送请求,发现服务端只返回了200
成功的状态码,但并不允许请求该文件的内容。
此时陷入瓶颈,该如何请求该文件就是一个很重要的问题,在此之前先说一个题外知识。
文件扩展名末尾的波浪号~
的含义
由于这是解决本题的一个既关键(涉及到如何请求到
CustomTemplate.php
)又不怎么关键(知道怎么做即可,貌似无需理解其含义)的点,所以还是拿出来单独说一说。
在一个文件名的末尾(Linux 中是不分后缀的)添加 ~ ,是一种约定,这种约定起源于emacs
编辑器,后来也被joe``vim``Gedit
编辑器所采用,通常用于在编辑文件之前通过在原文件尾附加~
来创建一个该文件的备份备份,方便搞砸了以后进行恢复。
例如 使用命令cp xxxxx{,~}
至于为什么选择~
作为备份文件的后缀:~
是编号最高的可打印ASCII
字符,所以在ls
查看时,会备份文件排序在原始字符之后(传统的ASCII
排序)。
所以根据上述介绍,我们就可以尝试在CustomTemplate.php
末尾附加~
去请求其备份文件,看看是否存在,具体来收就是请求CustomTemplate.php~
文件。
很幸运,存在备份文件,顺利过渡到下一步。
在CustomTemplate.php
寻找利用途径
通过观察代码得出以下信息:
通过
__construct()
可以看出该类在初始化实例对象时需要一个参数,会传递给其私有变量$template_file_path
之后又会赋给私有变量$lock_file_path
。
明晰了上述关系后,需要重点关注的是__destruct()
函数。
在此看到了所需要的unlink()
功能用于 “删除” 题目所指示的文件
而该析构函数所删除的文件正式在构造函数中传入参数的路径下的文件,所以只需要构造一个包含要删除路径的序列化对象即可完成该功能。
构造特殊的序列化包
由于要手动构造序列化后的包要计算每个字段的长度(参考之前对序列化后数据结构的介绍),所以此处借助一个网站来实现序列化包的构造。
PHP Serialized Editor - Online Visual Editor for Serialized Data
若不借助该网站,要手动完成构造,可以用wc
来统计字符串中字符的数量(注意特殊格式)
此处以用户传递的session
中的序列化后的包的格式来构造新的包(原用户包的内容见上面截图)
这里要说明一个问题:前面说到,最终要传递的参数是$lock_file_path
的一个路径值,也就是题目中给出的/home/Carlos/morale.txt
(若不知道这个目录是如何得出的记得仔细回看一下题目),那么按理说,应该构造一个只包含该路径的CustomTemplate
序列化对象,例如:
O:14:"CustomTemplate":1:{s:14:"lock_file_path";s:23:"/home/carlos/morale.txt";}
这个构造也是正确的!
但这里尝试另一种方式,由于使用的上述工具,免得麻烦,所以直接在之前session
传递的那串序列化为基础进行了构造。
O:14:"CustomTemplate":2:{s:18:"template_file_path";s:23:"/home/Carlos/morale.txt";s:14:"lock_file_path";s:23:"/home/Carlos/morale.txt";}
这串构造看起来有很多没必要的字段,但可以过,所以姑且就用这种了。
攻击成功
同样开启Burp
并随便点击一个选项进行抓包,比如说My-Account
,之后将其中Cookie:session=
后面的内容替换为我们的这一串序列化字符 ,记得在Burp
右下角的Decode from
中进行替换,并进行Base64
编码,当然也可以到Decoder
中编码后直接复制来。
成功删除目标文件!
PHP 反序列化 POP 链
POP
又称之为面向属性编程(Property-Oriented Programing)
,常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程ROP``(Return-Oriented Programing)
的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击的目的。
这里以 2021 强网杯中的 反序列化题为例。
2021 强网杯-赌徒
源码
分析
题目中提示需要读取到
flag
文件,所以拿来源码首先找其中具有读取功能的函数,确认其存在于Room::Get_hint()
中所以接下来再向外看,寻找
Room
类中有没有魔法方法调用了Get_hint()
很幸运在
Room::__invoke()
中调用了Room::Get_hint()
回想
__invoke()
魔法方法被调用的契机是 尝试以调用函数的方式调用一个对象时所以接下来需要再来寻找有没有将该类当作函数调用的位置
同样是在
Room
类中,看到_get()
魔法方法中出现了啊将该类作为函数调用的情况只要将
a
赋为Room
的一个实例即可回想
_get()
该属性重载函数被调用的契机是 读取不可访问(protected
或private
)或不存在的属性的值时如果
file['filename']
是个实例化的Room
类,就会触发Room
的__get()
所以接下来接着寻找有没有类中的函数访问不了不可访问或不存在的属性值
又很幸运
Info::__toString()
魔法函数中调用了一个不存在的属性回想 调用
__toString()
的契机是 一个类被当成字符串使用时所以接下来接着寻找一个函数,该函数的返回值是一个字符串即可
终于在
Start::_sayhello()
函数中看到了echo
,只要将echo
后面的数据换为一个类,就是将一个类当成字符串使用,按照之前思路再找有没有哪个魔法函数调用了该函数,找到了Start::__weakup()
至此找到了一系列调用所需的源头Start::__weakup()
,之后的构造则应该是正好相反的结构
通过
Start::_sayhello()
将其中的this→name
赋为class Info
执行
Start::_sayhello()
触发Info
中的__toString()
将
Info::__toString()
中的$this->file['filename']
赋值为class Room
的一个对象写的更形象点就是:
$this->file['filename'] = new Room
由于Room
中是不存在ffiillee['ffiilleennaammee']
属性的,所以会调用_get()
执行
Info::__toString()
调用Room::_get()
将
Room
类中的a
赋值程对象,即可存在以函数调用调用类的情况,则最调用Room::__invoke()
最终调用
Get_hint()
方法拿到base64
后的flag
构造
得到。
但当把以上结果直接放入 URL 传递是不可以的,因为其中有一个 private 权限的,需要前后加%00
。
评论