The events involved all occur within a single HTTP
request and response, as follows:
The attacker submits some crafted input in an HTTP request.
The application processes the input, causing the attacker's injected SQL query to execute.
If applicable, the results of the query are returned to the attacker in the application's response to the request.
A different type of SQL injection attack is “second-order” SQL injection. Here, the sequence of events is typically as follows:
The attacker submits some crafted input in an HTTP request.
The application stores that input for future use (usually in the database), and responds to the request.
The attacker submits a second (different) request.
To
handle the second request, the application retrieves the stored input
and processes it, causing the attacker's injected SQL query to execute.
If applicable, the results of the query are returned to the attacker in the application's response to the second request.
Second-order SQL
injection is just as powerful as the first-order equivalent; however, it
is a subtler vulnerability which is generally more difficult to detect.
Second-order SQL
injection usually arises because of an easy mistake that developers make
when thinking about tainted and validated data. At the point where
input is received directly from users, it is clear that this input is
potentially tainted, and so clued-in developers will make some efforts
to defend against first-order SQL injection, such as doubling up single
quotes or (preferably) using parameterized queries. However, if this
input is persisted and later reused, it may be less obvious that the
data is still tainted, and some developers make the mistake of handling
the data unsafely at this point.
When creating a contact, the user can enter details such
as name, e-mail, and address. The application uses an INSERT
statement to create a new database entry for the contact, and doubles
up any quotation marks in the input to prevent SQL injection attacks
(see Figure 1).
The application also allows users to modify selected details about an
existing contact. When a user modifies an existing contact, the
application first uses a SELECT
statement to retrieve the current details about the contact, and holds
the details in memory. It then updates the relevant items with the new
details provided by the user, again doubling up any quotation marks in
this input. Items which the user has not updated are left unchanged in
memory. The application then uses an UPDATE statement to write all of the in-memory items back to the database (see Figure 2).
Let's assume that the
doubling up of quotation marks in this instance is effective in
preventing first-order SQL injection. Nevertheless, the application is
still vulnerable to second-order attacks. To exploit the vulnerability,
you first need to create a contact with your attack payload in one of
the fields. Assuming the database is Microsoft SQL Server, create a
contact with the following name:
The quotes are doubled up in your input, and the resultant INSERT statement looks like this:
INSERT INTO tblContacts VALUES ('a''+@@version+''a', '[email protected]',…
Hence, the contact name is safely stored in the database, with the literal value that you submitted.
Then, you need to go to the
function to update the new contact, and provide a new value in the
address field only (any accepted value will do). When you do this, the
application will first retrieve the existing contact details, using the
following statement:
SELECT * FROM tblUsers WHERE contactId = 123
The retrieved details
are stored briefly in memory. The value retrieved for the name field
will, of course, be the literal value that you originally submitted,
because this is what was stored in the database. The application
replaces the retrieved address in memory with the new value you
supplied, taking care to double up quotation marks. It then performs the
following UPDATE statement to store the new information in the database:
UPDATE tblUsers
SET name='a'+@@version+'a', address='52 Throwley Way',…
WHERE contactId = 123
At
this point, your attack is successful and the application's query is
subverted. The name retrieved from the database is handled unsafely, and
you are able to break out of the data context within the query and
modify the query's structure. In this proof-of-concept attack, the
database version string is copied into the name of your contact, and
will be displayed on-screen when you view the updated contact details:
Name: aMicrosoft SQL Server 7.00 – 7.00.623 (Intel X86) Nov 27 1998
22:20:07 Copyright (c) 1988–1998 Microsoft Corporation Desktop
Edition on Windows NT 5.1 (Build 2600: )a
Address: 52 Throwley Way
To perform a more effective attack, you would need to use the general techniques already described for injecting into UPDATE statements, again placing your attacks into one contact field and then updating a different field to trigger the vulnerability.
Finding Second-Order Vulnerabilities
Second-order SQL
injection is more difficult to detect than first-order vulnerabilities,
because your exploit is submitted in one request and executed in the
application's handling of a different request. The core technique for
discovering most input-based vulnerabilities, where an individual
request is submitted repeatedly with various crafted inputs and the
application's responses are monitored for anomalies, is not effective in
this instance. Rather, you need to submit your crafted input in one
request, and then step through all other application functions which may
make use of that input, looking for anomalies. In some cases, there is
only one instance of the relevant input (e.g., the user's display name),
and testing each payload may necessitate stepping through the
application's entire functionality.
Today's
automated scanners are not very effective at discovering second-order
SQL injection. They typically submit each request numerous times with
different inputs, and monitor the responses. If they then crawl other
areas of the application and encounter database error messages, they
will draw them to your attention, hopefully enabling you to investigate
and diagnose the issue. But they are not capable of associating an error
message returned in one location with a piece of crafted input
submitted in another. In some cases, there is no error message, and the
effects of the second-order condition may be handled blindly. If there
is only a single instance of the relevant persisted item, or persisting
it within the application requires multiple steps (e.g., a user
registration process), the problem is compounded further. Hence, today's
scanners are not able to perform a rigorous methodology for discovering
second-order vulnerabilities.
Without an understanding
of the meaning and usage of data items within the application, the work
involved in detecting second-order SQL injection grows exponentially
with the size of the application's functionality. But human testers can
use their understanding of that functionality, and their intuition about
where mistakes are often made, to reduce the size of the task. In most
cases, you can use the following methodology to identify second-order
vulnerabilities:
After
you have mapped out the application's content and functionality, review
it, looking for any items of user-controllable data that are persisted
by the application and reused in subsequent functions. Work on each item
individually, and perform the following steps on each instance.
Submit
a simple value within the item that is likely to cause problems if used
unsafely in an SQL query, such as a single quote or an alphanumeric
string with a single quote within it. If required, walk through any
multistage processes (such as user registration) to ensure that your
value is fully persisted within the application.
If
you find that the application's input filters block your input, to try to defeat the front-end input filters.
Walk
through all of the application's functionality where you have seen the
data item being explicitly used, and also any functions where it might
conceivably be implicitly used. Look for any anomalous behavior that may
indicate that the input has caused a problem, such as database error
messages, HTTP 500 status codes, more cryptic error messages, broken
functionality, missing or corrupted data, and so forth.
For
each potential issue identified, try to develop a proof-of-concept
attack to verify that an SQL injection vulnerability is present. Be
aware that malformed persisted data may cause anomalous conditions in
ways that are not directly vulnerable (e.g., integer conversion errors,
or failure of subsequent data validation). Try supplying the same input
with two quotation marks together, and see whether the anomaly goes
away. Try using database-specific constructs such as string
concatenation functions and version banners to confirm that you are
modifying an SQL query. If the anomalous condition is blind (i.e., it
does not return the results of the query or any error message), try
using time delay techniques to verify that a vulnerability is present.
You should be aware
that some second-order SQL injection vulnerabilities are fully blind and
have no discernible effects on the contents of any application
responses. For example, if an application function writes persisted data
to logs in an unsafe manner, and handles any exceptions gracefully, the
steps I just described will probably miss the vulnerability. To detect
these kinds of flaws, you need to repeat the preceding steps using
various inputs in step 1 designed to trigger time delays when used
unsafely in SQL queries, and then monitor all of the application's
functionality for anomalous delays. To do this effectively, you will
need to use syntax that is specific to the type of database being used
and the types of queries (SELECT, INSERT, etc.) being performed. In practice, this may be a very lengthy exercise indeed.
Why Second-Order Bugs Happen
Second-order SQL
injection is surprisingly common. The authors have encountered this
vulnerability in mature, security-critical applications such as those
used by online banks. Bugs such as this can go unnoticed for years,
because of the relative difficulty of detecting them.
Many, perhaps even
most, developers these days have some awareness of SQL injection
threats, and they know how to use parameterized queries to safely
incorporate tainted data into SQL queries. However, they also know that
writing parameterized queries involves a little more effort than
constructing simple dynamic queries. Many also have in mind a mistaken
concept of taint, in which user-supplied data needs to be handled safely
on arrival, but can then be treated as trusted.
A very common approach to
coding SQL queries is to use parameterized queries for data that is most
obviously tainted, such as that which is received from the immediate
HTTP request, and elsewhere to make a judgment in each case as to
whether the data is safe to use in a dynamic query. This approach is
dangerous. It can easily lead to oversights, where tainted data is
handled unsafely by mistake. Data sources that are trustworthy may
become tainted at a future time due to changes elsewhere in the code
base, unwittingly introducing second-order vulnerabilities. And the
mistaken concept of taint, where data needs to be handled safely only on
arrival, can lead to items appearing to be trustworthy when they are
not.
The most robust way to
defend against second-order vulnerabilities is to use parameterized
queries for all database access, and to properly parameterize every
variable data item which is incorporated into the query. This approach
incurs a small amount of superfluous effort for data which is genuinely
trustworthy, but it will avoid the mistakes described. Adopting this
policy also makes security review of code quicker and easier in relation
to SQL injection.
Note that some parts of
SQL queries, such as column and table names, cannot be parameterized,
because they constitute the structure which is fixed when the query is
defined, before data items are assigned to their placeholders. If you
are incorporating user-supplied data into these parts of the query, you
should determine whether your functionality can be implemented in a
different way; for example, by passing index numbers which are mapped to
table and column names server-side. If this is not possible, you should
carefully validate the user data on a whitelist basis, prior to use.