Hyaika Blog

Penguin is all you need

技术

一条链接,三个文件:Telegram 桌面版那个忘了转义的分号

一条链接,三个文件:Telegram 桌面版那个忘了转义的分号

一条链接,三个文件:Telegram 桌面版那个忘了转义的分号

目录

  • 你被拉进了一个群
  • 一条链接,为什么变成了两条进程
  • 分号:数据里不该出现的边界
  • interpret:——只给发布脚本留的专属通道
  • 三个默认值:漏洞从「理论可行」到「一键可得」
  • 我写了个最小复现:1 行变 3 条
  • 修复只有一行,而且它静悄悄

你被拉进了一个群

某天,有人把你拉进一个 Telegram 群。Telegram 的默认设置允许任何人这样做,不需要你点同意。

群里有一个链接,看起来是个普通网址。你点了它。然后你的账号,就不再只属于你了。

这个漏洞编号 CVE-2026-107181,CVSS 3.1 评分 8.1(High)。它的发现者 BeakSec 在 10 月 3 日公开了完整分析。整个攻击链拆开来看,没有一环是「黑客炫技」——每一环都是桌面软件里再普通不过的设计,只是它们恰好能首尾相连。

一条链接,为什么变成了两条进程

现代操作系统允许程序注册 URI scheme:tg:// 开头的链接归 Telegram 管。系统遇到这种链接,就启动 Telegram,把整条 URL 当作命令行参数传进去。

问题来了:如果 Telegram 已经在运行呢?

操作系统不会检查,它会照常再拉一个进程起来。两个一模一样的 Telegram 进程,谁是新来的?谁又是老板?

Telegram 自己的做法是:新进程试着连一个本地 socket。连上了,说明已经有一个实例活着,于是新进程把链接序列化成一行文本,扔给 socket 里的老进程,然后自己退出。

注意这一步:socket 不传对象,只传字节。新进程内存里的 URL 对象没法直接穿过这条通道,必须被压扁成一串字符——把结构化的数据变成文本,再把文本还原成结构化,这个过程叫序列化/反序列化。而每一次这样的搬运,都是「边界从结构里消失、变成字符」的时刻。

Telegram 的协议很简单:每条指令是 关键字:参数;,分号结尾。打开一个链接,就是:

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

老进程收到后,在每一个分号处切一刀,把每一段当作一条独立指令处理。

分号:数据里不该出现的边界

现在,如果这个「参数」里自己就带着一个分号呢?

一个正常的链接 tg://x?a=1 在 Telegram 里没有对应处理函数,点了也白点——它只是个载体。但攻击者构造的链接长这样:

tg://x?a=1;OPEN:interpret:指令文件;CMD:quit

对新进程来说,这个分号只是 URL 参数里的一个普通字符,它照样把整条链接序列化、原样写进 socket:

OPEN:tg://x?a=1;OPEN:interpret:指令文件;CMD:quit;

老进程呢?它按分号切分,得到三条指令:

OPEN:tg://x?a=1
OPEN:interpret:指令文件
CMD:quit

一条链接,变成了三条指令。这就是注入:数据里的一个字符,被解析器当成了边界。写这段代码的人忘了转义——socket 协议自己定义的分隔符,和 URL 里用户可控的字符,撞在了同一个人身上。

interpret:——只给发布脚本留的专属通道

注入本身还不够。如果 Telegram 只能接受 OPEN:、CMD:quit 这种无害指令,那最坏情况也就是把应用关掉。真正致命的是第四条路:OPEN: 接受任意 URL scheme,而 Telegram 内部注册了一个不对操作系统公开的 scheme——interpret:。

这个scheme是干嘛的?它是 Telegram 当年给自己发布流程准备的:新版构建好之后,脚本要把它发到频道里,配一段更新日志。与其手工操作,脚本写了一个小文本文件,里面写上「把哪个文件发到哪个频道」,然后让 Telegram 自己读这个文件、自己发。

这个「解释器」InterpretSendPath 做的事是:读取磁盘上任意路径的文件,发送到一个频道。它不确认是谁请求的,不弹任何确认框。

当年这个设计是合理的:只有发布脚本会走到这条路,而攻击者要是能碰发布脚本的机器,早就自己读文件了。问题在于——一个为特权通道设计的函数,被注入变成了一条链接就能触达的入口。

