CSRF漏洞学习

参考及引用的文章

浅谈csrf漏洞:https://xz.aliyun.com/t/7450?time__1311=n4%2BxnD0G0%3Dit0QDkID%2FiWRSD0xcDRlGerDWwD&alichlgref=https%3A%2F%2Fwww.google.com%2F#toc-3

CSRF漏洞的原理

CSRF漏洞产生的原因

(1)http协议使用session在服务端保存用户的个人信息,客户端浏览器用cookie标识用户身份;

(2)cookie的认证只能确保是某个用户发送的请求,但是不能保证这个请求是否是”用户自愿的行为”.

(3)这时,用户登录了某个web站点,同时点击了包含CSRF恶意代码的URL,就会触发CSRF

漏洞利用的条件

(1)用户必须登录A网站,生成了cookie

(2)登录的同时访问了恶意URL(包含CSRF恶意代码的URL)

换种解释就是网站的cookie在浏览器中不会过期,只要不关闭浏览器或者退出登录,那以后只要是访问这个网站,都会默认你已经登录的状态。而在这个期间,攻击者发送了构造好的csrf脚本或包含csrf脚本的链接,可能会执行一些用户不想做的功能(比如是添加账号等)

CSRF和XSS的不同

(1)XSS主要是获取用户的cookie信息,达到控制客户端的目的

XSS—->把你的腰牌(用户身份象征也就是cookie)偷到手,黑客自己去搞破坏.

CSRF主要是劫持用户身份,让客户端做一些不愿意做的事.

CSRF—->拿刀劫持你,”借助你的身份”来帮黑客做事.

(2)危害上来说,XSS更大;

(3)从应用难度上来说

CSRF需要满足登录某网站的状态,同时访问了恶意的URL,应用条件比较苛刻.

XSS只要一次点击或者存储到服务器即可.

CSRF漏洞检测

检测CSRF漏洞是一项比较繁琐的工作,最简单的方法就是抓取一个正常请求的数据包,去掉Referer字段后再重新提交,如果该提交还有效,那么基本上可以确定存在CSRF漏洞

随着对CSRF漏洞研究的不断深入,不断涌现出一些专[门针对CSRF漏洞进行检测的工具,如CSRFTester, CSRF Request Builder等。

以CSRFTester工具为例,CSRF漏洞检测工具的测试原理如下:使用CSRFTester进行测试时,首先需要抓取我们在浏览器中访问过的所有链接以及所有的表单等信息,然后通过在CSRFTester中修改相应的表单等信息,重新提交,这相当于一次伪造客户端请求。如果修改后的测试请求成功被网站服务器接受,则说明存在CSRF漏洞,当然此款工具也可以被用来进行CSRF攻击

DVWA-CSRF

Low级别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?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>' );//这行代码执行了SQL查询,更新数据库中的用户密码。如果执行失败,将会输出错误信息

// 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);//关闭MySQL连接,并返回关闭操作的结果。如果关闭操作成功,返回true;如果关闭操作失败,返回false。
}

?>
  1. isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"]):这部分代码首先检查$GLOBALS["___mysqli_ston"]是否已经设置且为一个有效的MySQL连接对象。如果是,则表示数据库连接已经建立,可以执行转义操作;如果不是,则表示数据库连接尚未建立,无法执行转义操作。
  2. mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ):如果数据库连接已经建立,将调用mysqli_real_escape_string函数对新密码进行转义处理。这个函数会对字符串中的特殊字符进行转义,以防止它们被误解为SQL语句的一部分,从而防止SQL注入攻击。
  3. (trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""):如果数据库连接未建立,或者转义操作失败,将会触发一个错误,并输出错误消息。这段代码似乎是为了提醒开发者修复MySQL转义函数调用的问题,因为它使用了已经弃用的mysql_escape_string函数,而不是推荐的mysqli_real_escape_string函数

该网站通过mysqli_real_escape_string()函数的过滤作用,将用户传入的数据中的特殊字符进行转义,对SQL注入做了防御。但没有对CSRF做任何防范措施

