fastjson 反序列化漏洞POC分析

来源:香依香偎@闻道解惑

fastjson 是阿里巴巴开源的,使用 Java 语言编写的 JSON 解析库,项目地址是 https://github.com/alibaba/fastjson,以速度快、性能高著称,使用范围非常广。

在2017年3月15日,Fastjson 官方主动爆出 Fastjson 在 1.2.24 及之前版本存在远程代码执行高危安全漏洞。本文对这个漏洞的POC进行分析。

零、Fastjson 反序列化的特点

先创建一个实体类 User,其中包括:

•    public  元素 name
•    private 元素 age   和它的 setter 函数
•    private 元素 prop  和它的 getter 函数
•    private 元素 grade 和它的 getter 函数

01-User.png

接下来使用 JSON.parseObject(),用指定类型的方式将这个类反序列化出来。

01-App.png

运行结果是:

01-result.png

从结果上看,我们可以得出以下结论:

•    User 对象的无参构造函数被调用
•    public String name 被成功的反序列化
•    private int age 被成功的反序列化,它的 setter 函数被调用
•    private Properties prop 被成功的反序列化,它的 getter 函数被调用
•    private String grade 没有被反序列化,仍然是默认值 null,getter 函数也没有被调用

前三点都自然,奇怪的是后两点。propgrade 同样是 private 类型,同样提供了 getter 函数没有提供 setter 函数,为什么 prop 可以通过 getter 函数来反序列化,而 grade 却没有?

这涉及到 fastjson 的一个特殊处理。对于只有 getter 函数,没有 setter 函数的 private 元素,fastjson 会按如下条件判断反序列化的时候是否调用其 getter 函数。

•    函数名称大于等于 4
•    非静态函数
•    函数名称以get起始,且第四个字符为大写字母
•    函数没有入参
•    函数的返回类型满足如下之一
◦    继承自Collection
◦    继承自Map
◦    是AtomicBoolean
◦    是AtomicInteger
◦    是AtomicLong

01-JavaBeanInfo

回到前面的问题。propgetter 函数 getProp() 满足上面的条件,它的返回类型 Properties 继承自 Map ,因此可以成功的被调用。而 gradegetter 函数 getGrade() 的返回类型 String 不满足返回类型的那个条件,因此没有被调用,grade 也无法被赋值。

那么,在反序列化的时候,向 grade 这样无法通过 settergetter 函数进行赋值的 private 元素,有什么方法可以赋值呢?有。就是使用 FEATURE.SupportNonPublicField

JSON.parseObject() 中使用 fastjson 的标签 FEATURE.SupportNonPublicField

02-App

执行结果是:

02-result

grade 已经成功被反序列化,此时 gradegetter 函数 getGrade() 并没有被调用。

FEATURE.SupportNonPublicFieldfastjson1.2.22 版本开始引入。

还有个疑问:上面的 App.java 中,json string 中使用 @type 标签指定了反序列化的目标类型为 com.xiang.fastjson.poc.User,在调用 JSON.parseObject() 时也指定了目标类型是 User.class。那如果这两个类型不一致,会发生什么?

需要说明的是,只要 JSON.parseObject() 的第二个参数中指定的类,与 json string@type 指定的类之间存在继承或转换关系,那么这个反序列化就会成功执行。比如把 JSON.parseObject() 的第二个参数设为 Object.class(也就是所有类的基类),那么反序列化就不会因为类型不匹配而失败。如果把第二个参数设为 String.class,那么所有支持 toString() 方法的类同样可以序列化成功。

但是如果两个类之间没有关联关系,那么反序列化的时候,是会直接返回错误拒绝反序列化,还是将对象反序列化完成再进行类型转换呢?

这个答案是:不确定。比如,我们修改 App.java,将 JSON.parseObject() 的第二个参数换成 Integer.classjson string 中仍然保持 com.xiang.fastjson.poc.User

03-App

执行结果是:

03-result

从结果看,fastjson 先按照 json string 中的 @type 将对象反序列化出来,然后再转换为 JSON.parseObject() 中指定的目标类型。即便两个类型不一致,json string 指定的对象对应的 gettersetter 函数也一样会被调用。

再换一下,把 JSON.parseObject() 的第二个参数换成 ASMUtils.class

04-App

执行结果是:

04-result

这一次的结果上,fastjson 却首先检查了两个类型是否匹配,不匹配直接抛出异常,没有调用各个字段的方法进行反序列化操作。

fastjson 内置了一些常用类的反序列化处理类,这些常用类的列表在 ParseConfig.java 中可以看到。

04-deserializemap

其中有一些处理类中(比如IntegerBigInteger等)没有检查类型是否匹配,就直接进行反序列化处理;有一些没有进行检查,但是反序列化过程会因为语法错误而失败。而对于大多数不属于常用类的情况,fastjson 是会进行检查不同类型之间的关联关系的(如上面的ASMUtils)。

好吧,这一部分太晕了。有没有更简单一些的用法,不用考虑这些匹配原则?有,就是 JSON.parse()

05-App

执行结果是:

05-result

同样也支持 Feature.SupportNonPublicField 设置。

06-App

执行结果是:

06-result

可以看到,JSON.parse(),完全是按照 json string 中指定的类进行反序列化,不用考虑指定目标类的情况,因此可能是更广泛的用法吧。不过少了一次类型检查,也会引入更多的安全风险。

一、TemplatesImpl POC分析

首先是 TemplatesImpl 的 POC,来自于 廖神

