0x00、前言
本文学习fastjson以及在1.2.24版本下的TemplatesImpl链利用,学习下分析思路,尽量理解漏洞在源码上的触发过程。
0x01、fastjson描述
fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。
官方地址:https://github.com/alibaba/fastjson
官方中文地址:https://github.com/alibaba/fastjson/wiki/Quick-Start-CN
优点:
- 性能速度快
- 使用广泛
- 测试完备(虽然现在各版本爆出来的问题有点多)
- 使用简单
- 功能完备
格式跟jackson很像,区别jackson数据要求比较严格,其中提交的数据只能为对应Javabean的key,不能添加多余不在javabean的key进行提交,因此会报错,而fastjson不会
新建maven项目,然后环境添加:
直接添加对应版本到maven依赖即可
<dependencies> |
0x02、fastjson简单使用
主要学习序列化和反序列化接口
简单使用:
创建基础实例类People类
public class People { |
序列化-toJSONString()
还有一些其它的序列化方法
// 将Java对象序列化为JSON字符串,支持各种各种Java基本类型和JavaBean |
序列化toJSONString()的使用:JSON.toJSONString(Class class)
JSON.toJSONString(Class class,SerializerFeature ...)
可以使用SerializerFeature.WriteClassName参数将类名加到序列化json字段的@type中
public static void main(String[] args) throws Exception{ |
反序列化-parseObject()、parse()、parseArray()
还有一些其他不同参数的使用方法
// 将JSON字符串反序列化为JavaBean |
反序列化parseObject()、parse()用法JSON.parseObject()
public static void main(String[] args) throws Exception{ |
- parseObject()方法,需要设置第二个参数(Class class)将序列化数据读取应用到对应类里,否则会返回JSON对象
- parse()方法是直接将序列化数据应用到@type声明的类中,如果不声明@type变量,则会返回JSON对象。
- parseArray()方法就是源数据是集合的形式(即[]中的内容)
另外一些使用方法直接参考中文手册即可
https://www.w3cschool.cn/fastjson/
getter/setter自动调用
在使用fastjson中,发现
JSON.parseObject(parse2)
方法会自动调用目标类的构造方法、get方法和set方法。JSON.parseObject(parse2,People.class)
和JSON.parse(parse2)
方法只会调用目标类的构造方法和set方法
get方法调用
parseObject(parse2)
方法中会通过反射读取目标元素的getter获取值并存储在hashmap中
set方法、构造方法调用
但在setter调用的调试中,只追到下图的包,通过deserialze()方法反射获取成员值,循环读取但是无法抓到每个获取方法的过程
getter/setter获取
上面提到在调用对应get或者set方法时,通过getter/setter获取对应方法,然后直接调用在getter/setter获取对应的成员值反序列化中的获取getter/setter前的调用栈大体如下,没找到前面调用过程的文章,瞎跟了好一会,比较容易跟丢,后面按我理解跟出来的调用情况,用图展示的话拉的比较长,就用调用链形式展示,以反序列化调试展示
JSON.parseObject(String text) |
进入JavaBeanInfo.build()方法查看getter/setter的设置情况
首先通过反射获取Clazz类的属性、方法、构造方法
获取后,先是判断构造器是否为空,如果为空,则判断该类是否是接口或者抽象类,如果是的话,然后创建JSNOcreator注解,(底层源码没注解看着还是费劲好些流程不是特别明白)
setter构建
经过前面的一些判断,再到下面的setter构建,循环获取反射里的方法名
会经过几个条件判断:
条件判断,方法名长度不能小于4
if (methodName.length() < 4) { |
条件判断,方法不能是静态方法
if (Modifier.isStatic(method.getModifiers())) { |
条件判断,方法的返回类型必须是void类型或者返回方法本身。主要是作用筛选是不是set方法,get方法会返回字符串就符合下面的判断
if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) { |
条件判断,方法的参数个数必须为1个
Class<?>[] types = method.getParameterTypes(); |
条件判断,方法名开头必须是set开头
if (!methodName.startsWith("set")) { // TODO "set"的判断放在 JSONField 注解后面,意思是允许非 setter 方法标记 JSONField 注解? |
条件判断,判断TypeUtils.compatibleWithJavaBean是否开起,compatibleWithJavaBean为false表示首字母小写
|
条件判断,判断set后面的字母
//判断第四个字符是否是以“_”或者“f” |
通过上面的筛选获取截选后的字段的属性值,如果属性值不存在或者类型为boolean类型,就对其变量名前面加上is拼接,再查询拼接后的字段的属性值
Field field = TypeUtils.getField(clazz, propertyName, declaredFields); |
最后将符合条件的添加入fieildList中
add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, |
总结必须满足的条件就是:
- 方法名长度不能小于4
- 方法不能是静态方法
- 方法的返回类型必须是void类型或者返回方法本身
- 方法的参数个数必须为1个
- 方法名开头必须是set开头
getter构建
流程差不多
条件判断,方法名长度不能小于4
if (methodName.length() < 4) { |
条件判断,方法不能是静态方法
if (Modifier.isStatic(method.getModifiers())) { |
条件判断,方法名必须以”get”开头,并且方法名的第四个字母必须是大写
if (methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3))) { |
条件判断,方法的返回类型必须继承(Collection、Map、AtomicBoolean、AtomicLong)四个类的其中一种
if (Collection.class.isAssignableFrom(method.getReturnType()) // |
然后把满足条件的放进fieldinfo中
add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, 0, 0, 0, annotation, null, null)); |
接着获取方法的是否存在JSONField的注解方法,类似下图,判断该类是否存在JSONField的注解方法,并且是否可以反序列化,反序列化设置默认为true
JSONField annotation = method.getAnnotation(JSONField.class); |
然后获取字段信息,查看该注释方法名在fieildList中否存在
FieldInfo fieldInfo = getField(fieldList, propertyName); |
满足上面条件就将信息存进fieildList中
add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, 0, 0, 0, annotation, null, null)); |
总结必须满足的条件就是:
- 方法名长度不能小于4
- 方法不能是静态方法
- 方法名开头必须是get开头
- 方法的返回类型必须继承(Collection、Map、AtomicBoolean、AtomicLong)四个类的其中一种
最后通过返回javaBeanInfo将上面的fieldinfo一起放进beaninfo中
return new JavaBeanInfo(clazz, builderClass, defaultConstructor, null, null, buildMethod, jsonType, fieldList); |
0x03、TemplatesImpl知识回顾
TemplatesImpl在cc2链学习中涉及到,这里直接引用cc2分析的部分吧
ClassLoader#defineClass
ClassLoader为类加载器,可以将字节码文件(.class文件),通过loadClass函数加载类名,返回一个Class对象,同时ClassLoader类下面存在defineClass方法,可以将byte[]字节数组信息还原成一个Class对象,在学javassist中,了解到javassist可以动态生成字节码文件,包括了一些恶意代码文件,可进而通过ClassLoader类加载器将这些恶意的字节码文件转化为java类进行调用,达到执行恶意代码的目的
其中类加载阶段:
ClassLoader#loadClass(类加载,从类缓或父加载器等位置寻找类) |
由于ClassLoader#defineClass方法为protect修饰,因此可通过反射进行调用
简单实现
public static void main(String[] args) throws Exception { |
TemplatesImpl
TemplatesImpl这个类简述功能就是对xsl格式文件生成的字节码转换成XSLTC模板对象,期间会处理字节码,因此重写了defineClass方法,具体描述可查看TemplatesImpl了解
重写了defineClass
方法,并且没有定义域,可以在其他类进行调用使用,而ClassLoader#defineClass
定义域是受保护的,在很多情况中调用受限,因此这也是TransletClassLoader#defineClass
作为很多序列化漏洞入口,而不是使用ClassLoader#defineClass
但该defineClass()
的调用并不会实例化,需要通过newInstance()
进行实例化。
依次看调用情况
defineTransletClasses()
defineClass
在defineTransletClasses
方法中被调用
其中需要的满足条件:
_bytecodes
不能为空,为空会直接抛出异常if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}_tfactory
需要实例化
创建的TransletClassLoader
(Translet类的加载器)对象,其中_tfactory.getExternalExtensionsMap()
中的_tfactory
对象为TransformerFactoryImpl
类对象,等同于调用TransformerFactoryImpl.getExternalExtensionsMap()
方法,但其中_tfactory
对象初始赋值为null,直接执行会报错,因此需要将_tfactory
进行实例化,才能调用TransformerFactoryImpl.getExternalExtensionsMap()
方法。TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});两种实现方法都可以实例化,第一种通过
TransformerFactoryImpl()
构造方法实现实例化对象,第二种通过反射直接实现实例化对象。setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
setFieldValue(templates, "_tfactory", TransformerFactoryImpl.class.newInstance());父类需要为ABSTRACT_TRANSLET
类
通过for循环对字节文件类进行循环定义并赋值给class数组,其中会判断当前获取的字节类的父类是否为ABSTRACT_TRANSLET
类,是的话会讲该类序号赋值给_transletIndex
,否则不是的话会抛出异常(表意为只有存在父类为ABSTRACT_TRANSLET
类的translet
类才能被实例化),从而在getTransletInstance
类中的AbstractTranslet
实例化步骤将父类为ABSTRACT_TRANSLET
的类进行实例化for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();
// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}_bytecodes
字节码需要设置为byte[][]数组,_bytecodes
变量声明为byte[][]类型,如果直接通过javassist toBytecode()生成byte[]数组运行会报错。
因此需要将一维数组转化为二维数组。byte[] bytecode=ct.toBytecode();
byte[][] bytecodes=new byte[][]{bytecode};
接着查看defineTransletClasses
方法的上层调用情况
其中有三处对该方法进行了调用,其中只用getTransletInstance
方法有上层调用,其他两个方法没有被其他方法进行调用。
getTransletInstance()
其中需要的满足条件:
_name
不能为空,为空会直接返回null
if (_name == null) return null;
_class
必须为空,才能调用defineTransletClasses
方法if (_class == null) defineTransletClasses();
最后通过创建AbstractTranslet
对象将class文件类进行实例化
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); |
接着查看getTransletInstance
方法的上层调用情况
newTransformer()
方法进行了调用
newTransformer()
在调用构造函数的时候调用了getTransletInstance
方法,返回Translet类的实例,其中没有需要的满足条件。
接着查看newTransformer()
方法的上层调用情况
其中有5处显示进行了调用,但只有getOutputProperties
方法调用的本类的newTransformer()
方法,其他四种都是调用的其他类重写的newTransformer()
方法。
getOutputProperties()
该方法直接执行了newTransforme
方法,没有其他条件限制,查询getOutputProperties
的上层调用,没有在本类发现其调用,因此最后的执行方法就在getOutputProperties
完整链
到newTransformer
方法执行就能达到触发了,他上层getOutputProperties
方法也进行了调用,也可以算进去当作延伸出来的链。
getOutputProperties() |
实现demo
public static void main(String[] args) throws Exception { |
0x04、fastjson-TemplatesImpl利用分析
只有正向分析了解执行过程了,逆向分析思路实在难理
POC
参考网上的POC:
package com.test.fastjson; |
前半段就是javassist
字节码生成恶意代码,在上面ClassLoader
和TemplatesImpl
分析的时候提过了,区别就是生成的恶意字节码经过Base64
编码过一次,为什么会经过Base64
编码,分析过程去了解。
下半段主程序就是fastjson
反序列化TemplatesImpl
类,加载恶意字节码,同时添加TemplatesImpl
执行需要满足的几个条件属性,最后添加_outputProperties
字段目的经过转换调用getoutputProperties()
方法执行恶意代码,后面又加了一个_name
参数和allowedProtocols
参数。
最后在parseObject
反序列化的时候添加了Feature.SupportNonPublicField
参数,突破访问私有属性限制,因为TemplatesImpl
类大部分属性都是private
保护属性,fastjson默认无法序列化保护属性的变量。
分析过程
调试首先进入对应参数类型的parseObject
方法
判断input输入是否为空,然后创建默认JSONparser
解析器
跟进DefaultJSONParser
方法,先通过分词器(laxer,用于对input值进行符号分割,获取对应的截选字段)获取开头字符是否是”{“
是的话,就设置token
为JSONtoken.LBRACE
,值为12
然后对解析器parser
进行parseObject
序列化操作,Type
对象为输入的Object.class
对象
跟进parseObject
方法,判断token
所属类型,然后获取反序列化解析器
跟进getDeserializer
方法,根据Type(即输入的Object.class
对象)获取对应的反序列化器,获取的反序列化器为JavaObjectDeserializer
获取反序列化器后,判断对象类型后,对反序列化器进行反序列化操作。
跟进parse
方法
通过Token
判断后进入判断体分支,创建新的json
对象,然后对该json
对象和fieldName
字段(当前类对象为Object,没有解析字段,所以值为null)进行反序列化解析
再次跟进parseObject
方法
前面的一大段操作都是通过分词器laxer
获取的字符进行判断是否合法
一直到key值的判断,key为获取的@type
,typeName为@type
的值即输入的com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
通过加载类获取一个TemplatesImpl
类对象clazz
获取TemplatesImpl
类对象后,再次寻找对应的反序列化器
由于TemplatesImpl
类并不在derializers
的列表中,因此derializer
为null
,derializer
列表大部分为hashmap
但type(TemplatesImpl)
属于Class
,因此进入另一个获取反序列化器的方法getDeserializer((Class<?>) type, type)
,继续跟进getDeserializer
方法
前半段依旧是获取type
的反序列化器,同样derializer
还是为null
不同上一个获取反序列化器的方法,接下来获取clazz的类名即(com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl),然后判断该类名是否是黑名单(java.lang.Thread)中的内容
后面就是对className
进行分类判断,最后没有找到对应类的反序列化器,最后通过createJavaBeanDeserializer
方法对当前clazz
创建一个反序列化器
跟进createJavaBeanDeserializer
方法,其中asmEnable
默认开启,然后判断clazz
类是否支持字节码操作,然后进行JavaBeanInfo
创建
在setter/getter
分析中提到的build
方法主要是通过反射遍历获取类中的方法名和数据保存在Fieldlist
中然后放入beaninfo
数组中。
beaninfo
创建后,后面的代码部分就是针对Beaninfo
的字段属性进行循环遍历检查合法性,属性查询完后,创建JavaBeanDeserializer
反序列化器
JavaBeanDeserializer
方法中先是声明beanInfo.sortedFields
的FieldDeserializer
反序列化器,然后循环读取sortedFields
排序后的属性给fieldInfo
变量,然后创建新的字段反序列化器createFieldDeserializer
,最后新反序列化器放入sortedFieldDeserializers
数组中
后半段是循环获取beanInfo.fields
的属性,然后通过getFieldDeserializer
方法在sortedFieldDeserializers
数组中查找是否匹配,匹配的话就放入到fieldDeserializers
数组当中。
到此,JavaBeanDeserializer
反序列化器创建成功,并放入到反序列化器当中,最后返回反序列化器derializer
获取到反序列化器后,正式对该反序列化器进行反序列化操作
反序列化的前半部分都是对类的一些属性信息进行判断(token
的判断,laxer
分词器的创建等)
通过循环读取sortedFieldDeserializers
数组获取反序列化器中的属性数据,获取过后再循环判断获取的类的分类所属,在进行对应类属性赋值
经过上面的对sortedFieldDeserializers
获取的当前属性信息的操作后,创建对解析器对象进行实例化
实例化对象后,开始解析该对象在解析器中的字段属性
字段反序列化器会模糊匹配key
,跟进smartMatch
方法
先判断Key
是否存在,存在然后通过getFieldDeserializer
方法查找key
的反序列化器,如果找不到该key
的反序列化器,然后判断key
是否以is
为开头,随后循环获取sortedFieldDeserializers
数组内容获取其中的fieldInfo、fieldClass、fieldName
等属性
因为在getFieldDeserializer
中匹配Key
是否在sortedFieldDeserializers
数组中,由于数组中的值没有”_”开头,因此找不到key
的反序列化器
条件都不满足,找不到FieldDeserializer
属性反序列化器,然后就对属性进行字符替换操作,此时的Key
由"_outputProperties"
被替换成"outputProperties"
然后再次调用getFieldDeserializer
方法,查询是否存在key2
的反序列化器,此时sortedFieldDeserializers
数组中存在outputProperties
字段,因此能够找到反序列化器
返回属性反序列化器,此时获取到getoutputProperties
方法(build中提到的getter/setter建立会存储get和set方法,反序列化器查询匹配到字段后会返回该方法的反序列化器)
找到属性反序列化器后,对该属性进行反序列化解析
解析过程中通过getFieldValueDeserilizer
方法获取属性值的反序列化器,并对属性值反序列化器进行反序列化解析获取值
然后将获取的值通过setValue
方法赋值给字段属性
跟进setValue
方法,通过反射获取字段属性的方法
最后通过反射执行Object
对象的Method
方法,即执行TemplatesImpl.getoutputProperties()
,触发漏洞
流程总结
根据上面的分析过程,稍微理一下fastjson的执行流程
JSON对象解析 |
base64编码问题
在解析字段属性中,解析到"_bytecodes"
然后获取对应反序列化器,对反序列化器进行再次解析
再次跟进,此时属性反序列化器为ObjectArrayCodec
,调用ObjectArrayCodec
的反序列化方法deserialze
过程中会对参数内容集合进行解析
进入对集合进行解析,此时laxer
经过nexttoken()
方法后重新赋值,再次对value
进行反序列化解析
跟进反序列化方法,此时laxer
匹配JSONToken.LITERAL_STRING
,调用bytesValue()
方法
跟进bytesValue()
方法,在这里对txt
也就是Input
的内容进行base64
解码,所以_bytecodes
需要经过base64编码
0x05、总结
跟完TemplatesImpl链,头都大了,总的来说就是通过创建序列化器去存储序列化对象的相关信息,再通过反序列化器解析反序列化类对象、类字段、方法等信息,最后再解析反序列化器。跟一遍下来的感觉就是不停的创建反序列化器,再解析反序列化器的内容,结合getter触发代码执行,虽然绕了很多,但在这里面也学到了很多,理解的过程也不完全,很多细节上的代码段也没理解透意思,就在大体框架走了一遍,可能还存在一些理解错误的地方,后面遇到了再改吧。
0x06、参考链接
https://github.com/alibaba/fastjson/wiki/Quick-Start-CN
https://www.w3cschool.cn/fastjson/
https://www.yuque.com/tianxiadamutou/zcfd4v/rwx6sb
https://y4er.com/posts/fastjson-learn/