正常输入密码,然后抓包

image-20240518213310958

用burpsuite自带的CSRF PoC进行攻击,将其生成的把CSRF HTML复制到本地,然后用该浏览器访问

image-20240518213331819

点击提交请求后自动跳转到我们的页面,并且此时密码已被成功修改

image-20240518213356304

以上过程中要注意的是一定不要中途更换浏览器,访问csrf.php的时要同一个浏览器访问,并且还要保证你登录DVWA的cookie没有过期,不然会因为缺少身份验证信息而执行失败

Medium级别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?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);
}

?>

stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ])!=-1:检查当前请求的Referer头中是否包含当前服务器的域名。如果包含,则条件为真,表示请求来源于当前服务器;如果不包含,则条件为假,表示请求不是从当前服务器发出的

stripos():这是一个字符串函数,用于在一个字符串中查找子字符串的位置,不区分大小写

综上,与low级别相比,该级别不能够跨域访问

但是只要在报头的referer处输入:127.0.0.1就可以成功绕过

所以同样我们抓包,构造poc,这次我们把密码改成admin007并保存为127.0.0.1.html文件

image-20240512200317403

然后使用浏览器进行访问,点击提交请求后自动跳转到我们的页面,并且此时密码已被成功修改

High级别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php

if( isset( $_GET[ 'Change' ] ) ) {
//用于检查令牌(Token)的有效性,以确保请求的合法性,如果令牌验证失败时要重定向的页面为index.php
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);
}

//生成一个会话令牌,并将其存储在会话中
generateSessionToken();

?>

经过分析发现High级别的代码加入了Anti-CSRF token机制,用户每次访问改密页面时,服务器会返回一个随机的token,向服务器发起请求时,需要提交token参数,而服务器在收到请求时,会优先检查token,只有token正确,才会处理客户端的请求

CSRF防御之道

1、尽量POST

GET太容易被CSRF攻击了,用POST可以降低风险,但也不能保证万无一失, 攻击者只要构造一个form就可以,但需要在第三方页面做,这样就增加暴露的可能性。

2、加入验证码

在POST的基础上可以再加一个验证码,用户每次提交数据时都需要在表单中填写验证码,这个方案大幅度的降低CSRF攻击,一些简单的验证码可能会被hacker破解,但一般情况下,验证码是很难被破解的。

3、验证Referer

就像上面的Medium级别那样,在验证时添加一个Referer,判断请求的来源地址是否是当前网页,如果是,则可以认为该请求是合法的,否则就拒绝用户请求。

4、Anti CSRF Token

CSRF攻击之所以能够成功,是因为攻击者可以伪造用户的请求,该请求中所有的用户验证信息都存在于cookie中,因此攻击者可以在不知道用户验证信息的情况下直接利用用户的cookie来通过安全验证。由此可知,抵御CSRF攻击的关键在于:在请求中放入攻击者所不能伪造的信息,并且该信总不存在于cookie之中。

在开发过程中我们可以在HTTP请求中以参数的形式加入一个随机产生的token,并在服务端进行token校验,如果请求中没有token或者token内容不正确,则认为是CSRF攻击而拒绝该请求

bp’s lab

Lab:没有防御措施的 CSRF 漏洞

题目提示电子邮件更改功能容易受到 CSRF 的攻击

所以我们登录账号之后转到电子邮件更改界面,进行抓包,将其发送到重放器中

image-20240518213429263

可以看到报头中有referer,我们尝试删除并再次发送查看回显,发现一切正常,说明后端并没有验证referer

存在csrf漏洞,右键生成poc,复制html到bp的漏洞利用服务器中,可以自己先测试一下,但完成测试后要改一下邮箱地址,发送给受害者,题目解决

Lab:令牌验证取决于请求方法的 CSRF

依旧是电子邮件更新功能容易受到攻击

登录账号之后转到电子邮件更改界面,进行抓包,将其发送到重放器中

这次有令牌进行验证,如下:

image-20240518213503852

