#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ nmap.py - version and date, see below Source code : https://code.google.com/p/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 Licence : GPL v3 or any later version 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 . Test strings : ^^^^^^^^^^^^ >>> import nmap >>> if __get_last_online_version() != __version__: ... raise ValueError('Current version is {0} - Last published version is {1}'.format(__version__, __get_last_online_version())) >>> nm = nmap.PortScanner() >>> try: ... nm.scan(arguments='-wrongargs') ... except nmap.PortScannerError: ... pass >>> 'error' in nm.scan('yahoo.fs', arguments='-sP')['nmap']['scaninfo'] True >>> r=nm.scan('127.0.0.1', '22-25') >>> r=nm.analyse_nmap_xml_scan(open('../scanme_output.xml').read()) >>> nm.command_line() './nmap-6.40/nmap -sV -oX scanme_output.xml scanme.nmap.org' >>> nm.scaninfo() {'tcp': {'services': '1,3-4,6-7,9,13,17,19-26,30,32-33,37,42-43,49,53,70,79-85,88-90,99-100,106,109-111,113,119,125,135,139,143-144,146,161,163,179,199,211-212,222,254-256,259,264,280,301,306,311,340,366,389,406-407,416-417,425,427,443-445,458,464-465,481,497,500,512-515,524,541,543-545,548,554-555,563,587,593,616-617,625,631,636,646,648,666-668,683,687,691,700,705,711,714,720,722,726,749,765,777,783,787,800-801,808,843,873,880,888,898,900-903,911-912,981,987,990,992-993,995,999-1002,1007,1009-1011,1021-1100,1102,1104-1108,1110-1114,1117,1119,1121-1124,1126,1130-1132,1137-1138,1141,1145,1147-1149,1151-1152,1154,1163-1166,1169,1174-1175,1183,1185-1187,1192,1198-1199,1201,1213,1216-1218,1233-1234,1236,1244,1247-1248,1259,1271-1272,1277,1287,1296,1300-1301,1309-1311,1322,1328,1334,1352,1417,1433-1434,1443,1455,1461,1494,1500-1501,1503,1521,1524,1533,1556,1580,1583,1594,1600,1641,1658,1666,1687-1688,1700,1717-1721,1723,1755,1761,1782-1783,1801,1805,1812,1839-1840,1862-1864,1875,1900,1914,1935,1947,1971-1972,1974,1984,1998-2010,2013,2020-2022,2030,2033-2035,2038,2040-2043,2045-2049,2065,2068,2099-2100,2103,2105-2107,2111,2119,2121,2126,2135,2144,2160-2161,2170,2179,2190-2191,2196,2200,2222,2251,2260,2288,2301,2323,2366,2381-2383,2393-2394,2399,2401,2492,2500,2522,2525,2557,2601-2602,2604-2605,2607-2608,2638,2701-2702,2710,2717-2718,2725,2800,2809,2811,2869,2875,2909-2910,2920,2967-2968,2998,3000-3001,3003,3005-3007,3011,3013,3017,3030-3031,3052,3071,3077,3128,3168,3211,3221,3260-3261,3268-3269,3283,3300-3301,3306,3322-3325,3333,3351,3367,3369-3372,3389-3390,3404,3476,3493,3517,3527,3546,3551,3580,3659,3689-3690,3703,3737,3766,3784,3800-3801,3809,3814,3826-3828,3851,3869,3871,3878,3880,3889,3905,3914,3918,3920,3945,3971,3986,3995,3998,4000-4006,4045,4111,4125-4126,4129,4224,4242,4279,4321,4343,4443-4446,4449,4550,4567,4662,4848,4899-4900,4998,5000-5004,5009,5030,5033,5050-5051,5054,5060-5061,5080,5087,5100-5102,5120,5190,5200,5214,5221-5222,5225-5226,5269,5280,5298,5357,5405,5414,5431-5432,5440,5500,5510,5544,5550,5555,5560,5566,5631,5633,5666,5678-5679,5718,5730,5800-5802,5810-5811,5815,5822,5825,5850,5859,5862,5877,5900-5904,5906-5907,5910-5911,5915,5922,5925,5950,5952,5959-5963,5987-5989,5998-6007,6009,6025,6059,6100-6101,6106,6112,6123,6129,6156,6346,6389,6502,6510,6543,6547,6565-6567,6580,6646,6666-6669,6689,6692,6699,6779,6788-6789,6792,6839,6881,6901,6969,7000-7002,7004,7007,7019,7025,7070,7100,7103,7106,7200-7201,7402,7435,7443,7496,7512,7625,7627,7676,7741,7777-7778,7800,7911,7920-7921,7937-7938,7999-8002,8007-8011,8021-8022,8031,8042,8045,8080-8090,8093,8099-8100,8180-8181,8192-8194,8200,8222,8254,8290-8292,8300,8333,8383,8400,8402,8443,8500,8600,8649,8651-8652,8654,8701,8800,8873,8888,8899,8994,9000-9003,9009-9011,9040,9050,9071,9080-9081,9090-9091,9099-9103,9110-9111,9200,9207,9220,9290,9415,9418,9485,9500,9502-9503,9535,9575,9593-9595,9618,9666,9876-9878,9898,9900,9917,9929,9943-9944,9968,9998-10004,10009-10010,10012,10024-10025,10082,10180,10215,10243,10566,10616-10617,10621,10626,10628-10629,10778,11110-11111,11967,12000,12174,12265,12345,13456,13722,13782-13783,14000,14238,14441-14442,15000,15002-15004,15660,15742,16000-16001,16012,16016,16018,16080,16113,16992-16993,17877,17988,18040,18101,18988,19101,19283,19315,19350,19780,19801,19842,20000,20005,20031,20221-20222,20828,21571,22939,23502,24444,24800,25734-25735,26214,27000,27352-27353,27355-27356,27715,28201,30000,30718,30951,31038,31337,32768-32785,33354,33899,34571-34573,35500,38292,40193,40911,41511,42510,44176,44442-44443,44501,45100,48080,49152-49161,49163,49165,49167,49175-49176,49400,49999-50003,50006,50300,50389,50500,50636,50800,51103,51493,52673,52822,52848,52869,54045,54328,55055-55056,55555,55600,56737-56738,57294,57797,58080,60020,60443,61532,61900,62078,63331,64623,64680,65000,65129,65389', 'method': 'connect'}} >>> nm.all_hosts() ['74.207.244.221'] >>> nm['74.207.244.221'].hostname() 'scanme.nmap.org' >>> nm['74.207.244.221'].state() 'up' >>> nm['74.207.244.221'].all_protocols() ['addresses', 'tcp', 'vendor'] >>> nm['74.207.244.221']['tcp'].keys() dict_keys([80, 9929, 22]) >>> nm['74.207.244.221'].has_tcp(22) True >>> nm['74.207.244.221'].has_tcp(23) False >>> nm['74.207.244.221']['tcp'][22] {'product': 'OpenSSH', 'state': 'open', 'version': '5.3p1 Debian 3ubuntu7', 'name': 'ssh', 'conf': '10', 'extrainfo': 'Ubuntu Linux; protocol 2.0', 'reason': 'syn-ack', 'cpe': 'cpe:/o:linux:linux_kernel'} >>> nm['74.207.244.221']['tcp'][22] == nm['74.207.244.221'].tcp(22) True >>> nm['74.207.244.221']['tcp'][22]['state'] 'open' >>> nm.scanstats()['uphosts'] '1' >>> nm.scanstats()['downhosts'] '0' >>> nm.scanstats()['totalhosts'] '1' >>> 'timestr' in nm.scanstats().keys() True >>> 'elapsed' in nm.scanstats().keys() True >>> nm.listscan('192.168.1.0/30') ['192.168.1.0', '192.168.1.1', '192.168.1.2', '192.168.1.3'] >>> nm.listscan('localhost/30') ['127.0.0.0', '127.0.0.1', '127.0.0.2', '127.0.0.3'] >>> import os >>> if os.getuid() == 0: ... r=nm.scan('127.0.0.1', arguments='-O') ... len(nm['127.0.0.1']['osclass'])>0 ... len(nm.csv()) > 0 ... else: ... True ... True True True >>> if os.getuid() == 0: ... r=nm.scan(hosts='127.0.0.1', ports='139', arguments="-sC -T4") >>> if os.getuid() == 0: ... nm['127.0.0.1']['hostscript'][0].keys() dict_keys(['output', 'id']) >>> if os.getuid() == 0: ... r=nm.scan('192.168.0.254', arguments='-O') ... len(nm['192.168.0.254']['vendor']) > 0 ... else: ... True True """ __author__ = 'Alexandre Norman (norman@xael.org)' __version__ = '0.3.4' __last_modification__ = '2014.06.22' import collections import csv import io import os import re import shlex import string import subprocess import sys import types import xml.dom.minidom 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 regex = re.compile('Nmap version [0-9]*\.[0-9]*[^ ]* \( http://.* \)') # 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: 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]) # store stdout for line in self._nmap_last_output.split('\n'): 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 is_nmap_found == False: 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)) self.scan(hosts, arguments='-sL') return self.all_hosts() def scan(self, hosts='127.0.0.1', ports=None, arguments='-sV'): """ 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. hosts = string for hosts as nmap use it 'scanme.nmap.org' or '198.116.0-255.1-127' or '216.163.128.20/20' ports = string for ports as nmap use it '22,53,110,143-4564' arguments = string of arguments for nmap '-sU -sX -sC' :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)) else: assert type(hosts) is str, 'Wrong type for [hosts], should be a string [was {0}]'.format(type(hosts)) assert type(ports) in (str, type(None)), 'Wrong type for [ports], should be a string [was {0}]'.format(type(ports)) assert type(arguments) is str, 'Wrong type for [arguments], should be a string [was {0}]'.format(type(arguments)) for redirecting_output in ['-oX', '-oA']: assert not redirecting_output 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()' h_args = shlex.split(hosts) f_args = shlex.split(arguments) # Launch scan args = [self._nmap_path, '-oX', '-'] + h_args + ['-p', ports]*(ports!=None) + f_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 = [] if len(nmap_err) > 0: regex_warning = re.compile('^Warning: .*') for line in nmap_err.split('\n'): if len(line) > 0: rgw = regex_warning.search(line) if rgw is not None: sys.stderr.write(line+'\n') pass 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) def analyse_nmap_xml_scan(self, nmap_xml_output=None, nmap_err='', nmap_err_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 : # # #
# # # # # # # # # # # # #