一.引言
1.CSRF概述:
CSRF(Cross-site request forgery),即跨站請求偽造。指攻擊者利用服務器對用户的信任,從而欺騙受害者在不知情的情況下執行由攻擊者發起的惡意請求,從而完成非法操作(修改密碼,轉賬等)。在CSRF的攻擊場景中,攻擊者會偽造一個請求(一般是鏈接),用户一旦點擊了鏈接,整個攻擊就完成了。所以CSRF也被稱為是"one click"攻擊。
2.與XSS的區別:
①.XSS是利用用户對服務器端的信任,CSRF是利用服務器對用户的信任。在XSS攻擊中,惡意JS腳本知識在用户的瀏覽器上執行,服務器只是載體。
②.XSS攻擊是將惡意代碼植入被攻擊的服務器,利用用户對瀏覽器的信任完成攻擊。而CSRF攻擊中,攻擊者會將惡意代碼存放在自己的服務器上,誘使受害者訪問,受害者在不知情的情況下執行了惡意代碼,從而利用瀏覽器向用户服務器發送看起來"合理"的請求。
3.攻擊的要點:
①.服務器沒有對請求的來源進行校驗。
②.受害者處於登錄的狀態。
③.攻擊者需要找到一個可以修改並獲取到敏感信息的請求。
簡要概述完CSRF的基本概念後,我們開始練習DVWA中CSRF訓練的low到high級別的關卡。
二.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
$current_user = dvwaCurrentUser();
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . $current_user . "';";
$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);
}
?>
審計代碼,我們可以看到用代碼沒有任何校驗,僅僅只是判斷數據是否提交和前後密碼是否一致,因此我們可以直接惡意鏈接來修改密碼。我們使用burpsuite攔截修改密碼的請求,並使用其內置的工具,如下:
然後將生成的PoC複製在自己的遠程主機上,如下:
然後再在url欄輸入你遠程主機的地址,就可以成功修改密碼了!
三.Medium級別
我們同樣打開源碼分析:
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
$current_user = dvwaCurrentUser();
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . $current_user . "';";
$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);
}
可以看到再Low的基礎上校驗了Referer,即請求是從哪裏發來的。這裏使用了stripos函數不區分大小寫地檢查HTTP頭的Referer字段,看是否能找到當前所在地主機名(目標網站的域名),即$_SERVER[ 'SERVER_NAME' ])的返回值,我這裏是http://dvwa:8083。因此這裏stripos的執行邏輯是:
如果 stripos 返回 0(在開頭找到):0 !== false → 結果為 true(合法);
如果 stripos 返回 false(沒找到):false !== false → 結果為 false(非法)。
所以要想成功修改密碼,就要使Referer字段嚴格等於目標網站域名,即http://dvwa:8083(這裏看你自己的本地域名),我們可以使用burpsuite來修改HTTP頭部的Referer字段。
我們先攔截之前的遠程主機的網頁地址,再去修改為目標網站的域名。
然後我們放行,便成功修改了密碼。
四.High級別
打開源碼開始分析:
Source
vulnerabilities/csrf/source/high.php
<?php
$change = false;
$request_type = "html";
$return_message = "Request Failed";
if ($_SERVER['REQUEST_METHOD'] == "POST" && array_key_exists ("CONTENT_TYPE", $_SERVER) && $_SERVER['CONTENT_TYPE'] == "application/json") {
$data = json_decode(file_get_contents('php://input'), true);
$request_type = "json";
if (array_key_exists("HTTP_USER_TOKEN", $_SERVER) &&
array_key_exists("password_new", $data) &&
array_key_exists("password_conf", $data) &&
array_key_exists("Change", $data)) {
$token = $_SERVER['HTTP_USER_TOKEN'];
$pass_new = $data["password_new"];
$pass_conf = $data["password_conf"];
$change = true;
}
} else {
if (array_key_exists("user_token", $_REQUEST) &&
array_key_exists("password_new", $_REQUEST) &&
array_key_exists("password_conf", $_REQUEST) &&
array_key_exists("Change", $_REQUEST)) {
$token = $_REQUEST["user_token"];
$pass_new = $_REQUEST["password_new"];
$pass_conf = $_REQUEST["password_conf"];
$change = true;
}
}
if ($change) {
// Check Anti-CSRF token
checkToken( $token, $_SESSION[ 'session_token' ], 'index.php' );
// Do the passwords match?
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = mysqli_real_escape_string ($GLOBALS["___mysqli_ston"], $pass_new);
$pass_new = md5( $pass_new );
// Update the database
$current_user = dvwaCurrentUser();
$insert = "UPDATE `users` SET password = '" . $pass_new . "' WHERE user = '" . $current_user . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert );
// Feedback for the user
$return_message = "Password Changed.";
}
else {
// Issue with passwords matching
$return_message = "Passwords did not match.";
}
mysqli_close($GLOBALS["___mysqli_ston"]);
if ($request_type == "json") {
generateSessionToken();
header ("Content-Type: application/json");
print json_encode (array("Message" =>$return_message));
exit;
} else {
echo "<pre>" . $return_message . "</pre>";
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
可以看到這裏的源碼很有些長,我們只找和其中的關鍵部分,可以看到這裏引用了token校驗的機制。
所謂token用通俗的話來説,就是 Web 世界裏的 “動態驗證碼”,就像你去銀行轉賬(提交請求),除了要出示身份證(Cookie),還必須輸入手機上收到的動態驗證碼(Token);而這個驗證碼只有你自己能拿到(銀行發給你的手機),每次都不一樣(一次性),別人猜不到(隨機性)。當你(合法用户)訪問 DVWA 的 CSRF 頁面時,服務器會生成一個隨機、不可預測的字符串,把這個 Token 存到服務器的 Session 裏,服務器把這個 Token偷偷放到 HTML 表單的隱藏字段裏,這個字段用户看不見,但提交表單時會自動帶上。只有Cookie和token都一致才能執行修改密碼的請求。
一.先確保Cookie一致(即身份憑證條件):
這是實現攻擊的基礎,只有擁有登錄Cookie,才能被服務器識別。否則即使獲取到了token,也會被服務器認不出來,從而被重定向到登錄界面以進行校驗。而SameSite Cookie,就是瀏覽器用來從根源上掐斷第一個條件的安全機制。
那SameSite是什麼?它的核心原理又是咋樣的呢?
1.SameSite是 Cookie 的一個安全屬性,用來控制 Cookie 在跨站請求中是否被攜帶,它的核心目標就是解決 CSRF 攻擊的根源問題:[瀏覽器自動攜帶跨站 Cookie]
2.SameSite 有 3 個可選值,規則是完全不同:
①.Strict(最嚴格): 只有和目標站點完全同站的請求,才會攜帶 Cookie;任何跨站請求,哪怕是用户主動點擊的鏈接 / 表單,
也不會攜帶有Cookie. 是防禦CSRF攻擊的有效手段。
②.Lax(瀏覽器默認值): 寬鬆模式,[僅允許用户主動觸發的,並且使用 GET 請求」攜帶 Cookie;POST 請求、自動發起的
請求(iframe、AJAX)都不會攜帶
③.None(完全放開): 不限制跨站Cookie,必須配合 HTTPS 使用。完全無法防禦CSRF攻擊。
瞭解了這個之後,我們可以在DVWA的核心目錄dvwa/includes/dvwaPage.inc.php中找到相關的設置:
function dvwa_start_session() {
// This will setup the session cookie based on
// the security level.
$security_level = dvwaSecurityLevelGet();
if ($security_level == 'impossible') {
$httponly = true;
$samesite = "Strict"; //可以看到只有impossible級別設置了嚴格的SameSite屬性
}
else {
$httponly = false;
$samesite = ""; //其他級別SameSite均設置為了空,即瀏覽器的默認屬性Lax(寬鬆模式)
}
$maxlifetime = 86400;
$secure = false;
$domain = parse_url($_SERVER['HTTP_HOST'], PHP_URL_HOST);
接下來我們通過burpsuite抓包來對比high級別(屬於其他級別)默認屬性(Lax)和嚴格屬性(Strict)的區別:
首先我們根據這一關卡的請求特性在自己的遠程主機上創建一個修改密碼的惡意代碼並保持登錄狀態:
<html>
<body>
<form action="http://dvwa:8083/vulnerabilities/csrf/?" method="GET"> <!--此處根據自己的靶場地址替換 -->
<input type="hidden" name="password_new" value="high123" />
<input type="hidden" name="password_conf" value="high123" />
<input type="hidden" name="Change" value="Change" />
<input type="hidden" name="user_token" value="408434287610a1526384becc4f69883d" /> <!-- 此處的token是由存儲型xss獲取,後面會説,這裏主要是為了進行演示-->
<input type="submit" value="Click Me" />
</form>
</body>
</html>
1.在SameSite默認屬性下:
可以看到Cookie存在於請求中。
2.SameSite嚴格屬性下:
發現請求請求中並沒有攜帶Cookie,放行後便會DVWA服務器會直接拒絕請求,並重定向到登錄頁面以進行登錄校驗
所以SameSite這樣的安全機制的確可以有效的防範CSRF攻擊。
二.再確保token一致(請求校驗條件):
為了演示如何繞過token,我們先把SameSite屬性改為默認值(即Lax模式)並重啓web服務。
由於瀏覽器同源策略的影響,我們無法通過外部惡意代碼獲取到token,那有沒有什麼辦法即符合即符合瀏覽器同源策略有可以拿到CSRF頁面的token呢?此時,存儲型XSS便閃亮登場,所以説如果一個網站為了防範CSRF而設置了token機制,但網站也存在着一個XSS漏洞,那這時便可以利用XSS來獲取token。由於XSS和目標網站同源(即協議,域名,端口均相等),便可以不受瀏覽器同源策略的限制,通過執行惡意JS代碼來獲取存儲在DOM中的token。
*為什麼使用存儲型XSS,其它的方式呢?*
存儲型XSS更加穩定且穩定,只需要將惡意代碼注入,等用户訪問時就可以執行。而反射型XSS需要用户點擊特定的惡意鏈接,鏈接一旦關閉,XSS 就失效了,需要每次都騙受害者點鏈接。
瞭解了這些我們開始實踐,我們切換到DVWA中的XSS(Stored)漏洞界面,查看源碼,審計後發現留言框進行了嚴格的轉義處理,無法注入。但標題欄只對*<script>*標籤進行了過濾,因此這是唯一的注入點。由於會有字數的限制,我們使用burpsuite進行繞過前端字數限制。在標題欄輸入以下代碼:
iframe src="../CSRF" onload=alert(frames[0].document.getElementsByName('user_token')[0].value)>
iframe 是 HTML 標籤,作用是在當前頁面中嵌入另一個頁面。src="../CSRF" 是相對路徑:存儲型 XSS 頁面的路徑是 /vulnerabilities/xss_s/,而 CSRF 頁面的路徑是 /vulnerabilities/csrf/,用 ../CSRF 就能正確跳轉到 CSRF 模塊的頁面。
這一步的目的是讓受害者的瀏覽器,在加載存儲型 XSS 頁面時,自動加載 CSRF 頁面(修改密碼頁面)。
onload 是 iframe 的事件,當嵌入的 CSRF 頁面完全加載完成後,會觸發這個事件裏的代碼。
document.getElementsByName('user_token')[0].value這是 DOM 操作代碼,作用是在 CSRF 頁面的 DOM 中,找到 name="user_token" 的輸入框,然後獲取它的 value(也就是當前受害者會話的 user_token),然後再通過彈窗顯示。
打開bp攔截在txtName(即標題欄)寫入以上代碼:
放行後再次打開頁面就可看到彈窗顯示token了!
注意不要點擊確定,否則會再次刷新token.我們將上面的token替換到之前創建的遠程主機代碼中的token值,再開一個標籤頁訪問遠程主機上的惡意代碼就可以成功執行修改密碼的請求了!
至此,本關通關!
五.CSRF的防禦
以上僅僅只是靶場環境,用於教學,因此故意簡化了場景。但在真實環境中往往會有多重防線防範CSRF:
1.給所有 Cookie 設置SameSite=Strict或SameSite=Lax,配合HttpOnly和Secure;
2.所有敏感操作(改密、轉賬、刪除、修改)必須使用 CSRF Token;
3.Token 必須和 Session 強綁定、不可預測、一次性或短期有效;
4.極高風險操作必須加二次校驗(原密碼、短信驗證碼);
5.配置嚴格的 CORS 策略,禁止不必要的跨域請求;
6.配置嚴格的 CORS 策略,禁止不必要的跨域請求;
7.做好 XSS 的防禦(輸入輸出過濾、CSP),防止 XSS+CSRF 的組合攻擊;
8.敏感操作必須用 POST 請求,不要用 GET 請求;