尝试直接修改令牌值,失败,更改邮箱请求被拒绝

标题:令牌验证取决于请求方法的 CSRF,给了我们另一种思路

我们尝试右键更改请求方式为get,然后对令牌值进行随意修改,请求成功,说明更改请求方式后后端没有验证令牌值

所以我们直接生成poc,复制html到bp的漏洞利用服务器中,送给受害者,题目解决

Lab:CSRF,其中令牌验证取决于令牌的存在

依旧是电子邮件更新功能容易受到攻击

登录账号之后转到电子邮件更改界面,进行抓包,将其发送到重放器中

有令牌验证,但是根据标题中的令牌验证取决于令牌的存在这句话,尝试直接把令牌全部删除,发送请求成功

于是直接生成Poc,复制html到bp的漏洞利用服务器中,送给受害者,题目解决

Lab:令牌与用户会话无关的 CSRF

此实验室的电子邮件更改功能容易受到 CSRF 的攻击。它使用令牌来尝试防止 CSRF 攻击,但它们没有集成到站点的会话处理系统中

这次我们拥有两个账号,首先登录一个账号并抓包,这次的令牌既不能修改,删除后也没有起到任何作用,更改请求方式也不行

查看代理模块的HTTP历史记录中的/my-account?id=wiener,检查后发现token值是在该条目中生成的,具体代码为: <input required type="hidden" name="csrf" value="MwT3m0JuawJpnibLHLd4XDpvLMw9GMWx">

于是我们把这个token值复制到另一个账号的修改邮箱的抓包界面中替换一下,点击发送,请求成功

这说明了后端只检查令牌是否正确,没有检查令牌的来源是否是该用户

于是我们可以生成poc,复制html到bp的漏洞利用服务器中,需要注意,token一旦被使用过便不会再发挥作用,所以我们需要一个从来没有被用过的令牌

重新把更改邮箱的页面再加载一下,抓下包,获得新的token值,这时要注意不能把该请求放行,就把它挂在那边就好了,然后把得到的新的令牌值复制到代码里面,存储后发送给受害者,题目解决

1
2
3
4
5
6
7
8
9
10
11
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="https://0a0000880458d70780af219200c400fa.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="123456&#64;123" />
<input type="hidden" name="csrf" value="dxpxBVZLqU1g14UUD2dv4omHuDneB7yH" />
<input type="submit" value="Submit request" />
</form>
<img src="https://0a0000880458d70780af219200c400fa.web-security-academy.net/?search=test%0d%0aSet-Cookie:%20csrfKey=ZfUOIWcnpwgpxoFwNzvS9oeXgQxA03PJ%3b%20SameSite=None" onerror="document.forms[0].submit()">
</body>
</html>

此实验室的电子邮件更改功能容易受到 CSRF 的攻击。它尝试使用不安全的“双重提交”CSRF 预防技术。

我们登录账号后更新邮箱,抓包,如下:
image-20240518213554009

可以发现Cookie中的csrf和传参中的csrf是一样的,观察 到csrf body 参数的值是通过其与 csrf cookie 进行比较来验证,也就是说我们只需要这两个csrf的值一样便可以成功绕过

返回主页执行搜索功能,到代理中的HTTP历史记录中找到该条目,发送到重放器中,发送请求,回显如下:
image-20240513235945586

发现搜索词反映在 Set-Cookie 标头中。由于搜索功能没有 CSRF 保护,可以使用它向受害者用户的浏览器注入 cookie

比如:/?search=test%0d%0aSet-Cookie:%20csrf=fake%3b%20SameSite=None(有涉及到xss)

