-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"## The 2015 commit was a feature\n\nOn 2015-03-19, Mats Erik Andersson committed `fa3245ac` to `inetutils` with the title \"telnetd: Enable autologin in legacy mode.\" The body of the ChangeLog entry says the quiet part: \"Without Kerberos authentication the autologin code was not determining what user name to hand over to the login service. Adding the user name, when applicable, resolves the issue.\"\n\nThe fix was a one-line edit to the template that telnetd uses to build the command line for `/usr/bin/login`:\n\n```diff\n #else /* !SOLARIS */\n- PATH_LOGIN \" -p -h %h %?u{-f %u}\"\n+ PATH_LOGIN \" -p -h %h %?u{-f %u}{%U}\"\n #endif\n```\n\n`%?u{A}{B}` is the template's if-else: if `user_name` is populated (Kerberos has authenticated the caller), insert A; otherwise insert B. Before the commit, B was the empty string. The autologin path simply did nothing when Kerberos was absent, and `/usr/bin/login` fell through to interactive prompt. After the commit, B was `{%U}`: insert the value of whatever the client claimed in the `USER` environment variable.\n\nThe same commit added the `case 'U'` arm to `_var_short_name` in `telnetd/utility.c`:\n\n```c\ncase 'U':\n return getenv (\"USER\") ? xstrdup (getenv (\"USER\")) : xstrdup (\"\");\n```\n\nThe feature the commit advertises is \"let the client tell us who they are when we cannot ask Kerberos.\" The bug is that the way the feature was implemented, the client tells the server who they are by writing characters that get spliced into the argument list of a root setuid binary.\n\n## The chain: NEW-ENVIRON to execv\n\n`expand_line` walks the template character by character. `%U` triggers `_var_short_name`, which returns `getenv(\"USER\")` verbatim. The expanded command string flows into `telnetd/pty.c:start_login`:\n\n```c\ncmd = expand_line (login_invocation);\nif (!cmd)\n fatal (net, \"can't expand login command line\");\nargcv_get (cmd, \"\", &argc, &argv);\nexecv (argv[0], argv);\n```\n\n`argcv_get` is GNU's shell-like word splitter from `argcv.h`. It tokenizes on whitespace, honors quotes, and produces an `argv` array. With the pre-patch template and an unauthenticated caller whose `USER` env var is `-f root`, the expanded `cmd` is:\n\n```\n/usr/bin/login -p -h -f root\n```\n\n`argcv_get` splits that into `{\"/usr/bin/login\", \"-p\", \"-h\", \"\", \"-f\", \"root\"}`. `execv` runs it. `login(1)` from util-linux reads `-f`: \"do not perform authentication, user is preauthenticated.\" The named user is `root`. The connection's pty becomes a root shell before the caller has typed a password.\n\nThe telnet client never authenticated. The `USER` environment variable arrived over the NEW-ENVIRON option, which RFC 1572 defined in 1994 for client-to-server environment exchange. Telnet servers have read `USER` from NEW-ENVIRON since 1994. The shell tokenizer split the value on whitespace. `login(1)` honored its own flag. None of those four components did anything they were not built to do.\n\nThe runnable form:\n\n```bash\nUSER='-f root' telnet -a localhost\n```\n\n`telnet -a` is the client's \"negotiate the NEW-ENVIRON option and ship the current `USER` env value.\" On a Trisquel 11 laptop with the default `inetutils-telnetd` package, Simon Josefsson's advisory documents the result: an interactive `root@trisquel:~#` prompt.\n\n## The CVE names one variable. The patch sanitized seven.\n\nPaul Eggert's first patch (`fd702c0`, 2026-01-20 01:10 PT) is six lines:\n\n```diff\n case 'U':\n- return getenv (\"USER\") ? xstrdup (getenv (\"USER\")) : xstrdup (\"\");\n+ {\n+\t/* Ignore user names starting with '-' or containing shell\n+\t metachars, as they can cause trouble. */\n+\tchar const *u = getenv (\"USER\");\n+\treturn xstrdup ((u && *u != '-'\n+\t\t\t && !u[strcspn (u, \"\\t\\n !\\\"#$&'()*;<=>?[\\\\^`{|}~\")])\n+\t\t\t? u : \"\");\n+ }\n```\n\n`u && *u != '-'` rejects leading dashes, so `-f` no longer reaches argv. `strcspn` against the shell-metachar set rejects whitespace, redirection, quoting, command separators, and anything `argcv_get` would treat as a delimiter. Fail either filter, return the empty string. That commit closes the public proof of concept. NVD's vulnDescription names exactly this variable.\n\nFour hours later, Simon Josefsson committed `ccba9f7`. It extracts the same logic into a function and calls it from every variable expansion:\n\n```diff\n+static char *\n+sanitize (const char *u)\n+{\n+ /* Ignore values starting with '-' or containing shell metachars, as\n+ they can cause trouble. */\n+ if (u && *u != '-' && !u[strcspn (u, \"\\t\\n !\\\"#$&'()*;<=>?[\\\\^`{|}~\")])\n+ return u;\n+ else\n+ return \"\";\n+}\n\n case 'h':\n- return xstrdup (remote_hostname);\n+ return xstrdup (sanitize (remote_hostname));\n case 'l':\n- return xstrdup (local_hostname);\n+ return xstrdup (sanitize (local_hostname));\n case 'L':\n- return xstrdup (line);\n+ return xstrdup (sanitize (line));\n case 't':\n q = strchr (line + 1, '/');\n if (q) q++; else q = line;\n- return xstrdup (q);\n+ return xstrdup (sanitize (q));\n case 'T':\n- return terminaltype ? xstrdup (terminaltype) : NULL;\n+ return terminaltype ? xstrdup (sanitize (terminaltype)) : NULL;\n case 'u':\n- return user_name ? xstrdup (user_name) : NULL;\n+ return user_name ? xstrdup (sanitize (user_name)) : NULL;\n case 'U':\n- { ... inline filter from Paul's patch ... }\n+ return xstrdup (sanitize (getenv (\"USER\")));\n```\n\nSix brand-new `sanitize()` calls. Plus the refactor of Paul's `case 'U'` to use the same extracted function. Seven cases total. The cases:\n\n- `%h` is `remote_hostname`, populated by `getnameinfo()` or `gethostbyaddr()` reverse DNS at connection setup.\n- `%l` is `local_hostname`, from the server's own `gethostname()`.\n- `%L` is the pty `line` device path.\n- `%t` is `line` with the path prefix stripped.\n- `%T` is `terminaltype`, populated by the client over the TERMINAL-TYPE subnegotiation defined by RFC 1091.\n- `%u` is `user_name`, populated by the Kerberos path when AUTHENTICATION is compiled in.\n- `%U` is the `USER` env var the CVE names.\n\nTwo of the six cases the CVE does not name are directly attacker-controlled. `%h` flows from reverse DNS, owned by whoever runs the PTR record for the attacker's IP. `%T` flows from the client's TERMINAL-TYPE subnegotiation, which the attacker types byte for byte. Either one would carry `-f root` into argv on any future template that happened to drop them next to the login binary.\n\nSimon's advisory said this aloud, in the same paragraph that asked for a CVE:\n\n> \"Thus there is potential for similar vulnerabilities for other variables. On non-GNU/Linux systems, only the remote hostname field is of interest. The `remote_hostname` variable is populated in the function `telnetd_setup` from telnetd/telnetd.c by calling `getnameinfo()` or `gethostbyaddr()` depending on platform. This API is generally not considered to return trusted data, thus relying on it to not return a value such as 'foo -f root' is not advisable.\"\n\nCVE-2026-24061 covers `case 'U'`. The patch series closes that case and six others the same patch session decided were the same shape.\n\n## The third commit was the architectural fix\n\nTwo days later, on 2026-01-22, Simon committed `38b78ad`. Title: \"Pass USER to /bin/login after a '--' delimiter.\"\n\n```diff\n #else /* !SOLARIS */\n- PATH_LOGIN \" -p -h %h %?u{-f %u}{%U}\"\n+ PATH_LOGIN \" -p -h %h %?u{-f -- %u}{-- %U}\"\n #endif\n```\n\n`--` is `login(1)`'s end-of-options marker. Everything after it parses as a positional username, not a flag. With `--` in the template, even an unsanitized `-f root` lands as `argv[5] = \"-f root\"`, a username `login(1)` rejects as invalid. The structural defense is one delimiter, three bytes long, in a template that has existed since the 1990s.\n\nThe patch sequence read in order: sanitize one input, sanitize six more inputs, then add the delimiter that would have made the input filter unnecessary. The fix the bug deserved was the third commit. The first two are what shipped on the morning of the disclosure because the third commit was not ready yet.\n\nThe Solaris and SOLARIS10 template branches still do not have `--`:\n\n```c\n#ifdef SOLARIS10\n PATH_LOGIN \" -p -h %h %?T{-t %T} -d %L %?u{-u %u}{%U}\"\n#elif defined SOLARIS\n PATH_LOGIN \" -h %h %?T{%T} %?u{-- %u}{%U}\"\n```\n\nThe SOLARIS branch on line 55 has a `--` between `%?u{` and `%u`, the Kerberos arm, but not before `{%U}`, the env-var fallback the 2015 commit added. The SOLARIS10 branch has no delimiter anywhere. Both branches rely on `sanitize()` and only on `sanitize()`. If `sanitize()`'s metachar set ever drifts off the shell tokenizer's metachar set, or `login(1)` on Solaris adds a flag whose first character is a letter rather than `-`, those branches are the bug again. The Linux branch has both barriers. The other two branches have one.\n\n## The pattern: convention was the allowlist\n\nRFC 1572 (Telnet Environment Option, January 1994) defines NEW-ENVIRON and lists `USER` as one of the \"well-known\" environment variables a telnet client may send. The RFC's definition: \"the name of the user for which the login is being attempted.\" The convention is that `USER` is a username. Usernames on POSIX systems are `[a-zA-Z0-9._-]+` per `useradd`'s default `NAME_REGEX`, and that convention has lived in every spec author's head and every operator's `/etc/passwd` for the thirty-two years since the RFC was published.\n\nThe convention was never enforced. NEW-ENVIRON's wire format is `IAC SB NEW-ENVIRON IS VAR \"USER\" VALUE `. The VALUE field is bytes. inetutils-telnetd parsed at the byte-string type and accepted everything the type permitted. The implicit allowlist (username-shaped strings) was spec prose. The 2015 autologin commit promoted that byte string into argv. The 2026 patch installed a narrower allowlist (no leading `-`, no shell metachars) inside `sanitize()`. What gets through `sanitize()` is whatever happens to satisfy that filter today, plus any `case 'X'` a future maintainer adds to `_var_short_name` without remembering to wrap the return in `sanitize()`.\n\nThis is [Convention Is The Allowlist](/patterns/convention-is-the-allowlist), the protocol-environment-variable instance. The convention lived in RFC 1572 and `/etc/passwd`. The code never installed it. The patch's narrow filter is the developer making the convention partially explicit on the morning of the disclosure. The Solaris template branches are the same convention still informal.\n\n## The 2007 prior art was on bugtraq\n\nCVE-2007-0882 was assigned on 2007-02-12 against Sun Solaris 10 `in.telnetd`. The mechanism was identical: `in.telnetd` passed the `USER` environment variable to `login(1)` as argv, `login`'s `-f` flag bypassed authentication, the operational form was `telnet -l '-f root' host`. The bug was on bugtraq within twenty-four hours of disclosure. Sun's patch added a `--` separator.\n\nMats Erik Andersson's 2015 commit shipped the `{%U}` branch eight years after that disclosure. The codebase is different. The mechanism is not.\n\nSimon's advisory opens, exactly:\n\n> \"If you are tired of modern age vulnerabilities, and remember the good old times on bugtraq, I hope you will appreciate this one. If someone can allocated a CVE, we will add it in future release notes.\"\n\nThe acknowledgment that the trick is old is the disclosure author's reading of the same prior art. The trick was old. The trick worked, eleven years dormant in `inetutils`, until Carlos Cortes Alvarez (Kyu Neushwaistein) tried `USER='-f root' telnet -a localhost` against a Trisquel laptop on 2026-01-19 and got a root shell.\n\n## The KEV gap was six days\n\nThe oss-security disclosure landed at 15:00 CET on 2026-01-20. NVD published CVE-2026-24061 the following day. CISA added it to the Known Exploited Vulnerabilities catalog on 2026-01-26 with a federal-agency remediation deadline of 2026-02-16. GreyNoise Labs observed eighteen unique source IPs running exploit attempts within eighteen hours of the KEV addition, against fewer than three thousand reachable hosts on Censys's `port:23 protocol:TELNET` count.\n\nThe campaign was real. The targets are not high-value. The math is that any vulnerability whose proof of concept fits in a one-line shell command will be scanned in the same week it is disclosed, regardless of how legacy the surface is. `inetutils-telnetd` is not running on Fortune 500 perimeters. It is running on Trisquel laptops, on FOSS-only embedded NAS units, on academic lab routers, and on whatever fraction of Censys's 3K telnet banners actually run inetutils rather than busybox. The attackers do not care about the population. The cost of trying the payload is one TCP connection.\n\n## What the third commit admits\n\nThe first two patches sanitize input. The third adds the `--` delimiter that makes the input filter unnecessary on Linux. The three-commit sequence is a public record of the maintainers working out, in real time, what the actual fix was.\n\nPoC: [tc4dy/CVE-2026-24061-PoC-Exploit](https://github.com/tc4dy/CVE-2026-24061-PoC-Exploit), [K3ysTr0K3R/CVE-2026-24061](https://github.com/K3ysTr0K3R/CVE-2026-24061).","closing_line":"The CVE names one switch case. The patch sanitized seven, added one delimiter, and left two of the three template branches still relying entirely on the input filter.","hook_md":"GNU InetUtils telnetd through 2.7 lets an unauthenticated remote caller log in as root by setting the `USER` environment variable to `-f root` and sending it over the telnet NEW-ENVIRON option. The CVE record names that one variable. Paul Eggert's initial fix at 01:10 PT on 2026-01-20 sanitized one switch case. Four hours later Simon Josefsson extracted the sanitize logic into a function and called it from six more cases the CVE does not name. Two days after that, a third commit added `--` to the login command template so the argv barrier finally matched the input filter. CVE-2026-24061's affected-products field reflects what the public PoC demonstrated, not what the patch closed.","post_id":611,"slug":"inetutils-telnetd-cve-2026-24061-patch-sanitized-seven","title":"CVE-2026-24061: The CVE Names One Variable. The Patch Sanitized Seven.","type":"initial","unreadable_sentence":"CVE-2026-24061's affected-products field reflects what the public PoC demonstrated, not what the patch closed."} -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCaiu6HQAKCRDeZjl4jgkQ Jg08AP9mPRgyVFN4GKTIaTxUUqyPrd15f9LKL5Nmgzc5CkZGZAEA1sMik0SS88FU ftB/TXhzPksx1DFzjdj9wk+MWwdXsg0= =qfqw -----END PGP SIGNATURE-----