本文主要针对FastJson的反序列化过程进行详细分析,以及poc案例分析,留作学习笔记,以供日后复习
前言:网上关于FastJson的分析文章一大片,本文只是笔者在实践操作中理解的一些东西,不算特别详细,留作日后复习,欢迎一起交流
什么是FastJson?
Fastjson是一个由阿里巴巴维护的一个json库。它采用一种“假定有序快速匹配”的算法,是号称Java中最快的json库。
0x01 反序列化分析
先来看看一张反序列化流程图
可以看到其主要的功能都是在DefaultJSONParser类中实现的,在这个类中会应用其他的一些外部类来完成后续操作。ParserConfig主要是进行配置信息的初始化,JSONLexer主要是对json字符串进行处理并分析,反序列化在JavaBeanDeserializer中处理。
本文中会先通过一些简单的案例来帮助理解FastJson在反序列化数据时都做了哪些事情。
先来创建一个类,用于反序列化
package demo2;
import java.util.Properties;
public class User {
private int age;
private String name;
private Properties properties;
public int getAge() {
System.out.println("getAge()");
return age;
}
public String getName() {
System.out.println("getName");
return name;
}
public void setName(String name) {
System.out.println("setName");
this.name = name;
}
public Properties getProperties() {
System.out.println("getProperties()");
return properties;
}
@Override
public String toString() {
return "User{" +
"age=" + age +
", name='" + name + '\'' +
", properties=" + properties +
'}';
}
}
然后再来创建一个测试类
package demo2;
import com.alibaba.fastjson.JSON;
public class Demo1 {
public static void main(String[] args) {
//String strU = "{\"@type\" : \"demo2.User\", \"age\" : 18, \"name\" : \"kevin\", \"properties\" : {}}";
String strU = "{\"age\" : 18, \"name\" : \"kevin\", \"properties\" : {}}";
Object obj = JSON.parse(strU);
System.out.println(obj);
}
}
无@type分析
我们先来看看默认不加@type的情况下,parse是如何解析字符串的
首先在11行打一个断点,如图
然后debug模式启动,进入到parse方法,如图
因为一开始只传了一个参数,所以调用重载的parse方法,F7点进去
一开始text不为null,进入else方法。可以看到这里创建了一个DefaultJSONParser对象,这里其实做了很多事情,我们一个一个来看,之前我的很大一部分困惑也就是在这里解开的。
先通过F7进入ParserConfig.getGlobalInstance()方法,看看里面做了什么操作
可以看到这里返回了一个global对象,global对象就是new ParserConfig()对象,我们看看对象实例化时做了哪些操作。
可以看到在无参的构造方法里调用了另外一个构造方法,我们再来看看另外一个构造方法做了什么操作。
可以看出来,这一步主要就是为ParserConfig对象的成员变量初始化,第二张图就是将相应类型的类和相对应的反序列化器绑定在一起,通过key/value的形式。个人感觉理解这里的操作对后面的过程会比较重要,不然会有点懵。
然后回到JSON.class文件的else判断,我们再进入DefaultJSONParser的构造函数,看看做了哪些操作
这里做的操作就多了点,我们一步一步来分析,首先明白input变量就是我们传入parse方法的值,然后我们进入JSONScanner构造函数,传入了input和features
可以看到我们进入了JSONScanner.class类中,这里的features不是我们关注的重点,主要看构造函数里做了什么。
先是给this.text成员变量赋值为input,也就是我们需要反序列化的值,然后取出长度赋值给this.len,这里的this.bp是父类的成员变量,我门再JSONScanner类中是看不到定义的,按住Ctrl,然后鼠标左键就可以点进去
可以看到是在JSONLexerBase.class类文件中定义的,还有ch变量也是都是我们后面会用到的
回到JSONScanner.class往下走,可以看到调用了this.next(); 我们F7跟进去看看
可以看到,先是将this.bp+1然后赋值给index变量,之后通过判断给this.ch赋值并返回,如果index >= this.len,也就是>=我们反序列化的值的长度,返回\u001a,说明读到末尾了,不再继续读了。否则的话,返回this.text.charAt(index),根据index取出text字符串中指定位置的字符并返回。
不难理解,其实每一次调用next()就意味着,遍历获取字符串中的值,然后将这个值返回给this.ch,用于作比较。
然后我们回到DefaultJSONParser.class,进入另外一个构造函数
可以看到,核心的初始化都是在这里完成的,这里的int ch = lexer.getCurrent(),我们跟进去看一下
可以看到ch的值就是我们反序列化字符串的第一位字符,是通过JSONScanner.class类中的next()方法获取返回的,我们刚刚也分析过。因为JSONScanner继承自JSONLexerBase,所以给this.ch赋值也就等同于给JSONLexerBase的ch变量赋值
然后往下走,因为ch == “{”,所以会进入第一个if判断,获取下一个字符,设置token为12,tonken就是在这里初始化赋值的。
回到创建对象的地方,往下走,我们来看看创建的DefaultJSONParser对象
可以看到里面的值都已经初始化好了,这里有一点漏讲了,就是symbolTable中的symbols对象里面的数据也是初始化的时候赋值的,小细节。
然后调用DefaultJSONParser#parse()方法,跟进去
将JSONScanner对象赋值给lexer变量,然后switch判断token,通过前面分析已知token为12,所以会进入case 12:里面
我们接着跟进构造方法
可以看到通过构造方法给JSONObject成员变量this.map赋值为HashMap对象,初始大小为16
回到DefaultJSONParser.class,调用了this.parseObject((Map)object, fieldName); 跟进去
因为token为12,所以会进入else方法
这里主要还有一个知识点要讲,就是lexer.scanSymbol(this.symbolTable, ‘"’); 这个方法主要作用是做字符截取的操作,截取"key"当中的key,添加并返回key,我们跟进去看看
这里主要就是循环截取要反序列化的字符,直到截取到",然后会进入addSymbol方法面对我们传入的字符进行截取,取出"key",中的key值。
我们跟进去
可以很清楚的看到,根据前面记录的下标位置,截取buffer字符串,也就是获取age字符串然后返回。
后面但凡是调用lexer.scanSymbol(this.symbolTable, ‘"’),都是做这样的处理,截取"中间的值"。
可以看到value的值为age,后面是一样的操作,就不重复截图了。然后会将value的值也取出来,最后存放到map集合中,也就是JSONObject对象中
直到遇到"}",表示到结尾了,会将JSONObject对象返回
最后会回到JSON#parse方法中,返回封装后的JSONObject对象
至此,不加@type情况下的反序列化解析就分析完了,看控制台输出
加@type分析
也是一样debug,进入parse方法
初始化我们前面讲过了,就不多说了,接着往下走,进入parser.parse()方法
这时会调用DefaultJSONParser#parseObject,跟进去
这里会对字符串进行截取,按照上面分析的"@type",通过lexer.scanSymbol(this.symbolTable, ‘"’); 将@type截取出来,作为key,接着往下看
往下会有一个判断,因为key == JSON.DEFAULT_TYPE_KEY,所以会进入该方法,通过同样的截取字符串的方式,将"demo2.User"中的demo2.User截取出来,赋值给ref,这也就是我们可控的类。
然后会调用TypeUtils.loadClass(ref, this.config.getDefaultClassLoader()); 通过类加载器加载类并返回对应的类对象,我们跟进去看看
可以看到经过一些列判断,最后会进入else方法,然后通过当前线程获取类加载器,加载我们指定的类,最后返回类对象,这里其实就是后期绕过autoType的地方,不过现在我们不深究。
然后我们回到DefaultJSONParser#parseObject
可以看到这里会调用ParseConfig#getDeserializer方法,根据给定类型返回指定的反序列化器,我们跟进去看看
这里时根据type类型,去this.derializers.get(type) 中取相对应的反序列化器,如果没有,返回null,这个this.derializers 我们上面有分析过,将指定类型作为key,指定类型的反序列化器作为value存到了IdentityHashMap集合中。
接着往下走
因为type类型为Class,所以进入方法,调用this.getDeserializer((Class)type, type),我们继续跟进去
程序一路向下,进入到else,因为className为demo2.User所以不会退出,接着往下
又经过了一些列类型判断,因为都不匹配,所以会走到else,调用this.createJavaBeanDeserializer(clazz, (Type)type),我们跟进去看看
前面一些不重要的步骤跳过了,因为asmEnable为true,所以会进入if方法,调用JavaBeanInfo.build(clazz, type, this.propertyNamingStrategy),这个方法也很关键,封装成员变量和方法等操作都是在这个方法里完成的,我们得跟一下
可以看到这里通过反射将我们传入的clazz,也就是demo2.User的类对象中的所有成员变量和所有public的方法都获取了出来,继续往下看
这里就是通过循环将所有的方法都取出来,进行相应的判断,符合条件的才会被添加到List fieldList = new ArrayList() 这个list集合中,留作后续处理,这里应该也就是网上分析最多的一个地方了吧。
我们把代码拷贝出来,看一看完整的判断是什么样的
这个不难理解,就不过多分析,从我们给出的测试类来看,满足条件的只有public void setName(String name)方法,因为方法名称大于4,非静态方法且返回值类型为void,其他的几个方法在这个判断中是不成立的,所以不会进入该方法。
可以看到和我们分析的一样,setName进入了if方法,接着往下走
可以看到这里还有个判断,如果方法名以set开头,则进入该方法
接着往下会看到,通过substring截取,将Name的N转换为小写n,然后截取n后面的字符,就是提取属性名,赋值给propertyName。
然后调用TypeUtils.getField(clazz, propertyName, declaredFields),我们跟进去
可以看到通过遍历成员变量,和我们传入的名称做判断,如果相匹配则返回名称相对应的成员变量对象
再往下我们可以看到,field已经赋值为相对应的成员变量对象了,然后调用add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, fieldAnnotation, (String)null)) 方法,将这些类里面的信息都存入fieldList中,我们先跟到FieldInfo构造方法中
可以看到在构造方法里面给成员变量赋值,可以看到有哪些参数,接着往下
可以看到这里进行了一些访问权限设置,然后回到for循环
可以看到封装好的FieldInfo对象里面包含哪些对象
之后会继续循环判断,知道方法都判断完,然后会往下走,还有一个for循环和一个判断
也是一样拷贝出来,分析一下
和前面的判断有点小区别,可以看出这个主要是针对get方法的,我们的测试类中,符合条件的只有public Properties getProperties() 方法,非静态,方法名长度大于4且第四个字符为大写,无形参,最后返回值类型是属于Map,因为Properties extends Hashtable -> Hashtable implements Map,所以满足要求
可以看到进到了if方法里,接着往下走
可以看到也是一样的操作,对数据进行封装,添加到fieldList集合中
可以看到也是和name是一样的,后面的循环就不截图了,可以直接跳出方法回到beanInfo = JavaBeanInfo.build(clazz, type, this.propertyNamingStrategy) 方法
可以看到返回的beanInfo对象,封装了类中的一些成员变量和方法对象
接着往下走,会在这里创建一个JavaBeanDeserializer对象,我们跟进去
这里的this.sortedFieldDeserializers我们要留一下,它是一个FieldDeserializer[]数据对象,我们后面会用到。
这里就是通过遍历JavaBeanInfo对象中的成员变量sortedFields数组中的FieldInfo对象,使用config.createFieldDeserializer(config, beanInfo, fieldInfo) 创建FieldDeserializer对象,之后存入this.sortedFieldDeserializers数组中,以供后续使用
可以看到返回的是一个DefaultFieldDeserializer对象
回到ParseConfig#getDeserializer,可以看到返回的是JavaBeanDeserializer对象
再回到DefaultJSONParser#parseObject 方法,可以看到调用了deserializer.deserialze(this, clazz, fieldName),继续跟进去
由于这段代码实在是又臭又长,所以截取核心功能代码
这个方法是通过反射将属性的值设置到相应的对象上,我们跟进去
这里也有个小知识点,this.smartMatch(key) 方法会对key做一些处理,我们跟进去看一下
可以看到如果key的第一个字符为 _ 或者 - ,那么将会被替换为空,这也就解释了为什么有些payload会在变量名上加一个 _ 了,加不加都不受影响
可以看到这里也调用了一次parseField(parser, object, objectType, fieldValues)方法,这里的Object为我们指定反序列化的User类,成员变量还没赋值,跟进去
可以看到是根据特定的类型获取的特定反序列化器,因为name是String类型,所以获取的就是StringCodec
继续往下走
到了关键的一步,我们跟进去
先是做了不为空和类权限判断,然后获取方法对象,因为前面条件都不满足,所以进入else,通过反射调用User对象的setName方法,给成员变量赋值
可以看到已经成功为name变量赋值,并且触发了setName方法
然后我们再来看看properties的赋值
跟进去,直接进入核心方法
因为getProperties方法的返回值符合这条判断,所以会进入该方法,通过反射执行getProperties方法,这也就解释了TemplatesImpl反序列化利用链是如何触发的了。
可以看到触发了getProperties方法
最后反序列化利用链我有时间再写吧,太晚了,其实能把这一套看懂,反序列化利用链也就没什么问题了,就是解析时触发了特定的方法而已
完结撒花
最后附上借鉴的博客
Fastjson 流程分析及RCE分析
FastJson 反序列化学习
可能还有一些,没记录,学习就是这样,付出越多回报也才越多!
免责声明:本站提供安全工具、程序(方法)可能带有攻击性,仅供安全研究与教学之用,风险自负!
转载声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
订阅查看更多复现文章、学习笔记
公众号:伟盾网络安全
专注网络安全,用心做好安全这件事。
个人博客:博客文章来源:https://www.toymoban.com/news/detail-464353.html
个人知乎:知乎文章来源地址https://www.toymoban.com/news/detail-464353.html
到了这里,关于FastJson反序列化分析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!