SQL injection used to be a lot easier a few years ago when it was less known, web application security was less mature, and errors were often exposed. It's very easy to use a variety of methods to cause errors to display database names, table names, column names, and even row values... when errors are enabled. These days, the SQL injection flaws that I am finding are largely of the "blind" type. To take a rough guess, I'd estimate this to be the case at least 8 out of 10 times. That is fine because blind SQL injection is still relatively easy to exploit.... with SQL injection in general still being used to greatsuccess in the wild.
There are plenty of SQL Injection tools out there that will work with blind or error-based vulnerabilities. Many of these are installed and ready to run on the BackTrack 4 R2. SQLMap is a good one but there are a lot and your success will vary. These tools can do more than just extract database data. They can get you root. Sometimes this just isn't in the cards for a variety of reasons and you just want to show proof of concept that you can pull back sensitive data through the web server. I love Burp's Intruder tool for this. I'll demonstrate some techniques below and use HacmeBank as a target even though errors are completely visible in this purposefully vulnerable app and blind techniques are not necessary.
The first thing we do is identify the vulnerable request:
We can send this request to the Repeater tool and inject the SQL syntax, " ' waitfor delay '0:0:30'-- " (omit the double quotes). The vulnerable web application will pass this SQL command directly to the login query causing a 30-second pause.
That's all just great but we want to do better than just pause the database during our login query. Let's set the HTTP timeout length from 60 seconds to 29 seconds in Burp's timeout options.
Ok, now we'll send our delay injection request over to the Intruder tool. This time the SQL syntax, " 'if (len(user)=1) waitfor delay '00:00:30'-- ." It's also necessary to now mark our payload position in Intruder. Put it where the "1" is.
Now that the payload position is marked, we need to define the payload. The SQL question is "How long is the USER variable?" Using a numeric payload, we'll guess 1 through 30, a wide margin indeed.
One more thing to do is to set the Intruder threads to 1, otherwise when one thread delays the SQL database, the others will be delayed as well and false positives will abound.
Now we should get the length of the "USER" variable in the SQL server when this Intruder attack is started. When the correct payload number is guessed, the application will pause for 30 seconds, expiring our 29-second Burp timeout value.
At this point a good thing to know is what those 3 characters are that comprise the "USER" variable's length. Just open another Intruder tab and we'll change the attack quite a bit. This time the SQL syntax will be " ' if (ascii(lower(substring((user),1,1)))=100) waitfor delay '00:00:30'-- " and there are actually two changing payloads (highlighted in bold). One is the position of the character and the second is the ASCII decimal code of the character in that position.
The first payload needs to be numeric and range from 1 to 3 since we know that's the number of the character positions whose ASCII codes we want to guess.
For the next payload, we look up our ASCII codes and ponder a bit. 48-126 will get us 0-9, A-Z, and a-z. In our SQL syntax above, you'll notice that we're using the "lower()" function to reduce the number of ASCII code values to guess but our number range will include them anyway so we're not saving any time there. Since we're asking for a username, I doubt there are any special characters (32-47) in it so we'll just be lazy and use 48-126.
Running this attack should yield the three ASCII codes for the SQL "USER" variable. Sorting the results by length will put all of the 0-length, timed out, "true," responses in line.
Reading the timed out (true) values:
- Payload 1 value 1 = payload 2 value 100 which is "d"
- Payload 1 value 2 = payload 2 value 98 which is "b"
- Payload 1 value 3 = payload 2 value 111 which is "o"
Running this attack should produce the ASCII codes for the first table that contains the word "user."
That looks like 102=f, 115=s, 98=b, 95=_, 117=u, 115=s, 101=e, 114=r, 115=s (fsb_users). Right on, so now we want the column names for this "fsb_users" table. For that we need to query the native Microsoft SQL "information_schema.columns" table. To do that, we'll use the following SQL syntax: " ' if (ascii(lower(substring((select top 1 column_name from information_schema.columns where table_name='fsb_users'),1,1)))=100) waitfor delay '00:00:30'-- ." Again we'll use two payloads, one for character position and the other for ASCII decimal code.
Running this attack will enumerate the first column in the "fsb_users" table.
That spells "user_id" which is not very important to me but it does mean that the syntax for the next column in the "fsb_users" table will be, " ' if (ascii(lower(substring((select top 1 column_name from information_schema.columns where table_name='fsb_users' and column_name>'user_id'),1,1)))=100) waitfor delay '00:00:30'-- ."
Running this attack yields the second column name of the "fsb_users" table.
That's better because this looks like a column name for which I would be interested in row values. The "user_name" table can be used again, iteratively, to retrieve the third column using the following syntax, " ' if (ascii(lower(substring((select top 1 column_name from information_schema.columns where table_name='fsb_users' and column_name > 'user_name'),1,1)))=100) waitfor delay '00:00:30'-- "
However, let's take a leap of faith and guess at the password column using the "like" operative: " ' if (ascii(lower(substring((select top 1 column_name from information_schema.columns where table_name='fsb_users' and column_name like '%pass%'),1,1)))=100) waitfor delay '00:00:30'-- "
Running this will give us the name of the first column in the "fsb_users" table that contains the string, "pass."
So now we have the predictable, "password" column which we can pair with the "user_name" column to start pulling some rows out. The SQL syntax will be, " ' if (ascii(substring((select top 1 user_name from fsb_users),1,1))=100) waitfor delay '00:00:30'-- ." I took the "lower()" function out just in case the usernames are case-sensitive in the login function.
I want to make sure we account for enough characters that might comprise the first user_name value so I'll bump payload 1 up to the range, 1 - 15. We'll leave the second payload the same as before.
Running this attack should output the first "user_name" value in the "fsb_users" table.
So the first "user_name" is "Jake_Reynolds." The next query will target this user's password. The SQL syntax is, " ' if (ascii(substring((select top 1 password from fsb_users where user_name = 'Jake_Reynolds'),1,1))=100) waitfor delay '00:00:30'-- ." The payload settings can be left alone assuming the user's password is 15 characters or less in length.
Running this attack should reveal the password for the user, "Jake_Reynolds." Any web application worth it's salt is going to hash the password columns and use salt so that collision attacks are difficult. HacmeBank contains an unhashed database column. The following screenshot illustrates why you should make sure and encrypt or hash your password columns:
"Jake_Reynolds/P@55w0rd" it is then. Let's test it out of course.
Works for me. Let's take it a little further though. I have run up against web applications that filter for various SQL syntax like "select" and other basic SQL key words whilst still using vulnerable dynamic SQL statements on the back end. I've always maintained that input validation is not a very effective means of preventing SQL injection and that real remediation means changing your database access layer to use parameterized/precompiled queries.
One way I've bypassed input filters that look for common words like "if" and "select" is to create a variable, cast it as hex, and execute it. Take the following SQL syntax for instance:
" ';declare @P varchar(4000);set @P=cast(0x69662028617363696928737562737472696e67282873656c65637420746f7020312070617 373776f72642066726f6d206673625f757365727320776865726520757365725f6e616d65203d20274a6 16b655f5265796e6f6c647327292c312c3129293d313030292077616974666f722064656c6179202 730303a30303a333027 AS varchar(4000));exec(@P);-- "
The 0x69662028617363696928737562737472696e67282873656c65637420746f7020312070617 373776f72642066726f6d206673625f757365727320776865726520757365725f6e616d65203d20274a6 16b655f5265796e6f6c647327292c312c3129293d313030292077616974666f722064656c6179202
730303a30303a333027 is simply a hex-encoded version of the string, " if (ascii(substring((select top 1 password from fsb_users where user_name = 'Jake_Reynolds'),1,1))=100) waitfor delay '00:00:30' . "
One extra thing we need to do is to add payload processing rules in Intruder to hex-encode both of our payloads since they occur within the hex cast.
Now when we run this attack, we get the same results as before and we retrieve the password for "Jake_Reynolds" with one exception. Our values are now ASCII-hex-encoded values of ASCII-decimal codes.
This time, our results are hexadecimal values for our decimal ASCII codes. So if you follow it:
- Payload 1 value 0x31 = 1 = Payload 2 value 0x38 0x30 = 80 = "P"
- Payload 1 value 0x32 = 2 = Payload 2 value 0x36 0x34 = 64 = "@"
- Payload 1 value 0x33 = 3 = Payload 2 value 0x35 0x33 = 53 = "5"
and so on until you get "P@55w0rd," an admittedly bad password to use on my precious FoundStone bank account. Anyway, that's blind, delay-based, SQL injection data extraction the hard way using BurpSuite to make it easier.