6.4.6 SQL注入

针对应用后端数据库的一种非常流行的攻击是SQL注入。SQL注入是代码注入的一种方式。和一般使用JavaScript针对浏览器的XSS代码注入不同,SQL注入针对应用在后端数据库上执行的SQL语句。这种攻击包括将SQL注入到动态构建的查询,然后在后端数据库上运行。最常见的是,恶意的输入直接连接到应用代码中的SQL语句,但是也可以发生在存储过程中。通过注入SQL语法,语句的逻辑可以被修改以执行不同的操作。对用于查询数据库的用户输入域的快速测试是在该值的最后发送一个单引号。在SQL语法中,单引号限定字符串的开始或者结束。因此,当单引号注入到易受攻击的SQL语句中时,就有可能扰乱字符串定界符的配对,生成一个应用错误,这就表示潜在的SQL注入漏洞。

http://www.website.com/users.asp?id=alex'

如果这个请求生成一个错误,就是不正确处理引号的一个很好的征兆,应用可能容易遭到SQL注入攻击。另一种常见的对数字域的攻击是注入OR 1=1,这改变了WHERE条件语句的解释方式。测试示例如下:

http://www.website.com/userProfile.asp?id=1378 OR 1=1

仔细研究id等于1378以及1378 OR 1=1时应用表现的差异,能够指出SQL注入漏洞。

SQL注入漏洞可能在任何影响数据库查询的应用参数中找到。攻击点包括URL参数、POST数据和cookie值。识别SQL注入漏洞的最简单方法是在参数值中添加无效或者意外的字符,观察应用响应中的错误。这种基于语法的方法在应用不阻止来自数据库错误信息的时候最有效。在实现这种错误处理时(或者存在一些简单的输入校验),漏洞也可以通过测试应用行为校验SQL构成的语义技术来识别。

