updated on 20231226:

这篇文章写于20210605,由于记录的内容涉及到工作中碰到的某个漏洞,因此隐藏。现在两年多过去了,我觉得里面记录的问题应该不复存在,所以公开。

现在回过头来阅读,文中对CORS和CSRF的理解不是特别准确,甚至有误人子弟之嫌,另外有些内容也已经过时了,所以读者切勿照搬全信。

今年又做了一次CORS的分享,并且附上了示例代码,感兴趣的可以在这里看到:https://github.com/A11riseforme/CORS-Workshop

以下是原文:

最近碰到的一些开发者好像对CORS和CSRF有点迷糊,我指出一个漏洞的时候,他对着另外一个地方哼哧哈哧地胡搞。这篇文章不讲代码,只讲一些概念,最后讲一个今天坑了我几个小时的漏洞,涉及到一些边角的知识(也有可能是猜想)

CORS其实不是一个漏洞,而是一种技术实现,CORS Misconfiguration才是一种漏洞,而CSRF本身就是一种漏洞。

CORS顾名(Cross-Origin Resource Sharing)思义,是用来在网站发起跨域请求的时候,允许initiator获取响应的一种技术(因为SOP的原因,默认是做不到的)。具体实现就不说了,大概就是在返回头中加上 *Access-Control-Allow-** 的头,用来指定可以获取响应的方法,源,头,具体看MDN的文档就可以了。

CSRF有一个更加形象的名称,用来形容攻击的场景:Session Riding。大概意思就是用户访问网站的时候,是会带上cookie的,所以一些敏感操作可以在不知不觉中引诱用户完成。防御方法有很多,token,referer,还有比较有意思的SameSite cookie,但是我并不打算具体讲,还是看MDN的文档。但是有一些边角案例可以自己debug一下:Chrome现在是已经lax by default了,加上前面两分钟的grace period,firefox还没有跟进,但是可以在about:config的配置页面搜索samesite关键字对相应的默认设置进行修改。有心人可以测试一下firefox开启lax by default之后和谷歌有啥区别,具体可以看一下(non-)top level navigation,和两分钟grace period前后的情况下,firefox和chrome的区别。这个知识大概和“茴香豆的茴字有几种写法”的知识用处差不多。

然后有些程序员认为我把CORS正确配置了,就不会有CSRF了,然而这个也是错的。注意到上面我说CSRF的时候压根就没提CORS?通俗一点的讲,CORS正确配置可以让攻击者没办法读到跨域请求的响应,但是对于CSRF攻击而言,正确的请求从被攻击者的浏览器中发出去的那一刻起,就是成功了,获不获取得到响应,并不重要(可能为了完善攻击链,有CORS权限的话更好)

回到CORS,在发送跨域请求的时候,有一个很重要的头,Origin,注意这个头无法被javascript以任何方法设置,属于浏览器的一个安全措施(仔细想一下,如果可以被绕过,那CORS有啥意义?)。这个头在发送跨域请求的时候被浏览器自动带上,用来被服务端判别initiator是否应该被允许获取到响应。很多CORS的misconfiguration就是直接将Access-Control-Allow-Origin 的值设置为Origin的值了。还有的是信任某个域名下的全部子域,这样的话,任何一个子域有xss的话,CORS就会出现问题了。

另外,在用xhr或者fetch发送跨域请求的时候,在一定情况下,浏览器会发送一个preflight的OPTION方法的请求,用来预先获取CORS配置,然后根据响应来决定要不要发真正要发的请求。上面说的一定情况,其中包括了所有非simple request的request(这其实是废话),具体哪些才是simple request,还是该看MDN的文档,这里大发慈悲发个链接,简单来说,就是被设置了除Accept,Accept-Language,Content-Language,Content-Type以外头,或者Content-Type被设置成了除application/x-www-form-urlencoded,multipart/form-data,text/plain以外的其他类型。以上所说只针对xhr,fetch的话直接将mode为no-cors即可绕过preflight request的发送,当然你把mode设置为no-cors之后肯定是不能获取响应的。

但是要说CSRF和CORS Misconfiguration之间有没有什么联系,答案是肯定有的,因为CORS Misconfiguration之所以是一个漏洞,就是因为首先CSRF的攻击,然后Access-Control-Allow-Origin的配置错误,使得非同源下的initiator可以读取response,导致敏感信息泄露。可以这么说:*CSRF是验证CORS Misconfiguration的先决条件。*

现在讲一下今天debug的一个漏洞:

最开始是一个CSRF操作敏感信息,这种情况直接用一个自动提交的form就可以了验证了,然后自然是python3 -m http.server 12321; firefox http://localhost:12321/csrf.html 一气呵成了。

结果在burp里看到response里有一行Access-Control-Allow-Origin: http://localhost:12321,暗喜,这还能提升PoC体验,可以根据response来判断敏感操作有没有成功。

搞了一个新的PoC,用的xhr,放到网站上去,再访问,

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://\/api/test/*. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://\/api/test/*. (Reason: CORS request did not succeed).

切换到network标签reload一下,发现POST请求之前还有一个OPTION请求,应该就是传说中的preflight request了,响应里没有包含Access-Control-Allow-Origin头。

这下就难搞了,难道这个没有严格按照CORS标准开发吗,OPTION不给配CORS?那有没有办法不让他发这个preflight request?

换成fetch,mode切换成no-cors,确实没有发送preflight request了,但是那这跟用表单提交有啥区别?都不能读取响应。

再后来找到了simple request的定义,试着把Content-Type从 application/json 换到 text/plain(有很多程序并不会严格检查content-type,而是直接parse post body),再访问,果然没有preflight request了,但是还是显示

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://\/api/test/*. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).

难道这么快就修复了吗?本地试一下,没问题啊。把两个request发送到comparer去对比,发现不同的就只有Origin和Referer头了,难道这个网站CORS配置只允许localhost?把域名换成127.0.0.1

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://\/api/test/*. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).

果然,伟大的程序员,CORS配置成允许localhost了,这程序员应该是面向debug编程的吧。

几个小时下来,最后还只是一个CSRF而已,小丑竟是我自己。