时间:2026-02-16 12:57
人气:
作者:admin
1.CSRF概述:
CSRF(Cross-site request forgery),即跨站请求伪造。指攻击者利用服务器对用户的信任,从而欺骗受害者在不知情的情况下执行由攻击者发起的恶意请求,从而完成非法操作(修改密码,转账等)。在CSRF的攻击场景中,攻击者会伪造一个请求(一般是链接),用户一旦点击了链接,整个攻击就完成了。所以CSRF也被称为是"one click"攻击。
2.与XSS的区别:
①.XSS是利用用户对服务器端的信任,CSRF是利用服务器对用户的信任。在XSS攻击中,恶意JS脚本知识在用户的浏览器上执行,服务器只是载体。
②.XSS攻击是将恶意代码植入被攻击的服务器,利用用户对浏览器的信任完成攻击。而CSRF攻击中,攻击者会将恶意代码存放在自己的服务器上,诱使受害者访问,受害者在不知情的情况下执行了恶意代码,从而利用浏览器向用户服务器发送看起来"合理"的请求。
3.攻击的要点:
①.服务器没有对请求的来源进行校验。
②.受害者处于登录的状态。
③.攻击者需要找到一个可以修改并获取到敏感信息的请求。
简要概述完CSRF的基本概念后,我们开始练习DVWA中CSRF训练的low到high级别的关卡。
打开靶场后我们查看源代码,如下:
<?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
$current_user = dvwaCurrentUser();
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . $current_user . "';";
$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);
}
?>
审计代码,我们可以看到用代码没有任何校验,仅仅只是判断数据是否提交和前后密码是否一致,因此我们可以直接恶意链接来修改密码。我们使用burpsuite拦截修改密码的请求,并使用其内置的工具,如下:


然后将生成的PoC复制在自己的远程主机上,如下:

然后再在url栏输入你远程主机的地址,就可以成功修改密码了!

我们同样打开源码分析:
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
$current_user = dvwaCurrentUser();
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . $current_user . "';";
$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);
}
可以看到再Low的基础上校验了Referer,即请求是从哪里发来的。这里使用了stripos函数不区分大小写地检查HTTP头的Referer字段,看是否能找到当前所在地主机名(目标网站的域名),即$_SERVER[ 'SERVER_NAME' ])的返回值,我这里是http://dvwa:8083。因此这里stripos的执行逻辑是:
如果 stripos 返回 0(在开头找到):0 !== false → 结果为 true(合法);
如果 stripos 返回 false(没找到):false !== false → 结果为 false(非法)。
所以要想成功修改密码,就要使Referer字段严格等于目标网站域名,即http://dvwa:8083(这里看你自己的本地域名),我们可以使用burpsuite来修改HTTP头部的Referer字段。
我们先拦截之前的远程主机的网页地址,再去修改为目标网站的域名。