其中的 ``SameSite=None:在默认情况下,浏览器在跨站请求(例如从一个网站向另一个网站发送请求)时不会发送第三方Cookie。这是浏览器的同源策略的一部分,旨在防止跨站请求伪造(CSRF)攻击和其他安全威胁。然而,有时候我们确实需要在跨站请求时发送Cookie,比如在使用单点登录(Single Sign-On)或者嵌入其他网站的资源(例如图片或iframe)时。这就需要使用SameSite=None属性来解除浏览器的限制。当设置了SameSite=None`属性时,表示该Cookie可以在跨站请求中发送

于是我们构造poc如下:

1
2
3
4
5
6
7
8
9
10
11
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="https://0ad300b7031c7394812e7087006a0086.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="123456&#64;123" />
<input type="hidden" name="csrf" value="fake" />
<input type="submit" value="Submit request" />
</form>
<img src="https://0ad300b7031c7394812e7087006a0086.web-security-academy.net/?search=test%0d%0aSet-Cookie:%20csrf=fake%3b%20SameSite=None" onerror="document.forms[0].submit();"/>
</body>
</html>

保存,将其发送给受害者,题目解决

Lab:通过方法覆盖绕过 SameSite Lax

在更改邮箱地址界面抓包,把抓到的发送到重放器中,发送,得到的响应头中有一条为:X-Frame-Options: SAMEORIGIN这说明了允许该页面在相同源(即相同域名)的 <frame><iframe> 或者 <object> 标签中加载

除此之外也没有任何的令牌来限制

查看响应发现网站在设置会话 cookie 时没有明确指定任何 SameSite 限制。因此,浏览器将使用默认的 Lax 限制级别,这意味着会话 cookie 将在跨站点 GET 请求中发送,只要它们涉及顶级导航

顶级导航:指的是浏览器的地址栏导航,即在浏览器的地址栏输入网址并按下回车,或者通过点击链接进行的页面跳转,这种跳转会改变浏览器的顶级窗口内容

于是我们尝试更改请求方式为get,然后发送,回显表示该页面只允许post传参

向get传参中添加 &_method=POST:用于在HTML表单不支持的情况下模拟PUT、DELETE等HTTP方法。这种技术通常在RESTful风格的Web应用中使用,以绕过浏览器和HTML表单的限制

再次发送,成功,所以我们便可以在漏洞利用服务器中输入如下代码

1
<script>document.location="https://0a77000403d1fecd80be4987002a0052.web-security-academy.net/my-account/change-email?email=1236%40123&_method=POST"</script>

保存并发送给受害者,题目解决

Lab:通过客户端重定向绕过 SameSite Strict

首先在更改电子邮箱的界面抓包,把抓到的发送到重放器中

image-20240514235123283

发送,得到的响应头中有一条为:X-Frame-Options: SAMEORIGIN

尝试更改请求方式为get,然后发送,回显说明修改电子邮箱成功,无需其他操作便可以成功修改,当时以为已经成功了,直接去漏洞利用服务器中写代码,发送给受害者,但是失败了,表明直接这样修改的话是行不通的

回到主页上进入任意一篇博客中进行评论,然后查看bp的代理模块中的HTTP历史记录,查看了 /post/comment/confirmation?postId=2,其响应中有好东西如下:
image-20240514235551430

于是我们转去查看条目/resources/js/commentConfirmationRedirect.js,所需函数如下:

1
2
3
4
5
6
7
redirectOnConfirmation = (blogPath) => {
setTimeout(() => {
const url = new URL(window.location);
const postId = url.searchParams.get("postId");
window.location = blogPath + '/' + postId;
}, 3000);
}

易得这是一个可以重定向页面的函数,上面的 blogPath/postpostId为2,于是会重定向到相关的页面

根据这个函数我们可以联想到目录穿越,于是进行尝试:/post/comment/confirmation?postId=../../../my-account,成功重定向到该界面,接着尝试:/post/comment/confirmation?postId=../../../my-account/change-email?email=admin%40145623%26submit=1(特殊字符记得编码),成功修改邮箱

于是在漏洞利用服务器中可以写出如下代码:

1
2
3
<script>
document.location = "https://0acd0042033dadfd81f92ff80050003c.web-security-academy.net/post/comment/confirmation?postId=1/../../my-account/change-email?email=pwned%40web-security-academy.net%26submit=1";
</script>

保存,发送给受害者,题目解决