DVWA靶機通關係列--1.Brute Force(暴力破解)(解題思路+審計分析)

RDrug發表於2020-11-29

DVWA靶機通關係列--1.Brute Force(暴力破解)(解題思路+審計分析)


前言

在這裡插入圖片描述

最近在複習一些基礎的常見web漏洞,經典的靶機大家肯定最先想到的就是DVWA了,我用的是1.10版本,目前裡面的題型包括了14個專題板塊,題目難度也分為低(low)中(medium)高(high)和不可能(impossible)幾個等級。

Brute Force(暴力破解)
Command Injection(命令注入)
CSRF(跨站請求偽造)
File Inclusion(檔案包含)
File Upload(檔案上傳)
Insecure CAPTCHA(不安全驗證碼)
SQL Injection(SQL隱碼攻擊)
SQL Injection (Blind)(SQL盲注)
Weak Session IDs(弱會話IDs)
XSS (DOM)(XSS DOM型)
XSS (Reflected)(XSS 反射型)
XSS (Stored)(XSS儲存型)
CSP Bypass(內容安全策略)
JavaScript(JS攻擊)

藉此機會,在複習這些漏洞的過程中也和大家分享我的一些做題經驗。
實驗過程中會經常用到Burp Suite工具,沒有此軟體的可以參考我的另一篇文章學習如何安裝:https://blog.csdn.net/ElsonHY/article/details/109731444


提醒:本系列只針對低中高三種難度的講解,不可能等級我們主要來分析他的防禦方式。由於本專題無法一次性寫完,小R決定會不定時進行更新。

0x01 Brute Force(暴力破解)

CLASS:LOW

在這裡插入圖片描述
先隨意輸入使用者名稱和密碼,檢視報錯顯示使用者名稱 和/或 密碼不正確,語句嚴謹無法判斷使用者名稱是否有效。
密碼爆破:開啟BurpSuite抓包,可以看出是GET型別,將包匯入Intruder(測試器),在positions設定攻擊型別和負載位置,攻擊型別選擇狙擊手模式(Sinper),username的值我用admin嘗試,(也可以直接將username的值也加入負載,配置對應的使用者字典。)負載位置設定在password的值上,下一步點選Payloads選項卡。在這裡插入圖片描述
在例表中新增中選擇密碼字典(BP內建字典),也可以載入自己的字典檔案,然後點選開始攻擊。
在這裡插入圖片描述

在爆破結果中檢視響應長度和其他不一樣的請求,進行對比確認,最後確定密碼"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);
}

?> 

php原始碼直接用GET呼叫了使用者名稱和密碼的值在資料庫中進行查詢,沒有使用防爆破的機制,導致了爆破的可能性。
另外,在引用SQL語句的部分沒有進行值的有效過濾,導致了有SQL隱碼攻擊的危險,注入方式如下:
使用者名稱:admin’-- (注意最後有空格)或者admin’#
密碼:隨便輸入或者不輸入。
結果登入成功。
在這裡插入圖片描述

CLASS:MEDIUM

隨便輸入,結果和上一題一樣。
在這裡插入圖片描述
和low難度一樣,抓包然後進行爆破。
在這裡插入圖片描述
怎麼就成功了?那這和low等級有什麼區別,來分析一下原始碼,看看有什麼不同的地方。
原始碼分析:

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

?>

通過分析,php原始碼主要增加了mysqli_real_escape_string()函式和在登入的時候用到了sleep()函式。
mysqli_real_escape_string()主要作用是對特殊字元進行轉義,對sql注入起到一定的防禦效果。

下列字元受影響:
\x00
\n
\r
\
'
"
\x1a

sleep()函式只是延長了爆破的時間並不能很有效地阻止爆破的行為。

CLASS:HIGH

同樣先對頁面的回顯進行測試,發現報錯語句一樣。
於是,先抓包看看。
在這裡插入圖片描述
可以看到GET傳遞的值裡面多了user_token這個值,很大可能是來進行驗證每次請求的,嘗試爆破,驗證一下想法。
在這裡插入圖片描述
爆破的結果全是302,檢視響應全是重定向到一個空白響應,說明token生效了。

這裡給大家解釋一下token 的作用:
token主要用來防禦csrf,而無法防止暴力破解, 這是因為將一個隨機產生的token加入請求之後,每次請求token都會改變,csrf攻擊者只能竊取受害者的cookie卻不能偽造當次請求的token,token無法防止暴力破解的原因是token每次都會隨頁面返回到前端,攻擊者只要想辦法自動化獲得前端的token即可進行暴力破解攻擊。

見招拆招~BurpSuite是有辦法解決需要token驗證的爆破方法的。
首先一樣抓包,讓後將包送到測試器(Intruder),有效負載選擇password和token的值,攻擊方式選擇“音叉(也有叫草叉的…pitchfork)”。
在這裡插入圖片描述
然後先到Oprtions,音叉攻擊的執行緒要設定為1,否則不能啟動攻擊。在這裡插入圖片描述
然後下拉找到Grep-Extract選項,點選新增,點選“獲得回覆”,得到響應包,在相應包中找到token的值,選中,自動生成選中值的正規表示式,然後接下來記得複製這個值,後面要用到,點選OK,結束正規表示式的設定。
“在從響應中提取一下專案”這一項打勾
在這裡插入圖片描述
在這裡插入圖片描述
回到Payloads,負載1依舊給他新增密碼字典,主要是負載2有所區別,負載2的有效載荷類別選擇“遞迴搜尋”,選擇之後在“有效載荷選項”選項中會自動獲取到上一步生成的正規表示式,然後把上一步從響應包裡面複製的token值貼上到“第一個請求的初始有效負載”裡面。
在這裡插入圖片描述
點選“開始攻擊”,攻擊結束後從結果中找到正確的密碼。
在這裡插入圖片描述
原始碼分析:

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

?>

果然,php原始碼中比上一個等級增加了checkToken() 進行token的驗證,細心還可以發現使用者名稱和密碼的處理採用了 stripslashes() 函式,作用是對字串中的反斜槓進行去除。

CLASS:IMPOSSIBLE

原始碼:

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

?> 

當輸入錯誤3次,鎖定15分鐘的可靠方式防止了爆破,同時採用PDO(PHP Data Object,PHP資料物件)機制更為安全,不會在本地對SQL進行拼接。當呼叫prepare()時,將SQL模板傳給MySQL Server,傳過去的是佔位符“?”,不包含使用者資料,當呼叫execute()時,使用者的變數值才傳遞到MySQL Server,分開傳遞,阻止了SQL語句被破壞而執行惡意程式碼。

相關文章