Reported by Mark Dowd from McAfee: 1. Upper Layer Protocol Mismatch A problem exists within the netfilter code that is responsible for matching upper layer protocols with rules that are installed by ip6tables (using the protocol "-p" switch) on the system. When a packet is fragmented, the following code in ip6_packet_match() is executed to evaluate whether the incoming packet is a match or not: ---- /* look for the desired protocol header */ if((ip6info->flags & IP6T_F_PROTO)) { int protohdr; unsigned short _frag_off; protohdr = ipv6_find_hdr(skb, protoff, -1, &_frag_off); if (protohdr < 0) return 0; *fragoff = _frag_off; dprintf("Packet protocol %hi ?= %s%hi.\n", protohdr, ip6info->invflags & IP6T_INV_PROTO ? "!":"", ip6info->proto); if (ip6info->proto == protohdr) { if(ip6info->invflags & IP6T_INV_PROTO) { return 0; } return 1; } /* We need match for the '-p all', too! */ ---- This code uses ipv6_find_header() to encapsulate the following logic for fragmented packets: a. If the fragment is a 0-offset fragment, cycle through extension headers to find the upper layer protocol for this packet. If one is not found, return -1. b. If the fragment is not a 0-offset fragment and the fragment header's nexthdr value is not an extension header, return it as the upper layer protocol. c. If the fragment is not a 0-offset fragment and the fragment header's nexthdr value is an extension header, return -1. Using these rules, it is possible to create a fragmented packet that incorrectly evades a match. To illustrate this, consider a scenario where the following rule is installed: ip6tables -A INPUT -p udp -j DROP This rule should drop all UDP packets. This rule can be evaded by sending the following pair of packets: [ IP Header (nxthdr=fraghdr) ] [ fraghdr (offset=0, nxthdr=destopts) ] [ destopts (nxthdr=routeopts) ] [ IP Header (nxthdr=fraghdr) ] [ fraghdr (offset=X, nxthdr=routeopts) ] [ routeopts (nexthdr=UDP) ] [ UDP Data ] The first fragment will not match the DROP udp rule because it doesn't contain a UDP header, and the second one will not match the rule either because the fragment offset is non-zero and the nexthdr in the fragment extension is not UDP. Kernel Versions tested: 2.6.18 2.6.16 2.6.15 (Others supporting IPv6/IPTables are also suspected to be vulnerable) 2. Extension Header match bypass The attack described in the previous scenario can also be used to evade rules that are attempting to block certain extension headers from appearing in the input packets. This is also due to the way ipv6_find_header() is invoked and the same rules are essentially followed, except that when a non-zero offset fragment is encountered the target match will always fail. Consider the following rule: ip6tables -A INPUT -m rt -j DROP The following packet pair will evade this rule: [ IP Header (nxthdr=fraghdr) ] [ fraghdr (offset=0, nxthdr=destopts) ] [ destopts (nxthdr=destopts) ] [ IP Header (nxthdr=fraghdr) ] [ fraghdr (offset=X, nxthdr=destopts) ] [ destopts (nxthdr=routeopts)] [ routeopts ] ... Because the option appears in a non 0-offset fragment, the rule fails to match this packet and it will be successfully delivered (Same Kernel Versions Tested)
The function ipv6_find_hdr() was first introduced with 2.6.14-rc2 and later updated with 2.6.16-rc1. The code in question before 2.6.14 and so also in the RHEL3 kernel looks like this: /* look for the desired protocol header */ if((ip6info->flags & IP6T_F_PROTO)) { u_int8_t currenthdr = ipv6->nexthdr; struct ipv6_opt_hdr *hdrptr; u_int16_t ptr; /* Header offset in skb */ u_int16_t hdrlen; /* Header */ ptr = IPV6_HDR_LEN; while (ip6t_ext_hdr(currenthdr)) { /* Is there enough space for the next ext header? */ if (skb->len - ptr < IPV6_OPTHDR_LEN) return 0; /* NONE or ESP: there isn't protocol part */ /* If we want to count these packets in '-p all', * we will change the return 0 to 1*/ if ((currenthdr == IPPROTO_NONE) || (currenthdr == IPPROTO_ESP)) return 0; hdrptr = (struct ipv6_opt_hdr *)(skb->data + ptr); /* Size calculation */ if (currenthdr == IPPROTO_FRAGMENT) { hdrlen = 8; } else if (currenthdr == IPPROTO_AH) hdrlen = (hdrptr->hdrlen+2)<<2; else hdrlen = ipv6_optlen(hdrptr); currenthdr = hdrptr->nexthdr; ptr += hdrlen; /* ptr is too large */ if ( ptr > skb->len ) return 0; } /* currenthdr contains the protocol header */ dprintf("Packet protocol %hi ?= %s%hi.\n", currenthdr, ip6info->invflags & IP6T_INV_PROTO ? "!":"", ip6info->proto); if (ip6info->proto == currenthdr) { if(ip6info->invflags & IP6T_INV_PROTO) { return 0; } return 1; } /* We need match for the '-p all', too! */ if ((ip6info->proto != 0) && !(ip6info->invflags & IP6T_INV_PROTO)) return 0; }
The attack talks about circumventing the possibly installed DROP rule and make the function return a non-match. RHEL3 doesn't seem to be affected to the fragment rule workaround.