引言
結合DVWA中的CSRF模塊源碼對CSRF漏洞進行一下總結分析。
CSRF,全稱Cross-site request forgery,翻譯過來就是跨站請求偽造,是指利用受害者尚未失效的身份認證信息(cookie、會話等),誘騙其點擊惡意鏈接或者訪問包含攻擊代碼的頁面,在受害人不知情的情況下以受害者的身份向(身份認證信息所對應的)服務器發送請求,從而完成非法操作(如轉賬、改密等)。CSRF與XSS最大的區別就在於,CSRF並沒有盜取cookie而是直接利用。
環境
metasploitable2(包含DVWA)
burpsuit
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); } ?>
GET方式得到三個參數,change、password_new、password_conf。如果password_new和password_conf相同,那么更新數據庫,並沒有任何防CSRF的措施。這里我們有多重攻擊方式,比如直接構造鏈接、構造短鏈接和構造攻擊頁面。
構造鏈接:
http://127.0.0.1/dvwa/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change#
既然參數是GET方式傳遞的,那么我們可以直接在URL鏈接中設置參數,如果用戶用登陸過該網站的瀏覽器(服務器會驗證cookie)打開這個鏈接,那么將直接把參數傳遞給服務器,因為服務器並沒有防CSRF的措施,所以直接可以攻擊成功,密碼將被改為123456。到那時如果用戶在沒有登陸過這個網站的瀏覽器上打開這個鏈接,並不會更改密碼,而是跳轉到登錄界面。因為服務器在接受訪問時,首先還要驗證用戶的cookie,如果瀏覽器上並沒有之前登錄留下的cookie,那攻擊也就無法奏效。這么看來CSRF攻擊的關鍵就是利用受害者的cookie向服務器發送偽造請求。
短鏈接:
上面的攻擊鏈接太明顯的,參數直接就在URL中,這樣很容易就會被識破,為了隱藏URL,可以使用生成短鏈接的方式來實現。就是將原鏈接轉換等一個比較短的URL鏈接,打開短鏈接和打開原鏈接等價的。
構造攻擊頁面:
真實CSRF攻擊中,攻擊者為了隱藏自己的攻擊手段,可能構造一個假的頁面,然后放在公網上,誘導受害者訪問這個頁面,如果受害者訪問了這個頁面,那么受害者就會在不知情的情況下完成了CSRF攻擊。自己測試可以寫一個本地頁面,也可以利用burpsuit直接生成攻擊頁面代碼。方法如下:
1、抓取更改密碼的數據包,利用engagement tools生成CDRF PoC,訪問點擊提交之后就可以更改密碼。
2、復制生成的html代碼即為我們需要的攻擊頁面代碼。
3、訪問點擊提交之后就可以更改密碼。
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); } ?>
stripos(a,b)返回 b 存在於 a,字符串開始的位置,字符串起始位置為0,如果未發現 b 則返回false。代碼檢查了保留變量HTTP_REFERER (http包頭部的Referer字段的值,表示來源地址)是否包含SERVER_NAME(http包頭部的 Host 字段表示要訪問的主機名)。針對這一過濾規則,我們只要想辦法繞過,那么我們后面的代碼和low級別的基本都一樣了,很容易實現CSRF攻擊。由於我是本地phpstudy搭建的DVWA,所以http包中Host字段就是本機---127.0.0.1,而Referer字段就是本地搭建的DVWA頁面的地址,故也包含127.0.0.1。所以這個對於本地搭建的DVWA是無效的,但是在現實場景中一般是不包含的,所以我們可以通過更改頁面文件名來繞過stripos函數。繞過方法:
假如服務器地址為192.168.66.66,即為SERVER_NAME,我們只需要把我們構造的惡意頁面文件名改為192.168.66.66.html,HTTP_REFERER就會包含192.168.66.66.html,就可以繞過stripos了。
high
先看源碼:
<?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(); ?>
high級別的源碼中加入了Anti-csrf token機制,由checkToken函數來實現,用戶每次訪問更改密碼頁面時,服務器會返回一個隨機的token,之后每次向服務器發起請求,服務器會優先驗證token,如果token正確,那么才會處理請求。所以我們在發起請求之前需要獲取服務器返回的user_token,利用user_token繞過驗證。這里我們可以使用burpsuit的CSRF Token Tracker插件可以直接繞過user_token驗證。使用步驟如下:
1、安裝CSRF Token Tracker插件
2、進入插件之后添加Host和Name
3、抓包repeat就ok了
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(); ?>
Impossible級別的源碼中也使用驗證user_Token和原始密碼來防止CSRF,如果沒有當前密碼無法進行修改密碼。db->prepare采用的是PDO模式,防止SQL注入。
CSRF防御
Set-Cookie:SameSite
禁止第三方網站帶Cookies,可以在響應頭Set-Cookie設置SameSite屬性,表示Cookie為同源網站而非第三方網站。
驗證碼校驗或CSRF Token
在請求地址中添加token驗證或者驗證碼校驗,驗證碼和CSRF Token都具有隨機性。缺點就是可能會降低用戶體驗。
驗證HTTP Referer字段
Referer字段表示http請求的來源地址,服務器對每一個請求驗證Referer值,查看請求來源是否合法,可以在一定程度上防御CSRF攻擊。