255 First: A Deep Dive into CVE-2026-24061

255 (IAC): Interpret As Command (Introduction)

I wrote this post because I wanted to understand exactly why the CVE-2026-24061 proof of concept (PoC) works. I could run it, but when I read the code the mechanism was not obvious, and I did not really understand what it was doing during the Telnet setup phase. That sent me down a rabbit hole into Telnet negotiation, NEW-ENVIRON, and the point where telnetd hands control to the system login process. This write-up is the result of that digging, with the aim of making the PoC readable and understandable rather than just something you can execute.

CVE-2026-24061 is triggered during Telnet session setup but the impact is realised later, when authentication begins. The issue occurs before any user interaction, when Telnet negotiation is used to establish state that is then inherited by the system login process. This is not a flaw in the Telnet protocol itself. It is an GNU InetUtils telnetd implementation issue, where user-controlled setup data is carried forward and affects how /usr/bin/login is started. This affects GNU InetUtils telnetd up to and including version 2.7. Upstream has published patches and distributors have issued updates.

The PoC shows how a Telnet client can supply structured data during connection setup which is later consumed when the login program is started. Telnet does not perform authentication, but it does determine the state that the login process starts with, and that is where this vulnerability sits.

Background: How Telnet Really Works

From a user perspective, Telnet appears simple. A connection is made, a banner appears, and the user is prompted for credentials. Behind the scenes there is an initial negotiation phase that occurs before the login program is invoked.

This negotiation phase allows the client and server to agree on capabilities such as terminal behaviour and environment variable handling. These exchanges are not visible to the user and do not involve typed input. They are carried out using control bytes embedded in the data stream.

A key concept is that Telnet is not purely text based. It combines human readable characters with protocol commands. These commands are introduced by a special byte value, 255, known as IAC or Interpret As Command.

Any data following this byte is treated as an instruction rather than text.

Root Cause

The root cause of CVE-2026-24061 is a misplaced trust boundary during the Telnet connection setup phase. Specifically, GNU InetUtils telnetd accepts and retains client-supplied session state before authentication has begun and before the login program is executed. In affected versions, that pre-auth state can influence how the daemon starts the system login process.

This is not a flaw in the Telnet RFCs. Telnet negotiation is just the delivery mechanism. The vulnerability is in telnetd’s implementation, where unauthenticated setup-time data is allowed to bleed into the login invocation path.

In the vulnerable code path, telnetd builds its login invocation from a template string (commonly referred to as login_invocation) and expands placeholders into a final command line. The important placeholder here is %U, which resolves to the daemon’s USER environment variable. In the vulnerable flow, unauthenticated Telnet session setup state can influence the environment telnetd later consults (including USER), meaning %U can expand to attacker-controlled content.

The following excerpt shows how telnetd maps %U to the daemon’s USER environment variable (formatting condensed for readability):

/* telnetd expands %U from the daemon environment */
case 'U':
    return getenv ("USER") ? xstrdup (getenv ("USER")) : xstrdup ("");

The vulnerability becomes exploitable because the value substituted via %U is not guaranteed to be a “plain username”. If the substituted value begins with - and includes whitespace (for example -f root), it stops behaving like data and starts behaving like arguments from the perspective of /usr/bin/login. At that point the daemon is no longer passing “a username”; it is effectively allowing untrusted pre-auth state to participate in login’s option parsing.

The template below is representative of the login_invocation style used by GNU InetUtils telnetd and shows the design choice that matters for this issue: conditional expansion that can fall back to %U when no authenticated username is available. This snippet is illustrative (not a verbatim copy from upstream):

/* Illustrative example (not verbatim upstream code) */
const char *login_invocation =
    PATH_LOGIN " -p -h %h %?u{-f %u}{%U}";

Once a daemon is constructing a command line from a template, the second ingredient that creates risk is executing the expanded string as a single command rather than invoking login with a strictly constructed argv[]. The snippet below is deliberately simplified pseudocode to show the general pattern; it is not real telnetd code:

