一个存在了32年的漏洞惊现Telnet服务器

Hacker News Top 新闻

摘要

在GNU inetutils Telnetd中发现了一个预认证远程代码执行漏洞(CVE-2026-32746),由于其遗留代码库,影响了多个操作系统。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/09/17 06:07

# 一个32岁的漏洞走进Telnet服务器(GNU inetutils Telnetd CVE-2026-32746预认证RCE) 来源:https://labs.watchtowr.com/a-32-year-old-bug-walks-into-a-telnet-server-gnu-inetutils-telnetd-cve-2026-32746/ 很久很久以前,在一个没有二进制利用缓解措施、Unix系统仍广泛存在的时代,一个预认证Telnetd漏洞诞生了。事实上,这个漏洞诞生得如此之早(早在1994年),甚至可能比你年纪还大。为了说明其存在时间之久:它与开创性电影《黑客》同年问世。那是如此久远的年代,RISC还只是一个遥远的梦想。仔细想想,也许它甚至是Zero Cool本人的产物?无论如何。近期,这个漏洞被无情地终结了。 ### 我们在看什么? 如果你不熟悉Telnet,没关系。Telnet是一种网络协议,通过TCP/IP提供命令行界面与远程服务器通信。换句话说,就是远程代码执行服务。典型的部署方式包含认证屏障,要求你在访问系统Shell前先登录。它还以明文方式传输数据——没错,它会以明文在网络上传输你的用户名和密码。如今事实上的替代品是SSH,而Telnet正变得越来越罕见。 ### 什么是CVE-2026-32746? CVE-2026-32746由DREAM安全研究团队发现,是一个基于BSS段的缓冲区溢出漏洞,允许攻击者破坏大约400字节的相邻变量。它存在于LINEMODE SLC(设置行模式字符)协商处理程序中。虽然严格来说它只影响GNU inetutils,但大多数厂商的Telnetd实现都基于相同代码,使得影响范围巨大且难以准确评估。我们确认这绝对包括所有主流Linux发行版(我们已检查过)。 遇到这样的漏洞,我们本以为互联网会炸开锅——然而近一周过去了,仍未出现深入分析。我们认为不如先发布我们的研究进展。我们将梳理几件事:我们如何定位漏洞、它能让攻击者做什么(以及在什么条件下),还会讨论为什么这个漏洞的实际利用比你想象的更像是打开潘多拉魔盒。但让我们先从一个显而易见的问题开始——我们确信这是所有生活在技术乌托邦的人们最想问的:为什么现在还要用Telnet?! ## 影响范围? 嗯,这是个棘手的问题。补丁已应用于inetutils-telnetd,但存在许多分支,多年来经过不断修改,这个漏洞被复制粘贴到了各个系统中。我们已在至少以下系统中识别出此CVE: - inetutils-telnetd本身 - Ubuntu - Debian - FreeBSD 13(https://github.com/freebsd/freebsd-src/blob/release/13.5.0/contrib/telnet/telnetd/slc.c?ref=labs.watchtowr.com#L120-L139)/ FreeBSD 15 Port(https://github.com/cschuber/freebsd-telnet/blob/b5dd01748b7502f924fce5acbd0cbfd6ae3dfd4f/contrib/telnet/telnetd/slc.c?ref=labs.watchtowr.com#L121-L139) - NetBSD 10.1(https://github.com/NetBSD/src/blob/0c68c1d9688de20a9c362bd84a74965ede947609/libexec/telnetd/slc.c?ref=labs.watchtowr.com#L127-L145) - Citrix NetScaler - Apple Mac Tahoe(https://github.com/apple-oss-distributions/remote_cmds/blob/b9a6fd09f17da3781b70c0c11838524146e0682d/telnetd/slc.c?ref=labs.watchtowr.com#L125-L143) - Haiku(https://github.com/haiku/haiku/blob/f833d107c6174e0ddd435dc0287609f9cda0ddc3/src/bin/network/telnetd/slc.c?ref=labs.watchtowr.com#L121-L139) - TrueNAS Core(https://github.com/truenas/os/blob/truenas/13.3-stable/contrib/telnet/telnetd/slc.c?ref=labs.watchtowr.com#L121-L139) - uCLinux(https://github.com/scs/uclinux/blob/eb0cf9617bd22b69ad625575a95cf4fa2c140d55/user/telnetd/slc.c?ref=labs.watchtowr.com#L116-L131) - libmtev(https://github.com/circonus-labs/libmtev/blob/e03277aa18847b23cbc9887550d6b229cf5db99c/src/mtev_console_telnet.c?ref=labs.watchtowr.com#L217-L235) - DragonFlyBSD(https://github.com/DragonFlyBSD/DragonFlyBSD/blob/388588cfb81ee391b92eda14679949c8a8e3b596/libexec/telnetd/slc.c?ref=labs.watchtowr.com#L116-L134) ### 2026年了,为什么还用Telnet?MCP在哪里? Telnet自远古时代就存在了。你们很多人现在可能正对着屏幕吼:“谁会用这么不安全的协议?!”好吧,有些人可能会惊讶地发现,历史悠久的Telnet在生产系统中依然广泛存在,原因令人惊讶地多样。也许这是厂商唯一支持的协议(“这台数控机床每分钟宕机损失X千美元,而你想加装……什么?SSH客户端?老兄,它运行在8位微控制器上!”)。也许存在某些深层技术原因导致无法迁移。事实是:人们仍在运行它,正如它在每个主流发行版仓库中的强势存在所证明的那样。它确实经受住了时间的考验。 ### Telnet里有漏洞?! Telnet不就是协议层的Netcat吗?我们的一些读者可能天真地认为Telnet只是TCP流。确实,有些实现正是如此:一个连接到Shell的TCP套接字。简单,不可能被攻破,对吧?然而,事实并非如此。Telnet支持一系列功能:终端控制(想开启回显?关闭它?)、客户端窗口大小协商、认证甚至加密。我们确信在某个地方,某个可怜的家伙负责维护所有这些功能的企业安全性。众所周知,“更多功能”意味着“更多攻击面”。大家都记得CVE-2026-24061(https://nvd.nist.gov/vuln/detail/CVE-2026-24061?ref=labs.watchtowr.com)吧?那个漏洞中,一个用于与服务器共享环境变量的Telnet协议功能被滥用于远程代码执行。相当痛苦,虽然影响范围比今天这个漏洞小,但利用起来容易得多。 这个特定漏洞位于Telnet协议的“LINEMODE”功能中。正如RFC 1184(https://datatracker.ietf.org/doc/html/rfc1184?ref=labs.watchtowr.com)所述: > 当本地端启用编辑的行模式时,网络流量减少为每个命令行几个数据包,而不是每个输入字符几个数据包。这对延迟较大的网络非常有用,因为用户在输入命令行时能获得本地响应时间,只在命令输入后才承受网络延迟。它还有助于减少按数据包计费网络的成本…… 是的,这个漏洞如此古老,可以追溯到网络按“数据包计费”的时代。功能本身的真正细节对我们来说并不那么重要(我们的首要目标就是“黑掉所有东西”)。然而,要访问漏洞代码,我们需要知道如何启用它,这意味着我们需要了解一点Telnet如何协商连接参数。 ### 紧张的谈判 为了互操作性,Telnet客户端和服务器默认不会启用这些高级选项。当连接首次建立时,会通过`0xFF`定义的“IAC”或“解释为命令”字节进行带内信号传输(呵呵,这什么时候出过错?)。有大量可协商的选项,例如本地回显,或者你的电传打字机速度(还记得那些吗?我们不记得了)。然而,正如之前暗示的,我们感兴趣的特性是“LINEMODE”选项。该特性定义的一个名为SLC(“设置行模式字符”)的部分特别有趣。它允许服务器与客户端通信,告知某些特殊功能——比如“退格键”这类炫酷新奇的技术——应由特定控制代码表示。让我们深入一点,看一个假设的协商过程。首先,我们通过TCP连接到Telnet服务器。服务器立即发送以下数据: ``` IAC DO LINEMODE (或十六进制 0xFF 0xFD 0x22) ``` 这里,“DO”表示服务器正在请求该功能。要启用它,客户端响应“WILL”: ``` IAC WILL LINEMODE (0xFF 0xFB 0x22) ``` 一旦协商完成,服务器会发送一系列三字节三元组列表,每个三元组指示要替换的特殊字符、“支持级别”和实际字符值。然后客户端可以仔细查看,决定是否修改任何值,如果需要,就发送一个包含新值的回复——同样是一组三字节三元组列表。 ``` IAC SB LINEMODE LM_SLC IAC SE (0xFF 0xFA 0x03 0xFF 0xF0) ``` 服务器会忠实地将所有这些值存储在一个固定大小的全局数组中,不进行任何边界检查。等等,什么?!没错,这就是漏洞。从1994年起就未被发现,各位。补丁(https://codeberg.org/inetutils/inetutils/pulls/17/files?ref=labs.watchtowr.com)堪称信息安全领域中最接近现代艺术的产物。但等等!还有更“有趣”的!你问还有什么更有趣?好吧,如果*完全相同的漏洞*早在2005年就存在于Telnet客户端而非服务器端呢?是的,这是真的,伙计们。CVE-2005-0469是这个漏洞的“孪生兄弟”——本质上相同,但出现在客户端,其中名为`slc_add_reply`的函数缺少边界检查。修复方法与今天这个漏洞的边界检查补丁完全相同。幸运的是,二十年后,有人想到了检查服务端是否存在同样的漏洞。他们说历史不会重复,但确实会押韵。 ### 走向利用 当然,正如我们许多读者所知,这还不是故事的结束——不,这仅仅是我们痛苦征程的开始。 - 是的,这是一个新漏洞。 - 是的,CVSS评分高达三万万亿分。 但坏人真的能*利用*它做些有用的事吗?嗯。这正是我们要回答的问题。不幸的是,答案有些模糊,需要一些细微的解释。是的,我们可以用受控数据溢出全局变量(从而破坏其后的内存位置)。但欢乐时光到此为止,现实世界开始了。首先,我们能发送到服务器的数据有些受限。如前所述,它是三元组形式:一个函数、一个标志和一个值,每个1字节长。这些各自都有限制,大多在`process_slc`函数中找到。让我们看看它。 ```c void process_slc (register unsigned char func, register unsigned char flag, register cc_t val) { register int hislevel, mylevel, ack; /* * Ensure that we know something about this function */ if (func > NSLC) { add_slc (func, SLC_NOSUPPORT, 0); return; } ``` 这是第一个限制——如果`func`字节大于`NSLC`常量(即`0x1e`),三元组的其余部分将被丢弃。虽然第一个字节完好通过,但其余字节被设置为`SLC_NOSUPPORT`(零)和零本身。然后通过`add_slc`将此三元组添加到全局变量中。不理想,但我们还能处理,对吧?函数的其余部分长什么样? ```c /* * Process the special case requests of 0 SLC_DEFAULT 0 * and 0 SLC_VARIABLE 0. Be a little forgiving here, don't * worry about whether the value is actually 0 or not. */ if (func == 0) { if ((flag = flag & SLC_LEVELBITS) == SLC_DEFAULT) { default_slc (); send_slc (); } else if (flag == SLC_VARIABLE) { send_slc (); } return; } ``` 啊。这也有点麻烦——如果函数为零,我们的三元组*也*不会进入全局缓冲区。嗯。那三元组的其余部分呢?它们应该没有类似的限制吧?好消息是`process_slc`函数不再给我们添麻烦。然而,它确实调用了`change_slc`,后者对我们原来那么好的三元组进行进一步的“改造”。该函数旨在根据服务器的能力转换客户端发送的值,服务器在接受SLC之前会执行一些修改: ```c hislevel = flag & SLC_LEVELBITS; mylevel = slctab[func].defset.flag & SLC_LEVELBITS; /* * If client is setting a function to NOSUPPORT * or DEFAULT, then we can easily and directly * accomodate the request. */ if (hislevel == SLC_NOSUPPORT) { slctab[func].current.flag = flag; slctab[func].current.val = (cc_t) _POSIX_VDISABLE; flag |= SLC_ACK; add_slc (func, flag, val); return; } ``` 这里发生了什么?!好吧,服务器取“标志”(我们三元组中的第二个字节)的最低两位来设置“hislevel”。这表示客户端支持的级别(Telnet服务器似乎认定客户端为男性)。如果这是`SLC_NOSUPPORT`(零),则标志的`SLC_ACK`(0x80)位被设置,并添加到SLC数组中。最终结果是,如果标志字节的最低两位为零,最高位会被设置。嗯。`0xFF`值也会得到特殊处理——根据当时的需求,可能是祝福也可能是诅咒。如你所见,三元组中的每个字节都会被检查,如果发现是`0xff`,则会加倍为`0xff 0xff`。这使我们能将三元组从3字节扩展到4、5甚至6字节。这对漏洞的可利用性有关键影响。 ```c if ((*slcptr++ = (unsigned char) func) == 0xff) *slcptr++ = 0xff; if ((*slcptr++ = (unsigned char) flag) == 0xff) *slcptr++ = 0xff; if ((*slcptr++ = (unsigned char) val) == 0xff) *slcptr++ = 0xff; ``` 这确实阻止了我们偷偷混入奇数个连续的`0xff`字节——我们无法表示`0x11 0xff 0x22`,尽管可以表示`0x11 0xff 0xff 0x22`甚至`0x11 0xff 0xff 0xff 0xff 0x22`。这是一个主要障碍,特别是在64位x86系统上,通常在特定位置有重复的`0xff`字节。然而,这片乌云确实带来了一线希望:对齐。由于三元组中的三个字节各有不同的约束,我们可以用`0xff`填充缓冲区,稍微不同地对齐我们的三元组,也许——只是也许!——能将我们需要的数据挤进我们想要的位置。 `0xFF`在三元组中的位置 | 字节大小 | 写入的字节 | 对齐偏移 --- | --- | --- | --- 无 | 3 | `[F, flag, V]` | +0 func | 4 | `[0xFF, 0xFF, flag, V]` | +1 val | 4 | `[F, flag, 0xFF, 0xFF]` | +1 func + val | 5 | `[0xFF, 0xFF, flag, 0xFF, 0xFF]` | +2 这种技术在尝试将特定的`func`、`flag`或`val`对齐到相邻变量内的特定偏移量时非常有用。例如,`val`字节可以对齐以覆盖指针的最低有效字节(LSB),实现部分覆盖。从这里开始,代码就变得不那么好简洁解释了,但最终结果是,我们可以确定需要相当大的巧思才能找到满足所有要求的恶意输入。 **然后事情变得更难了。** 我们必须将发送的数据放入单个Telnet“子协商”数据包中,该数据包大小为0x200字节。我们要溢出的`slcbuf`变量本身大小为0x6C字节。它已经包含四个头部字节,这很幸运,但这将我们限制在覆盖紧随其后的0x190字节。这看起来似乎很多,但让我们看看这能给我们带来什么。 ### 我们的尝试 从这里开始,一切都特定于某个Telnetd二进制文件。每次编译时,编译器会按其认为合适的方式放置全局变量,这意味着在一个系统上位于我们覆盖窗口内的全局变量在另一个系统上可能不同。我们花了一些时间审查手头的实现。我们发现了一些有趣的东西,尽管也伴随着残酷的现实。需要预先指出的是,我们没有找到一条直接的利用路径……

相似文章

Linux内核中因单个错误字符导致的高危漏洞

Ars Technica

Linux内核中一个错误的字符引入了一个use-after-free漏洞(CVE-2026-53111),允许非特权用户在Debian和Ubuntu系统上将权限提升至root;该漏洞已修复并移植回旧版本。

OpenBSD的PPP协议栈中存在一个存在27年的认证绕过漏洞

Lobsters Hottest

OpenBSD的PPP协议栈中存在一个存在27年的认证绕过漏洞,攻击者通过发送长度为零的用户名字段和密码字段,利用PAP处理器中缺失的边界检查,无需凭证即可获得完整的PPPoE访问权限。同样的代码还允许内核堆内存越界读取。