SnakeYaml反序列化过程与漏洞原理分析(CVE-2022-1471)
文字描述难免有所补足,请移步视频讲解:SnakeYaml反序列化过程与漏洞原理分析(CVE-2022-1471)
产品介绍
SnakeYaml是一个完整的YAML1.1规范Processor,支持UTF-8/UTF-16,支持Java对象的序列化/反序列化,支持所有YAML定义的类型。
漏洞概述
该漏洞源于程序在进行反序列化过程中未对用户输入内容做合法性验证,导致了恶意代码执行。
受影响版本
<= SnakeYaml 1.33
漏洞分析
调试环境
使用idea新建基于maven的java项目,在pom.xml中引入snakeyaml包
1 | <dependency> |
编写测试类
1 | package org.example; |
在dnslog平台获取一个域名填到payload中,然后运行程序观察dnslog平台得域名解析情况,若有解析记录,则证明存在漏洞。
漏洞原理
本文我们不从漏洞手,而是首先了解以下snakeyaml时怎样进行反序列化的,观察在反序列化过程中都有哪些重要的步骤,反序列化过程中又是为何能够解析到我们提供的域名,进而能够进行远程类加载,执行恶意代码。首先我们需要打初始的断点:
虽然漏洞的触发入口时在load方法,但是我们首先需要关注的时yaml对象的初始化过程。
从上图,yaml队形初始化过程中共计涉及到5个对象的构建
对象 | 功用 |
---|---|
Contructor | 对象构造器,负责JavaBean对象的构造 |
Representer | 表示器,不知道有啥用,因为没有具体到它的方法,它在本漏洞中扮演的角色不是特别重要 |
DumperOptions | JavaBean对象序列化选项 |
LoaderOptions | yaml序列化数据反序列化配置选项,在漏洞修复版本中就是在该类中增加了新的反序列化选项来限制自定义的全局TAG |
Resolver | 分析器,具体作用定位不太好描述,另外还有个parser,和resolver解释很像,没太区分明白,当然仔细研究的话肯定还是可以区分。 |
load方法会调用loadFromReader从streamReader中加载对象,而streamReader其实又封装了stringReader对象,面向对象这层层封装还是挺恶心的。
可以看到就是封装了一些元数据,包括数据窗口的大小,数据长度,数据流,流是否结束标志,缓冲区等,而在StringReader中也仅封装了字符串长度与字符串字面量而已。
loadFromReader方法主要完成composer(构建器)的创建以及数据的构建。
构建器里主要封装了解析器、分析器、反序列化配置、锚点、递归节点等数据。
解析器里封装了扫描器,扫描器负责对序列化字符串进行逐个字符扫描以获取token、元素入口与出口、获取键与值、块入口与出口等。
分析器主要负责通过数据内容来推测对应的数据类型,Yaml语法中主要包含三种数据类型即序列、映射与标量,所谓序列就是列表、数组类似的概念,映射可以理解为Python中的字典、Java中的Map,标量时Yaml中的最小的不可再分的基本数据类型,包括字符串、整数、浮点数、日期时间等类型。
通过contrutor的getSingleData方法可以获得到一个type指定类型的对象。其首先需要调用composer的getSingleNode方法获取到一个Node对象,该Node对象时对我们传入的序列化数据的格式化解析与封装。然后电泳constructDocument方法通过Node对象获取最终的目标对象。
我们首先关注getSingleNode方法。进入该方法会首先通过解析器对事件进行配置
可以看到获取事件时其实时同故宫state对象进行创建的,而state对象其实是一个Production接口的实例。
其有很多的实现类,分别对应不同的节点类型的不同处理阶段。
我们在初始化解析器的时候会初始化state对下个为ParseStreamStart的实例
故此处调用的produce方法即是org.yaml.snakeyaml.parser.ParserImpl.ParseStreamStart#produce
,在该方法中获取了获取了流解析开始事件并重置了state为下一阶段的隐式文档解析开始状态。每一个事件中都包含两个重要的标志Mark,一个称之为startMark一个称之为endMark,分别标识了当前事件处理的开始与结束位置
在获取到事件后,判断事件是否为流结束事件,若不是则进入到节点解析逻辑。在此逻辑开始时会调用getEvent方法获取一个开始事件,待composeNode方法调用完毕后,再嗲用getEvent方法获取到一个结束事件,最后对锚点与记录的递归节点进行清理。
在composeNode方法中主要有两个分支,如果你的序列化数据设置了锚点与别名此时会进入第一个分支,若没有则会进入到else分支,我们这里会进入到else分支。
在else分支中还是首先会把事件往前推一次,获得的NodeEvent主要包含三种类型分别为ScalarNode SequenceNode MappingNode,即我们开始谈到的Yaml格式的三种类型标量、序列以及映射,很明显我们这里会进入到序列的处理逻辑里面。
在composeSequenceNode方法中首先设置事件为SequenceStartEvent,即序列处理开始,然后会尝试获取Tag,关于tag的内容可以在https://yaml.org/spec/1.2.2/#tag-shorthands了解,然后创建一个SequenceNode对象,在构造SequenceNode对象时会将children当作参数传进去赋值给属性this.value,我们知道ArrayList是引用类型,若以外秒对children的修改将影响到SequenceNode中的值。然后嗲用composeNode方法继续进行节点后见并添加到children中作为SequenceNode的子节点。到这里就已经开始递归解析了,直到解析到StreamEnd。
在获取到Node后,就进入到目标对象构建的流程中
在constructObject方法中还对获取到的node对象进行了缓存检查
在调用getConstructor时会首先从预设好的13中内置构造器中尝试选择,若没有匹配的则返回一个ConstructYamlObject的构造器,然后调用器ConstructYamlObject的getConstructor方法通过反射的方式获得目标类的Class实例,然后根据Node类型获取到不同的几点构造器,此处为ContructSequence
然后调用器construct方法,该方法也时通过节点tag的不同类型选择不同的处理逻辑,这里会进入到可变对象的对象构建逻辑中。由下图,在这个逻辑中会首先获取当前Node的value属性,也就是子节点的尺寸然后与获得的目标类的构造方法的参数尺寸进行比较,若成功匹配则依次对直接点调用constructObjec递归构建对象然后作为父节点的构造方法参数传参同通过反射的方式进行实例化。
在我们提供的payload中,也就是最终我们要构建的类是javax.script.ScriptEngineManager,构造该类调用的是其含有一个参数的有参构造方法,其参数为一个类加载器java.net.URLClassLoader,而在构造java.net.URLClassLoader时也是调用其含有一个参数的有参构造方法不过其参数时一个数组,所以我们会考到java.net.URLClassLoader后嵌套了两层[[,其数组类型为java.net.URL。我们知道URLClassLoader会从传入的URL进行远程的类加载工作,那么这个payload极有可能涉及到远程恶意类加载与实例化。那么ScriptEngineManager的构造方法又做了什么呢
可以看到这是一个很明显的SPI调用过程,我们只需要在远程部署一个实现了ScriptEngineFactory接口的类,并子啊其静态代码块中写入命令执行的代码,并在其Classpath:META-INFO/services/javax.script.ScriptEngineFactory文件中应用该实现类即可完成利用。具体的远程RCE可以上网自行寻找。
在最新版的snake中该漏洞得到了修复,官方通过在loadConfigs对象中引入了新的配置项并且限制了反序列化递归调用嵌套深度的方式来对反序列化漏洞进行了限制。
限制递归深度在org.yaml.snakeyaml.composer.Composer#increaseNestingDepth方法中检查,最大深度默认为50层
目标类类型检查在下图处检查,首先节点Tag是否是自定义的全局tag如果是则调用org.yaml.snakeyaml.inspector.TagInspector#isGlobalTagAllowed方法进行检查,TagInspector是一个接口,其默认实现是org.yaml.snakeyaml.inspector.UnTrustedTagInspector。isGlobalTagAllowed方法默认返回false,即不允许使用全局的Tag。
我们在编写代码的时候如果需要指定发序列化的目标类类型则必须要实现TagInspector接口,重写isGlobalTagAllowed方法,然后将其注入到LoaderOptions实例化对象中。
默认不允许使用全局tag
实现TagInspector接口,此处直接返回true,也可以在isGlobalTagAllowed方法中根据tag来进行逻辑判断决定允许哪些类能被反序列化。
重写isGlobalTagAllowed方法后的结果
远程类加载
在分析的过程中我发现了一些好玩的地方,手下时除了load方法可以利用该payload外,如果我们使用的时loadAs方法被人制定了目标类的类型应该怎么办?
当然如果类型不匹配,即ScriptEngineFactory如果不是目标类的子类或这实现类,就没得玩,但是如果是一些抽象级别比较高的类还是大有可为的,比如xxxx组件就是这么干的,嗯一个CVE到手了。
两外我们可以利用Yaml语法中的TAG特性来实现WAF绕过,我们只需要对payload进行以下的变形
1 | %TAG !m! tag:yaml.org,2002:java |
依旧RCE,还可以用来绕WAF
修复措施
- 升级软件到不受影响的版本或最新版,最新版maven仓库地址:https://mvnrepository.com/artifact/org.yaml/snakeyaml/2.1。
- 限制服务器出网
参考链接
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-1471
- https://www.cvedetails.com/cve-details.php?t=1&cve_id=CVE-2022-1471
- https://www.cnnvd.org.cn/home/globalSearch?keyword=CVE-2022-1471
- https://www.cnblogs.com/nice0e3/p/14514882.html
- https://y4tacker.github.io/2022/02/08/year/2022/2/SnakeYAML%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%8F%8A%E5%8F%AF%E5%88%A9%E7%94%A8Gadget%E5%88%86%E6%9E%90/
- https://www.cnblogs.com/R0ser1/p/16213257.html