十月三日安全研究员 BeakSec 公开了 Telegram Desktop 的一条完整攻击链。漏洞编号 CVE-2026-107181CVSS 评分 8.1属于高危。入口低得离谱攻击者把你拉进一个群在群里放一个普通 https 链接你点一下账号就不再只属于你。不需要安装任何东西不需要输入密码也不需要你在 Telegram 里点击任何东西只需要一次跳转。漏洞在九月十六日的提交 db3405699f 中被修复随 7.2.9 版本于九月十七日发布更新日志里却只提了一句渲染修复。本文不复述这条新闻而是把它拆成三段可以分别审视的工程缺陷。第一段是进程间序列化没有转义分隔符第二段是一个内部协议被外部输入触达。第三段是本地会话密钥的封装在默认配置下形同虚设三段串起来才构成一次账号接管。每一段单独看都不致命合起来却把一次点击变成了凭据交付。这也是它值得逐层拆开的原因真正的风险很少来自一个惊人的错误多来自三个都不算严重的默认值。按厂商时间线漏洞在六月二十五日经 ZDI 上报厂商在九月十六日独立修复。九月三十日 ZDI 以已修复结案披露权回到研究者手上十月三日发布完整分析。CVE 编号则在十月七日才正式分配比修复晚了三周。这条时间线本身说明一件事修复已经上线但绝大多数用户并不知道自己该升级。一条链接为什么能变成三条指令Telegram Desktop 在系统里注册了 tg 协议遇到 tg:// 链接系统就启动它。启动时系统把整条 URL 当作命令行参数递过去。如果 Telegram 已经在运行新启动的进程必须自己判断出自己是多余的那一个。它判断的方式是尝试连接一个本地套接字这个套接字由早已在运行的实例持有。连得上就说明有实例活着新进程把链接交给老进程然后自己退出。这个设计本身没有问题它是桌面应用实现单实例的常规做法。套接字只能传字节不能传对象所以内存里的 URL 对象必须先被拍平成一行文本。这个动作叫序列化它的逆操作叫反序列化两者是所有跨边界数据结构的必经环节。Telegram 用的格式非常朴素一条关键字一个参数一个分号收尾。发送端按这个格式拼装一行链接对应一条指令。接收端把收到的字节按分号切开每一段都当成一条独立指令重新执行。到这里为止分号既是数据的一部分也是结构的边界两种身份没有区分。如果参数本身含分号发送端不会转义它接收端却仍然把它当作边界。于是一条链接到了对面就变成两条指令这就是注入的全部机制。看一个最小例子一条链接里塞进分号和另一条命令。tg://x?a1;CMD:quit发送端认为这是一整条 URL因为分号对它来说只是查询串里的普通字符。接收端切开之后得到两段第一段是打开链接第二段是退出命令。CMD 只接受 show 和 quit能造成的最大后果是关掉应用。这个例子也解释了为什么只加过滤不够分号在发送侧是数据在接收侧是结构两边对同一个字符的理解不一致。真正的杠杆在四条被接受的命令里唯一危险的那条也就是 OPEN它对协议头没有任何过滤。OPEN 接受任意 URL这条没有过滤的通道才是攻击链的第二节。值得补一句实现细节接收端在切分后对重建出的 URL 还做了长度截断上限是 8192 个字符。这个上限防的是超长输入防不住语义层面的注入因为注入不需要长。上面那行还不到二十个字符就已经把一条打开链接的指令改写成两条。interpret: 把发布工具变成了读文件原语顺着代码往下挖会找到一个只在 Telegram 内部注册的协议头叫 interpret。操作系统不认识它因为它从来没有注册为系统级协议处理器。它只存在于 Telegram 自己的代码里被启动链接列表像普通 URL 一样捡起来。这一点很重要不走系统协议注册也就绕过了系统级的安全提示。它原本的用途很正当官方发版时需要把构建包连同更新日志发到一个频道。与其手工操作脚本会先写一个小的指令文件写明目标频道、要发的文件、以及配文。然后用 interpret 协议把这个文件的路径交给 Telegram让它自己完成发布。指令文件是纯文本一行一个字段例如来源账号、目标频道、文件路径和配文。其中来源账号会与当前登录账号比对用来防止从错误的账号发版。但这行是可选的不写就跳过检查等于这道防线默认关闭。目标频道则必须是频道或超级群这个限制在攻击者眼里反而是便利。真正的执行函数叫 InterpretSendPath它做三件事打开指定文件、读出内容、发送到指定会话。它不做的事情更重要不校验调用者是谁也不弹任何确认框。当它只从命令行被调用时这个设计没有问题因为攻击者得先拿到本机落脚点。可一旦同一条路径能从套接字抵达也就是能从一条被点击的链接抵达性质就完全变了。一个特权动作缺少授权检查这是链条上的第二个缺陷也是最容易被低估的那个。把两段拼起来攻击者只需要一条形如 OPEN 加 interpret 的注入指令。指令文件里指向要偷的文件目标频道指向攻击者自己的群。剩下的问题是那个被读取的指令文件要怎么放到受害者磁盘上一个可预测的位置。还有一个小细节值得注意这个函数每次调用只发送一个文件。它看起来像是限制了攻击面实际上只是把攻击次数从一次变成多次。三个文件一把空密码的锁答案比想象中简单直接当成聊天附件发过去。Telegram Desktop 的默认设置会自动下载群里收到的文件阈值是 8 MiB。文件落在一个标准目录下沿用发送者选定的文件名不需要受害者点击。Windows 上的路径是用户目录下的 Downloads再进 Telegram Desktop 子目录。路径里似乎还有一个未知量也就是 Windows 用户名但这一点也被绕开了。interpret 同样接受相对路径而相对路径从 Telegram 自己的工作目录解析。在 Windows 上那是数据目录位于用户主目录下三层Downloads 恰好就在主目录里。于是一条带三个点回退的相对路径就能在不知道用户名的情况下命中目标。唯一需要小心的是文件名冲突重名会让系统改存成带序号的名字路径就不再确定。解决办法是选一个足够独特的文件名让碰撞概率降到可以忽略。另外有些格式走内部缓存比如贴纸、GIF 和语音它们不会落到可寻址的磁盘路径。所以攻击者只能用普通文档格式来投递指令文件这是这条链上为数不多的约束。解释器每次调用只发送一个文件指令文件里写多个 file 行只有最后一行生效。这个限制也不成立因为注入不会被第一条命令截断每个分号都能再开一条。三个目标就是三个指令文件加一条链接里三条堆叠的命令。接下来要回答的是为什么读三个文件就等于拿走账号。Telegram 不在本地明文保存数据包括会话授权在内一切都经过加密。它采用密钥封装一把数据加密密钥负责加密用户数据本身是长随机值。另一把密钥加密密钥只负责加密那把数据密钥它不是密码而是从密码经派生函数得来。派生需要一个盐值这个盐值和被加密的数据密钥一起明文存在 key_datas 文件里。默认情况下 Telegram Desktop 没有设置本地密码需要用户自己去设置里开启。没有密码时参与派生的输入是空字符串密钥加密密钥就只由空串和公开的盐决定。也就是说拿到 key_datas 一个文件就能重算出封装密钥并解出数据密钥。有了数据密钥其余数据全部可解包括会话授权文件本身。会话授权所在的文件名并不是随机生成的它由默认数据名派生每台机器都一样。第三个文件是账号数据的索引本身不含秘密但缺少它会话不会加载。攻击者也可以自己构造一份索引这个文件更多是顺手带走而不是必需。把这三个文件放进一个全新的 tdata 目录启动 Telegram受害者的会话就打开了。这就是从一次点击到账号接管的完整链路中间没有任何一步需要受害者输入密码。换个角度看真正的失败点不在加密算法而在密钥派生输入的默认值。空字符串作为口令让封装层的全部密码学强度都归零。投递链接时踩到的两个细节攻击需要一个点击而且这个点击必须发生在 Telegram 之外。在聊天里点击 tg:// 链接不会经过套接字而是在进程内直接处理没有注入点。普通 https 链接则会交给系统浏览器打开因为 Telegram Desktop 没有内置浏览器。于是攻击者发一条普通 https 链接让自己控制的服务器把它重定向到构造好的 tg:// 地址。重定向发生在新进程与老进程之间受害者看到的只是一个正常网页跳转。浏览器是否弹确认框取决于用户此前是否已经使用过这个协议处理器。这一步是整条链里唯一需要用户配合的环节也是最容易被低估的一环。研究者的概念验证把七步写得很清楚建群拉人、发三个指令文件、发链接、等点击。六步之后三个文件已经被上传到攻击者的群全程没有出现任何确认对话框。第七步是攻击者用这三个文件重建 tdata 目录直接打开受害者的账号。整个过程里受害者只做了一件事就是点了一下那个看起来无害的链接。修复提交做了什么官方修复落在提交 db3405699f九月十六日进入代码库次日随 7.2.9 发布。它做的最直接的一件事是把 interpret 协议和支持函数整体删除。那个函数本来就是为官方发版脚本服务的内部工具删掉它不影响任何用户功能。第二件事是给单实例套接字上的记录分隔符加上转义。写入前字段里的特殊字符先按百分号加十六进制编码读出后再解码。这样数据里的分号不再可能变成结构边界注入的物理前提被消除。提交还加了两条纵深防御同一条连接上出现 OPEN 记录时跳过 CMD 与 CTRL 记录。以及一旦这条连接上出现过非本地 URL后续的本地文件路径一律丢弃。这两条不是必需品但它们把同一类错误在别处复发的后果压低了。值得注意的是修复方式整个协议头被删除而不是给它补一个权限判断。对于只为内部脚本存在的特权通道删除通常比加固更彻底。同一条链上的两处缺陷一处用转义修一处用删除修这个取舍值得记住。下面这张表把修复前后两条路径摆在一起差别一目了然。环节7.2.8 及更早7.2.9 及以后字段序列化分号原样写入接收端按分号切分百分号十六进制转义切分后解码interpret 协议存在可读任意文件并外发协议头与执行函数整体删除命令组合OPEN 与 CMD、CTRL 可同时生效出现 OPEN 时跳过 CMD 与 CTRL路径处理非本地 URL 之后仍接受本地路径非本地 URL 之后丢弃本地路径从工程角度看这次修复的形状比补丁本身更值得借鉴。它没有试图让危险函数变得安全而是让它不再可达。对内部工具类代码减少可达性往往比增加校验更省事也更不容易漏。为什么这类缺陷会在桌面端反复出现把注意力从 Telegram 移开这条链的形状其实很常见。桌面应用为了做单实例普遍会用本地套接字或命名管道转发启动参数。这类通道很少被当作不可信输入面来设计因为它在直觉上属于本机内部通信。但它的输入源头恰恰是可以被外部网页和链接触发的命令行参数。第二个常见形状是私有协议头它们绕过系统协议注册因此也绕过了系统级的确认弹窗。当一个特权动作既没有系统确认也没有应用内确认它就成了一个无提示的原语。第三个形状是本地数据保护默认不启用用户不主动设密码加密就等于零。这三个形状各自独立只有在被串起来时才产生高危后果。更麻烦的是它们分布在三个不同的模块通常由三拨不同的人维护。套接字协议属于平台层协议头属于应用层密钥封装属于存储层。单看任何一层评审都能给出合理结论把三层连起来看才会看到这条链。所以真正值得复用的判断是不要只看单个组件是否安全要看可达性图。一个函数从命令行调用是否安全和它从网络可达的链接调用是否安全是两个完全不同的问题。很多漏洞报告的根因都可以归结为这两个问题被混为一谈。把可达性图画出来的成本很低只需要问一句这个输入最初来自哪里。可运行的复现与加固代码下面的脚本用三十行复现这条链的协议层并给出转义后的对照。它不依赖 Telegram只依赖标准库可以直接运行。import urllib.parse SEP ; def vulnerable_build(command, argument): return f{command}:{argument}{SEP} def encode_field(value): return urllib.parse.quote(value, safe) def build_record(command, argument): return f{command}:{encode_field(argument)}{SEP} def parse_records(payload): records [] for chunk in payload.split(SEP): if not chunk: continue command, _, argument chunk.partition(:) records.append((command, urllib.parse.unquote(argument))) return records if __name__ __main__: evil tg://x?a1;OPEN:interpret:instructions.txt print(no escaping :, parse_records(vulnerable_build(OPEN, evil))) print(with escape :, parse_records(build_record(OPEN, evil)))运行结果是两组长度不同的记录列表前者被切成了两条后者仍然是一条。差别只来自一个 quote 调用但它决定了参数里的分号是数据还是边界。把这个思路推广协议设计里有三条可以照抄的规则。第一任何分隔符都必须有转义方案且转义必须在写入侧完成。第二接收侧不能在切分之后再判断合法性切分本身就已经是不可逆的信息损失。第三跨进程通道上的字段要显式标注可信级别来自命令行与外部的字段默认不可信。这三条不解决权限问题但能把注入这一类问题从根上移除。权限问题需要另外一条规则特权动作必须有明确的调用者校验。如果这个动作只为内部脚本存在最省事的做法是让它根本不可从外部抵达。把这段代码放进单元测试也很直接断言解析结果的长度即可。未转义版本断言长度为一会失败转义版本断言长度为一会通过。这类测试写起来只要几行却能在协议演化的每一次改动里守住边界语义。上线前该查的五件事如果你在维护桌面客户端下面五项可以直接照着查一遍。第一项列出所有本地 IPC 通道逐个确认输入字段是否转义是否有长度上限。第二项列出所有未注册到系统的私有协议头确认它们是否能被外部链接触发。第三项给每个特权动作标出调用者校验的位置找不到校验就当作缺陷。第四项检查本地数据加密是否依赖用户主动开启默认关闭就等于没有。第五项检查自动下载是否会把文件写到可预测路径并允许相对路径解析。这五项的共同点在于它们都能在没有攻击者的情况下单独验证。你不需要构造完整攻击链只需要确认每个环节是否存在。对普通用户来说可操作的缓解有三条按有效性排序。最有效的是升级到 7.2.9 或更高版本这是唯一真正关闭问题的动作。其次是打开逐文件询问保存位置自动下载不生效指令文件就落不到磁盘上。再次是把谁能拉你进群限制为联系人窃取的文件只能发到频道或超级群这条断掉了交付点。设置本地密码也有价值它不能阻止文件被读走但能让被偷走的会话无法使用。这四条缓解里只有第一条是根治其余三条都只是抬高门槛。把这几条合起来看这条链最值得记住的不是某个具体函数名。而是一个朴素的事实安全边界由数据的可达路径决定不由代码的归属模块决定。一个只在内部使用的工具一旦能被一条链接触达它就必须按对外接口来审查。Telegram 的修复恰好体现了这一点删掉协议头比给协议头加校验更彻底。如果你在自己的项目里也维护着类似的内部通道今天就可以拿它对照上面五项查一遍。