这篇文章以Portswigger官网的Lab来总结CSRF相关的知识点,CSRF的基础原理这里不再赘述,涉及到的浏览器基础知识主要是和Cookie相关,可以见Cookie安全属性。在做LAB的过程中也学到了很多以前没有注意到的知识点,下面就根据各个LAB来归纳总结。
LAB记录
CSRF Token的设计缺陷
CSRF vulnerability with no defenses:不多说,对重要的表单提交没有任何的防护。
CSRF where token validation depends on request method:使用CSRF Token作为表单的隐藏参数,但是在请求方法是GET的情况下没有进行校验。
CSRF where token validation depends on token being present:使用CSRF Token作为表单的隐藏参数,在CSRF Token参数不提交的情况下不进行校验。
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,所以归于一类
- CSRF where token is tied to non-session cookie
使用CSRF Token作为表单的隐藏参数,CSRF Token与另一个Cookie参数csrfKey
绑定而不与用户会话绑定
1 | POST /email/change |
此时如果我们能控制受害者的csrfKey
并且能知道对应的token就可以绕过限制。而浏览站点发现首页的搜索功能,会通过设置Cookie的形式将用户最后一次搜索的内容记录下来
测试后又发现此处存在CRLF注入,那么我们可以利用这一点来设置用户Cookie的方式来重新设置受害者的csrfKey
,然后再使用攻击者的csrf
参数构造表单:
最终的Payload如下:
1 | <html> |
受害者在点击恶意链接后,首先会通过img
标签,触发CRLF注入将自身站点的Cookie参数csrfKey
设置成攻击者的,同时包括SameSite=None
,这样这个Cookie就会被允许跨站点提交;而由于这不是一个合法图片,之后会触发onerror
方法,将第一个表单中的内容进行提交,十分的巧妙。
- CSRF where token is duplicated in cookie
在测试中发现,服务器是直接根据当前页面名为csrf
的Cookie参数生成包含对应Token的表单页面,即token的内容完全复制cookie中的参数。所以假如能控制受害者Cookie参数,就等于能控制Token,这个LAB中仍然存在CRLF注入问题,所以攻击手法和上个LAB一样。
利用_method
参数重写http方法绕过
- SameSite Lax bypass via method override
在这个LAB中,Cookie设置了Lax
属性,POST的方法无法携带Cookie,而导航类的GET方法可以。又因为有些Web框架支持通过_method
参数覆写http请求方法,所以可以用一条URL构造非GET方法的HTTP请求,POC如下:
1 | <script> |
当受害者点击链接后,页面将受害者导航至修改邮箱的页面并携带Cookie,同时利用URL参数_method
将HTTP请求方法设置成POST,从而完成CSRF攻击
利用客户端重定向绕过
- SameSite Strict bypass via client-side redirect
SameSite值为Strict,这里的客户端跳转本质上来讲是一种可以用来重新构造HTTP请求的Gadget。在测试过程中发现,当提交任意评论后,会有一个/post/comment/confirmation?postId=
的请求,然后在这个页面停留一会后页面会进行自动跳转。从/resources/js/commentConfirmationRedirect.js
内可以看到实现原理,利用postId
来动态生成跳转的地址,然后等待3秒后跳转,当然从数据包里也可测试出来。
于是思路就是:
构造链接跳转到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
所以完整的POC如下
1 | <html> |
表单中的postId其实就是将如下payload进行html编码的结果:
../../../my-account/change-email?email=JJBB%40normal-user.net&submit=1&_method=POST
利用其他子域名的漏洞绕过
- SameSite Strict bypass via sibling domain
这个LAB中存在一个WebSocket协议的聊天功能,这个地方使用Cookie进行会话记录,并且观察到在向Server发送READY状态后,服务器会返回当前身份的历史会话
所以可以利用如下的POC进行攻击
1 | <script> |
但Cookie属性为Strict
无法跨站提交
但测试中发现资源文件的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的目的。
以下是最终的POC,即将上一步的WebSocket的POC URL编码后贴到反射型XSS参数处,再使用BurpSuite自带的模块生成CSRF POC即可
1 | <html> |
受害者在点击链接后向cms域名发起登录请求,触发反射型XSS;之后在cms域名提交CSRF表单获取聊天记录并利用外带技术发送的DNSLOG服务器。这个LAB攻击的巧妙之处在于利用兄弟域名上的漏洞绕过了Cookie的SameSite=Strict
的限制,利用CORS
策略绕过了跨域发送HTTP请求的限制。
默认Lax策略在120s的生效期
- SameSite Lax bypass via cookie refresh
这个LAB涉及到一个知识点:没有显式的设置SameSite属性,此时浏览器默认按照Lax策略执行,但是为了不影响单点登录,在最开始的120秒内不会生效(如果显式的设置SameSite=Lax
则没有这个问题),我在本地测试了下2分钟内,还真可以CSRF。也就是说需要在用户获取Cookie后的120秒内攻击成功或者在攻击前强制刷新用户Cookie
分析OAUTH的登录过程,发现当访问/social-login
时,如果已经走过一遍OAUTH的认证流程了,已经存在OAUTH服务器Cookie的情况下,会自动进行第三方登录。所以此时的思路如下:
先让浏览器访问
/social-login
获取一个新的服务器Cookie在120秒内提交CSRF表单
1 | <form method="POST" action="https://YOUR-LAB-ID.web-security-academy.net/my-account/change-email"> |
但是由于浏览器安全策略,在默认情况下会拦截无交互的弹窗行为
所以只能继续在界面上诱导用户点击了,POC如下:
1 | <form method="POST" action="https://0ab900040476404284911ee2001a008f.web-security-academy.net/my-account/change-email"> |
Referer校验的设计缺陷
- CSRF where Referer validation depends on header being present
存在Referer
校验,但如果去掉Referer
则不进行校验
可以利用<meta name="referrer" content="no-referrer">
来告诉浏览器不携带referrer
头,完整POC如下:
1 | <html> |
- 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 | <html> |
受害者在点击CSRF链接后进行表单提交,Referer
头原本是攻击者的服务器,但由于history.pushState
方法修改了历史条目,Referer
在后缀添加了一个目标服务器的域名作为GET参数,从而绕过了限制。
知识总结
PortSwigger的LAB质量确实高,全刷完学到很多以前不知道的知识点
- Cookie在没有显式得设置
SameSite
的条件下,浏览器默认采用Lax
策略,但会在120秒后才生效,如果显式的设置则没有这个问题 - 可以利用
history.pushState
语句在不刷新页面的情况下修改当前URL地址,从而达到修改Referrer
的目的 - 可以用
<meta name="referrer" content="no-referrer">
,告诉浏览器不携带Referrer
头部 - 可以在页面来源的服务端配置
Referrer-Policy: unsafe-url
,告诉浏览器携带Referrer
头部时不去掉URL参数
防御方式
- CSRF tokens:对于重要表单,引入CSRF Token,该Token需要与当前用户的会话绑定,在提交表单的同时,校验当前Token的合法性。
- SameSite cookies:设置Cookie的
SameSite
属性为Strict
或Lax
。虽然从LAB上看还是有机会绕过,但本质都是利用其他漏洞组合进行绕过。 - Referer-based validation:Referer容易被绕过,校验需要严格检查,需要考虑
Referer
缺失的情况,在获取URL后,利用语言的URL API进行解析判断是否为合法的host