这个 POC 的入口点在 TemplatesImpl 类中 getOutputPerpeties() 函数。由于 TemplatesImplprivate 元素 _outputProperties 只有 getter 没有 setter ,同时其 getter 函数的返回类型 Properties 又继承自 Map,因此这个 getter 函数 getOutputProperties() 满足前面说的调用条件,可以被 fastjson 反序列化 TemplatesImpl 时调用到 。POC 的调用链是:

  • getOutputProperties()
    • -> newTransformer()
      • -> getTransletInstance()
        • -> defineTransletClasses()`

09-templatesimpl-01.png

09-templatesimpl-02.png

09-templatesimpl-03.png

09-templatesimpl-04.png

POC 代码如下:

07-exploit.png

07-poc.png

执行之后成功弹出计算器:

07-result.png

上面的POC中,Exploit 类继承自 AbstractTranslet。如果不让 Exploit 类继承也可以,只要在 json str 中,_outputProperties 之前增加 _transletIndex_auxClasses 的设置就好了。

08-exploit.png

08-poc.png

执行之后同样弹出计算器:

08-result.png

TemplatesImpl 的 POC,在利用的时候有几个限制:

  1. 由于 POC 中关键元素 _bytecode 没有对应的 publicgettersetter 函数,因此需要服务器上解析 json 的时候,不管是使用 JSON.parse() 还是使用 JSON.parseObject(),都需要设置 Feature.SupportNonPublicField
  2. Feature.SupportNonPublicField 是在 1.2.22 版本引入,而这个漏洞在 1.2.25 版本就被封堵。这就要求目的服务器上的 fastjson 版本必须在 1.2.22 到 1.2.24 之间。
  3. 如果目的服务器使用的是 JSON.parseObject(),那么第二个参数必须是和 TemplatesImpl 不冲突的类,比如 Object.classString.classInteger.class 等。

二、JdbcRowSetImpl POC分析

JdbcRowSetImpl 的 POC 同样来自 廖神

JdbcRowSetImpl 的调用入口在 setAutoCommit() ,调用链很短:

  • setAutoCommit()
    • -> connect()
      • -> InitialContext.lookup()

10-JdbcRowSetImpl-01.png

10-JdbcRowSetImpl-02.png

需要设置的属性只有 dataSourceNameautoCommit 两个。由于它们的 setter 函数 setDataSourceName()setAutoCommit() 都是 public 类型,因此这里 不需要设置 SupportNonPublicField 属性 ,可以直接触发。

POC代码为

11-JdbcRowSetImpl-Poc.png

搭建好 RMI 的环境之后,执行 POC ,成功弹出计算器。

11-JdbcRowSetImpl-result.png

JdbcRowSetImpl 的 POC,使用限制为:
1 fastjson 的版本范围为 1.2.24 及以下的所有版本。
2 如果目的服务器使用的是 JSON.parseObject(),那么第二个参数必须是和 JdbcRowSetImpl 不冲突的类,比如 Object.classString.classInteger.class 等。

三、fastjson 的修复

反序列化漏洞的修补,通常都是通过白名单或黑名单的形式,禁止具有恶意功能的类进行反序列化。fastjson 使用的是黑名单的方式,具体而言就是从 1.2.25 版本开始,fastjson 增加了两个处理:

  1. autotype 功能默认关闭,同时提供手动 enable_autotype 的设置。
  2. 默认开启了黑名单,危险类所在的包不允许进行反序列化。

我们将 fastjson 升级到最新的 1.2.41 版本,再执行上面 JdbcRowSetImpl 的 POC,结果是直接抛了异常:

12-1.2.41-autoType-close.ong.png

查看抛异常的地方,

12-1.2.41-checkautotype.png

查看denyList的定义,可以找到目前定义的黑名单列表。

12-denyList.png

•    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

com.sum.rowset.JdbcRowSetImpl 正好符合其中的 com.sun. 这个黑名单前缀,因此被屏蔽。

四、denyList 黑名单的绕过

fastjson 修补之后默认关闭了 autoType 功能,同时开启了黑名单。

但是这个黑名单功能的实现还是有问题的。在 autoType 功能打开的情况下(总有些偷懒的开发人员会这么干的),我们可以绕过这个黑名单的限制。

首先,我们通过命令行参数 -Dfastjson.parser.autoTypeSupport=true 的方式开启 autoType 功能,执行一下 JdbcRowSetImplPoc

13-open-autotype.png

在 ParserConfig 的 880 行抛出异常。查看代码,这次是被黑名单 denyList 给拦截了。

13-denyList-block.png

继续往下看,在 926 行调用了 TypeUtils.loadClass() 来反序列化生成类。

14-TypeUtils-loadClass.png

点进去看看,在 TypeUtils 的 1143 行,对于类名由 L; 包装的情况下,这里会直接去掉类名前后的 L;,然后再 loadClass()

14-TypeUtils-loadClass-L.png

这就意味着,我们只需要将 json string 中的类名前后增加 L;,也就是把 POC 中 @type 的值从 com.sun.rowset.JdbcRowSetImpl 改成 Lcom.sun.rowset.JdbcRowSetImpl; ,就能绕过黑名单的检查,同时也能完成 JdbcRowSetImpl 的反序列化!

15-Lcom.sun.rowset.JdbcRowSetImpl-.png

用命令行参数 -Dfastjson.parser.autoTypeSupport=true 开启 autoType 功能,再执行一下新的 JdbcRowSetImplPoc

15-result.png

成功!