这个例子显示出使用注释符号能够用来判断请求语句是否被顺利地结束了,如果添加了注释符号且没有产生错误,这就意味着注释符号前的语句已经顺利地被结束。如果出现了错误,这就需要攻击者进行更多的请求尝试。
3.2 判断数据库类型
攻击者一旦确定了正确的注入句法后,就会开始利用注入去判断后台数据库的类型,这个步骤比确定注入句法要简单得多。攻击者一般会使用以下几种技巧,这些技巧是基于不同类型数据库引擎在具体实现上的差异。下面只介绍如何区分Oracle和MS SQL Server:
最简单的办法,就是前面提到的利用字符串的连结符号,在注入句法已经确定的情况下,攻击者可以对WHERE语句自由地添加额外的表达式,那么就可以利用字符串的比较来区分数据库,例如:
AND 'xxx' = 'x' + 'xx' (或者 AND %27xxx%27+%3D+%27x%27+%2B+%27xx%27)
通过将+替换成||,就可以判断出是数据库是Oracle还是MS SQL Server,或者是其他类型。
其他的办法是利用分号字符(即;),在SQL中,分号是用来将几个SQL语句连接在同一行中。在注入时,也可以在注入代码中使用分号,但Oracle驱动程序却不允许这样使用分号。假设在前面使用注释符号时没有出现错误,那么在注释符号前加上分号对MS SQL Server是没有影响的,但如果是Oracle就会产生错误。另外,还可以使用COMMIT语句来确认是否允许在分号后再执行其他语句(例如,注入语句xxx' ; COMMIT --),如果没有出现错误就可以认为允许多句执行。
最后,表达式还可以被替换成能返回正确值的系统函数,由于不同类型的数据库使用的系统函数也是不同的,因此也可以通过使用系统函数来确定数据库类型,比如2.3节提到的MS SQL Server的日期函数getdate()与Oracle的sysdate.
3.3 构造注入利用代码
当所有相关的信息都已获得后,攻击者就可以开始进行注入利用,而且在构造注入利用代码过程中也不再需要详细的错误信息,构造利用代码本身可以参考其他描述标准SQL注入攻击的文档。
由于对于普通的SQL注入利用,已经有很多其他论文进行了详细的讨论,故本文只会在下一节介绍一种UNION SELECT注入。
4 UNION SELECT注入
尽管通过篡改SELECT…WHERE语句来注入对于很多应用程序非常有效,但在盲注情况下,攻击者仍然愿意使用UNION SELECT语句,这是因为与WHERE语句所进行的操作不同,使用UNION SELECT可以让攻击者在没有错误信息的情况下依然能访问数据库中所有表。
进行UNION SELECT注入需要预先获知数据库的表中的字段个数和类型,而这些信息一般被认为在没有详细错误信息的提示下是不可能获得的,但本文下面就将给出解决该问题的方法。
另外需要注意的是,进行UNION SELECT的前提是攻击者已经确定了正确的注入句法,本文的前面一节已经阐明了这在盲注条件下是可以实现的,而且在使用UNION SELECT语句之前,SQL语句中所有的插入语符号都应该已经完成配对,从而可以自由地使用UNION或者其它指令进行注入。UNION SELECT还要求当前语句和最初的语句查询的信息必须具有相同的数和相同的数据类型,不然就会出错。
4.1 统计列数
当错误信息没有被屏蔽时,要获取列数只需要在进行UNION SELECT注入时每次尝试使用不同的字段数即可,当错误信息由“列数不匹配”变成“列的类型不匹配”时,当前尝试的列数就是正确的。但在盲注条件下,由于我们对无法获悉错误信息究竟是哪个,所以该方法也就失去了作用。
新的办法是利用ORDER BY语句,在SELECT语句最后加上ORDER BY能够改变返回的记录集的次序,一般是按一个指定的列名的值进行排序。例如,当通过产品号查询产品时,一个有效的注入语句如下:
SELECT ProdNum FROM Products WHERE (ProdID=1234) ORDER BY ProdNum --
AND ProdName=’Computer’) AND UserName=’john’
人们往往会忽略的是ORDER BY语句后还可以使用数字指代列名,在上例中如果ProdNum是查询请求返回的记录中的第一列,则注入1234) ORDER BY 1--返回的结果是一样的。由于上例查询请求只返回一个字段,注入1234) ORDER BY 2 --就会出错,即返回的记录无法按指定的第二个字段排序。这样,ORDER BY就可以被利用来对列数进行统计了。由于每个SELECT语句都至少返回一个字段,故攻击者可以先在注入句法中添加ORDER BY 1来确定语句是否能被正确执行,有时对字段的排序也可能会产生错误,这时添加关键字ASC或DESC可以解决该问题。一旦确定ORDER BY句法是有效的,攻击者就会对排序列号从列1到列100进行遍历(或者到列1000,直到列号被确定为无效),理论上当出现第一个错误时,前一个列号就是要统计的列数,但在实际情况中,有些字段可能不允许排序,那么在出现第一次错误时可以再多尝试一到两个数字,以确认列号已遍历完。
4.2 判断列的数据类型
在统计完列数后,攻击者需要再判断列的数据类型,在盲注情况下判断类型也是有技巧的,由于UNION SELECT要求前后查询语句查询的字段类型相同,故如果字段数有限,可以简单地利用UNION SELECT语句对字段类型进行暴力穷举(brute force),但如果字段数较多,判断就会出现问题。根据前文,字段的类型只有数字、字符串和日期三种可能的类型,一旦字段数有10个,那么就意味着有310(约60,000)种可能的组合,假设每一秒可以自动进行20次尝试,穷举一遍也需要近一个小时,如果字段数更多,那么测试所需时间就会令人难以忍受。
一种简单的办法是利用SQL的关键字NULL,与静态字段的注入需要区分是数字类型还是字符类型不同,NULL可以匹配任何一种数据类型。因此可以注入一个所有查询字段都为NULL的UNION SELECT语句,那么就不会出现任何类型不匹配的错误。让我们再举一个与前面类似的例子:
SELECT ProdNum,ProdType,ProdPrice,ProdProvider FROM Products
WHERE (ProdID=1234 AND ProdName=’ Computer’) AND UserName=’john’
假设攻击者已经获得了列数(在该例中为4),那么就可以很简单地构造一个UNION SELECT语句,其中所有查询字段都为NULL,还需要构造一个不会产生权限问题的FROM语句。对于MS SQL Server,即使忽略FROM语句也不会出错,但对于Oracle,则可以使用一个名叫dual的表。最后,还需要一个值一定为FALSE的WHERE语句(比如WHERE 1=2),这是为了确保查询不会返回只包含null值的记录集,以杜绝产生其他可能的错误。那么针对MS SQL Server的注入语句如下:
SELECT ProdNum,ProdType,ProdPrice,ProdProvider FROM Products
WHERE (ProdID=1234) UNION SELECT NULL,NULL,NULL,NULL
WHERE 1=2 -- AND ProdName=’ Computer’) AND UserName=’john’
这个NULL注入语句有两个目的,主要目的是构造一个不会产生任何错误的UNION SELECT语句以测试UNION语句是否可以被执行,另一个目的是为了对数据库类型的判断进行100%确认(可以通过在FROM语句里添加一个数据库开发商预置的表名进行测试)。
如果NULL注入语句被顺利执行,那么就可以快速地对每个列的类型进行判断。在每一轮尝试中,只对一个字段类型进行测试,由于类型只有三类,所以每个字段最多被测试三次就会有结果,这样尝试的次数最多是列数的三倍,而不是以3为底数以列数为指数的次数。假设ProdNum属于数字类型,其它三个字段都属于字符串类型,那么以下顺序的注入语句就可以判断出正确的类型:
1234) UNION SELECT NULL,NULL,NULL,NULL WHERE 1=2 --
无错 句法正确,使用的是MS SQL Server数据库
1234) UNION SELECT 1,NULL,NULL,NULL WHERE 1=2 --
无错 第一个字段是数字类型
1234) UNION SELECT 1,2,NULL,NULL WHERE 1=2 --
出错 第二个字段不是数字类型
1234) UNION SELECT 1,’2’,NULL,NULL WHERE 1=2 --
无错 第二个字段是字符串类型
1234) UNION SELECT 1,’2’,3,NULL WHERE 1=2 --
出错 第三个字段不是数字类型
1234) UNION SELECT 1,’2’,’3’,NULL WHERE 1=2 --
无错 第三个字段是字符串类型
1234) UNION SELECT 1,’2’,’3’,4 WHERE 1=2 --
出错 第四个字段不是数字类型
1234) UNION SELECT 1,’2’,’3’,’4’ WHERE 1=2 --
无错 第四个字段是字符串类型
攻击者现在就已经获得了每一列的数据类型,盲注还可以被应用于从数据库的表中获取数据,比如获得数据表的列表以及它们各自的列名,还可以从应用程序中获得数据,而这些技术在其他一些关于SQL注入的论文中已经有讨论,故本文不再继续介绍。