Fastjson反序列化分析
Fastjson是一个Java库,可以将Java对象转换位JSON格式,当然也可以将JSON字符串转换位Java对象
Fastjson特性:
- 提供了toJSONString()和parseObject()方法来对将Java对象与JSON相互转换。调用toJSONString方法即可将对象转换为JSON字符串,parseObject方法则反过来将JSON字符串转换为对象
- Java泛型的广泛支持
- 支持任意复杂对象
pom.xml
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency>
|
Person.java
package com.test;
public class Person { private String name; private Integer age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age;
}
public void setAge(Integer age) { this.age = age; } }
|
测试类
public static void main(String[] args) { Person person1 = new Person(); person1.setName("tom"); person1.setAge(18);
String str1 = JSONObject.toJSONString(person1); System.out.println(str1);
Person person2 = new Person(); person2.setName("lisa"); person2.setAge(20);
String str2 = JSONObject.toJSONString(person2, SerializerFeature.WriteClassName); System.out.println(str2); }
|
我们来看下这两种不同方法的区别
输出:
可以发现第二种在反序列化中,还多打印了get、set方法的键和值
{"age":18,"name":"tom"} {"@type":"com.test.Person","age":20,"name":"lisa"}
|
Autotype功能
允许用户在反序列化数据种通过”@type”指定反序列化的Class类型
特性:反序列化过程种会触发get方法、set方法
测试代码:在Person类的set和get方法上加上打印语句
将字符转换为对象的方法
String str3 = "{\"@type\":\"com.test.Person\",\"age\":20,\"name\":\"lisa\"}";
Object obj = JSONObject.parse(str3); System.out.println(obj);
JSONObject obj2 = JSONObject.parseObject(str3); System.out.println(obj2);
|
输出:
第一种方法将json字符串转为相应的对象,第二种是将json字符串转为相应的JSONObject对象
com.test.Person@2077d4de {"name":"lisa","age":20}
|
区别:
我们看下parseObject方法,实际上是将对象进行强转,本质上还是调用了parse
我们在对象上新增get/set方法打印说明,看下get/set调用情况
package com.test;
public class Person { private String name; private Integer age;
public String getName() { System.out.println("call getName Method"); return name; }
public void setName(String name) { System.out.println("call setName Method"); this.name = name; }
public Integer getAge() { System.out.println("call getAge Method"); return age;
}
public void setAge(Integer age) { System.out.println("call setAge Method"); this.age = age; } }
|
使用parse进行反序列化
String str3 = "{\"@type\":\"com.test.Person\",\"age\":20,\"name\":\"lisa\"}"; Object obj = JSONObject.parse(str3); System.out.println(obj);
|
输出
call setAge Method call setName Method com.test.Person@2077d4de
|
使用parseObject进行反序列化
输出
call setAge Method call setName Method call getAge Method call getName Method {"name":"lisa","age":20}
|
总结:
- parseObject方法其实也是调用parse方法,会触发该类的构造函数、get、set方法
- parse会识别并调用目标类的setter方法
Fastjson反序列化流程分析
如果我们能找到一个类,在反序列化这个类的对象时,fastjson调用其中的setter或者getter方法来给它赋值,同时这个赋值方法存在漏洞,可以执行恶意代码,那么就可以远程代码执行
Person.java
package com.test;
import java.io.IOException;
public class Person { private String name; private Integer age;
public String getName() { System.out.println("call getName Method"); return name; }
public void setName(String name) throws IOException { Runtime.getRuntime().exec("calc"); System.out.println("call setName Method"); this.name = name; }
public Integer getAge() { System.out.println("call getAge Method"); return age;
}
public void setAge(Integer age) { System.out.println("call setAge Method"); this.age = age; } }
|
打个断点尝试跟进
调用了另外一个重载,继续跟进
发现下面方法重新调用,继续跟进
调用到了构造方法
ch赋值json字符串的第一个字符,如果判断为{则,laxer获取下一个字符以及lexer.token赋值给12
之后又走进parser.parse(),我们走进看下
继续跟进
可以看到这里用到了我们前面赋值的lexer.token(),我们前面赋值12,就会走进12的分支
我们可以看到第一行创建了一个接收object,第二行调用了parseObject方法
我们继续跟进第二行,前面做了一系列的判断,lexer.skipWhitespace做了空白字符过滤
我们看下如何实现的,单引号不等于反斜杠,则进入对空格换行等进行判断
判断为false,我们直接跳回,走进这个判断
走进scanSymbol方法,从英文单词了解是扫描一个符号的意思,传进来的参数是一个双引号,可以看到方法最后是返回一个@type,所以说可以看得出是取两个双引号的值
后面又对空白字符,获取字符,这里判断不是冒号,继续往下走
可以看到这里,获取下一个双引号,又调用了lexer.scanSymbol方法,又截取到com.test.Person
之后用TypeUtils.loadClass加载我们这个Person
之后往下获取生成了解析器,我们看下如何获取的解析器,走进该方法
走进这个方法
获取到className
对类名进行黑名单排查
判断是不是java.awt.开头,为false继续往下
判断是否以java.time.为开头,我们直接走到后面的else if判断也不成立,继续往下走
前面做了一些检查,走到这里开始获取了classLoader
derializer判断为true走进方法,返回了null
直到这里终于生成一个解析器
走进这个方法,我们前面快速步过,到这里走到了build,这个beanInfo保存了Person类里面的所有方法和一些变量以及构造方法
beanInfo保存了构造方法,可以通过beanInfo获取构造方法,通过反射生成对象
可以看到下面获取了构造器和字段,之后这是一个循环获取字段
最后返回给了derializer
我们执行返回,往下走
下面就是最关键地方,调用解析器去解析,我们进入这个方法
后面我们一直步过是看不见方法调试,因为后面是asm机制临时生成的代码在调试的时候是不可见的,直接继续往下调试,最后调用了set方法
asm机制:ASM是一个通用的Java字节码操作和分析框架。 它可以用于修改现有类或直接以二进制形式动态生成类
最终调用了set方法,弹出计算器
如果是使用parseObject,最后还会到下面这里
我们可以跟进这个
又调用下面的toJSON
往下走
之后走到这个
继续走到了get方法,这里其实就调用到了get字段的方法
调用栈
条件
满足条件的setter:
- 函数名长度大于4且以set开头
- 非静态函数
- 返回类型为void或当前类
- 参数个数为1
满足条件的getter:
- 函数名长度大于等于4
- 非静态方法
- 以get开头且第4个字母为大写
- 无参数
- 返回值类型继承自Collection或Map或AtomicBoolean或Atomiclnteger或AtomicLon