基本知识
Fastjson是阿里巴巴集团旗下的一款基于json序列化的开源项目,主要功能是用于将对象与json格式之间的相互转换。和php中的json序列化一样,java中的json序列化也只是保存对象的属性及其内容。在fastjson中反序列化主要使用JSON.parseObject
方法和JSON.parse
方法将json数据还原为对象。
JSON.parseObject
和JSON.parse
的区别parse和parseObject,这两种方法的功能都是将序列化数据转化为对象,但是具体的执行流程有些许不同。对于parse来说,反序列化时会调用目标类中的setter方法和某些满足条件的getter方法,而parseObject会调用目标类的全部setter和getter方法包括不存在的属性,但是在默认情况下只会调用公开属性的getter和setter方法。
fastjson触发getter方法的条件如下:
- 继承自Collection||Map||AtomicBoolean||AtomicInteger||AtomicLong 这几种类型的属性。字面量创建形式:Collection:[],Map:{},AtomicBoolean:true,AtomicInteger:1,AtomicLong:1
- 没有参数传入
- 只有getter没有setter
- 非静态方法
- 方法名的长度大于等于4,且第四个字符为大写字符
从安全的角度来看
JSON.parseObject
比JSON.parse
更加不安全,因为其会调用所有的getter和setter方法,无论属性是否存在。而JSON.parse
更多只是调用setter方法。@type
属性在fastjson中默认开启
@type
属性,该属性用于指明反序列化的对象类。
fastjson序列化数据格式:{"@type":类名,"属性名":"属性值","属性名":"属性"....}
事实上java中的json反序列化和php中的反序列化很像。都是通过传入设置好属性的格式化数据,将其还原为对象。并且都涉及类似魔术方法的概念,利用一些自动调用的方法进行进一步利用。
Java的json反序列化和普通的反序列化最大的区别是,json反序列化只会保留对象的属性,而java反序列化会将对象的所有内容,包括属性、方法、类名等等,都写入字节码文件中。且json格式是可读的,而java反序列化后是字节码文件,不可读。
关于fastjson的反序列化漏洞,我找到三种比较典型利用方式,接下来就一一调试分析。
BasicDataSource 利用链
漏洞分析
BasicDataSource
利用链可以调用的反序列化类有两个,分别是org.apache.commons.dbcp.BasicDataSource
和org.apache.tomcat.dbcp.dbcp.BasicDataSource
。为了调试的方便,我这里只以org.apache.tomcat.dbcp.dbcp.BasicDataSource
为例。直接定位到文件的getConnection
方法:
跟入createDataSource
方法:
继续步入createConnectionFactory
方法:
在createConnectionFactory
方法可以看到调用Class.forName
方法。而Class.forName
方法的作用就是用于实现动态加载类。Class.forName
的使用方法有两种:
1 | Static Class forName(String name) |
简单的利用demo:
1 | package com.test; |
第一种方法用于加载传入的类名,若找不到则抛出ClassNotFoundException
异常。若类中存在静态初始化器的话,会主动调用该类的静态代码段。采用第二种方法则可以通过设置第二个参数,手动指定是否执行类中的静态代码段。最后一个参数指明动态加载器。实际上不论采用哪一种方式,最终的目的都是实现动态加载对象。理论上来说都是没有问题的,但是并不是每一个动态加载器都会老老实实地从本地寻找类名进行加载,比如com.sun.org.apache.bcel.internal.util.ClassLoader
。
如果forName
方法的第一个参数仅仅只能指明类名,那就没有很大的安全隐患。但是在com.sun.org.apache.bcel.internal.util.ClassLoader
,允许name
传入经过BCEL编码的字节码文件。也就是说我们可以将name
设置为BCEL编码的evil
类的字节码文件,通过com.sun.org.apache.bcel.internal.util.ClassLoader
进行加载,由于第二个参数在这里是true
,就会自动调用evil
类中的静态代码段。
当我们指定使用com.sun.org.apache.bcel.internal.util.ClassLoader
加载类,则进入com.sun.org.apache.bcel.internal.util.ClassLoader
的loadClass
方法:
若检测到采用$$BCEL$$
编码,则调用createClass
方法:
在createClass
方法中进行解码操作,并且获取其字节码。接下来回到loadClass
方法中:
采用defineClass
方法从字节码中还原出一个类并加载,而静态代码块就是在加载的时候执行,优先于各种代码块和构造方法。因此我们可以将恶意代码写在静态代码块中。
关于BasicDataSource
内部的利用链分析就到此为止,相对来说还是比较简单的,更多还是利用到java动态加载的知识点。我们需要思考的是如何调用getConnection
方法。
这里就引出fastjson的反序列化机制。就像我上文描述的当我们使用JSON.parseObject
进行反序列化的时候,就能触发目标类中的所有getter方法,包括不存在属性和私有属性的getter方法。这里我们利用到的getConnection
就是属于一个不存在的Connection
属性的getter方法。而且基于pop(面向属性编程),我们可以在json序列化数据中指定driverClassLoader
和driverClassName
。最终我们就可以利用JSON.parseObject
完成一次反序列化攻击,实现rce。
完整调用栈:
POC
1 | package com.test; |
1 | package com.test; |
基于com.sun.rowset.JdbcRowSetImpl
的fastjson利用链
漏洞分析
fastjson反序列化漏洞问题的本质就是getter和setter的滥用。因此我们在挖掘fastjson反序列化漏洞利用时,肯定要把精力放在各个类的getter和setter方法上。在com.sun.rowset.JdbcRowSetImpl
中的setAutoCommit
方法中就存在JNDI注入:
跟入connect
方法:
在这里就很明显能看出来存在一个典型的JNDI注入,this.getDataSourceName
方法就是用于获取dataSource
属性。这里我们进入setDataSourceName
方法,看看如何对dataSource
属性进行赋值。
跟入super.setDataSourceName
:
这里就可以看出来在json数据中设置的dataSourceName
属性最终赋值给dataSource
属性。也就是说JNDI注入点可控,接下来就是常规JNDI注入思路,在外部搭建一个RMI
服务,绑定远程服务器上的evil.class
字节码文件,通过动态加载引入字节码文件实现RCE。
POC
1 | import com.sun.jndi.rmi.registry.ReferenceWrapper; |
1 | import com.alibaba.fastjson.JSON; |
1 | public class ExportObject { |
将ExportObject
字节码文件放在可访问的web根目录下即可。
基于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
的对象注入
漏洞分析
之前说到关于fastjson触发getter方法的几种条件,在默认情况下fastjson只会反序列化公开的属性,因此当我们需要对私有属性进行赋值时,需要在JSON.parseObject
方法中设置Feature.SupportNonPublicField
参数。我们将目光定位到com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
中的getOutputProperties
方法:
跟入newTransformer
方法:
继续跟入getTransletInstance
方法:
在这里就能看到调用newInstance
方法实例化一个对象,如果前面的参数可控的话就能实现一个对象注入。因为这里对_name
进行判断,因此我们需要在payload中对其进行赋值。我们跟入defineTransletClasses
方法,看看是如何操作_class
属性的。
这里就用到我们在payload中构造的_bytecodes
属性,通过defineClass
方法,从字节码中还原出一个类并加载,返回一个类。再回到getTransletInstance
方法中,对这个对类进行实例化操作,生成一个对象。在我复现的这个版本中并没有利用到_tfactory
属性,但是在某些版本中需要用到这个属性否则会导致异常退出。
整体思路捋清楚就要开始研究如何传参赋值。这里比较有意思的是,在_bytecodes
属性中使用base64编码字节码。其实也是因为fastjson在获取属性赋值的时候对其进行base64解码操作,具体如下:
POC
1 | package com.test; |
实际上这种利用方式很鸡肋,首先需要在JSON.parseObject
中设置Feature.SupportNonPublicField
参数。而该参数在fastjson1.2.22之后才引入,也就是只有在fastjson1.2.22才能利用这种攻击方式。且大部分情况下我们并不能控制JSON.parseObject
的第二个参数,第三个参数和第四个参数。很多时候都是JSON.parseObject(input)
就完事了。
小结
由于fastjson也是java中处理json数据的主要组件之一,因此该组件的任何漏洞都有可能在具体场景中产生巨大危害,我们不能否认这些漏洞的危害性。但是就我个人认为,这三种利用手法的有效性还是有很明显的区别。我是认为JdbcRowSetImpl
的攻击面最大,BasicDataSource
次之,TemplatesImpl
的利用面最狭隘。
通过这三种不同的利用手法,我也看到了针对fastjson攻击思路的多样性。JdbcRowSetImpl
采用JNDI注入,BasicDataSource
利用defineClass
加载字节码对象,而TemplatesImpl
是结合newInstance
进行对象注入。虽然名义上都是反序列化漏洞,可是其具体成因不同,当我们想要进行漏洞挖掘的时候,也应该是从这几个薄弱点下手。