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:
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
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.
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 lowercasep
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.
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:
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:
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.
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.
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.
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:
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:
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:
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:
The finalĀ CREATE TABLE
Ā query will be as follows:
SQL Statements
INSERT
TheĀ INSERTĀ statement is used to add new records to a given table. The statement following the below syntax:
The syntax above requires the user to fill in values for all the columns present in the table.
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:
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:
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:
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:
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:
The query above will select data present in column1 and column2 only.
DROP
We can useĀ DROPĀ to remove tables and databases from the server.
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
:
To rename a column, we can useĀ RENAME COLUMN
:
We can also change a columnās datatype withĀ MODIFY
:
Finally, we can drop a column usingĀ DROP
:
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:
We specify the table name, each column and its new value, and the condition for updating records. Let us look at an example:
Query Results
Sorting Results
We can sort the results of any query usingĀ ORDER BYĀ and specifying the column to sort by:
By default, the sort is done in ascending order, but we can also sort the results byĀ ASC
Ā orĀ DESC
:
It is also possible to sort by multiple columns, to have a secondary sort for duplicate values in one column:
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:
If we wanted to LIMIT results with an offset, we could specify the offset before the LIMIT count:
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:
The query above will return all records which satisfy the given condition. Let us look at an example:
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:
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
:
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
:
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:
The result of theĀ AND
Ā operation isĀ true
Ā if and only if bothĀ condition1
Ā andĀ condition2
Ā evaluate toĀ true
:
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
:
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ā:
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:
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:
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
:
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:
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:
Payload | URL 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
:
The final query should be as follow:
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:
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.
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:
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:
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:
We can inject aĀ UNION
Ā query into the input, such that rows from another table are returned:
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Ā UNION
ing 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:
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:
This query would return:
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
Now we can find the version:
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
:
Payload | When to Use | Expected Output | Wrong Output |
---|---|---|---|
SELECT @@version | When we have full query output | MySQL 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 output | 1 | Error with other DBMS |
SELECT SLEEP(5) | Blind/No Output | Delays 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:
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:
Now, letās do the same using aĀ UNION
Ā SQL injection, with the following payload:
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:
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:
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:
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:
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:
OurĀ UNION
Ā injection payload will be as follows:
or:
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:
Once again, we can use the following payload with the above query:
If we had many users within the DBMS, we can addĀ WHERE user="root"
Ā to only show privileges for our current 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:
From here, we can addĀ WHERE grantee="'root'@'localhost'"
Ā to only show our current userĀ root
Ā privileges. Our payload would be:
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:
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:
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
.
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:
- User withĀ
FILE
Ā privilege enabled - MySQL globalĀ
secure_file_priv
Ā variable not enabled - 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:
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:
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ā:
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:
If we go to the back-end server andĀ cat
Ā the file, we see that tableās content:
It is also possible to directlyĀ SELECT
Ā strings into files, allowing us to write arbitrary files to the back-end server.
When weĀ cat
Ā the file, we see that text:
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:
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:
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:
We can reuse our previousĀ UNION
Ā injection payload, and change the string to the above, and the file name toĀ shell.php
:
The output of theĀ id
Ā command confirms that we have code execution and are running as theĀ www-data
Ā user.