0x00 介绍
Fastjson是一款由阿里巴巴公司开发的Java语言编写的高性能JSON解析库。它采用了特殊的算法和数据结构,可以快速地将JSON格式的字符串解析为Java对象,或将Java对象序列化为JSON字符串
Fastjson提供了两个主要接口来分别实现对于Java Object的序列化和反序列化操作。
JSON.toJSONString
JSON.parseObject/JSON.parse
0x01 简单使用
这是一个简单的JavaBean
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public class Person { private String name; private int age;
public Person() { System.out.println("构造函数"); }
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(person)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class FJTest01 { public static void main(String[] args) { Person person = new Person(); person.setName("hello"); person.setAge(6); String jsonString = JSON.toJSONString(person); System.out.println(jsonString); } }
构造函数 setName setAge getAge getName {"age":6,"name":"hello"}
|
- JSON.toJSONString(person,SerializerFeature.WriteClassName)
是Fastjson序列化时的一个配置选项,用于设置是否在序列化过程中写入类名。
在默认情况下,Fastjson序列化对象是不会写入类名的
如果添加了第二个参数SerializerFeature.WriteClassName,Fastjson会在序列化对象时会多出一个**@type,即写入被序列化的对象的类名信息。这样在反序列化时,Fastjson会自动根据类名信息绑定正确的Java**类型,从而简化了反序列化的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class FJTest01 { public static void main(String[] args) { Person person = new Person(); person.setName("hello"); person.setAge(6);
String jsonString1 = JSON.toJSONString(person, SerializerFeature.WriteClassName); System.out.println(jsonString1); } }
构造函数 setName setAge getAge getName {"@type":"org.example.Person","age":6,"name":"hello"}
|
反序列化
解析为JSONObject类型或者JSONArray类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class FJTest02 { public static void main(String[] args) { String jsonstring ="{\"@type\":\"org.example.Person\",\"age\":6,\"name\":\"hello\"}"; Object parse = JSON.parse(jsonstring); System.out.println(parse); System.out.println(parse.getClass().getName()); } }
构造函数 setAge setName org.example.Person@4459eb14 org.example.Person
|
- JSON.parseObject(String text)
JSON文本解析成JSONObject类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class FJTest02 { public static void main(String[] args) { String jsonstring ="{\"@type\":\"org.example.Person\",\"age\":6,\"name\":\"hello\"}"; JSONObject jsonObject = JSON.parseObject(jsonstring); System.out.println(jsonObject); } }
构造函数 setAge setName getAge getName {"name":"hello","age":6}
|
- *JSON.parseObjectparseObject(String text, Class clazz)*
JSON文本解析成对应的**.class**
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class FJTest02 { public static void main(String[] args) { String jsonstring ="{\"@type\":\"org.example.Person\",\"age\":6,\"name\":\"hello\"}"; Person person = JSON.parseObject(jsonstring, Person.class); System.out.println(person); System.out.println(person.getClass().getName()); } }
构造函数 setAge setName org.example.Person@4459eb14 org.example.Person
|
0x02 流程分析
反序列化
断点位置
1
| JSONObject jsonObject = JSON.parseObject(jsonstring);
|
首先会到parseObject里面,去调用parse
1 2 3 4 5 6 7
| public static JSONObject parseObject(String text) { Object obj = parse(text); if (obj instanceof JSONObject) { return (JSONObject) obj; } return (JSONObject) JSON.toJSON(obj); }
|
一路走到parse,创建了个DefaultJSONParser对象
1 2 3 4 5 6 7 8 9 10
| 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; }
|
在parse中的DefaultJSONParser,会在该方法中盘断json字符串是否以**{或[**开头,然后分配不同的解析流程,以其他字符开头的后面的解析过程中会报错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public DefaultJSONParser(final Object input, final JSONLexer lexer, final ParserConfig config){ this.lexer = lexer; this.input = input; this.config = config; this.symbolTable = config.symbolTable; int ch = lexer.getCurrent(); if (ch == '{') { lexer.next(); ((JSONLexerBase) lexer).token = JSONToken.LBRACE; } else if (ch == '[') { lexer.next(); ((JSONLexerBase) lexer).token = JSONToken.LBRACKET; } else { lexer.nextToken(); } }
|
比较重要的是parse中调用的parse方法,在parse里面会先创建一个空的JSONObject
然后再调用parseObject
1 2 3 4 5 6 7 8 9 10
| public Object parse(Object fieldName) { final JSONLexer lexer = this.lexer; switch (lexer.token()) { case LBRACE: JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField)); return parseObject(object, fieldName); } }
|
在parseObject会先去获取key,也就是**@type**,会对其对应的值进行类加载操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public final Object parseObject(final Map object, Object fieldName) { try { boolean setContextFlag = false; for (;;) { Object key; if (ch == '"') { key = lexer.scanSymbol(symbolTable, '"'); lexer.skipWhitespace(); ch = lexer.getCurrent(); if (ch != ':') { throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key); } } } } finally { this.setContext(context); } }
|
如果匹配到**@Type,首先会利用TypeUtils.loadClass**去加载这个类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public final Object parseObject(final Map object, Object fieldName) { try { boolean setContextFlag = false; for (;;) { if (key ==JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) { String typeName = lexer.scanSymbol(symbolTable, '"'); Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader()); ObjectDeserializer deserializer = config.getDeserializer(clazz); return deserializer.deserialze(this, clazz, fieldName); } } } finally { this.setContext(context); } }
|
主要是来看一下getDeserializer,经过一系列的调用
getDeserializer#createJavaBeanDeserializer -> createJavaBeanDeserializer#JavaBeanInfo.build
创建了个derializer
跟进到bulid方法中,最主要的是后面的几个for方法
其作用是构建JavaBeanInfo对象,用于描述Java类的结构信息,包括字段、方法、构造函数等。
在setter的循环中,如果添加到fieldList后,同时符合的getter也不会存进去
1 2 3 4 5 6 7 8 9 10 11 12
| public static JavaBeanInfo build(Class<?> clazz, Type type, PropertyNamingStrategy propertyNamingStrategy) { for (Method method : methods) for (Field field : clazz.getFields()) for (Method method : clazz.getMethods()) return new JavaBeanInfo(clazz, builderClass, defaultConstructor, null, null, buildMethod, jsonType, fieldList); }
|
总结一下setter、getter需要符合的条件有哪些
1 2 3 4
| 长度大于4 非静态函数 返回类型为void或当前类 参数个数为1个
|
1 2 3 4
| 长度大于4 非静态方法 无参数 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong
|
而且在创建derializer之前,反射加载的类会对类进行黑名单检查,在1.2.24后面的版本中会使用到
1 2 3 4 5 6 7 8 9 10 11 12
| public ObjectDeserializer getDeserializer(Class<?> clazz, 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); } } }
|
调用setter方法
之后一路返回到parseObject中
调用setter方法主要是在deserialze中
一路走到com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze方法中的**fieldDeser.setValue(object, fieldValue)**方法
在package com.alibaba.fastjson.parser.deserializer.FieldDeserializer#setValue中
有个method.invoke(object, value)调用了setter方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public final Object parseObject(final Map object, Object fieldName) { try { boolean setContextFlag = false; for (;;) { if (key ==JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) { ObjectDeserializer deserializer = config.getDeserializer(clazz); return deserializer.deserialze(this, clazz, fieldName); } } } finally { this.setContext(context); } }
|
调用getter方法
我们可以看到在上面的例子中,不只是调用了setter,还调用了getter
具体是在最开始调用parseObject的地方,有个toJSON方法中调用了getter方法
1 2 3 4 5 6 7
| public static JSONObject parseObject(String text) { Object obj = parse(text); if (obj instanceof JSONObject) { return (JSONObject) obj; } return (JSONObject) JSON.toJSON(obj); }
|
一路走到toJSON,在toJSON中调用了getObjectWriter方法
主要是用来根据指定的类信息获取对应的ObjectSerializer对象
先通过缓存查找,若找不出,在根据ObjectSerializerFactory工厂对象中查找对应的ObjectSerializer实例
1 2 3 4 5 6 7 8
| public static Object toJSON(Object javaObject, SerializeConfig config) { ObjectSerializer serializer = config.getObjectWriter(clazz);
}
|
接着往下有个if判断,主要关注try中的getFieldValuesMap方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public static Object toJSON(Object javaObject, SerializeConfig config) { if (serializer instanceof JavaBeanSerializer) { JavaBeanSerializer javaBeanSerializer = (JavaBeanSerializer) serializer; JSONObject json = new JSONObject(); try { Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject); for (Map.Entry<String, Object> entry : values.entrySet()) { json.put(entry.getKey(), toJSON(entry.getValue())); } } catch (Exception e) { throw new JSONException("toJSON error", e); } return json; } }
|
在getFieldValuesMap中的for循环里,调用了getPropertyValue方法
1 2 3 4 5 6 7 8 9
| public Map<String, Object> getFieldValuesMap(Object object) throws Exception { Map<String, Object> map = new LinkedHashMap<String, Object>(sortedGetters.length); for (FieldSerializer getter : sortedGetters) { map.put(getter.fieldInfo.name, getter.getPropertyValue(object)); } return map; }
|
在getPropertyValue方法中又调用了get方法
1 2 3 4
| public Object getPropertyValue(Object object) throws InvocationTargetException, IllegalAccessException { Object propertyValue = fieldInfo.get(object); }
|
跟进去get方法后可以看到,利用invoke调用了getter方法
1 2 3 4 5 6 7
| public Object get(Object javaObject) throws IllegalAccessException, InvocationTargetException { if (method != null) { Object value = method.invoke(javaObject, new Object[0]); return value; } return field.get(javaObject); }
|
0x03 反序列化漏洞
根据前面的分析,可以知道parse会调用setter方法,parseObject会调用setter和getter方法,并且都会调用构造函数
由于存在autoType特性,所以说
如果**@type中标识的类里的构造函数、setter和getter存在恶意代码,那么就可能存在fastjson**反序列化漏洞
Demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class CalcTest { public String calc;
public CalcTest() { 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"); } }
|
1 2 3 4 5 6
| public class FastjsonTest { public static void main(String[] args) { String JsonCalc = "{\"@type\":\"org.example.CalcTest\",\"Calc\":\"kkk\"}"; JSON.parseObject(JsonCalc); } }
|
成功执行了setter中的代码
0x04 总结
根据上面的分析可以知道,我们只要找到合适的JavaBean,并且满足以下两点就有可能存在漏洞点
- 该类的构造函数、setter方法、getter方法中的某一个存在危险操作,比如造成命令执行
- 可以控制该漏洞函数的变量(一般就是该类的属性)
0x05 参考资料
https://goodapple.top/archives/832
https://drun1baby.top/2022/08/04/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Fastjson%E7%AF%8701-Fastjson%E5%9F%BA%E7%A1%80/#Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-Fastjson-%E7%AF%87-01-Fastjson-%E5%9F%BA%E7%A1%80
https://meizjm3i.github.io/2019/06/05/FastJson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E8%A7%A3%E6%9E%90%E6%B5%81%E7%A8%8B/