返回资讯中心
外部精选
软件工程
#社区热议

Telegram Desktop 漏洞可致任意用户文件被窃取

来源标题:Telegram Desktop vulnerability allowed any user's file to be stolen

Telegram Desktop 的单实例 IPC 存在未转义分隔符漏洞,攻击者可通过一条点击链接读取磁盘上的任意文件并发送给攻击者。该漏洞结合内部 interpret: URI 方案,无需确认即可在受害者不知情时窃取包括会话文件在内的登录凭据。

Hacker Newsg-b-r2026/10/10 · 21 天前30 分钟阅读

以下正文由 AI 翻译自 Hacker News(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。

Telegram Desktop 单实例 IPC 中一个未转义的分隔符,使得一次链接点击就能从磁盘读取任意文件并发送给攻击者,其中包括 session 文件。

有人把你拉进一个 Telegram 群组。聊天中出现一个链接。你点了它,你的 Telegram 账号就不再只属于你了。

Telegram Desktop 会把点击的链接通过本地 socket 以文本形式交给已经在运行的自身实例,并且从不转义它用来分隔命令的那个字符。于是一个精心构造的链接不会作为一个指令到达:它会作为多个指令到达。

我发现的这条链有两个缺陷。第一个是那个注入。第二个是注入的命令所能触及的东西:一个内部 URI scheme,interpret:,它会读取指令文件中指定的文件并发送到某个聊天,既不检查是谁发起的请求,也没有确认步骤。二者结合,就把一次链接点击变成了任意文件读取。在这篇文章中,我会逐步走一遍这条链,然后用它来窃取受害者登录所需的文件。

操作系统允许程序注册 URI scheme,这样当遇到那种链接时,系统就知道该启动哪个应用。Telegram Desktop 注册了 tg。从那以后,系统就知道 tg://... 链接属于 Telegram,并以该 URL 作为命令行参数启动它。

如果 Telegram 没有在运行,进程就会启动,把这个字符串当作参数,转换成一个 URL 对象并在内部处理:单进程,无需通信。

但如果 Telegram 已经在运行呢?操作系统既不知道也不检查:它照样启动一个新进程,和第一个一模一样。Telegram 自己得判断出自己是多余的那个,而它判断的方式是尝试连接一个本地 socket。

已经在运行的实例是服务端:它自启动以来就一直在那个 socket 上监听。新进程是客户端。如果它成功连接,说明已经有一个实例存活,于是它把链接交出去然后退出。

socket 不传递对象,它传递字节。新进程在内存中持有的 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,就仿佛它刚从命令行到达一样。

// sandbox.cpp:453-463 (abbreviated)
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,因为对它来说那个分号只是查询中的一个普通字符。它把它展平并写入 socket:

OPEN:tg://x?a=1;CMD:quit;

正在运行的实例在每个分号处切开,得到两条指令而不是一条:

OPEN:tg://x?a=1
CMD:quit

这就是那个注入,也是两个缺陷中的第一个。

上面的例子注入的是 CMD:,但别被名字误导:它只接受 show 和 quit,所以它最坏也就是关掉应用。

总共接受四条命令,其中三条是无害的。第四条是 OPEN:,而细节就在这里:它接受任意 URL,对 scheme 没有任何过滤。

深入代码会发现 Telegram 内部还有另一个 URI scheme,叫做 interpret:。

操作系统不知道该如何处理以 interpret: 开头的链接,因为它在任何地方都没有被注册为协议处理器:它只存在于 Telegram 自己的代码内部,代码像处理其他任何东西一样从 start-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 用来发布自身版本的工具。当新版本发布时,构建归档需要连同 changelog 作为说明文字一起发布到某个频道。与其手动操作,不如让一个脚本写一个小的文本文件,指定频道、要发送的文件和要写入的文字,然后以该文件路径启动 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.

(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →