☑️Exploitation

Database Enumeration

Version

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.

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

Previously we tried @@version and it gave us 10.3.22-MariaDB-1ubuntu1. The MariaDB DBMS is very similar to MySQL.

Now we know, to pull of information using UNION SELECT, we need to properly form our SELECT queries. And for that 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.

But, the INFORMATION_SCHEMA Database is a different database than the one we are injecting queries to. 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;

INFORMATION_SCHEMA.SCHEMATA

The table in the INFORMATION_SCHEMA Database that we need to look for is the SCHEMATA table. It 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.

Listing Databases

Previously we saw that column 1 is hidden and it starts from column 2. So we can inject the SCHEMA_NAME column in column 2.

We can also print the current database that the web app is using, with the database() function.

Listing Tables

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.

We find that there are two databases and one of them is the dev database which looks juicy. We can use the following payload to find the tables within the dev database:

Dumping 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:

The table has two columns named username and password. We can use this information and dump data from the table.

Dumping 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

In addition to gathering data from various tables and databases within the DBMS, a SQL Injection can also be leveraged to perform many other operations, such as reading and writing files on the server and even gaining remote code execution on the back-end server.

Privileges

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.

Current 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:

So our UNION Injection payload will be as one of the following:

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:

grantee and privilege_type columns in the user_privileges table of information_schema database.

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

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

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:

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].

The source code shows us the entire PHP code, which could be inspected further to find sensitive information like database connection credentials or find more vulnerabilities.

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.

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

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:

But we can only use UNION injection with SELECT statement. So we have to look for the variable in the INFORMATION_SCHEMA database again. 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.

So this will be the final SQL query to inject:

And with UNION:

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.

Writing Table Content To Files

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:

Writing Strings To Files

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

This will create the file test.txt with the owner being MySQL!

Tip: Advanced file exports utilize the 'FROM_BASE64("base64_data")' function in order to be able to write long/advanced files, including binary data.

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.

So once we write a file into the webroot location with the following command for example:

We dont see any errors which means we were able to write successfully. And to test it we can simply go to http://site/proof.txt and it should just say 'file written successfully'

Writing A Web Shell Into Root Directory

Now that we know we can write into the web root directory we can write a web shell.

This will save the shell in /shell.php file. We can run any command passed to the 0 parameter. For example with ?0=id in our URL:

Last updated