DVWA 通关指南:Cross Site Request Forgery (CSRF)
Cross Site Request Forgery (CSRF)
CSRF is an attack that forces an end user to execute unwanted actions on a web application in which they are currently authenticated. With a little help of social engineering (such as sending a link via email/chat), an attacker may force the users of a web application to execute actions of the attacker's choosing.
CSRF 是一种攻击手段,它迫使直接用户在当前经过身份验证的 web 应用程序上执行不需要的操作。借助社会工程(如通过电子邮件/聊天发送链接),攻击者可能会迫使 web 应用程序的直接用户执行攻击者选择的操作。
A successful CSRF exploit can compromise end user data and operation in case of normal user. If the targeted end user is the administrator account, this can compromise the entire web application.
在正常用户的情况下,成功利用 CSRF 会危害直接用户的数据和操作。如果目标直接用户是管理员帐户,这可能会危及整个 web 应用程序。
This attack may also be called "XSRF", similar to "Cross Site scripting (XSS)", and they are often used together.
这种攻击也可以称为 “XSRF”,类似于“跨站点脚本(XSS)”,它们经常一起使用。
Your task is to make the current user change their own password, without them knowing about their actions, using a CSRF attack.
你的任务是使用CSRF攻击让当前用户在不知道自己的行为的情况下更改自己的密码。
Low Level
There are no measures in place to protect against this attack. This means a link can be crafted to achieve a certain action (in this case, change the current users password). Then with some basic social engineering, have the target click the link (or just visit a certain page), to trigger the action.
开发者目前还没有针对这一种攻击的措施,这意味着可以构建一个链接来实现某个操作(在本例中,更改当前用户的密码)。然后用一些基本的社会工程,让目标点击链接(或只是访问某个页面)来触发动作。
源码审计
源码如下,服务器收到修改密码的请求后,将使用 GET 方法接收参数 password_new 和 password_conf,并校验这 2 个参数是否相同。如果相同,说明用户输入新密码并且确认过了,就会在数据库中修改密码。同时我们也可以看到这段源码中,并没有任何的防 CSRF 机制。
<?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);
}
?>
值得一提的是服务器对请求的发送者会利用 cookie 进行身份验证,这个在源码中无法体现。
攻击方式
攻击者会构造一个 url 如下,当用户点击这个连接,并且他的登录状态(例如 cookie)还未过期时,他的密码就会被修改掉。
http://(你的 DVWA 的 url)?password_new=123&password_conf=123&Change=Change#
当然这个 url 还是很明显的,攻击者一般为了让用户上当会使用诸如 url 压缩或者进行 url 编码等操作,让你看不出是修改密码的页面。或者是构造一个假的页面如下,这个页面会把修改密码的界面隐藏,因此从表面上看这像是个普通的 404 页面。受害者点击这个连接访问时,会误认为自己点了个已经炸掉了的连接,但是 CSRF 攻击已经完成了。
<img src="(你的 DVWA 的 url)?password_new=hack&password_conf=hack&Change=Change#" border="0" style="display:none;"/>
<h1>404<h1>
<h2>file not found.<h2>
值得一提的是,CSRF 是利用受害者的 cookie 向服务器发送伪造请求,所以如果受害者使用了没有 cookie 的另一个浏览器登录时,攻击不会触发。
Medium Level
For the medium level challenge, there is a check to see where the last requested page came from. The developer believes if it matches the current domain, it must of come from the web application so it can be trusted.
对于中等级别,网页会有一个检查以查看最后请求的页面来自何处。开发人员认为如果它与当前域匹配,那么它必须来自该 web 应用程序才能被信任。
It may be required to link in multiple vulnerabilities to exploit this vector, such as reflective XSS.
可能需要链接多个漏洞来利用此漏洞,例如反射 XSS。
源码审计
源码如下,此时网页就不是直接装载变量了,而是添加了一些对 CSRF 的防御机制。PHP 的 stripos() 函数用于查找字符串在另一字符串中第一次出现的位置(不区分大小写),SERVER 是一个包含了诸如头信息(header)、路径(path)、以及脚本位置(script locations)等等信息的数组。开发者检查了保留变量 HTTP_REFERER(http 包头的 Referer 参数的值,表示来源地址)中是否包含 SERVER_NAME(http 包头的 Host 参数,要访问的主机名),希望通过验证 http 来源的机制抵御 CSRF 攻击。
<?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);
}
?>
Referer 字段指示访问这个网页的来源,也就是说当访问这个网页的来源是其本身时,才可以完成改密码操作。
攻击方式
由于 http 包头的 Referer 字段值中必须包含主机名,所以可以将用于攻击的网页文件名改为 http 报头的 host 字段。例如改密码的页面位于 192.168.101.2 上,而攻击页面位于 10.10.10.10 上,此时我们可以把攻击页面的文件名命名为 “192.168.101.2.html”,这样在校验 Referer 字段值时就会通过从而完成 CSRF 攻击。
High Level
In the high level, the developer has added an "anti Cross-Site Request Forgery (CSRF) token". In order by bypass this protection method, another vulnerability will be required.
开发人员添加了一个“反 CSRF 令牌”,需要联合使用另一个漏洞才能绕过此保护方法。
源码审计
源码如下,代码加入了 Anti-CSRF token 机制,当用户每次访问改密页面时,服务器会返回一个随机的 token。向服务器发起请求时,需要提交 token 参数,而服务器在收到请求时会检查 token,只有 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();
?>
攻击方式
现在要想进行 CSRF 攻击就必须获取到用户的 token,而要想获取到 token 就必须利用用户的 cookie 去访问修改密码的页面,然后截取服务器返回的 token 值。这里可以利用 XSS(Stored) 的 high 级别的漏洞,我们注入一个攻击脚本,使得每次打开页面时都弹出 token 值。
注入的 payload 如下,别忘了 high 级别的 XSS(Stored) 需要抓包后改 name 参数。
<iframe src="../csrf/" onload=alert(frames[0].document.getElementsByName('user_token')[0].value)>
Impossible Level
In the impossible level, the challenge will extent the high level and asks for the current user's password. As this cannot be found out (only predicted or brute forced), there is not an attack vector here.
该级别下网页要求用户提供当前使用的密码,由于用户当前使用的密码是无法发现的(只能爆破),因此这里没有攻击载体。
<?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();
?>
总结
CSRF 跨站请求伪造攻击是攻击者利用目标用户的身份,以目标用户的名义执行某些错误操作的攻击方式,会极大地威胁用户的权益。CSRF 攻击的 2 个重点是:
- 目标用户登录了网站,并且能正常执行该网站的操作;
- 目标用户访问了攻击者制作的攻击页面。
CSRF 漏洞的防御方式是验证请求的 Referer 字段值,如果该字段值是以自己的网站开头的域名,则说明该请求是来源于自己,就可以通过验证进行访问。当该字段值是其他网页的域名或者空白时,就说明这有可能是 CSRF 攻击,这时候就应该拒绝这个请求。
不过这种方法能起到的作用有限,因为攻击者可以用其他方式绕过验证。还有一种更合适的方式是在请求中放入攻击者不能够伪造的信息,例如使用 Anti-CSRF token 机制,让访问者需要通过一个随机生成的 token 进行验证。还有可以通过一些密保问题,因为这些问题的答案理论上只有访问者自己知道,攻击者也无法伪造。