然后我们放行,便成功修改了密码。
打开源码开始分析:
Source
vulnerabilities/csrf/source/high.php
<?php
$change = false;
$request_type = "html";
$return_message = "Request Failed";
if ($_SERVER['REQUEST_METHOD'] == "POST" && array_key_exists ("CONTENT_TYPE", $_SERVER) && $_SERVER['CONTENT_TYPE'] == "application/json") {
$data = json_decode(file_get_contents('php://input'), true);
$request_type = "json";
if (array_key_exists("HTTP_USER_TOKEN", $_SERVER) &&
array_key_exists("password_new", $data) &&
array_key_exists("password_conf", $data) &&
array_key_exists("Change", $data)) {
$token = $_SERVER['HTTP_USER_TOKEN'];
$pass_new = $data["password_new"];
$pass_conf = $data["password_conf"];
$change = true;
}
} else {
if (array_key_exists("user_token", $_REQUEST) &&
array_key_exists("password_new", $_REQUEST) &&
array_key_exists("password_conf", $_REQUEST) &&
array_key_exists("Change", $_REQUEST)) {
$token = $_REQUEST["user_token"];
$pass_new = $_REQUEST["password_new"];
$pass_conf = $_REQUEST["password_conf"];
$change = true;
}
}
if ($change) {
// Check Anti-CSRF token
checkToken( $token, $_SESSION[ 'session_token' ], 'index.php' );
// Do the passwords match?
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = mysqli_real_escape_string ($GLOBALS["___mysqli_ston"], $pass_new);
$pass_new = md5( $pass_new );
// Update the database
$current_user = dvwaCurrentUser();
$insert = "UPDATE `users` SET password = '" . $pass_new . "' WHERE user = '" . $current_user . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert );
// Feedback for the user
$return_message = "Password Changed.";
}
else {
// Issue with passwords matching
$return_message = "Passwords did not match.";
}
mysqli_close($GLOBALS["___mysqli_ston"]);
if ($request_type == "json") {
generateSessionToken();
header ("Content-Type: application/json");
print json_encode (array("Message" =>$return_message));
exit;
} else {
echo "<pre>" . $return_message . "</pre>";
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
可以看到这里的源码很有些长,我们只找和其中的关键部分,可以看到这里引用了token校验的机制。
所谓token用通俗的话来说,就是 Web 世界里的 “动态验证码”,就像你去银行转账(提交请求),除了要出示身份证(Cookie),还必须输入手机上收到的动态验证码(Token);而这个验证码只有你自己能拿到(银行发给你的手机),每次都不一样(一次性),别人猜不到(随机性)。当你(合法用户)访问 DVWA 的 CSRF 页面时,服务器会生成一个随机、不可预测的字符串,把这个 Token 存到服务器的 Session 里,服务器把这个 Token偷偷放到 HTML 表单的隐藏字段里,这个字段用户看不见,但提交表单时会自动带上。只有Cookie和token都一致才能执行修改密码的请求。
一.先确保Cookie一致(即身份凭证条件):
这是实现攻击的基础,只有拥有登录Cookie,才能被服务器识别。否则即使获取到了token,也会被服务器认不出来,从而被重定向到登录界面以进行校验。而SameSite Cookie,就是浏览器用来从根源上掐断第一个条件的安全机制。
那SameSite是什么?它的核心原理又是咋样的呢?
1.SameSite是 Cookie 的一个安全属性,用来控制 Cookie 在跨站请求中是否被携带,它的核心目标就是解决 CSRF 攻击的根源问题:[浏览器自动携带跨站 Cookie]
2.SameSite 有 3 个可选值,规则是完全不同:
①.Strict(最严格): 只有和目标站点完全同站的请求,才会携带 Cookie;任何跨站请求,哪怕是用户主动点击的链接 / 表单,
也不会携带有Cookie. 是防御CSRF攻击的有效手段。
②.Lax(浏览器默认值): 宽松模式,[仅允许用户主动触发的,并且使用 GET 请求」携带 Cookie;POST 请求、自动发起的
请求(iframe、AJAX)都不会携带
③.None(完全放开): 不限制跨站Cookie,必须配合 HTTPS 使用。完全无法防御CSRF攻击。
了解了这个之后,我们可以在DVWA的核心目录dvwa/includes/dvwaPage.inc.php中找到相关的设置:
function dvwa_start_session() {
// This will setup the session cookie based on
// the security level.
$security_level = dvwaSecurityLevelGet();
if ($security_level == 'impossible') {
$httponly = true;
$samesite = "Strict"; //可以看到只有impossible级别设置了严格的SameSite属性
}
else {
$httponly = false;
$samesite = ""; //其他级别SameSite均设置为了空,即浏览器的默认属性Lax(宽松模式)
}
$maxlifetime = 86400;
$secure = false;
$domain = parse_url($_SERVER['HTTP_HOST'], PHP_URL_HOST);
接下来我们通过burpsuite抓包来对比high级别(属于其他级别)默认属性(Lax)和严格属性(Strict)的区别:
首先我们根据这一关卡的请求特性在自己的远程主机上创建一个修改密码的恶意代码并保持登录状态:
<html>
<body>
<form action="http://dvwa:8083/vulnerabilities/csrf/?" method="GET"> <!--此处根据自己的靶场地址替换 -->
<input type="hidden" name="password_new" value="high123" />
<input type="hidden" name="password_conf" value="high123" />
<input type="hidden" name="Change" value="Change" />
<input type="hidden" name="user_token" value="408434287610a1526384becc4f69883d" /> <!-- 此处的token是由存储型xss获取,后面会说,这里主要是为了进行演示-->
<input type="submit" value="Click Me" />
</form>
</body>
</html>
1.在SameSite默认属性下:

可以看到Cookie存在于请求中。
2.SameSite严格属性下:

发现请求请求中并没有携带Cookie,放行后便会DVWA服务器会直接拒绝请求,并重定向到登录页面以进行登录校验

所以SameSite这样的安全机制的确可以有效的防范CSRF攻击。
二.再确保token一致(请求校验条件):
为了演示如何绕过token,我们先把SameSite属性改为默认值(即Lax模式)并重启web服务。
由于浏览器同源策略的影响,我们无法通过外部恶意代码获取到token,那有没有什么办法即符合即符合浏览器同源策略有可以拿到CSRF页面的token呢?此时,存储型XSS便闪亮登场,所以说如果一个网站为了防范CSRF而设置了token机制,但网站也存在着一个XSS漏洞,那这时便可以利用XSS来获取token。由于XSS和目标网站同源(即协议,域名,端口均相等),便可以不受浏览器同源策略的限制,通过执行恶意JS代码来获取存储在DOM中的token。
*为什么使用存储型XSS,其它的方式呢?*
存储型XSS更加稳定且稳定,只需要将恶意代码注入,等用户访问时就可以执行。而反射型XSS需要用户点击特定的恶意链接,链接一旦关闭,XSS 就失效了,需要每次都骗受害者点链接。
了解了这些我们开始实践,我们切换到DVWA中的XSS(Stored)漏洞界面,查看源码,审计后发现留言框进行了严格的转义处理,无法注入。但标题栏只对*<script>*标签进行了过滤,因此这是唯一的注入点。由于会有字数的限制,我们使用burpsuite进行绕过前端字数限制。在标题栏输入以下代码:
iframe src="../CSRF" onload=alert(frames[0].document.getElementsByName('user_token')[0].value)>
iframe 是 HTML 标签,作用是在当前页面中嵌入另一个页面。src="../CSRF" 是相对路径:存储型 XSS 页面的路径是 /vulnerabilities/xss_s/,而 CSRF 页面的路径是 /vulnerabilities/csrf/,用 ../CSRF 就能正确跳转到 CSRF 模块的页面。
这一步的目的是让受害者的浏览器,在加载存储型 XSS 页面时,自动加载 CSRF 页面(修改密码页面)。
onload 是 iframe 的事件,当嵌入的 CSRF 页面完全加载完成后,会触发这个事件里的代码。
document.getElementsByName('user_token')[0].value这是 DOM 操作代码,作用是在 CSRF 页面的 DOM 中,找到 name="user_token" 的输入框,然后获取它的 value(也就是当前受害者会话的 user_token),然后再通过弹窗显示。
打开bp拦截在txtName(即标题栏)写入以上代码:

放行后再次打开页面就可看到弹窗显示token了!

注意不要点击确定,否则会再次刷新token.我们将上面的token替换到之前创建的远程主机代码中的token值,再开一个标签页访问远程主机上的恶意代码就可以成功执行修改密码的请求了!

至此,本关通关!
以上仅仅只是靶场环境,用于教学,因此故意简化了场景。但在真实环境中往往会有多重防线防范CSRF:
1.给所有 Cookie 设置SameSite=Strict或SameSite=Lax,配合HttpOnly和Secure;
2.所有敏感操作(改密、转账、删除、修改)必须使用 CSRF Token;
3.Token 必须和 Session 强绑定、不可预测、一次性或短期有效;
4.极高风险操作必须加二次校验(原密码、短信验证码);
5.配置严格的 CORS 策略,禁止不必要的跨域请求;
6.配置严格的 CORS 策略,禁止不必要的跨域请求;
7.做好 XSS 的防御(输入输出过滤、CSP),防止 XSS+CSRF 的组合攻击;
8.敏感操作必须用 POST 请求,不要用 GET 请求;
下一篇:test