/* Pseudocode – illustrates the risk pattern, not real telnetd code */
char *login_cmd = expand_template(login_invocation);   /* expands %h, %u, %U etc */
char *argv[] = { "/bin/sh", "-c", login_cmd, NULL };
execv(argv[0], argv);

Telnet is only how the unauthenticated value arrives. The bug is that telnetd allows that value to cross the boundary into the way /usr/bin/login is started, where it can be interpreted as options rather than as a username.

Further reading: upstream GNU InetUtils fix/patch notes and the SafeBreach root cause analysis:

Why Typing the Payload Does Not Work

Typing the payload directly at the login prompt does not produce the same result because it occurs at an entirely different stage of execution. By the time the prompt is displayed, the login program is already running and is explicitly expecting a username as input. Any value entered at this point is handled as user data and processed according to normal authentication rules. Flags or control characters supplied here are not reinterpreted as execution parameters.

In contrast, the exploit succeeds because the payload is delivered earlier, during the setup phase, before the login program exists. The key difference is timing: this value is accepted during session setup and later influences how telnetd starts /usr/bin/login. When an unsafe implementation later consumes those variables, it does so under the assumption that they are trusted. The distinction is not the payload itself, but when and how it is introduced. Timing, rather than content, is what enables the vulnerability.

How the Proof of Concept Exploit Interacts with Telnet

The proof of concept does not rely on malformed packets or undefined behaviour. Instead, it implements a minimal but correct Telnet client that explicitly handles protocol negotiation. The exploit operates entirely within the bounds of the protocol as specified.

Telnet embeds control instructions directly into the data stream. These instructions are identified by a leading control byte and are processed separately from user visible text. The proof of concept monitors the incoming stream for these control sequences and responds to them manually, rather than delegating this behaviour to a standard Telnet client implementation.

This approach allows the client to make deliberate decisions about which options it accepts and how it responds when the server initiates negotiation.

To clarify where this vulnerability occurs within the lifecycle of a Telnet connection, the following diagram illustrates the execution flow from initial connection through to login initialisation. It highlights the distinction between protocol negotiation and authentication logic, and shows the point at which environment variables are supplied and accepted by the server. The key observation is that the NEW-ENVIRON exchange takes place entirely before the login process is started, allowing unauthenticated input to influence the execution context inherited by the authentication mechanism.

Telnet control commands and their meaning

Telnet distinguishes protocol instructions from user text by embedding control commands directly into the data stream. These commands are identified by a leading byte with the numeric value 255, referred to as IAC or Interpret As Command. When this byte is encountered, the receiver knows that what follows must be interpreted as protocol logic rather than displayable text.

Immediately following the IAC byte is a command code. These command codes are single byte values that define how negotiation proceeds. The most relevant codes used by the proof of concept are shown below:

255  IAC   Interpret As Command
253  DO    Request the other side to enable an option
254  DONT  Request the other side to disable an option
251  WILL  Agree to enable an option
252  WONT  Refuse to enable an option
250  SB    Begin subnegotiation
240  SE    End subnegotiation
39   NEW-ENVIRON Environment variable exchange option

These values are the exact bytes transmitted on the wire.

DO, WILL, DONT, and WONT in practice

Negotiation always follows a request and response pattern. One side requests an option using DO or DONT, and the other responds using WILL or WONT.

For example, when the server wants to know whether the client supports environment variable exchange, it sends the following sequence of bytes:

255 253 39

Which translates to:

IAC DO NEW_ENVIRON

This means that the server is asking the client whether it supports the NEW-ENVIRON option.

The proof of concept exploit responds explicitly rather than allowing a standard Telnet client to decide. The response is:

255 251 39

Which translates to:

IAC WILL NEW_ENVIRON

This exchange is implemented directly in the code:

if cmd == DO and opt == NEW_ENVIRON:
    sock.sendall(bytes([IAC, WILL, NEW_ENVIRON]))