语法测试包括将带有扰乱数据库查询语法目的的字符插入参数,目标是找到一个数据库执行查询时产生错误的字符,然后通过应用传播并且从服务器响应中返回。我们从最常见的注入字符——单引号(')开始。记住,单引号用于描述SQL语句中的字符串值。第一次SQL注入测试是这样的:

http://website/aspnuke/module/support/task/detail.asp?taskid=1'

服务器的响应,正如在浏览器中看到的那样,显示了一个数据库错误和应用试图提交给数据库的无效查询。查找接近图6-1中错误信息结尾处的WHERE tsk.TaskID=1'字符串可以看到注入字符结束的位置。

图6-1 详细的错误信息

现在我们来看看这种攻击成功的原理:字符串连接。许多Web应用中的查询都有由用户输入修改的子句。在前一个示例中,detail.asp文件使用taskid参数作为查询的一部分。

下面是源代码的一部分。看看带下划线的使用taskid的部分(为了可读性删除了一些代码行):


sStat = "SELECT tsk.TaskID, tsk.Title, tsk.Comments" &_
...
"FROM tblTask tsk " &_
...
"WHERE tsk.TaskID = " & steForm("taskid") & " " &_
"AND tsk.Active <> 0 " &_
"AND tsk.Archive = 0"
Set rsArt = adoOpenRecordset(sStat)

使用字符串连接来创建查询是SQL注入的根源之一。当参数值被直接放入字符串中时,攻击者可以轻松地注入恶意输入修改查询表现。所以并非创建一个如下的有效查询所用的数字参数,


SELECT tsk.TaskID, tsk.Title, tsk.Comments FROM tblTask tsk
WHERE tsk.TaskID = 1 AND tsk.Active <> 0 AND tsk.Archive = 0

攻击者引入一个不配对的引号字符扰乱语法:


SELECT tsk.TaskID, tsk.Title, tsk.Comments FROM tblTask tsk
WHERE tsk.TaskID = 1' AND tsk.Active <> 0 AND tsk.Archive = 0

不正确的语法造成了错误,这通常传回用户的浏览器。常见的错误信息如:


[Microsoft][ODBC SQL Server Driver][SQL Server]Incorrect syntax...

插入单引号和生成一个错误不会泄露密码或者使攻击者绕过访问限制,但是这常常是个先决条件。当然,这种技术取决于应用返回某种指出数据库错误发生的消息。表6-1列出数据库生成的一些常见错误字符串。这个列表不是完整的,但是应该能给你提供一些错误信息的概念。在许多情况下,实际的SQL语句和错误信息相伴。还要注意,这些错误在不同数据库平台和开发语言中都有所不同。

表6-1 常见数据库错误信息

最终,有些错误在语句构建和查询发送到数据库之前发生在应用层。表6-2列出一些这种错误信息。区分错误发生的位置很重要。产生解析错误(例如试图将字符串转换成整数)和重写数据库查询的攻击对应用的威胁有很大的不同。

表6-2 常见解析错误

任何用户可以修改的动态数据都代表着潜在的攻击方向。记住,cookie值和其他参数一样应该测试。图6-2显示了非常旧的phpBB版本所用的cookie附加单引号之后出现的错误。

图6-2 由于意外的cookie值而出现的详细错误信息

现在我们已经确定了寻找SQL注入漏洞的方法,应该确定这个漏洞对应用安全的影响了。在cookie值中插入单引号产生错误或者用MOD 函数替换POST参数是一回事,能够从数据库中读取任意信息又是另一回事。

数据库存储信息,首先想到针对数据的攻击也就不足为奇了。但是,如果我们能够用SQL注入改变查询的逻辑,那么就可能改变应用中的处理流程。登录提示是一个很好的例子。数据库驱动的应用可能使用类似于下例的方法校验用户名和密码:


SELECT COUNT(ID) FROM UserTable WHERE UserId='+ strUserID +
' AND Password=' + strPassword + '

如果用户提供了与UserTable中的记录匹配的UserId和Password参数,COUNT(ID)将会等于1。这样,应用将允许用户通过登录页面。如果COUNT(ID)为NULL或者0,则意味着UserId和Password不正确,不允许用户访问应用。

现在,想象一下如果在用户名参数上没有进行输入校验。我们可以以确保SELECT语句成功的方式重写查询——只需要一个用户名!下面是修改后的查询:


SELECT COUNT(ID) FROM UserTable WHERE UserId='mike'-- ' AND Password=''

注意,用户名包括一个单引号和一个注释定界符。单引号正确地描述了UserId(mike),双短线后面跟上一个空格代表注释,这意味着符号右边的所有内容都被忽略。用户名应该以如下的形式输入到登录表单:


mike'--%20

这样,我们已经使用SQL注入改变了应用的处理流程,而不是试图读取任意数据。这种攻击对登录页面可能有效,使我们能够查看用户的简档信息或者绕过访问控制。表6-3列出了其他一些可以尝试的作为参数值部分的SQL构造。这些都是原始的载荷;对空格和其他字符要进行编码,这样它们的意义在HTTP请求中不会被改变。例如,空格可以用%20或者加号(+)编码。

表6-3 修改查询的字符

因为数据库包含了应用的核心信息,它们代表着引人注目的目标。希望捕捉用户名和密码的攻击者可能尝试对一些应用的用户进行仿冒和社会工程攻击。另一方面,攻击者可以尝试从数据库获得每个人的凭据。

子查询

子查询能够读取范围从布尔型的指示(记录是否存在或者等于某个值)到任意数据(完整的记录)的信息。子查询也是用于基于语义的漏洞识别的一种较好的技术。正确设计的子查询使攻击者能够推断请求是否成功。最简单的子查询使用逻辑AND操作符强制查询为假或者为真:


AND 1=1
AND 1=0

现在,重要的是子查询可以被注入,使得查询的原始语法不会遭到破坏。注入到一个简单的查询中很容易:


SELECT price FROM Products WHERE ProductId=5436 AND 1=1

更复杂的查询具有多级括号和JOIN子句,可能不容易用基本的方法注入。在这种情况下,我们改变方法,关注于创建能由之推断某些信息的子查询。例如,下面是对查询示例的简单改写:


SELECT price FROM Products WHERE ProductId=(SELECT 5436)

我们可以使用(SELECT foo)子查询技术并将其扩展为更有用的测试,避免大部分扰乱语法的问题。我们不常常有对原始查询语法的访问权,但是子查询的语法如SELECT foo是我们制作的。在这种情况下,我们不需要担心左括号和右括号或者其他字符的数量匹配问题。当子查询用作一个值时,它的内容先于查询的其余部分解析。在下面的例子中,我们尝试计算默认的mysql.user表中名为“root”的用户的数量。如果只有一项,那么我们将会看到和使用值5436(5435+1=5436)时相同的响应。


SELECT price FROM Products WHERE ProductId=(SELECT 5435+(SELECT
COUNT(user) FROM mysql.user WHERE user=0x726f6f74))

这种技术可以用于任何数据库和任何特定的SELECT语句。基本上,我们只制作返回数字(或者真/假)值的语句。


SELECT price FROM Products WHERE ProductId=(SELECT 5435+(SELECT
COUNT(*) FROM SomeTable WHERE column=value))

子查询还可以进一步扩展,这样就不必限于推断SELECT语句的成功或者失败。它们可以用于枚举数值,但是较慢而不直接。例如,你可以应用按位枚举,从自定义的SELECT子查询中提取任何列的值。这是基于能够在注入AND 1=1和AND 1=0时区分服务器的不同响应。

按位枚举基于对一个数值的每一位确定该位置位(等价于AND 1=1)或者未置位(等价于AND 1=0)的测试。例如,下面是对于字符a(ASCII 0x61)的按位比较。这需要对应用进行8次请求以确定该值(实际上,ASCII文本只使用7位,但是我们为了完整性而引用全部8位)。


0x61 & 1 = 1
0x61 & 2 = 0
0x61 & 4 = 0
0x61 & 8 = 0
0x61 & 16 = 0
0x61 & 32 = 32
0x61 & 64 = 64
0x61 & 128 = 0
0x61 = 01100001 (binary)

SQL注入子查询的比较模板在下面的伪代码示例中展示。需要两个循环:一个用于枚举字符串的每个字节(i),另一个用于枚举字节中的每个位(n):


for i = 1 to length(column result):
for p = 0 to 7:
n = 2**p
AND n IN (SELECT CONVERT(INT,SUBSTRING(column,i,1)) & n FROM clause

这会创建如下的一系列子查询:


AND 1 IN (SELECT CONVERT(INT,SUBSTRING(column,i,1)) & 1 FROM clause
AND 2 IN (SELECT CONVERT(INT,SUBSTRING(column,i,1)) & 2 FROM clause
AND 4 IN (SELECT CONVERT(INT,SUBSTRING(column,i,1)) & 4 FROM clause
...
AND 128 IN (SELECT CONVERT(INT,SUBSTRING(column,i,1)) & 128 FROM clause

最后是一个从Microsoft SQL Server数据库枚举sa用户密码的查询(你需要在48个位置(i)上循环8次(n),一共发出384个请求)。Sa用户是Microsoft SQL Server数据库内建的管理员,与Unix root、Windows Administrator账户类似。所以如果sa用户密码可以通过Web应用提取,那将是很危险的。每当发回的响应匹配注入的AND 1=1,该位为1:


AND n IN
(
SELECT CONVERT(INT,SUBSTRING(password,i,1)) & n
FROM master.dbo.sysxlogins
WHERE name LIKE 0x73006100
)

子查询利用复杂的SQL构造推断SELECT语句的值。它们仅限于内部数据访问控制和可以包含在载荷中的字符。

UNION

SQL UNION操作符合并两个不同SELECT语句的结果集。这使开发人员能够使用一个查询从不同的表里读取数据,就像单个记录一样。下面是一个返回具有3列的记录的UNION操作符示例:


SELECT c1,c2,c3 FROM table1 WHERE foo=bar UNION
SELECT d1,d2,d3 FROM table2 WHERE this=that

UNION操作符的主要限制是每个记录集的列数必须一样。这不是特别难以克服的事情;只需要一些耐心和暴力方法。

第2个SELECT语句的列太少引起的缺列很容易处理。任何SELECT语句都可以接受重复的列名或者列值。例如,下面都是返回4列的有效查询:


SELECT c,c,c,c FROM table1
SELECT c,1,1,1 FROM table1
SELECT c,NULL,NULL,NULL FROM table1

第2个SELECT语句的列太多,也同样容易处理。在这种情况下,使用CONCAT 函数将所有结果连接到一列中:


SELECT CONCAT(a,b,c,d,e) FROM table1

我们来看看UNION操作符如何用在SQL注入攻击中。从理解UNION的工作原理到将其用到Web应用只是小小的一步。首先,验证一个参数容易遭到SQL注入攻击。通过在数字型参数之后附加一个字母进行这项工作,造成图6-3中的错误。注意,这个错误提供了原始查询的细节——最特别的是原始SELECT中的列数12。

图6-3 泄露数据库域的应用错误

我们还可以使用比较以下两个URL结果的“盲测”技术来测试这种漏洞:

http://website/freznoshop-1.4.1/product_details.php?id=43

http://website/freznoshop-1.4.1/product_details.php?id=MOD(43,44)

下面这个URL也可能生成错误(注意MOD 函数的无效用法):

http://website/freznoshop-1.4.1/product_details.php?id=MOD(43,a)

在任何情况下,下一步都是使用UNION操作符从数据库读取一些数据。第一步是匹配列数。我们用两个不同的请求验证列数(12)。我们将继续使用http://website/freznoshop-1.4.1/URL,在包含了UNION语句时整个URL有些冗长。我们预计需要12列,但是提交一个有11列的请求来论证UNION列集不匹配时发生的错误:


id=43+UNION+SELECT+1,1,1,1,1,1,1,1,1,1,1 /*

图6-4展示了这个id值提交给应用时返回的错误。注意,错误信息明确地指出了不匹配的列数。


id=43+UNION+SELECT+1,1,1,1,1,1,1,1,1,1,1 ,1/*

图6-4 使用列占位符建立有效的UNION查询

如果我们之后修改id参数中UNION右边的部分为12列,查询在语法上正确,就会接收到一个与id=43相关的页面。图6-5显示了没有出现错误的页面。

图6-5 成功的UNION查询显示用户id

当然,使用UNION操作符的真正原因是要读取任意的数据。到现在,我们仅仅发现了漏洞并匹配了列数。因为我们的应用示例使用MySQL数据库,我们将读取与MySQL相关的用户凭据。

MySQL存储数据库相关账户的风格与Microsoft SQL Server不同,但是我们现在可以访问默认的表名和列。注意图6-6中的响应。在表中有一个读做1.:root的条目——这是UNION查询返回的用户名(root)。以下是提交到id参数的值:


id=43+UNION+SELECT+1,cast(user+AS+CHAR(30)),1,1,1,1,1,1,1,1,1,1+FROM+
mysql.user/*

当然,获得前一个id值还有几个必要的中间步骤。初始测试从下列条目之一开始:


id=43'
id=43/*

然后转向使用UNION语句从任意表中提取数据。在这个例子中,在UNION语句右边的12个列之上创建一个匹配左边列数的SELECT语句是必要的。这个数字一般要通过反复试验得到,例如,尝试一列,然后两列、三列,以此类推。最后,我们发现第2列的结果将在Web应用中显示,这就是其他列使用1作为占位符的原因。

图6-6 成功的UNION查询揭露了用户名

提示  CAST 函数对于将用于用户名的MySQL内部存储类型(utf8_bin)转换为应用预期的存储类型(latin1_Swedish_ci)来说是必要的。CAST 函数是SQL 2003标准的一部分,得到所有流行数据库的支持。根据平台的不同,它可能是必要的,也可能不必要。

和许多SQL注入技术类似,UNION操作符在参数的值未包含在单引号中(像数字型参数),或者单引号不能作为载荷一部分的时候效果最好。在可以使用UNION时,方法很简单:

·识别漏洞。

·匹配原SELECT查询中的列数。

·创建自定义SELECT查询。

枚举

所有数据库都有一个与其安装和用户关联的信息集合。即使无法确定应用相关数据的位置,仍然可以列举多个表和其他信息,确定版本、补丁和用户。

SQL注入是针对数据存储进行的最有趣的攻击,但并不是唯一的。其他攻击也可以利用目录或者表中不恰当的安全策略。毕竟,如果你可以通过将URL参数从655321改成24601访问其他人的个人简档,那么就不需要注入恶意的字符或者尝试替代的语法。

依赖数据库访问的Web应用的最大挑战之一是如何安全地存储凭据。在许多平台上,凭据被存储在Web文档根目录之外的一个文本文件中。但是,在某些情况下,凭据可能硬编码于Web文档根目录中的一个应用源文件中。在后一种情况下,用户名和密码的机密性取决于预防源代码未授权访问。

SQL注入对策

应用的数据库包含关于应用及其用户的重要信息。对策应该包括了解可能对数据库进行的攻击类型,以及在特定防御无法胜任时尽可能减小入侵的影响。

对于Web应用来说,过滤用户提供的数据可能是最需要重申的措施。恰当的输入校验不仅保护应用免遭SQL注入攻击,而且能抵御其他参数操纵攻击。以数据库值为目标的输入校验可能很难。例如,单引号字符的危险性已经得到证明,但是如何处理像O払erry这样的名称,或者包含缩略词的句子?

数据库的绑定值所用的校验例程与其他值的过滤器没有很大的不同。下面是要记住的一些要点:

·转义字符: 单引号(撇号)在SQL语句中有特殊的含义。除非你使用能够百分之百地避免SQL语句中危险字符误解的预处理语句或者参数化查询,否则要确保对这样的字符(例如,\')进行转义,避免它们干扰查询。如果你依赖于字符串连接创建查询,就要始终这么做。

·拒绝字符: 你可以剥离已知的恶意字符,或者对预期数据来说不合适的字符。例如,电子邮件地址只包含特定标点符号的子集;它不需要括号。

·适当的数据类型: 只要有可能,就将整数值分配为整数数据类型,对所有用户提供数据都这么做。攻击者可能仍然制造一个错误,但是错误将发生在为参数赋值时,而不是在数据库中。

最强大的保护是由正确使用的参数化查询(也称预处理语句)提供的。下面的代码示例说明了在应用中实施参数化查询的一种方法:


SqlConnection conn = new SqlConnection(connectionString);
conn.Open();
string s = "SELECT email, passwd, login_id, full_name " +
"FROM members WHERE email = @email";
SqlCommand cmd = new SqlCommand(s);
cmd.Parameters.Add("@email", email);
SqlDataReader reader = cmd.ExecuteReader();

除了更安全外,参数化的代码还提供了性能上的好处。包括较少的字符串连接、不需要人工字符串转义,根据正在使用的DBMS,查询可能进行散列计算,并且存储起来供预编译执行。

对Web应用最具毁灭性的攻击之一是成功的SQL注入攻击。这种攻击直捣应用操纵的数据来源。如果可以侵入数据库,攻击者可能就不需要尝试暴力攻击、社会工程或者其他获得未授权访问的技术。理解这些漏洞的识别方法非常重要。

此外,对一种类型的攻击有效的对策对另一种攻击可能无效。最终,最好的防御是在应用中使用绑定参数的参数化语句或者预处理语句,尽可能地依赖数据库中的存储过程。