如标题,Fastjson 安全方面最基础的东西,漏洞成因分析以及简单审计一下反序列化过程
我或许应该每一篇多写一些东西,少分几篇?封面常驻的艾姬多娜图片要用完了(
最后还是希望大家都能技术进步事业顺利,下面正文开始
json
- 现代Web开发的首选数据格式,适合大多数场景,尤其是轻量级和高效的数据交换。
- 简洁高效:JSON格式简洁,数据体积小,适合网络传输。
- 复杂性有限:对于非常复杂的数据结构(如嵌套层次很深或需要命名空间),JSON可能不够灵活。
POST /api/user HTTP/1.1
Host: www.example.com
Content-Type: application/xml
Content-Length: 152
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
Accept: application/xml
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
<User>
<id>1</id>
<name>John</name>
<age>30</age>
<gender>Male</gender>
<deleted>0</deleted>
<createTime>2024-07-26T10:00:00Z</createTime>
<updateTime>2024-07-26T10:00:00Z</updateTime>
</User>
Fastjson
Fastjson是阿里巴巴开源的一个Java库,用于将Java对象转换成JSON格式的字符串,以及将JSON字符串转换成Java对象。这个过程通常被称为序列化和反序列化。Fastjson的特点是速度快、使用广泛,并且可以操作没有源码的Java对象
最大滴特点,就如名字一样:fast,很快啊
示例
导入依赖 pom.xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.20</version>
</dependency>
Person 类
写了一个测试用的类
package org.test;
import java.io.IOException;
public class Person {
private String name;
private int age;
public Person() {
System.out.println("Person constructor 1");
}
public Person(String name, int age) {
System.out.println("Person constructor 2");
this.name = name;
this.age = age;
}
public String getName() {
System.out.println("getName");
return name;
}
public void setName(String name) throws IOException {
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;
}
@Override
public String toString() {
return "人类{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
对象->json
package org.test;
import com.alibaba.fastjson.JSON;
public class Tojsoin {
public static void main(String[] args) {
Person user = new Person("rice", 18);
String jsonString = JSON.toJSONString(user);
System.out.println(jsonString);
}
}
json->对象
package org.test;
import com.alibaba.fastjson.JSON;
public class Toobj {
public static void main(String[] args) {
String json = "{\"age\":18,\"name\":\"rice\"}";
Person user = JSON.parseObject(json, Person.class);
System.out.println(user.getName());
}
}
Autotype
Fastjson 的 AutoType 功能 允许在序列化的 JSON 字符串中带上类型信息 ,这样在反序列化时,不需要显式传入目标类型,即可 自动识别 并还原为正确的 Java 对象
示例:
package org.test;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class AutotypeTest {
public static void main(String[] args) {
String jsonString = "{\"@type\":\"org.test.Person\",\"name\":\"Bob\",\"age\":25}";
Object obj = JSON.parseObject(jsonString);
System.out.println(obj.toString());
}
}
同上个示例的 json -> 对象 对比发现,传入 parseObject 的参数只剩下 json 本身了,类型在 json 数据中由 @type 进行了指定
头脑风暴(
在 Fastjson 将 json 转化为对象的过程中,发生什么了呢?是怎么完成的这个过程?
Fastjson 在反序列化的过程中并没有利用反射,或者别的方法去操作,而是调用对应的类内部的方法
首先 Fastjson 会创建一个我们对应类的对象,然后需要将我们 json 当中记录的对象属性赋值过去,这样自然而然就产生了两个需求,一个空的构造方法,以及对应着属性的 set 方法
Fastjson 当找到满足下面条件的 set 就会调用这个 set 方法
- 方法名长度大于 4 (set就已经三个字了,随便跟个单词就够了)
- 非静态方法
- 返回值为
- 返回值为 void 或当前类
- 以 set 开头且第四个字母为大写
- 参数个数为 1 个
如果找不着满足条件的 set 呢?那就会去尝试寻找 get,get 方法同样有要求
- 方法名长度大于 4
- 非静态方法
- 以 get 开头且第四个字母为大写
- 无参数传入
- 返回值类型继承自 Collection Map AtomicBoolean AtomicInteger AtomicLong
- 此属性没有 setter 方法
那么一个很明显的安全隐患就摆在眼前了,那假如 @type 指定了一个类,而那个类的 set 或者 get 方法有问题呢?
比如:
public void setName(String name) throws IOException {
System.out.println("setName");
Runtime.getRuntime().exec("calc");
this.name = name;
}
将 Person 类里面的 setName 修改为如上,添加了一行调用系统命令,弹计算器的操作,这时候,再去让 Fastjson 解析我们的 json(json 数据表面看起来没有变化),会不会触发恶意代码?

如图所示,执行代码,Fastjson 将数据转换为了对象,同时弹了计算器
那扩展一下来看,只要一个类的 set 里面存在危险方法,就可以利用 Fastjson 打开的这个口子去调用到
那 Fastjson 有没有安全校验呢?有的兄弟,有的
在 fastjson 1.2.24 版是做了一点点的,有一个黑名单,在 ParserConfig.java (只有class的话点击右上角弹出来的download source就有源码了)
private String[] denyList = new String[] { "java.lang.Thread" };
大概 147 行的位置,定义的 denyList 作为黑名单,之后每次进行 Autotype 反序列化 json 数据的时候,都会跑一遍这个黑名单:
String className = clazz.getName();
className = className.replace('$', '.');
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("parser deny : " + className);
}
}
每次都会循环跑一遍黑名单,如果检测到了就报一个错,但是这么一点的安全措施肯定是不够的,黑名单里也只是塞进了一个 Thread 类
Fastjson 漏洞
漏洞大致的原理成因其实刚刚也都说过了,@type + 恶意的 set 或 get
代码上的关键点
autotype 的判断
Object obj = JSON.parseObject(jsonString);
这是刚刚示例里调用的函数的地方,直接跟进一下 parseObject
public static JSONObject parseObject(String text) {
Object obj = parse(text);
if (obj instanceof JSONObject) {
return (JSONObject) obj;
}
return (JSONObject) JSON.toJSON(obj);
}
调用 parse 方法了,再跟进
public static Object parse(String text) {
return parse(text, DEFAULT_PARSER_FEATURE);
}
可以看到,parse 方法里面又调用了 parse,但是传参不同,并非同一个方法,那就再跟进(其实就在下面写着)
public static Object parse(String text, int features) {
if (text == null) {
return null;
}
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}
这一步中间部分可以看到创建了一个默认的 json 解析器 DefaultJSONParser,之后又调用了 parse,再跟进
public Object parse() {
return parse(null);
}
再跟进:
public Object parse(Object fieldName) {
final JSONLexer lexer = this.lexer;
switch (lexer.token()) {
case SET:
lexer.nextToken();
HashSet<Object> set = new HashSet<Object>();
parseArray(set, fieldName);
return set;
case TREE_SET:
lexer.nextToken();
TreeSet<Object> treeSet = new TreeSet<Object>();
parseArray(treeSet, fieldName);
return treeSet;
case LBRACKET:
JSONArray array = new JSONArray();
parseArray(array, fieldName);
if (lexer.isEnabled(Feature.UseObjectArray)) {
return array.toArray();
}
return array;
case LBRACE:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return parseObject(object, fieldName);
case LITERAL_INT:
Number intValue = lexer.integerValue();
lexer.nextToken();
return intValue;
case LITERAL_FLOAT:
Object value = lexer.decimalValue(lexer.isEnabled(Feature.UseBigDecimal));
lexer.nextToken();
return value;
case LITERAL_STRING:
String stringLiteral = lexer.stringVal();
lexer.nextToken(JSONToken.COMMA);
if (lexer.isEnabled(Feature.AllowISO8601DateFormat)) {
JSONScanner iso8601Lexer = new JSONScanner(stringLiteral);
try {
if (iso8601Lexer.scanISO8601DateIfMatch()) {
return iso8601Lexer.getCalendar().getTime();
}
} finally {
iso8601Lexer.close();
}
}
return stringLiteral;
case NULL:
lexer.nextToken();
return null;
case UNDEFINED:
lexer.nextToken();
return null;
case TRUE:
lexer.nextToken();
return Boolean.TRUE;
case FALSE:
lexer.nextToken();
return Boolean.FALSE;
case NEW:
lexer.nextToken(JSONToken.IDENTIFIER);
if (lexer.token() != JSONToken.IDENTIFIER) {
throw new JSONException("syntax error");
}
lexer.nextToken(JSONToken.LPAREN);
accept(JSONToken.LPAREN);
long time = ((Number) lexer.integerValue()).longValue();
accept(JSONToken.LITERAL_INT);
accept(JSONToken.RPAREN);
return new Date(time);
case EOF:
if (lexer.isBlankInput()) {
return null;
}
throw new JSONException("unterminated json string, " + lexer.info());
case ERROR:
default:
throw new JSONException("syntax error, " + lexer.info());
}
}
好长啊,功能就是对 json 的结构进行检测,左大括号,右大括号什么的
那检测到左大括号的话,说明 json 里面有我们要反序列化的一个对象,进入这个 case:
case LBRACE:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return parseObject(object, fieldName);
会去 new 一个 JSONObject 对象,然后传入 parseObject
parseObject 会提取 json 的第一个双引号(功能简单就不贴整个方法代码了),然后会提取出来第一个双引号里面的内容 @type
随后进入下面(320行位置)
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
String typeName = lexer.scanSymbol(symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());
刚才提取出来的内容去判断一下是不是 @type ,随后 loadClass (加载一个类,但不一定初始化),把 @type 的类给加载进来了
那既然类也加载进来了,接下来该着手反序列化了,367行
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);
获取 clazz (上一步加载的类)的反序列化器
public ObjectDeserializer getDeserializer(Type type) {
ObjectDeserializer derializer = this.derializers.get(type);
if (derializer != null) {
return derializer;
}
if (type instanceof Class<?>) {
return getDeserializer((Class<?>) type, type);
}
if (type instanceof ParameterizedType) {
Type rawType = ((ParameterizedType) type).getRawType();
if (rawType instanceof Class<?>) {
return getDeserializer((Class<?>) rawType, type);
} else {
return getDeserializer(rawType);
}
}
return JavaObjectDeserializer.instance;
}
先去一个 map 里面找一下,里面存了常见的类,如果找到就直接获取,找不到就会自己自动生成一个:
if (type instanceof Class<?>) {
return getDeserializer((Class<?>) type, type);
}
那就接着跟进
判断是否恶意类
(太长了,只节选一下关键的)
String className = clazz.getName();
className = className.replace('$', '.');
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("parser deny : " + className);
}
}
这不就是之前那段跑黑名单过滤的嘛,当然也不是,那就过(后面有一大坨各种判断的流程,就跳过了,看一下正式搞反序列化器的地方罢)
461行
derializer = createJavaBeanDeserializer(clazz, type);
接着跟进,然后找到关键部分(太长了)
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, type, propertyNamingStrategy);
注意到 build,关键,跟进之(从129到539全是这个build)
类和属性的遍历
for (Method method : methods)
for (Field field : clazz.getFields())
for (Method method : clazz.getMethods())
328 433 490这三个方法,分别遍历获取 set、filed(参数)、get
之后里面的一些逻辑判断的功能和效果同前面说的对 set 和 get 的要求
