CVE-2023-4809: FreeBSD pf bypass when using IPv6

A few months ago, as part of our investigations on IPv6 security in the NetSecurityLab @ Sapienza University, we discovered a vulnerability that allows attackers to bypass rules in pf-based IPv6 firewalls in particular conditions. Let’s see some details of this vulnerability.

This vulnerability has been assigned the ID CVE-2023-4809.

Background

FreeBSD is a FOSS UNIX-like operating system derived from the original BSD (Berkeley Software Distribution). It is widely used in routers, servers, and firewall appliances.

It includes three different firewalls: pf (which is the affected firewall), ipfw, and ipfilter. pf is derived from the OpenBSD pf, and it is also used in many dedicated operating systems such as pfSense and OPNSense.

IPv4 and IPv6 protocols allow for fragmenting packets when they are too large for the (smallest) MTU between the two endpoints. The destination host usually reassembles fragments, although firewalls and IDS/IPS may reassemble fragments for filtering purposes.

pf is capable of reassembling IP fragments when using the “scrubbing” feature. This feature allows matching the original IP packet against the filtering rules in layers higher than the network layer. Without scrubbing, the firewall can filter fragments only using information up to the network layer (e.g., MAC/IP addresses).

Fragmentation in IPv6

IPv6 packets have a fixed header size, plus an optional list of “extension headers” for optional features (such as IPSec, IP fragmentation, etc.).

The IPv6 fragmentation is implemented by splitting the payload of the IPv6 packet into multiple fragments: each fragment contains the packet identifier (the same for all fragments of a given packet), the fragment offset, the next header type, and a flag indicating whether that fragment is the last or not.

These IPv6 fragments are normal IPv6 packets, with the addition of an IPv6 fragmentation header. Normally, an IPv6 fragment contains only one fragmentation header.

A particular type of IPv6 fragment is the “Atomic fragment”: these packets are fragmented using only one fragment. In other words, the fragmented transmission is composed of only the first and only fragment.

The vulnerability

A FreeBSD with pf as firewall for IPv6 traffic and scrub enabled to reassemble IPv6 fragments is vulnerable to an attack that uses IPv6 fragmentation to bypass the rules.

Suppose we have two networks (one for the attacker and one for the victim) and the following ruleset for pf:

ATTACKER_IF = "em2"
VICTIM_IF = "em1"

# Drop packets when blocked
set block-policy drop

# don't filter on the loopback interface
set skip on lo

# Activate scrub and antispoof on $ATTACKER_IF and $VICTIM_IF interfaces
scrub on $ATTACKER_IF all
scrub on $VICTIM_IF all
antispoof quick for { egress $VICTIM_IF }
antispoof quick for { egress $ATTACKER_IF }

# Default action on no-match
block log all label "Default deny rule"

# Allow traffic from the victim network to the attacker network (but not the
# other way around!)
pass in quick on $VICTIM_IF label "allow incoming packets from victim net"
pass out quick on $ATTACKER_IF label "allow outgoing packets to attacker net"

# Block TCP, UDP, and ICMPv6 traffic from the attacker and allow the rest
pass out log from {any} to {any} keep state allow-opts label "let out anything from firewall host itself"
block in quick on $ATTACKER_IF inet6 proto {tcp, udp, ipv6-icmp} label "Block TCP/UDP/ICMPv6"
pass in  quick on $ATTACKER_IF inet6 label "Allow the rest of IPv6"

With this ruleset, we expect that all TCP, UDP, and ICMPv6 traffic is blocked from the $ATTACKER_IF interface because of the 2nd-last rule in the ruleset, while the rest is allowed (3rd-last rule and last one).

Fragmented traffic should not pose a threat because they should match these rules thanks to the scrub on $ATTACKER_IF all rule. And, for regular fragmented traffic, this statement is true.

Now, suppose that the attacker sends a non-compliant IPv6 atomic fragment, where the attacker puts multiple IPv6 fragmentation headers inside the packet.

This packet is clearly a violation of the IPv6 specifications (see RFC 8200, RFC 9099, and RFC 5722), as the fragmentation header can occur only once in a fragment. The correct course of action is to drop the packet.

Our tests show that FreeBSD handles these atomic fragments by rebuilding the original packet (thanks to the scrubbing process) but fails to apply any rule that applies to layers four and higher (e.g., the 2nd-last rule from the pf configuration above). By doing so, the fragment matches other rules in the firewall configuration, and it is allowed to pass through. Additionally, the scrubbing process has fixed the packet, so now any operating system behind the firewall accepts the packet.

We verified that we could establish a full two-way communication between the attacker and the victim behind the FreeBSD firewall, in addition to UDP traffic and ICMPv6 traffic between them.

FreeBSD advisory and solution

The FreeBSD advisory is available at this link: https://www.freebsd.org/security/advisories/FreeBSD-SA-23:10.pf.asc.

The solution is to update FreeBSD to the latest version. All FreeBSD versions up to (but not including) 13.2-STABLE, 13.2-RELEASE-p3, 12.4-STABLE, and 12.4-RELEASE-p5 are affected.

The fix was pushed in the Git repository in this commit: https://github.com/freebsd/freebsd-src/commit/76afcbb52492f9b3e72ee7d4c4ed0a54c25e1c48

Timeline

We followed the responsible disclosure process described by the FreeBSD Security Team. We reported the vulnerability at 2023-07-11 20:15:43 UTC and received the ACK at 2023-07-12 01:34:54 UTC. The fix was pushed in the FreeBSD Git repository at 2023-08-04 13:23:49 UTC, and the advisory was announced at 2023-09-06 18:10:00 UTC.

Acknowledgements

We would like to thank everyone involved, Philip, Ed, and Kristof from the FreeBSD project and Ad from OPNSense, for their support during the investigation and the prompt fix.