By responding with WILL, the client explicitly agrees to participate in environment variable exchange. This agreement is required for the exploit to proceed.

Refusing unnecessary options

To minimise unintended behaviour, the proof of concept refuses all other options requested by the server. If the server sends a DO request for any option other than NEW_ENVIRON, the client responds with WONT.

This behaviour is implemented as follows:

elif cmd == DO:
    sock.sendall(bytes([IAC, WONT, opt]))

At the byte level, this produces responses of the form:

255 252 <option>

Which translates to:

IAC WONT <option>

This ensures that the client only enables the single capability required for the exploit and avoids entering additional negotiation states.

Acknowledging server capabilities

In some cases, the server may announce that it will perform a particular option by sending a WILL command. When this occurs, the client acknowledges the capability by responding with DO.

This behaviour is implemented here:

elif cmd == WILL:
    sock.sendall(bytes([IAC, DO, opt]))

This maintains protocol correctness and prevents the connection from stalling due to unacknowledged negotiation.

Subnegotiation and the role of SB and SE

Some Telnet options require structured data rather than a simple yes or no. These options use subnegotiation. Subnegotiation blocks are framed by two specific command sequences:

255 250   IAC SB   Begin subnegotiation
255 240   IAC SE   End subnegotiation

Everything between these markers is option specific and interpreted according to the negotiated feature.

For the NEW-ENVIRON option, this data contains environment variable names and values. The exploit payload is delivered entirely inside such a block.

Why these codes matter to the exploit

These numeric command codes define when the server is willing to accept environment data and how that data must be structured. The proof of concept does not bypass negotiation. It completes it correctly.

By responding with WILL to a DO NEW_ENVIRON request, the client signals that it is capable of supplying environment variables. When the server then initiates subnegotiation, the client delivers the payload inside a correctly framed SB and SE block.

This exchange occurs before authentication begins and before the login program is executed. The Telnet Environment Option explicitly allows it, and the server accepts it as valid NEW-ENVIRON data.

The problem appears when the login program starts and reuses this data without checking where it came from.

Where negotiation becomes state

At this point in the exchange, negotiation has already completed. telnetd has accepted that the client supports NEW-ENVIRON and has initiated subnegotiation. The proof of concept does not influence this process further. Instead, it responds to the telnetd request by supplying environment data in the format defined by the protocol.

This is the moment where protocol mechanics transition into persistent server-side state. The control codes no longer describe capability. They define how the following bytes are to be stored and later reused.

Setting the USER environment variable

When the server initiates NEW-ENVIRON subnegotiation, the proof of concept constructs a response that supplies a USER value via the Telnet Environment Option. In the vulnerable inetutils telnetd implementation, that client-supplied USER value is later forwarded into /usr/bin/login without sufficient validation.

The relevant code is shown below:

def handle_subnegotiation(sock, sb_data, user_payload):
    if len(sb_data) > 0 and sb_data[0] == NEW_ENVIRON:
        env_msg = (
            bytes([IAC, SB, NEW_ENVIRON, IS, VAR]) +
            b'USER' +
            bytes([VALUE]) +
            user_payload.encode('ascii') +
            bytes([IAC, SE])
        )
        sock.sendall(env_msg)

This block sends a single Telnet subnegotiation message that declares a variable named USER and assigns it the value provided by user_payload. The framing bytes ensure that the data is interpreted as environment configuration rather than interactive input.

The payload itself is defined earlier in the proof of concept:

user_payload = "-f root"

At the time this value is sent, it is not interpreted or validated. telnetd accepts it during session setup and retains it as session state that is later used when starting /usr/bin/login.

What matters is that telnetd retains this client-supplied USER value and later expands it into the /usr/bin/login execution, where it can be parsed as arguments rather than a username.

Interpreting the payload at execution time

