Picture a TCP connection to port 23.
No username yet. Maybe no login: prompt. Maybe not even a banner. And still, the server is already doing something surprisingly complicated.
Before anyone logs in, Telnet can start negotiating options that date back to a very different Internet. Back when high latency meant satellite links, and some networks still charged by the packet, protocol designers tried to make remote sessions feel less painful. One result was LINEMODE, later documented in RFC 1184. Inside LINEMODE lives SLC, or Set Local Characters, a small mechanism for telling the terminal how to handle erase, interrupt, suspend, and similar control characters.
That old logic is still present in GNU Inetutils telnetd. And in the middle of it sits CVE-2026-32746.
The bug is simple in shape and serious in impact. While building an SLC reply, the server appends data into a fixed 108-byte buffer without properly checking whether the write cursor has already reached the end. A remote unauthenticated client can send enough SLC triplets in a single subnegotiation to push those writes past the buffer, corrupt nearby global state, and potentially turn later writes into something much more dangerous.
The important part is where this happens. Not after login. Not after PAM. Not after a password check fails. This code runs before /bin/login ever gets involved.
That is what makes this bug worth a closer look. It is not just a memory corruption issue in old C code. It is a pre-auth bug in legacy protocol machinery that is still reachable on real systems in 2026.
This post will layout why Telnet still matters, what happens on the wire before a login prompt appears, how the DREAM Security Research Team found the bug, how process_slc() and add_slc() create the vulnerable condition, why the 0xFF escaping rules make exploitation awkward, and what defenders should do about it.
Many picture Telnet’s model as “open a TCP connection and hope for a shell”. However, that is only the friendly version of the story.
A full Telnet stack is a negotiation. Bytes flow as data until 0xFF appears, and then the stream stops being just data and starts being protocol. IAC. WILL. WONT. DO. DONT. Sometimes subnegotiations wrapped in IAC SB … IAC SE. Sometimes terminal type, window size, environment handling, LINEMODE, and more.
Every one of those features adds parser surface. More importantly, that parser surface is reachable before classic Unix authentication. When we say pre-auth here, we do not mean “before LDAP” or “before the application checks a password.” We mean before the daemon even gets around to running /bin/login.
That is a lot of old protocol logic sitting between a TCP handshake and a login prompt. And old protocol logic has a habit of aging badly.
SSH won the Internet years ago. Everyone knows Telnet is plaintext. Everyone knows it should be gone.
And yet port 23 keeps showing up.
We can still find Telnet on embedded boards, industrial controllers, storage appliances, lab gear, network appliances, and internal management networks where somebody once decided “management VLAN only” was good enough. Sometimes the vendor never shipped SSH. Sometimes the device is too old or too fragile to replace. Sometimes the answer is just inertia.
So a bug in telnetd is not a museum piece. It is a long-tail infrastructure problem. And a bug that needs no password is worse than weak credentials. There is nothing to rotate, nothing to phish, and no account to disable. There is only reachability.
Before a user ever sees login: the client and server may already have had a fairly evolved conversation.
Typical pre-login traffic can include echo handling, suppress go-ahead, terminal type, window size, environment metadata, and LINEMODE. That last one was designed for slow or expensive networks. The idea was simple: let the client handle line editing locally so the network sees one packet per command instead of one packet per keystroke.
That made perfect sense in the early 1990s.
The code is still around today.
SLC is part of that machinery. It carries little triplets that describe special character behavior: which logical function is being configured, which flags apply, and which byte value goes with it. In theory, this is just terminal housekeeping. In practice, it is attacker-controlled input that the server may parse and answer before authentication.
That answer is where the bug lives.
In Inetutils, LINEMODE is option 0x22. The rough flow looks like this:
The vulnerable behavior shows up when the server processes a long SLC blob and builds its reply by calling add_slc() over and over, without checking whether slcptr has already reached the end of slcbuf.
The whole bug lays in one line: repeated appends into a fixed buffer, no effective bound.
The process did not start with a crash dump from production. We treated GNU Inetutils telnetd the way you should treat any network daemon speaking an old protocol: as an untrusted input parser.
That meant starting from the protocol itself. We read the relevant Telnet and LINEMODE material, including the RFC language around SLC, to understand what the server was expected to do on the wire before login. From there, we worked back into the implementation and looked for places where attacker-controlled negotiation data was parsed, transformed, and copied into fixed buffers.
Anything reachable before login was in scope. LINEMODE and SLC stood out early because they combine attacker-controlled wire data, fixed C buffers, and loops that append repeatedly. That combination tends to age badly.
Tracing the code led straight into telnetd/slc.c.
add_slc() advances slcptr as it appends reply bytes. process_slc() has a helpful branch for unknown function codes. If func > NSLC, it responds with add_slc(func, SLC_NOSUPPORT, 0) and returns. That means a malicious client can pack a large number of “unknown” triplets into a single SLC subnegotiation and force repeated writes into the reply buffer.
The issue was reported to GNU Inetutils with a reproducer and coordinated disclosure. The fix was then tracked under PR #17.
The reply buffer is a static array:
start_slc() writes a four-byte subnegotiation header into slcbuf and then sets: slcptr = slcbuf + 4; That leaves 104 bytes of body space before the end of the array. Then add_slc() starts appending bytes:
There is no check here that slcptr still points inside slcbuf.
The trigger path is simple. For unknown SLC function codes, process_slc() takes a shortcut:
NSLC is 18. So every triplet with func > 18 causes at least one more append into the reply buffer.
The math is not subtle. After the four-byte header, the array has room for roughly 35 minimal triplets at three bytes each. The 36th takes slcptr past the end. A single SLC subnegotiation can carry far more than that. Keep going and the write does not stop at one stray byte. It keeps walking into adjacent static storage.
This is not a tiny overflow. It is a sustained out-of-bounds write.
Overflowing a fixed buffer is bad. Overflowing a fixed buffer next to a write cursor is much worse.
In slc.c, slcbuf lives near other global state. Exact layout depends on the build, compiler, and surrounding code, but one nearby value is especially interesting: slcptr, the pointer that tells add_slc() where the next write goes.
If an attacker manages to corrupt slcptr, the next append does not just continue overflowing the original buffer. It can start writing to whatever address the corrupted pointer now holds. That is how a simple memory corruption bug starts looking like a write-what-where primitive.
This is also where the story gets less neat. Some builds place slcptr in an easy spot. Others do not. Different compiler options, sanitizers, and global layouts change what gets hit first. The vulnerability class stays the same, but the exploitation path is not identical across binaries.
The func > NSLC path is useful because it is predictable. Other paths are less cooperative.
If func == 0, process_slc() takes special-case paths and may call default_slc() or send_slc() instead of appending the triplet in the way an attacker might prefer. For known functions, change_slc() may rewrite flags or values before the final append.
So the attacker is not working with arbitrary free-form bytes from start to finish.
Even so, one reliable hammer remains: lots of triplets with func > 18, each one triggering add_slc(func, SLC_NOSUPPORT, 0) until the buffer is gone.
That is enough to make the bug real.
Telnet has another wrinkle. 0xFF is not just another byte. It is IAC, and if it appears inside data, the protocol doubles it.
That means one logical triplet does not always serialize to three bytes. Sometimes it becomes four. Sometimes five. Sometimes six. That changes alignment, and once you care about precise overwrite positions, alignment matters a lot.
At a high level:
This is a big part of why the bug is easier to classify than to weaponize. The overflow is real. The bytes you get are constrained.
From a defender’s point of view, this bug deserves its severity.
It is pre-auth, network reachable, requires no credentials, and lives in a service that may still run with high privileges. That is exactly the sort of bug defenders should patch or isolate quickly.
From an exploit developer’s point of view, the picture is messier.
Yes, there is memory corruption. Yes, the process may be privileged. But ASLR, PIE, NX, RELRO, 64-bit pointer layout, Telnet quoting rules, and per-binary global layout all change how hard it is to turn the bug into reliable code execution. A CVSS 9.8 score does not promise a universal one-click exploit.
This was not the first bounds-related bug in Telnet SLC handling.
Back in 2005, CVE-2005-0469 affected the client side in similar logic, where slc_add_reply also lacked the check it needed. Different binary, same kind of mistake, same family of protocol code.
Old protocol features have a habit of teaching the same lesson more than once.
A proper fix should stop add_slc() from advancing slcptr beyond the end of slcbuf.
That can show up behaviorally. A patched server may drop or truncate triplet data that an older build would have written past the end of the array. In theory, that gives defenders a fingerprinting angle. In practice, active probing against fragile Telnet stacks is not always a great idea.
For most teams, the boring answer is still the right one: inventory Telnet exposure, identify inherited or forked codebases, patch what you can, and isolate what you cannot.
Disable telnetd where possible.
Restrict TCP 23 from untrusted networks.
Upgrade to an Inetutils build that includes the relevant fix or a vendor backport.
Search asset inventories for exposed Telnet, especially in OT, embedded, and appliance-heavy environments where “legacy” often means “still critical.”
If port 23 shows up in a place nobody expected, assume there is more protocol surface behind it than the service name suggests.
Discovery and report: Adiel Sol, Arad Inbar, Erez Cohen, Nir Somech, Ben Grinberg, and Daniel Lubel of the DREAM Security Research Team.
Upstream fix: Collin Funk and the GNU Inetutils maintainers.
For readers looking for a shorter advisory-style summary, Dream Security Labs also published a public advisory on CVE-2026-32746 covering the vulnerability overview, affected versions, impact, and recommended mitigations: Dream Security Labs advisory on CVE-2026-32746