三个默认值:漏洞从「理论可行」到「一键可得」

到这里,攻击链还差最后一环:攻击者要怎么把自己的指令文件放到受害者磁盘上?

答案是三个默认值——Telegram Desktop 出厂时的默认配置,每一个单独看都合情合理:

默认①:群里收到的文件自动下载。 群文件最大 8MB 会自动落盘,无需点击。文件落到 C:\Users\<用户名>\Downloads\Telegram Desktop\ 下,文件名就是发送者起的名字——位置完全可预测。

默认②:无本地口令。 Telegram 把本地数据加密,但密钥链条是「口令→KEK→DEK」。不设本地口令时,喂进 KDF 的就是空字符串,而盐就明文存在 tdata/key_datas 里。读这一个文件,就够解开 DEK,进而解开所有本地数据——包括那个识别你身份的会话授权。

默认③:允许陌生人把你拉进群。 攻击者建一个群,把你拉进去,自动下载就发生在你「顺便进群看链接」的那一刻。连让你点「同意入群」都不需要。

三个默认值,通过自动下载落在可预测路径;一条被注入的链接(经由一个会 302 跳转到 tg:// 的普通网址),通过 socket 触发注入;interpret: 把三个文件读走、发到攻击者的频道;攻击者用这三份文件重建 tdata,启动 Telegram——你的会话就开了。其中那个神秘的文件夹名 D877F783D5D3EF8C 也不是随机的:它由字符串 data 派生,每台安装都一样。

三个默认值:一台没改过设置的 Telegram Desktop

顺带一提,这个「默认配置就是攻击面」的逻辑,跟国内桌面聊天软件是同构的——任何注册了 URI scheme、又通过本地 socket 传递链接的客户端,都有这样一条「没人知道它存在」的内部通道;区别只在于通道那头挂着的函数有没有权限、认不认人。

我写了个最小复现:1 行变 3 条

我不打算真的去复现攻击(也复现不了,没有 Windows 环境)。但注入的机制本身可以复现——它只是一个「分隔符不转义」的问题。

我写了个 30 行的模拟:一个 UNIX socket 服务器扮演「已在运行的 Telegram 实例」,收到一行文本后按分号切分并派发;一个客户端扮演「新进程」,把 URL 序列化后写进 socket。攻击者链接照抄原报告:

一条 tg:// 链接,两种待遇

tg://x?a=1;OPEN:interpret:指令文件;CMD:quit

未修复的版本(不转义,直接按分号切)输出:

收到 1 行,派发出 3 条指令:
  OPEN: tg://x?a=1
  OPEN: interpret:指令文件        ← 危险的那条,真的被单独派发了
  CMD: quit

修复的版本(发送端把分号转义成 %3B,接收端切完再解码)输出:

收到 1 行,派发出 1 条指令:
  OPEN: tg://x?a=1;OPEN:interpret:指令文件;CMD:quit    ← 整体是一条 URL,没人被坑

同一个链接,两种待遇。差异只有一行代码,但这一行决定了「分号是数据,还是边界」。

修复只有一行,而且它静悄悄

修复提交是 db3405699f,2026 年 9 月 16 日:彻底移除 interpret:// scheme 和 InterpretSendPath,并把 socket 协议的分隔符改成百分号十六进制转义——数据里的分号,从此再也变不成指令边界。9 月 17 日的 7.2.9 修好了问题。

有意思的是,这次修复是静悄悄的:7.2.9 的更新日志只提了一句「修复媒体查看器崩溃」,那条真正堵住攻击链的提交标题叫「Remove legacy interpret path helper」,没有任何安全公告。漏洞是 6 月 25 日通过 ZDI 报告的,厂商在 9 月独立修复后 ZDI 才在 9 月 30 日关案,披露权回到研究者手里——CVE 直到 10 月 7 日才分配。

回顾整条链:socket 序列化信任了 URL 的内容,分号切分信任了参数里的字符,解释器信任了调用者,出厂默认值信任了「用户会自己改设置」。每一层都觉得自己不是边界,于是边界不存在了——直到有人发现,它们恰好可以首尾相连。

修复只有一行转义。而这一行,把边界放回了它唯一该在的地方:发送端。

分享:

评论(0)

暂无评论,来写第一条吧~

发表评论