DVWA(Damn Vulnerable Web Application)
DVWA(Damn Vulnerable Web Application)是一個用來進行弱點安全測試的網站系統,旨在為安全專業人員測試自己的專業技能和工具提供合法的環境,幫助web開發者更好的理解web應用安全防範的過程。
DVWA共有十個模組,分別是Brute Force(暴力破解)、Command Injection(命令注入)、CSRF(跨站請求偽造)、File Inclusion(檔案包含)、File Upload(檔案上傳)、Insecure CAPTCHA(不安全的驗證碼)、SQL Injection(SQL注入)、SQL Injection(Blind)(SQL盲注)、XSS(Reflected)(反射型跨站腳本)、XSS(Stored)(儲存型跨站腳本)。
需要注意的是,DVWA 的程式碼分為四種安全級別:Low,Medium,High,Impossible。初學者可以通過比較四種級別的程式碼,接觸到一些PHP代碼審計的內容。
Cross Site Request Forgery (low)
CSRF(跨站請求偽造),全稱Cross-site request forgery(跨站請求偽造),是指利用受害者尚未失效的身份認證資訊(cookie、會話等),誘騙其點選惡意連結或者訪問包含攻擊程式碼的頁面,在受害人不知情的情況下以受害者的身份向(身份認證資訊所對應的)伺服器傳送請求,從而完成非法操作(如轉帳、改密碼等)。CSRF與XSS最大的區別就在於,CSRF並沒有盜取cookie而是直接利用。
在這一題,題目要我們輸入New password與Confirm new password,先嘗試隨意輸入串密碼。
例:123456
送出後,顯示密碼已更改成功。仔細觀察可發現網址變成
http://127.0.0.1/DVWA/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change#
也就是說,只要將這個網址貼給其他人,那麼當有人點擊後,他的密碼即會被改成123456。當然,前提是該使用者必須要先登入這個網站,才能成功觸發。
什麼意思呢? 我們直接看Source Code,第5、6行是輸入password_new與password_conf,第9行進行判斷 $pass_new == $ pass_conf 是否相同,接下來會進行將$pass_new進入到資料庫更新,過程中沒有進行任何防禦。
也就是說,假設今天管理員已經登入上這DVWA網站,而你將該網址傳送給管理員,並讓管理員點擊,即可成功更改管理員的密碼。
因為這只是練習的靶機網站,所以修改密碼的網址才會這麼明顯看出來,但在現實生活中,駭客會透過各種方法隱藏起來,甚至隱藏在偽造的網頁中,並添加短網址等,誘騙使用者點擊。
<?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
$html .= "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
$html .= "<pre>Passwords did not match.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
Cross Site Request Forgery (medium)
直接看Source code
<?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
$html .= "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
$html .= "<pre>Passwords did not match.</pre>";
}
}
else {
// Didn't come from a trusted source
$html .= "<pre>That request didn't look correct.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
主要關鍵點是
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false )
意思就是判斷來源地址與伺服器地址是否一樣,而stripos函數是指查詢字串在另一字串中第一次出現的位置。
我們利用Burp Suite來查看下效果是如何,可以觀察Host與Referer,這是在此級別中會去判斷的
如果受害者點擊了
http://192.168.43.162/DVWA/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#
可以發現缺少了Referer,所以驗證無法通過,導致無法實現CSRF。
那麼要如何繞過呢?
我們可以偽造一個Referer,例如 evil.com/192.168.43.162.html
在請求中添加Referer:192.168.43.162
<img src="http://192.168.43.162/DVWA/vulnerabilities/csrf/?password_new=1234&password_conf=1234&Change=Change#"border="0" style="display:none;"/>
<h1>CSRF demo<h1>
當使用者訪問192.168.43.162.html後,即可達到CSRF的攻擊。
Cross Site Request Forgery (High)
我們直接從Burp Suite查看結果,可觀察到多了一個user_token
接下來直接看Source Code,第5行增加了token機制,當使用者每次訪問時,都會得到一組token參數值並驗證。所以通常可利用XSS的方式來取得使用者的cookie、token值搭配CSRF修改密碼。
<?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
$html .= "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
$html .= "<pre>Passwords did not match.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
// Generate Anti-CSRF token
generateSessionToken();
?>
Cross Site Request Forgery (Impossible)
最後的級別,直接看Source Code,除了增加Anti-CSRF token外,也添加了PDO來防禦SQL Injection,更重要的是修改密碼功能也添加了需要輸入原始密碼才能修改。
所以就算我們用XSS的方式取得管理員的cookie,我們也是無法知道原始密碼,無法修改密碼。
<?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
$html .= "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
$html .= "<pre>Passwords did not match or current password incorrect.</pre>";
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>