来源:香依香偎@闻道解惑
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
。
成功!