尽管我在演示中使用的是 T-SQL 代码,但实际上您不需要使用动态 SQL 来构造数据操作语言 (DML) 语句,因此大多数包含 DML 代码的应用程序不易受到这些问题的困扰。
下面,我们来看看另一个根据用户输入构造动态 DDL 语句的例子,如图 10 所示。就像前面的例子一样,以下语句也存在截断问题:
set @escaped_oldpw = quotename(@old, ‘‘‘‘)
攻击者通过传递 @new = ‘123...‘(其中从第 127 个字符(无单引号)开始是 @old = ‘; SQL Injection‘),会使 SQL 语句如下所示:
set @escaped_newpw = quotename(@new, ‘‘‘‘)alter login [loginname]
with password = ‘123... old_password = ‘; SQL
Injection
Figure10Creating a Dynamic DDL Statement
create procedure sys.sp_password
@old sysname = NULL, -- the old (current) password
@new sysname, -- the new password
@loginame sysname = NULL -- user to change password on
as
-- SETUP RUNTIME OPTIONS / DECLARE VARIABLES --
set nocount on
declare @exec_stmt nvarchar(4000)
declare @escaped_oldpw sysname
declare @escaped_newpw sysname
set @escaped_oldpw = quotename(@old, ‘‘‘‘)
set @escaped_newpw = quotename(@new, ‘‘‘‘)
set @exec_stmt = ‘alter login ‘ + quotename(@loginame) +
‘ with password = ‘ + @escaped_newpw + ‘ old_password = ‘ +
@escaped_old
exec (@exec_stmt)
if @@error <> 0
return (1)
-- RETURN SUCCESS --
return (0) -- sp_password
尽管存储过程更可能出现这些问题,但并非所有存储过程都会导致安全漏洞。接下来介绍哪些存储过程需要仔细审查。
在 SQL Server 中,默认情况下,所有存储过程都在调用方的环境下执行。因此,即使某个过程存在 SQL 注入问题,对该过程具有执行权限的恶意的本地用户也无法提高其权限,并且注入的代码会在其环境下执行。但是如果您有内部维护脚本,作为计算机所有者或某个特定用户可以执行该脚本,那么调用方就可以在不同用户环境下执行代码,并将其权限提升为该用户的权限。
所有截断问题肯定都是 Bug,但它们不一定是安全漏洞。但最好还是修复这些问题,因为您并不知道将来谁会找出这些问题并对其加以利用。
您可以采取其他措施减少您的 SQL 代码中的注入漏洞。首先,在存储过程中避免使用动态 SQL 来构造 DML 语句。如果您无法避免使用动态 SQL,那么可以使用 sp_executesql。第二,正如本文所举的例子中说明的,您需要正确计算缓冲区的长度。最后,在 C/C++ 代码中,检查字符串运算返回值,并查看字符串是否已截断,如果已截断,则相应的结果错误。参见提要栏“漏洞检测方法”,了解您可以采取的措施的摘要。
通过截断检测注入
要利用自动化工具通过截断问题检测 SQL 注入,需要对所有会产生截断可能性的代码模式有非常清楚的了解。您可以针对不同的特定代码模式使用不同的字符串数据。在下述情形中,假定 n 是输入缓冲区的长度。
要检测 QUOTENAME 分隔问题,首先假设使用 QUOTENAME(或 C/C++ 应用程序采用的类似函数)来准备分隔标识符或字符串,并且分隔字符串缓冲区大小小于 2*n + 2。当分隔字符串缓冲区长度等于 n 时,要捕获这些问题,可传递未分隔的长字符串。尾部分隔符将被截断,利用其他某个输入变量,您将获得注入机会。
当分隔缓冲区长度为奇数时,要捕获这些问题,可传递单引号字符(或右方括号或双引号)的长字符串。由于 QUOTENAME 会将所有分隔符出现的次数增加一倍,并添加开始的分隔字符,因此当已转义的字符串缓冲区只能存放奇数个字符时,尾部分隔符会被截断。
当分隔缓冲区长度为偶数时,要捕获这些问题,可传递像 1‘、1‘‘、1‘‘‘、1‘‘‘‘ 等这样的字符串,每次迭代时使单引号(或右方括号)数量递增。由于 QUOTENAME 会使所有单引号的出现次数增加一倍,因此在返回的字符串中会有偶数个单引号,加上开始的分隔符和 1,最终会有偶数个字符。因此,尾部的分隔符会被截断。
如果使用 REPLACE(或 C/C++ 应用程序采用的类似函数)来准备已转义的字符串,并且当已转义的字符串缓冲区大小小于 2*n 时,您也可以检测出上述问题。当已转义的字符串缓冲区长度等于 n 时,要捕获这些问题,可传递像 1‘、12‘、123‘ 和 123...n‘ 等这样的字符串,每次迭代时使输入字符串的长度递增。在这种情况下,如果您达到合适的长度,那么 REPLACE 函数就会将最后一个单引号字符再增加一个。由于已转义的字符串变量不具备足够的缓冲区空间,因此最后一个单引号会被截断,并在传递时保存起来,从而为打破 SQL 语句提供了机会。
当已转义的缓冲区长度为奇数时,要通过 REPLACE 捕获问题,可传递长度逐渐递增的单引号字符串,如 ‘‘、‘‘‘ 和 ‘‘‘‘...‘(或者只传递单引号字符的长字符串)。在这种情况下,REPLACE 将使所有单引号的出现次数增加一倍。但是由于有的缓冲区长度是奇数,因此最后一个单引号会被截断,这就为打破语句提供了机会。
当已转义缓冲区长度为偶数时,要捕获这些问题,可传递像 1‘、1‘‘、1‘‘‘、1‘‘‘‘ 等这样的字符串,每次迭代时使单引号(或右方括号)数量递增。返回值在没有 1 开始的情况下将包含偶数个字符,因此整个返回值有奇数个字符。由于缓冲区长度是偶数的,因此尾部的单引号会被截断,从而为打破 SQL 语句提供了机会。
漏洞检测方法
使用代码审查 如果您要执行代码审查,可使用以下几种检测方法来检测 SQL 语句中存在的问题。
检测第一级或第二级 SQL 注入
识别用于执行动态 SQL 语句的 API。
检查是否对动态 SQL 语句中所使用的数据进行了任何数据验证。
如果没有执行过数据验证,则检查该数据是否对分隔字符(字符串使用单引号,SQL 标识符使用右方括号)进行了转义。
通过截断问题检测 SQL 的修改
检查要用于存储最终动态 SQL 语句的缓冲区长度。
计算在极限输入情况下存放 SQL 语句所需的最大缓冲区,并查看用于存放 SQL 语句的缓冲区是否足够大。
特别要注意 QUOTENAME 或 REPLACE 函数的返回值,如果输入数据的长度是 n 个字符,当所有输入字符都为分隔字符时,这些函数返回值的长度将为 2*n + 2 或 2*n。
对于 C/C++ 应用程序,应检查像 StringCchPrintf 这样用于准备 SQL 语句的 API 的返回值是否检查过缓冲区不足的错误。
通过截断问题检测 SQL 注入
检查用于存放分隔字符串或已转义字符串的缓冲区长度。
如果输入字符串的长度为 n,则您需要 2*n + 2 长度的缓冲区来存放 QUOTENAME 函数的返回值,需要 2*n 长度的缓冲区来存放 REPLACE 函数的返回值。
对于 C/C++ 应用程序,应检查与 REPLACE 同等函数的返回值是否检查过缓冲区不足的错误。
使用黑盒方法 如果您有自动化工具或智能模糊处理程序,那么可使用以下几种检测方法来检测 SQL 语句中存在的问题。
检测 SQL 注入问题
发送单引号作为输入数据,以捕获用户输入数据没有经过净化并被用作动态 SQL 语句中的字符串的情况。
使用右方括号(] 字符)作为输入数据,以捕获用户输入没有经过任何输入净化就用在 SQL 标识符中的情况。
检测截断问题
发送长字符串,就像您发送字符串检测缓冲区溢出一样。
通过截断问题检测 SQL 的修改
发送单引号字符(或右方括号或双引号)的长字符串。这将使 REPLACE 和 QUOTENAME 函数的返回值长度达到最大值,并可能截断用于存放 SQL 语句的命令变量。