Save thesmartshadow/001cea595e75fed6aaea7389666dc9eb to your computer and use it in GitHub Desktop.
Ali Firas (thesmartshadow)
agent/mibgroup/smux/smux.c , smux_accept() CWE-400 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N (8.7) CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (7.5)
snmpd accepts SMUX connections from inside its single-threaded select() loop and then does a blocking recvfrom() on the accepted socket. The five second receive timeout the function prepares is not installed until after the peer has authenticated, so a peer that connects and sends nothing is never timed out. Since smux_accept() runs synchronously in the main loop, that one stalled read stops the whole agent. Normal SNMP requests on the configured UDP transport go unanswered for as long as the attacker keeps the TCP socket open.
No credentials are needed, and no SMUX configuration is needed either. On any build that includes the smux module the listener comes up on 0.0.0.0:199 whether or not snmpd.conf mentions SMUX at all.
I pulled smux.c from every 5.x tag from v5.0 up to current master and compared smux_accept() in each. The ordering is identical everywhere: blocking read first, setsockopt(SO_RCVTIMEO) after smux_open_process() returns. Line numbers move around ( 670 / 714 in v5.8 through v5.9.4, 669 / 713 in v5.9.5.2, 668 / 712 in the 5.10 pre-releases, 673 / 718 in master) but nothing the logic changes. Debian 12 ships v5.9.3, which is in that range.
The ordering goes back to the SMUX rewrite of 1999-03-01, commit ee0daa3b7 ("completely re-written smux modules", patch from Nick Amato). The setsockopt call was already sitting after smux_open_process() in that commit. Nobody has looked at it since.
One later change matters for the fix. Commit 4800683c32 (2007-03-22, "Better handling of SMUX socket descriptors", SF patch 1678788) wrapped the read in a retry loop:
That loop retries on EAGAIN . So just moving the setsockopt earlier will not fix anything: the timeout fires, returns EAGAIN , and the loop goes straight back into the blocking read. Both things have to change together.
I could not find a CVE or advisory for this. Red Hat bug 110931 ("snmpd opens port 199 (smux) even if smuxpeer isn't present") notes the listener behaviour as a configuration surprise, but does not touch the read.
smux_accept() sets up a timeout at the top of the function:
then accepts the connection and immediately reads, unauthenticated, at smux.c:669 (line numbers here are v5.9.5.2):
tv has not been applied to the socket at this point. The setsockopt that applies it is 44 lines further down, after smux_open_process() has returned and authentication has succeeded, at smux.c:713 :
So the timeout only ever protects sockets that already authenticated. The one read an arbitrary host on the network can reach has no timeout on it.
What turns this from a leaked descriptor into a full outage is where smux_accept() gets called. It runs directly in the agent's main select() loop rather than handing the socket to the event loop as a non-blocking descriptor:
select() reports that the listening socket is readable, meaning a connection is pending. The handler then blocks on the accepted socket, whose readiness was never tested. There is no other thread. Everything stops.
Worth ruling out explicitly, because it is the obvious wrong guess: this is not peer table exhaustion. npeers++ runs at smux.c:721 , after authentication, so an unauthenticated attacker never increments the counter and SMUXMAXPEERS (10) is never approached. One connection is enough.
One TCP connection carrying zero bytes suspends SNMP service on the host. The agent usually runs as root, and on most deployments it is the monitoring path, so the outage is also blind: the operator loses the telemetry that would have reported it.
There is nothing to craft here. No handshake to finish, no credential to guess, no packet to build. Connect and wait. Service comes back when the attacker lets go of the socket, or when someone restarts snmpd .
The exposure is also wider than the feature's intended audience, because the listener does not require the feature to be configured. An operator who writes agentaddress udp:127.0.0.1:161 has bound SNMP to loopback and reasonably believes the agent is not reachable from the network. It is, on TCP/199, on every interface.
Start snmpd from a build with smux compiled in, using a config with no SMUX directives at all:
Check the listener is up before you start:
The GET below is a hardcoded SNMPv2c sysDescr.0 request, so the PoC does not depend on net-snmp's own client tools (which is the point, since the agent under test is the target).
Output on Debian 12 with the distribution snmpd package (net-snmp 5.9.3), run twice:
The agent answers normally, stops the moment the idle connection opens, is still dead past eight seconds (which is how you know the five second timeout is not in force), and recovers as soon as the socket closes.
What should happen instead: the accepted socket carries a receive timeout before any unauthenticated data is read, and a timeout closes the descriptor instead of restarting the read. An idle or slow peer should cost one file descriptor and nothing else.
This is a placement bug, not a design choice
The timeout value exists in the function and is initialised before accept() is even called. The code clearly intends a five second bound on SMUX reads. It just installs it on the wrong side of the authentication step.
Nothing in smux.c , smux.h , or README.agent-mibs marks the module deprecated or unsupported; README.agent-mibs lists the SMUX-backed tables as ordinary agent MIBs, and at least one major distribution ships it enabled.
Separately from the timeout, the listener contradicts the agent's own access control model. agentaddress is the documented way to restrict where snmpd is reachable, and the SMUX socket ignores it, binding INADDR_ANY regardless and doing so without any directive asking for it. Whatever the conclusion on the timeout, an operator who bound the agent to loopback did not agree to a world reachable TCP port.
Two changes in smux_accept() . Neither one is sufficient alone.
Install the timeout before the unauthenticated read, and retry only on EINTR . With a timeout in force, EAGAIN is the timeout firing, and it has to fall through to the existing length <= 0 branch that logs "peer died or timed out" and closes the descriptor. The now redundant setsockopt block further down can go.
A better fix, worth considering on its own: stop reading synchronously from the main loop entirely. Mark the accepted socket non-blocking and register it with the agent's existing select() set, so no peer, idle or slow or hostile, can occupy the event loop. That also closes the variant where a peer dribbles one byte every four seconds and holds the agent indefinitely without ever tripping a per-read timeout.
The default bind address deserves a look too. real_init_smux() only falls back to 127.0.0.1 when NETSNMP_ENABLE_LOCAL_SMUX is defined, which it is not in a default build; otherwise it binds INADDR_ANY . Defaulting to loopback, or not opening the listener at all unless a smuxpeer or smuxsocket directive is present, would remove this exposure for the large majority of installs that never use SMUX.
For a regression test: start the agent with a SMUX enabled build and no SMUX directives, open a TCP connection to 199 and send nothing, then within the timeout window plus some margin assert that a normal SNMP GET still succeeds and that the agent has closed the SMUX descriptor. Current code fails both halves.
Where this is exposed
Debian 12, package snmpd 5.9.3+dfsg-2+deb12u1:
smux is compiled in and NETSNMP_ENABLE_LOCAL_SMUX is undefined, so real_init_smux() binds INADDR_ANY . It is called unconditionally from agent/snmp_agent.c , not gated on any config directive. Starting the packaged agent with the two line config above gives:
Ubuntu and other Debian derivatives inherit this build config and should be assumed affected. Distributions that leave smux out of --with-mib-modules are not. Embedded and vendor firmware builds that pass a broad module list are worth checking one by one.
Preconditions for the whole thing: smux compiled into snmpd , and TCP/199 reachable. No SNMP credentials, no smuxpeer entry, no SMUX configuration of any kind.
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
