Advertisement
Guest User

camscan.py

a guest
May 4th, 2017
596
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 9.87 KB | None | 0 0
  1. #!/usr/bin/python -B
  2. # -*- coding: utf-8 -*-
  3. #
  4. #  camscan.py
  5. #
  6. #  Redistribution and use in source and binary forms, with or without
  7. #  modification, are permitted provided that the following conditions are
  8. #  met:
  9. #
  10. #  * Redistributions of source code must retain the above copyright
  11. #    notice, this list of conditions and the following disclaimer.
  12. #  * Redistributions in binary form must reproduce the above
  13. #    copyright notice, this list of conditions and the following disclaimer
  14. #    in the documentation and/or other materials provided with the
  15. #    distribution.
  16. #  * Neither the name of the project nor the names of its
  17. #    contributors may be used to endorse or promote products derived from
  18. #    this software without specific prior written permission.
  19. #
  20. #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21. #  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22. #  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23. #  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24. #  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25. #  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26. #  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27. #  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28. #  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29. #  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30. #  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31. #
  32.  
  33. __version__ = '0.1'
  34.  
  35. SSH_DEFAULT_PORT = 22
  36. DEFAULT_SLEEP_TIME = 0.5
  37. CISCO_NEW_LINE = '\r\n'
  38. CISCO_MORE_LINE = ' --More--'
  39. REPORT_BANNER = "{0:<17}  {1:<15}  {2:<15}  {3:<4}"
  40.  
  41. import argparse
  42. import getpass
  43. import logging
  44. from time import sleep
  45. from binascii import unhexlify
  46. import sys
  47. import os
  48.  
  49. sshlib = None
  50. try:
  51.     import ssh
  52.     sshlib = ssh
  53. except ImportError:
  54.     pass
  55.  
  56. try:
  57.     import paramiko
  58.     sshlib = paramiko
  59. except ImportError:
  60.     pass
  61.  
  62. if sshlib == None:
  63.     print 'Must have python-ssh or python-paramiko installed.'
  64.     sys.exit(1)
  65.  
  66. def macMatch(control, test, wildcards = True):
  67.     control = control.lower().split(':')
  68.     test = test.lower().split(':')
  69.     if len(control) != 6 or len(test) != 6:
  70.         raise Exception('malformed MAC')
  71.     for i in xrange(0, 6):
  72.         if wildcards and control[i] == '*':
  73.             continue
  74.         if control[i] != test[i]:
  75.             return False
  76.     return True
  77.  
  78. class CiscoSwitch:
  79.     def __init__(self, host, username = None, password = None):
  80.         if not username:
  81.             username = raw_input('Username: ')
  82.         if not password:
  83.             password = getpass.getpass('Password: ')
  84.  
  85.         self.host = host
  86.         self.port = SSH_DEFAULT_PORT
  87.         self.username = username
  88.         self.password = password
  89.  
  90.         self.ssh = None
  91.         self.logger = logging.getLogger('switch')
  92.         self.connect()
  93.  
  94.     def close(self):
  95.         self.ssh.close()
  96.  
  97.     def connect(self):
  98.         self.ssh = sshlib.SSHClient()
  99.         self.ssh.set_missing_host_key_policy(sshlib.AutoAddPolicy())
  100.         self.ssh.connect(self.host, port = self.port, username = self.username, password = self.password)
  101.         self.logger.info('successfully connected to: ' + self.username + '@' + self.host + ':' + str(self.port))
  102.         self.shell = self.ssh.invoke_shell()
  103.  
  104.         sleep(DEFAULT_SLEEP_TIME)
  105.         buff = ''
  106.         while self.shell.recv_ready()# flush the buffer in case there is a banner
  107.             buff += self.shell.recv(512)
  108.  
  109.         if self.privileges_sync() != 15:
  110.             if not self.privileges_elevate():
  111.                 self.logger.critical('could not obtain the necessary privileges')
  112.                 raise Exception('could not obtain the necessary privileges')
  113.  
  114.     def privileges_elevate(self):
  115.         self.logger.info('elevating privileges')
  116.  
  117.         self.shell.send('enable\n') # don't change this to CRLF
  118.         sleep(DEFAULT_SLEEP_TIME)
  119.         buff = ''
  120.         while self.shell.recv_ready():
  121.             buff += self.shell.recv(512)
  122.  
  123.         output = filter(lambda x: x, buff.strip().split(CISCO_NEW_LINE))
  124.         if not output:
  125.             raise Exception('failed to obtain privilege level 15')
  126.         if not output[-1].lower().startswith('password:'):
  127.             raise Exception('failed to obtain privilege level 15')
  128.  
  129.         self.shell.send(self.password + '\n')
  130.         sleep(DEFAULT_SLEEP_TIME)
  131.         buff = ''
  132.         while self.shell.recv_ready():
  133.             buff += self.shell.recv(512)
  134.         output = filter(lambda x: x, buff.strip().split(CISCO_NEW_LINE))
  135.  
  136.         retries = 3
  137.         while 'access denied' in output[0].lower() and retries:
  138.             self.logger.warning('enable password rejected')
  139.             self.shell.send('enable\n') # don't change this to CRLF
  140.             sleep(DEFAULT_SLEEP_TIME)
  141.             buff = ''
  142.             while self.shell.recv_ready():
  143.                 buff += self.shell.recv(512)
  144.  
  145.             output = filter(lambda x: x, buff.strip().split(CISCO_NEW_LINE))
  146.             if not output:
  147.                 raise Exception('failed to obtain privilege level 15')
  148.             if not output[-1].lower().startswith('password:'):
  149.                 raise Exception('failed to obtain privilege level 15')
  150.  
  151.             password = getpass.getpass('Enable Password For ' + self.username + '@' + self.host + ': ')
  152.             self.shell.send(password + '\n')
  153.             sleep(DEFAULT_SLEEP_TIME)
  154.             buff = ''
  155.             while self.shell.recv_ready():
  156.                 buff += self.shell.recv(512)
  157.             output = filter(lambda x: x, buff.strip().split(CISCO_NEW_LINE))
  158.             retries -= 1
  159.         if self.privileges_sync() == 15:
  160.             self.logger.info('successfully elevated to privilege level 15')
  161.             return True
  162.         return False
  163.  
  164.     def privileges_sync(self):
  165.         cmd = self.execute_command('show privilege')
  166.         if not cmd:
  167.             raise Exception('failed to identify privilege level')
  168.         if cmd[0].lower()[:27] != 'current privilege level is ':
  169.             raise Exception('failed to identify privilege level')
  170.         self.privilege_level = int(cmd[0].split()[-1])
  171.         return self.privilege_level
  172.  
  173.     def execute_command(self, command):
  174.         self.logger.info('running command: ' + str(command))
  175.  
  176.         self.shell.send(command + '\n')
  177.         sleep(DEFAULT_SLEEP_TIME)
  178.         buff = ''
  179.         while self.shell.recv_ready():
  180.             buff += self.shell.recv(512)
  181.         data = filter(lambda x: x, buff.strip().split(CISCO_NEW_LINE))[1:]
  182.  
  183.         while data[-1].startswith(CISCO_MORE_LINE):
  184.             del data[-1]
  185.             self.shell.send(' \n')
  186.             sleep(DEFAULT_SLEEP_TIME)
  187.             buff = ''
  188.             while self.shell.recv_ready():
  189.                 buff += self.shell.recv(512)
  190.             while buff[0] == '\x08' or buff[0] == ' ':
  191.                 buff = buff[1:]
  192.             data.extend(filter(lambda x: x, buff.strip().split(CISCO_NEW_LINE))[1:])
  193.         return data
  194.  
  195.     def get_cam_table(self):
  196.         cam_table = []
  197.  
  198.         raw_data = self.execute_command('show mac address-table')
  199.  
  200.         raw_data = raw_data[4:]
  201.         prompt = raw_data.pop()
  202.         while raw_data[-1] == prompt:
  203.             raw_data.pop()
  204.         raw_data.pop()
  205.  
  206.         if not raw_data:
  207.             return None
  208.         for line in raw_data:
  209.             line = line.split()
  210.             if len(line) != 4:
  211.                 self.logger.error('invalid line in CAM table found')
  212.                 return
  213.             entry = {}
  214.             entry['vlan'] = line[0]
  215.             entry['mac'] = ':'.join([x.encode('hex') for x in unhexlify(line[1].replace('.', ''))])
  216.             entry['type'] = line[2]
  217.             entry['ports'] = line[3]
  218.             if entry['ports'] != 'CPU':
  219.                 cam_table.append(entry)
  220.         return cam_table
  221.  
  222. def main():
  223.     parser = argparse.ArgumentParser(description = 'Cisco Switch MAC Anomaly Detector', conflict_handler = 'resolve')
  224.     parser.add_argument('-v', '--version', action = 'version', version = parser.prog + ' Version: ' + __version__)
  225.     parser.add_argument('-L', '--log', dest = 'loglvl', action = 'store', choices = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], default = 'ERROR', help = 'set the logging level')
  226.     parser.add_argument('-u', '--user', dest = 'ssh_user', action = 'store', required = False, help = 'default user to authenticate as')
  227.     parser.add_argument('-p', '--pass', dest = 'ssh_pass', action = 'store', required = False, default = None, help = 'default password to authenticate with')
  228.     parser.add_argument('-h', '--hosts', dest = 'host_file', action = 'store', required = True, type = argparse.FileType('r'), help = 'list of switches to scan')
  229.     parser.add_argument('-m', '--macs', dest = 'mac_file', action = 'store', required = True, type = argparse.FileType('r'), help = 'list of MAC addresses to ignore')
  230.     parser.add_argument('-r', '--report', dest = 'report_file', action = 'store', required = False, type = argparse.FileType('w'), default = 'report.csv', help = 'report CSV file')
  231.  
  232.     try:
  233.         arguments = parser.parse_args()
  234.     except IOError as error:
  235.         if hasattr(error, 'filename'):
  236.             print error.strerror + ': ' + error.filename
  237.         else:
  238.             print error.strerror
  239.         return 1
  240.  
  241.     logging.basicConfig(level = getattr(logging, arguments.loglvl), format = "%(levelname)-8s %(message)s")
  242.     logger = logging.getLogger('main')
  243.  
  244.     try:
  245.         ssh_user = arguments.ssh_user
  246.         if not ssh_user:
  247.             ssh_user = raw_input('Switch Username: ')
  248.         ssh_pass = arguments.ssh_pass
  249.         if not ssh_pass:
  250.             ssh_pass = getpass.getpass('Switch Password: ')
  251.     except KeyboardInterrupt:
  252.         return 1
  253.  
  254.     anomalies = 0
  255.     switch_ip = arguments.host_file.readline().strip()
  256.     while switch_ip:
  257.         logger.info('now processing switch: ' + switch_ip)
  258.         try:
  259.             switch = CiscoSwitch(switch_ip, username = ssh_user, password = ssh_pass)
  260.         except Exception as error:
  261.             logger.error('received an error while connecting to: ' + switch_ip)
  262.             switch_ip = arguments.host_file.readline().strip()
  263.             continue
  264.         cam_table = switch.get_cam_table()
  265.         logger.debug('retreived ' + str(len(cam_table)) + ' entries from the CAM table')
  266.         white_mac = arguments.mac_file.readline().strip()
  267.         while white_mac:
  268.             for entry in cam_table:
  269.                 if macMatch(white_mac, entry['mac']):
  270.                     continue
  271.                 if anomalies == 0:
  272.                     print REPORT_BANNER.format('MAC Address', 'Ports', 'Switch IP', 'VLAN')
  273.                     print REPORT_BANNER.format('-----------', '-----', '---------', '----')
  274.                 print REPORT_BANNER.format(entry['mac'], entry['ports'], switch_ip, entry['vlan'])
  275.                 arguments.report_file.write(",".join([entry['mac'], entry['ports'], switch_ip, entry['vlan']]) + os.linesep)
  276.                 anomalies += 1
  277.             white_mac = arguments.mac_file.readline().strip()
  278.         arguments.mac_file.seek(os.SEEK_SET, 0)
  279.         arguments.report_file.flush()
  280.         switch_ip = arguments.host_file.readline().strip()
  281.         switch.close()
  282.     print '\nIdentified ' + str(anomalies) + ' network anomalies.'
  283.     return 0
  284.  
  285. if __name__ == '__main__':
  286.     sys.exit(main())
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement