DVWA 黑客攻防演练(二)暴力破解 Brute Froce

暴力破解,简称”爆破“。不要以为没人会对一些小站爆破。实现上我以前用 wordpress 搭建一个博客开始就有人对我的站点进行爆破。这是装了 WordfenceWAF 插件后的统计的情况。

装了 WordfenceWAF 看到报告就深刻感受到国际友人对我这破站的安全性的深刻关怀了。你不封他们的 ip ,他们的程序就会像中了 “奇淫合欢散” 那些对你的网站锲而不舍地爆破。而下面会从 dvma 中学习如何爆破和如何防爆破。

初级

很简单的登录,代码可以点击 view source 可以看到

<?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);
}

?>

这提交太简单,简直引人犯罪,来看看人类满满的恶意吧。下面介绍在 Kali Linux 中比较常用的两个工具 burp suite 和 hydra

Burp Suite

Burp Suite 是 Kali Linux 中预装的非常强大的渗透工具。在这个部分主要用来抓包和进行吧爆破。(可以在这里下载,win,mac都可以用的

将安全等级设置为 low 后,打开 burp suite,设置火狐浏览器的网络走 burp suite 的代理(即localost:8080,如图所示),让 burp suite 拦截火狐的请求

如果你要修改 burp suite 端口的话,你可以在 proxy-> options 中修改

之后你可以在 DVWA 的 Brute Force 的页面上输入帐号 admin 和 密码,密码随便都可以,提交后,发现页面动不了了,因为 burp suite 拦截了请求了。 点击右键,将求的信息发送到入侵器(intruder)(你点击 Forward 会将请求跑完,如果错过了,可以在 Proxy->Http History 和 target 那里都能找到之前的记录 )

然后在 intruder->position 那里点击 clear ,清理掉所有的变量,然后再添加 password 部分。也就是爆破点只有 password 部分。

然后点击 payloads,添加一份常用密码(Kali Linux 在 /usr/share/john/password.lst 或者从 github 上搜一份),并移除注释。

然后再设置爆破的标准,因为密码错误的时候会提示 Username and/or password incorrect

所以报文如果不能匹配到 incorrect,就说明密码爆破成功。所以在 Options 那里设置成这样就行了

然后再点击上面,右上角的 start attack 就可以了

明显 password 就是密码了

Hydra

爆破的原理是一样的,不过 hydra 用的是命令行。而根据上面抓包得出的信息。(文档可以看这里) 可以马上用这条命令进行爆破。

hydra 192.168.0.110 -s 5678 -l admin -IVv -P /usr/share/john/password.lst http-get-form "/vulnerabilities/brute/:username=^USER^&password=^PASS^&Login=Login:incorrect.:H=Cookie: security=low;PHPSESSID=isk2inn2psu1sh56slicq6oim7"

常用参数

  • s :端口
  • l:用户名
  • L:用户列表文件
  • p:密码
  • P:密码文件
  • I:忽略现有的恢复文件,强制退出就有恢复文件了(不需要等10秒)
  • v:输出详细信息
  • V:输出每次尝试的用户和密码

而 http-get-form 的东西和上面说的都差不多只是一个是图形化界面一个是命令行参数罢了。应该很容易理解。得到的结果如下。

SQL 注入

如果你看上面的源代码的话,你会发现会有 SQL 注入的问题的。密码因为有 md5 一下所有不存在注入的问题,但是 $user 没有。。。所以往 $user 参数的方向去想。 比如 user 是 admin’# 时

$query = "SELECT * FROM `users` WHERE user = ‘$user’ AND password = ‘$pass’;"; //结果会是这样 $query = "SELECT * FROM `users` WHERE user = ‘admin’#’ AND password = ‘$pass’;" //mysql 语句中 # 后面都是注释,就变成 $query = "SELECT * FROM `users` WHERE user = ‘admin’

从而登录到了 admin 帐号。。。

中级

中级的界面是这样的

而代码与前面相比只是多了要用mysqli_real_escape_string函数进行验证,以及登录失败会 sleep(2)

将 用户名和密码转义,比如说 \n 被转义成 \\n,’ 转义成 \’,这可以抵御一些 SQL 注入攻击,但是不能抵御爆破。代码如下

<?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);
}

?> 
        

用 hydra 试试 

hydra 192.168.0.110 -s 5678 -l admin -IVv -P /usr/share/john/password.lst http-get-form "/vulnerabilities/brute/:username=^USER^&password=^PASS^&Login=Login:incorrect.:H=Cookie: PHPSESSID=pfoju9qirkvqmcnpgb49a2bop4; security=medium"
毫无疑问可以爆破的

高级

高级的页面代码

<form action="#" method="GET">
    Username:<br>
    <input type="text" name="username"><br>
    Password:<br>
    <input type="password" autocomplete="off" name="password"><br>
    <br>
    <input type="submit" value="Login" name="Login">
    <input type="hidden" name="user_token" value="b258081b421c1ee77b3b1d5a53be58ca">
</form>

服务端的代码

<?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();

?> 

高级点的代码的话,会检查 user_token

意思是,用户访问 login.php 的时候就生成一个 token 保存在 session,并让它登录的时候发送给服务器,如果服务器有这个 token 就证明这个请求是确实打开了 login.php 才再提交了。

登录失败或者根本就没 token 就将这个 token 从 session 中移除,生成新的 token 再执行之前的操作。

就如注释所言但这是用来防止 CSRF 攻击的,所谓的 CSRF 就是比如打开恶意网站,里面有张图片,或者伪造一个输入框利用网站 cookies 就可以直接“帮你”做删除数据之类的操作的。

这段代码防不了爆破,每次爆破之前获取页面的 token 不就可以了吗。 而 stripslashes 是用来还原 html 词汇,比如 a\tsay\t\’world\’ 之类,就会还原成 a say ‘world’ 对防御爆破没什么帮助的。

一些文章会用 python 去写,我觉得要写代码去做功能考虑去做,如果能工具去做的事绝不写代码。

(有点奉太郎的味道

所以下面主要是用 brup 实现,而基本操作 在 Kali Linux 中要用 brup v1.3.6 才行。v1.3.5 有录制宏的bug 。 burp suite 也不复杂。

爆破可以配置成这样,在作用域(scope)中设置匹配的 Url 及相关的宏,爆破前就能就获取页面的 token,并将之放到 url 参数中

下面是设置过程

录制请求

之后你可以在 DVWA 的 Brute Force 的页面上输入帐号 admin 和 密码,密码随便都可以,提交后,发现页面动不了了,因为 burp suite 拦截了请求了。

此时,如果你点击 Forward 会将请求会继续跑

配置作用域

在 target-> sitemap 那里配置作用域

设置匹配条件

在 project options -> session 的 Session Handle Rules 上,配置匹配的项目以及匹配后要做的事。 点击 Add 按钮创建匹配 Url 后要做的东西。

这里配置意思是说,这是只有爆破的时候才会启动(intruder),其他功能不会用到这个规则。

录制获取 token 的行为的宏

行为是,在爆破前获取 token ,并将之放到 url 中。

然后点击 Add 按钮,添加宏

选择需要那个需要获取 token 的页面,点击最下面的 ok 按钮

配置选项

创建要添加到 url 的参数

填写要放到参数名,双击 b59619b0e13a9016a524e686f47720e2 后帮你自动填好的。然后点击 ok

然后一直点击 Ok 就行了。

爆破设置

步骤和之前的一样。在 target->site map 中找到要爆破的请求

到 Intruder 页,设置要爆破的参数

配置常用密码字典

匹配出错情况

开始爆破

爆破结果如下

不可能级别

到这个级别,利用工具基本无法爆破。来看看代码吧

<?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();

?> 

这代码就多了很多锁定用户的逻辑了。而且 sql 查询也不用stripslashes( $pass ) 和 mysql_real_escape_string($pass )了,更加简洁和安全了。觉得最好还是加个验证码,比如是错了三次之后就要填写验证码之类的逻辑。

最后

  1. 任何手段都无法保护弱密码,如果密码就是 123456,password之类的弱密码就不需要爆破也能解决,当你的密码是有大小写的英文,有数字,有特殊符号(@#.)之类的8位数以上就非常难被爆破获得了
  2. 锁定的用户的逻辑其实是有漏洞的,假如我讨厌一个人,那么我写个程序每隔一段登录失败,他就永远登录不了了。。。
  3. 登录简单吗?一个简单的功能可以很简单,但也可以很不靠谱,老鸟与菜鸟实现同一种功能可能会考虑很多种情况,为何他会知道这种情况,可能是看书的,可能是看别人写的代码,但更有可能是坑过或者曾被坑过。。。这就是老鸟的价值了。