#!/usr/bin/env python # -*- coding: utf-8 -*- """ nmap.py - version and date, see below Source code : https://bitbucket.org/xael/python-nmap Author : * Alexandre Norman - norman at xael.org Contributors: * Steve 'Ashcrow' Milner - steve at gnulinux.net * Brian Bustin - brian at bustin.us * old.schepperhand * Johan Lundberg * Thomas D. maaaaz * Robert Bost * David Peltier Licence: GPL v3 or any later version for python-nmap This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ************** IMPORTANT NOTE ************** The Nmap Security Scanner used by python-nmap is distributed under it's own licence that you can find at https://svn.nmap.org/nmap/COPYING Any redistribution of python-nmap along with the Nmap Security Scanner must conform to the Nmap Security Scanner licence """ __author__ = 'Alexandre Norman (norman@xael.org)' __version__ = '0.6.1' __last_modification__ = '2016.07.29' import csv import io import os import re import shlex import subprocess import sys from xml.etree import ElementTree as ET try: from multiprocessing import Process except ImportError: # For pre 2.6 releases from threading import Thread as Process ############################################################################ class PortScanner(object): """ PortScanner class allows to use nmap from python """ def __init__(self, nmap_search_path=('nmap', '/usr/bin/nmap', '/usr/local/bin/nmap', '/sw/bin/nmap', '/opt/local/bin/nmap')): """ Initialize PortScanner module * detects nmap on the system and nmap version * may raise PortScannerError exception if nmap is not found in the path :param nmap_search_path: tupple of string where to search for nmap executable. Change this if you want to use a specific version of nmap. :returns: nothing """ self._nmap_path = '' # nmap path self._scan_result = {} self._nmap_version_number = 0 # nmap version number self._nmap_subversion_number = 0 # nmap subversion number self._nmap_last_output = '' # last full ascii nmap output is_nmap_found = False # true if we have found nmap self.__process = None # regex used to detect nmap (http or https) regex = re.compile( 'Nmap version [0-9]*\.[0-9]*[^ ]* \( http(|s)://.* \)' ) # launch 'nmap -V', we wait after #'Nmap version 5.0 ( http://nmap.org )' # This is for Mac OSX. When idle3 is launched from the finder, PATH is not set so nmap was not found for nmap_path in nmap_search_path: try: if sys.platform.startswith('freebsd') \ or sys.platform.startswith('linux') \ or sys.platform.startswith('darwin'): p = subprocess.Popen([nmap_path, '-V'], bufsize=10000, stdout=subprocess.PIPE, close_fds=True) else: p = subprocess.Popen([nmap_path, '-V'], bufsize=10000, stdout=subprocess.PIPE) except OSError: pass else: self._nmap_path = nmap_path # save path break else: raise PortScannerError( 'nmap program was not found in path. PATH is : {0}'.format( os.getenv('PATH') ) ) self._nmap_last_output = bytes.decode(p.communicate()[0]) # sav stdout for line in self._nmap_last_output.split(os.linesep): if regex.match(line) is not None: is_nmap_found = True # Search for version number regex_version = re.compile('[0-9]+') regex_subversion = re.compile('\.[0-9]+') rv = regex_version.search(line) rsv = regex_subversion.search(line) if rv is not None and rsv is not None: # extract version/subversion self._nmap_version_number = int(line[rv.start():rv.end()]) self._nmap_subversion_number = int( line[rsv.start()+1:rsv.end()] ) break if not is_nmap_found: raise PortScannerError('nmap program was not found in path') return def get_nmap_last_output(self): """ Returns the last text output of nmap in raw text this may be used for debugging purpose :returns: string containing the last text output of nmap in raw text """ return self._nmap_last_output def nmap_version(self): """ returns nmap version if detected (int version, int subversion) or (0, 0) if unknown :returns: (nmap_version_number, nmap_subversion_number) """ return (self._nmap_version_number, self._nmap_subversion_number) def listscan(self, hosts='127.0.0.1'): """ do not scan but interpret target hosts and return a list a hosts """ assert type(hosts) is str, 'Wrong type for [hosts], should be a string [was {0}]'.format(type(hosts)) # noqa output = self.scan(hosts, arguments='-sL') # Test if host was IPV6 try: if 'looks like an IPv6 target specification' in output['nmap']['scaninfo']['error'][0]: # noqa self.scan(hosts, arguments='-sL -6') except KeyError: pass return self.all_hosts() def scan(self, hosts='127.0.0.1', ports=None, arguments='-sV', sudo=False): """ Scan given hosts May raise PortScannerError exception if nmap output was not xml Test existance of the following key to know if something went wrong : ['nmap']['scaninfo']['error'] If not present, everything was ok. :param hosts: string for hosts as nmap use it 'scanme.nmap.org' or '198.116.0-255.1-127' or '216.163.128.20/20' :param ports: string for ports as nmap use it '22,53,110,143-4564' :param arguments: string of arguments for nmap '-sU -sX -sC' :param sudo: launch nmap with sudo if True :returns: scan_result as dictionnary """ if sys.version_info[0]==2: assert type(hosts) in (str, unicode), 'Wrong type for [hosts], should be a string [was {0}]'.format(type(hosts)) # noqa assert type(ports) in (str, unicode, type(None)), 'Wrong type for [ports], should be a string [was {0}]'.format(type(ports)) # noqa assert type(arguments) in (str, unicode), 'Wrong type for [arguments], should be a string [was {0}]'.format(type(arguments)) # noqa else: assert type(hosts) is str, 'Wrong type for [hosts], should be a string [was {0}]'.format(type(hosts)) # noqa assert type(ports) in (str, type(None)), 'Wrong type for [ports], should be a string [was {0}]'.format(type(ports)) # noqa assert type(arguments) is str, 'Wrong type for [arguments], should be a string [was {0}]'.format(type(arguments)) # noqa for redirecting_output in ['-oX', '-oA']: assert redirecting_output not in arguments, 'Xml output can\'t be redirected from command line.\nYou can access it after a scan using:\nnmap.nm.get_nmap_last_output()' # noqa h_args = shlex.split(hosts) f_args = shlex.split(arguments) # Launch scan args = [self._nmap_path, '-oX', '-'] + h_args + ['-p', ports]*(ports is not None) + f_args if sudo: args = ['sudo'] + args p = subprocess.Popen(args, bufsize=100000, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # wait until finished # get output (self._nmap_last_output, nmap_err) = p.communicate() self._nmap_last_output = bytes.decode(self._nmap_last_output) nmap_err = bytes.decode(nmap_err) # If there was something on stderr, there was a problem so abort... in # fact not always. As stated by AlenLPeacock : # This actually makes python-nmap mostly unusable on most real-life # networks -- a particular subnet might have dozens of scannable hosts, # but if a single one is unreachable or unroutable during the scan, # nmap.scan() returns nothing. This behavior also diverges significantly # from commandline nmap, which simply stderrs individual problems but # keeps on trucking. nmap_err_keep_trace = [] nmap_warn_keep_trace = [] if len(nmap_err) > 0: regex_warning = re.compile('^Warning: .*', re.IGNORECASE) for line in nmap_err.split(os.linesep): if len(line) > 0: rgw = regex_warning.search(line) if rgw is not None: # sys.stderr.write(line+os.linesep) nmap_warn_keep_trace.append(line+os.linesep) else: # raise PortScannerError(nmap_err) nmap_err_keep_trace.append(nmap_err) return self.analyse_nmap_xml_scan( nmap_xml_output=self._nmap_last_output, nmap_err=nmap_err, nmap_err_keep_trace=nmap_err_keep_trace, nmap_warn_keep_trace=nmap_warn_keep_trace ) def analyse_nmap_xml_scan(self, nmap_xml_output=None, nmap_err='', nmap_err_keep_trace='', nmap_warn_keep_trace=''): """ Analyses NMAP xml scan ouput May raise PortScannerError exception if nmap output was not xml Test existance of the following key to know if something went wrong : ['nmap']['scaninfo']['error'] If not present, everything was ok. :param nmap_xml_output: xml string to analyse :returns: scan_result as dictionnary """ # nmap xml output looks like : # # #
# # # # # # # # # # # # #