来源:香依香偎@闻道解惑
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 函数

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

运行结果是:

从结果上看,我们可以得出以下结论:
• User 对象的无参构造函数被调用
• public String name 被成功的反序列化
• private int age 被成功的反序列化,它的 setter 函数被调用
• private Properties prop 被成功的反序列化,它的 getter 函数被调用
• private String grade 没有被反序列化,仍然是默认值 null,getter 函数也没有被调用
前三点都自然,奇怪的是后两点。prop 和 grade 同样是 private 类型,同样提供了 getter 函数没有提供 setter 函数,为什么 prop 可以通过 getter 函数来反序列化,而 grade 却没有?
这涉及到 fastjson 的一个特殊处理。对于只有 getter 函数,没有 setter 函数的 private 元素,fastjson 会按如下条件判断反序列化的时候是否调用其 getter 函数。
• 函数名称大于等于 4
• 非静态函数
• 函数名称以get起始,且第四个字符为大写字母
• 函数没有入参
• 函数的返回类型满足如下之一
◦ 继承自Collection
◦ 继承自Map
◦ 是AtomicBoolean
◦ 是AtomicInteger
◦ 是AtomicLong

回到前面的问题。prop 的 getter 函数 getProp() 满足上面的条件,它的返回类型 Properties 继承自 Map ,因此可以成功的被调用。而 grade 的 getter 函数 getGrade() 的返回类型 String 不满足返回类型的那个条件,因此没有被调用,grade 也无法被赋值。
那么,在反序列化的时候,向 grade 这样无法通过 setter 或 getter 函数进行赋值的 private 元素,有什么方法可以赋值呢?有。就是使用 FEATURE.SupportNonPublicField。
在 JSON.parseObject() 中使用 fastjson 的标签 FEATURE.SupportNonPublicField:

执行结果是:

grade 已经成功被反序列化,此时 grade 的 getter 函数 getGrade() 并没有被调用。
FEATURE.SupportNonPublicField 从 fastjson 的 1.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.class,json string 中仍然保持 com.xiang.fastjson.poc.User。

执行结果是:

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

执行结果是:

这一次的结果上,fastjson 却首先检查了两个类型是否匹配,不匹配直接抛出异常,没有调用各个字段的方法进行反序列化操作。
fastjson 内置了一些常用类的反序列化处理类,这些常用类的列表在 ParseConfig.java 中可以看到。

其中有一些处理类中(比如Integer、BigInteger等)没有检查类型是否匹配,就直接进行反序列化处理;有一些没有进行检查,但是反序列化过程会因为语法错误而失败。而对于大多数不属于常用类的情况,fastjson 是会进行检查不同类型之间的关联关系的(如上面的ASMUtils)。
好吧,这一部分太晕了。有没有更简单一些的用法,不用考虑这些匹配原则?有,就是 JSON.parse()。

执行结果是:

同样也支持 Feature.SupportNonPublicField 设置。

执行结果是:

可以看到,JSON.parse(),完全是按照 json string 中指定的类进行反序列化,不用考虑指定目标类的情况,因此可能是更广泛的用法吧。不过少了一次类型检查,也会引入更多的安全风险。
一、TemplatesImpl POC分析
首先是 TemplatesImpl 的 POC,来自于 廖神。
这个 POC 的入口点在 TemplatesImpl 类中 getOutputPerpeties() 函数。由于 TemplatesImpl 的 private 元素 _outputProperties 只有 getter 没有 setter ,同时其 getter 函数的返回类型 Properties 又继承自 Map,因此这个 getter 函数 getOutputProperties() 满足前面说的调用条件,可以被 fastjson 反序列化 TemplatesImpl 时调用到 。POC 的调用链是:
- getOutputProperties()
- -> newTransformer()
- -> getTransletInstance()
- -> defineTransletClasses()`
- -> getTransletInstance()
- -> newTransformer()




POC 代码如下:


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

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


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

TemplatesImpl 的 POC,在利用的时候有几个限制:
- 由于 POC 中关键元素
_bytecode没有对应的public的getter和setter函数,因此需要服务器上解析 json 的时候,不管是使用JSON.parse()还是使用JSON.parseObject(),都需要设置Feature.SupportNonPublicField。 Feature.SupportNonPublicField是在 1.2.22 版本引入,而这个漏洞在 1.2.25 版本就被封堵。这就要求目的服务器上的fastjson版本必须在 1.2.22 到 1.2.24 之间。- 如果目的服务器使用的是
JSON.parseObject(),那么第二个参数必须是和TemplatesImpl不冲突的类,比如Object.class,String.class,Integer.class等。
二、JdbcRowSetImpl POC分析
JdbcRowSetImpl 的 POC 同样来自 廖神。
JdbcRowSetImpl 的调用入口在 setAutoCommit() ,调用链很短:
- setAutoCommit()
- -> connect()
- -> InitialContext.lookup()
- -> connect()


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

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

JdbcRowSetImpl 的 POC,使用限制为:
1 fastjson 的版本范围为 1.2.24 及以下的所有版本。
2 如果目的服务器使用的是 JSON.parseObject(),那么第二个参数必须是和 JdbcRowSetImpl 不冲突的类,比如 Object.class,String.class,Integer.class 等。
三、fastjson 的修复
反序列化漏洞的修补,通常都是通过白名单或黑名单的形式,禁止具有恶意功能的类进行反序列化。fastjson 使用的是黑名单的方式,具体而言就是从 1.2.25 版本开始,fastjson 增加了两个处理:
autotype功能默认关闭,同时提供手动enable_autotype的设置。- 默认开启了黑名单,危险类所在的包不允许进行反序列化。
我们将 fastjson 升级到最新的 1.2.41 版本,再执行上面 JdbcRowSetImpl 的 POC,结果是直接抛了异常:

查看抛异常的地方,

查看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
com.sum.rowset.JdbcRowSetImpl 正好符合其中的 com.sun. 这个黑名单前缀,因此被屏蔽。
四、denyList 黑名单的绕过
fastjson 修补之后默认关闭了 autoType 功能,同时开启了黑名单。
但是这个黑名单功能的实现还是有问题的。在 autoType 功能打开的情况下(总有些偷懒的开发人员会这么干的),我们可以绕过这个黑名单的限制。
首先,我们通过命令行参数 -Dfastjson.parser.autoTypeSupport=true 的方式开启 autoType 功能,执行一下 JdbcRowSetImplPoc。

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

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

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

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

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

成功!