Up to this point, everything described happens inside Telnet; the effect of the payload only becomes visible once Telnet hands control to the login program. Telnet itself does not interpret usernames, flags, or authentication options. Its role is limited to session setup and starting the login process. To understand why the injected value is meaningful, it is therefore necessary to look at how the login program interprets its inputs.

The following output shows the supported arguments for the system login binary on my machine:

┌──(maximus㉿testing)-[~]
└─$ login --help

Usage:
 login [-p] [-h <host>] [-H] [[-f] <username>]

Begin a session on the system.

Options:
 -p             do not destroy the environment
 -f             skip a login authentication
 -h <host>      hostname to be used for utmp logging
 -H             suppress hostname in the login prompt
     --help     display this help
 -V, --version  display version

For more details see login(1).

This output shows that -f is a documented and supported option. Its purpose is explicit. When supplied, it instructs the login program to skip authentication for the specified user. Under normal circumstances, this option is only used by trusted local components that have already established identity through other means.

The exploit does not cause the login program to behave unexpectedly. It causes the login program to follow a path that is normally only reachable by trusted local components, even though the state that triggers it originated from an unauthenticated source. When telnetd feeds that value into the login start-up, login ends up seeing -f root as arguments, not as a username.

This is why the payload is effective. It is not parsed or validated during authentication because it is not introduced during authentication. It is introduced earlier, stored as session state, and only interpreted when the login process consumes inherited state at execution time.

Why this is the exploit boundary

This is the point at which unauthenticated input stops being treated as transient protocol data and becomes part of the state the system uses to start the login process. During Telnet session setup, the server accepts environment variables supplied by the client and stores them as session state. At this point, nothing has failed. The data is accepted, but it is not yet used for anything security sensitive.

The failure occurs later, when that state is reused without reconsideration. Once Telnet negotiation completes, the server starts the login process. When the login program is executed, it inherits the environment that was established during session setup, including the USER variable provided earlier by the client.

In vulnerable implementations, the login process does not treat this inherited value as an opaque string. Instead, it is reused in a way that affects how the login process is invoked. This typically occurs through argument construction, wrapper scripts, or execution paths that assume the USER variable contains a legitimate username. At that point, the value is no longer just data. It becomes part of the login process itself.

By the time this happens, the login program is no longer handling network input. It is operating on process state that it assumes was created locally and can therefore be trusted. The inherited value is not validated or constrained because it is not expected to originate from an unauthenticated client. Authentication logic proceeds on the assumption that the execution context is sound.

Once the login process is started with this inherited environment, the outcome is determined immediately. Depending on how the value is consumed, authentication checks may be bypassed or altered before any password handling takes place. No further input from the client is required. The behaviour of the login process has already been set by the way it was invoked.

From the server’s point of view, nothing unusual has occurred. The login process starts in the expected way and runs according to the state it was given. Any access granted is a result of how the process was launched, not the result of a successful authentication exchange. At that point, the exploit is complete. Control has been obtained through manipulation of pre-authentication state rather than through interaction with the login mechanism.


Conclusion

CVE-2026-24061 demonstrates a failure that occurs entirely outside the visible authentication flow. The protocol negotiation is just the delivery mechanism; the vulnerability is in how telnetd builds the /usr/bin/login call. The issue does not sit behind a login prompt, and it does not depend on malformed input or unexpected protocol behaviour. It exists because state accepted during session setup is later reused by the authentication process without being reconsidered.

The proof of concept works by using standard Telnet negotiation and the Telnet Environment Option (NEW-ENVIRON) as specified. Environment variable exchange is negotiated correctly, data is supplied in the expected format, and the server accepts it as session state. The problem is not how the data is received, but how it is later consumed. By the time the login process starts, the execution context has already been shaped by unauthenticated input.

This is why the exploit cannot be reproduced through interactive use of the service. Typing values at a login prompt occurs too late. The decisive action happens earlier, during protocol negotiation, at a point where the server implicitly trusts what it is given.

← back