NEW AND IMPROVED UPDATE: Cleaning Up After a SQL Injection Attack, Part 2
[UPDATE: Added code to deal with replacing text in the ntext fields of SQL Server 2000.]
One of our clients got hit with a web attack a week or so ago. We're still not quite sure how this particular attack was carried out -- we're thinking an unpatched web server at the hosting facility -- but it did cause me to look at the log file of the web site to see who might have been able to overwrite index.htm in the root directory. (The FTP logs held the clue -- a rogue in Asia who cracked the password.)
As I said, it turned up nothing, but I did see a series of SQL Injection attacks -- none of which were successful (always check your variables, kids!) -- but they piqued my interest, so I took it apart.
I'm not sure if there's any way to discuss this in-depth without revealing the code. Revealing the code is a double-edged sword. I'd like people to be able to find this via the search engines in case they've been hit with it; but at the same time, I'd hate to see people use this to further spread malice... but I don't think this code is all that unqiue, or all that new, really...
A Study of An SQL Injection
In the log was the following line (IPs changed to protect the innocent and not-so-innocent):
Fascinating. It's pretty obvious that they're trying to inject some SQL as part of the URL. It's the standard trick...
So, I copied the querystring into my favorite text editor (that'd be TextPad) and broke out the querystring to this:
DO NOT RUN THIS CODE! IT'S DANGEROUS!
(OK, I changed the code a little to make it a bit more benign and so that it would fail if you pasted it into QA and ran it.)
I pasted that into Query Analyzer and pointed QA against a dummy database, so if I screwed up, I wasn't going to hurt anything...
I then changed the EXEC statement to a PRINT statement, so I could see what that big CAST statement was doing, and lo and behold a little bit of T-SQL code popped out.
In a nutshell, the code queries sysobjects for all the user tables in the database (xtype = 'u') and throws the table info into a cursor, and then it loops thru the cursor, checking on fields that it can append it's evilness onto -- namely, text, ntext, varchar and sysname columns.
(Running select xtype, name from systypes; which basically contains a list of available sql datatypes, and I compared them against the b.xtype values in the demon code.)
Here's the code as disassembled:
DECLARE @T VARCHAR(255),@C VARCHAR(255) DECLARE Table_Cursor CURSOR FOR SELECT a.name,b.name FROM sysobjects a,syscolumns b WHERE a.id=b.id AND a.xtype='u' AND (b.xtype=99 OR b.xtype=35 OR b.xtype=231 OR b.xtype=167) OPEN Table_Cursor FETCH NEXT FROM Table_Cursor INTO @T,@C WHILE(@@FETCH_STATUS=0) BEGIN PRINT('UPDATE ['+@T+'] SET ['+@C+']=RTRIM(CONVERT(VARCHAR(4000),['+@C+']))+''''') FETCH NEXT FROM Table_Cursor INTO @T,@C END CLOSE Table_Cursor DEALLOCATE Table_Cursor
Again, I defanged the routine by changing it to PRINT, so I can see what it spits out...
It spits out a whole pile of UPDATE statements, affecting every field of every table it found of the applicable types. (In practice, it wouldn't print the UPDATE statements, it would actually, you know, execute them...)
UPDATE [InProcessOrders] SET [StatusMessage]=RTRIM(CONVERT(VARCHAR(4000),[StatusMessage]))+''UPDATE [Handhelds] SET [RecKey]=RTRIM(CONVERT(VARCHAR(4000),[RecKey]))+'' UPDATE [Handhelds] SET [RecName]=RTRIM(CONVERT(VARCHAR(4000),[RecName]))+''
... and so forth.
But we can see that it appends its malicious SCRIPT tag at the end of every field in the hopes that it will someday be displayed unfettered on a webpage, where its payload can be hidden in an IFRAME.
Cleaning Up The Mess
So now you have a database that's infected with the evil code at the end of every data field. To get rid of it, you need to re-run the code, but with a replace statement instead of an appending of the field.
NOTE: If you have more than 4000 characters in a data field, go for your backup, because the malicious script only grabs the first 4000 characters and then appends itself; so this solution will leave you with truncated fields. If your fields are not over 4000 characters, you should be OK.
So if we take the disassembled code and just edit it just a little bit... change the UPDATE statement so that is REPLACES the ill-gotten script block with nothing, it's like the script block was never there. (Except in the aforementioned cases where the original data was over 4000 characters...)
DECLARE @T VARCHAR(255),@C VARCHAR(255) DECLARE Table_Cursor CURSOR FOR SELECT a.name,b.name FROM sysobjects a,syscolumns b WHERE a.id=b.id AND a.xtype='u' AND (b.xtype=35 OR b.xtype=231 OR b.xtype=167) OPEN Table_Cursor FETCH NEXT FROM Table_Cursor INTO @T,@C WHILE(@@FETCH_STATUS=0) BEGIN PRINT ('UPDATE ['+@T+'] SET ['+@C+']=REPLACE(['+@C+'],'''', '''')') FETCH NEXT FROM Table_Cursor INTO @T,@C END CLOSE Table_Cursor DEALLOCATE Table_Cursor DECLARE Table_Cursor CURSOR FOR SELECT a.name,b.name FROM sysobjects a,syscolumns b WHERE a.id=b.id AND a.xtype='u' AND b.xtype=99 OPEN Table_Cursor FETCH NEXT FROM Table_Cursor INTO @T,@C WHILE(@@FETCH_STATUS=0) BEGIN PRINT ('UPDATE ['+@T+'] SET ['+@C+']=cast(replace(cast(['+@C+'] as nvarchar(4000)),'''','''') as ntext)') FETCH NEXT FROM Table_Cursor INTO @T,@C END CLOSE Table_Cursor DEALLOCATE Table_Cursor
And there you have it. If you edit the code so that the bad URL you're trying to erase is in it (as opposed to my bogus evilsite.evl URL) It will generate all the SQL statements you need and then you can run them against your database.
(Of course, you can change the PRINT statement for some other statement that might do the trick...)
An Ounce of Prevention
Of course, the best way to protect yourself is to not allow the SQL Injection attack to occur in the first place. These attacks failed against our client's site because we tested to make sure the variables we were accepting via the URL were numbers. Since there were alphabetic characters in there, the page threw an ugly error and failed to render. (In these cases throwing an ugly error is fine, since I don't think anyone is really is looking at your pages.)
- Constrain and sanitize input data. Check for known good data by validating for type, length, format, and range.
- Use type-safe SQL parameters for data access. You can use these parameters with stored procedures or dynamically constructed SQL command strings. Parameter collections such as SqlParameterCollection provide type checking and length validation. If you use a parameters collection, input is treated as a literal value, and SQL Server does not treat it as executable code. An additional benefit of using a parameters collection is that you can enforce type and length checks. Values outside of the range trigger an exception. This is a good example of defense in depth.
- Use an account that has restricted permissions in the database. Ideally, you should only grant execute permissions to selected stored procedures in the database and provide no direct table access.
- Avoid disclosing database error information. In the event of database errors, make sure you do not disclose detailed error messages to the user.
Wikipedia has a good breakdown of what SQL Injection is.