#!/usr/bin/env python3 # Adapted from https://github.com/Aperocky/PortScan # Port scan utility that only reports an open port if some data is returned in response to sent data after connect import socket import threading import argparse import sys import re import os import time import logging import ssl import warnings from logging import Logger try: from queue import Queue from queue import Empty except: from Queue import Queue from Queue import Empty try: import resource # Expand thread number possible with extended FILE count. # This remain as low as 2048 due to macOS secret open limit, unfortunately resource.setrlimit(resource.RLIMIT_NOFILE, (2048, 2048)) except ModuleNotFoundError: pass # for windows support, skip this limitation warnings.filterwarnings("ignore", category=DeprecationWarning) LICENSE = ''' MIT License Copyright (c) 2018 Rocky Li Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ''' # allow for specific probes for services based on port number MESSAGE_TEMPLATES = { 6379: '*2\r\n$7\r\nCOMMAND\r\n$4\r\nDOCS\r\n', } DEFAULT_MESSAGE = 'GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n' RECV_BUFFER_SIZE=1024 # common Kubernetes ports DEFAULT_PORTS = [ 10250,10255,10256,14250,14268,15010,15012,15014,15020,15443,16685,20001,2379,3000,'30000-32767', 4001,4194,4317,4318,44134,443,53,5556,5557,5558,6379,6443,6666,'6782-6784',7000,7001,7002,80,8080,8081, 8082,8083,8084,8443,9001,9090,9093,9099,9100,9153,9411 ] #15021 def create_logger(loglevel: str, name: str) -> Logger: logger = logging.getLogger(name) logger.setLevel(loglevel) handler = logging.StreamHandler(sys.stderr) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) return logger # A multithreading portscan module class PortScan: # Regex Strings for parsing SINGLE_IP = r'^(?:\d{1,3}\.){3}\d{1,3}$' BLOCK_24 = r'^(?:\d{1,3}\.){3}0\/24$' GROUPED_IP = r'^\[.*\]$' def __init__(self, ip_str, port_str=None, thread_num=500, show_refused=False, wait_time=3, stop_after_count=None, dont_try_ssl=False, logger=logging.getLogger('PostScanner'), outputfile=None, nossldata=False): self.ip_range = self.read_ip(ip_str) if port_str is None: #self.ports = [22, 23, 80, 443] self.ports = self.read_port(','.join([str(a) for a in DEFAULT_PORTS])) else: self.ports = self.read_port(port_str) self.lock = threading.RLock() self.thread_num = thread_num if self.thread_num > 2047: self.thread_num = 2047 self.q = Queue(maxsize=self.thread_num*3) self.gen = None # Generator instance to be instantiated later self.show_refused = show_refused if wait_time <= 0: raise "Cannot have negative or zero wait time" self.wait_time = wait_time self.stop_after_count = stop_after_count self.ping_counter = 0 self.nossldata = nossldata self.dont_try_ssl = dont_try_ssl if outputfile: self.outputfile = open(outputfile, 'w') else: self.outputfile = None self.logger = logger self.logger.debug('IP Addresses to scan: {}'.format(','.join(self.ip_range))) self.logger.debug('Posts to scan: {}'.format(','.join([str(a) for a in self.ports]))) # Read in IP Address from string. def read_ip(self, ip_str): # Single IP address if re.match(PortScan.SINGLE_IP, ip_str): if all([x<256 for x in map(int, ip_str.split('.'))]): return [ip_str] raise ValueError('incorrect IP Address') # Block 24 IP address. if re.match(PortScan.BLOCK_24, ip_str): block_3 = list(map(int, ip_str.split('.')[:3])) if all([x<256 for x in block_3]): block_3s = '.'.join(map(str, block_3)) return [block_3s+'.'+str(i) for i in range(256)] raise ValueError('incorrect IP Address') # List of IP Address if re.match(PortScan.GROUPED_IP, ip_str): ip_str = ip_str[1:-1] elements = [e.strip() for e in ip_str.split(',')] master = [] for each in elements: try: sub_list = self.read_ip(each) master.extend(sub_list) except ValueError as e: self.logger.error("{} is not correctly formatted".format(each)) return master raise ValueError('incorrect Match') # Read in port range from string delimited by ',' def read_port(self, port_str): ports = port_str.split(',') port_list = [] for port in ports: if re.match('^\d+$', port): port_list.append(int(port)) elif re.match('^\d+-\d+$', port): p_start = int(port.split('-')[0]) p_end = int(port.split('-')[1]) p_range = list(range(p_start, p_end+1)) port_list.extend(p_range) else: raise ValueError('incorrect Match') return port_list # Standalone thread for queue def fill_queue(self): while True: if self.stop_after_count is not None and self.stop_after_count <= 0: return # Found satisfying number of open ports, stop populating queue if not self.q.full(): try: self.q.put(next(self.gen)) self.ping_counter += 1 except StopIteration: # Break condition break else: time.sleep(0.01) def worker(self): # Worker threads that take ports from queue and consume it while True: try: work = self.q.get_nowait() self.ping_port(*work) self.q.task_done() except Empty: return def ping_port(self, ip, port): status = None success = False if not self.dont_try_ssl: try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) # deprecated, might need to replace with ssl.PROTOCOL_TLS_CLIENT soon ish context.check_hostname = False context.verify_mode = ssl.CERT_NONE mysock = context.wrap_socket(sock) mysock.settimeout(self.wait_time) status = mysock.connect_ex((ip, port)) if status == 0: self.logger.debug('SSL connection successful to {}:{}'.format(ip, port)) if self.nossldata: self.logger.debug('Considering port open to following based on successful SSL connection {}:{}'.format(ip, port)) success = True except ssl.SSLError: # Does this mean a listening port??? pass except ConnectionResetError: pass except TimeoutError: pass if isinstance(status, type(None)): mysock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) mysock.settimeout(self.wait_time) status = mysock.connect_ex((ip, port)) if status == 0: if not success: if port in MESSAGE_TEMPLATES: message = MESSAGE_TEMPLATES[port].encode() else: message = DEFAULT_MESSAGE.encode() mysock.send(message) self.logger.debug('Sent probe to {}:{} : {}'.format(ip, port, message)) try: resp = mysock.recv(RECV_BUFFER_SIZE) self.logger.debug('Received response from {}:{}: {}'.format(ip, port, resp)) mysock.close() if len(resp) > 0: success = True except TimeoutError as e: #self.logger.debug('Timeout: {}'.format(e)) pass except ConnectionResetError as e: #self.logger.debug('Reset: {}'.format(e)) pass except ssl.SSLError as e: #TLSV13_ALERT_CERTIFICATE_REQUIRED means active TLS socket requiring client cert??? self.logger.info('SSL Error on data transfer for {}:{} : {}'.format(ip, port, e)) pass if success: with self.lock: if self.outputfile: self.outputfile.write('{}:{} OPEN\n'.format(ip, port)) self.outputfile.flush() self.logger.info('{}:{} OPEN'.format(ip, port)) self.open_results.append((ip,port)) if self.stop_after_count is not None: self.stop_after_count -= 1 elif status not in [35, 64, 65] and self.show_refused: with self.lock: self.logger.error('{}:{} ERRNO {}, {}'.format(ip, port, status, os.strerror(status))) return def run(self): # Generator that contains all ip:port pairs. self.gen = ((ip, port) for ip in self.ip_range for port in self.ports) queue_thread = threading.Thread(target=self.fill_queue) queue_thread.start() self.open_results = [] for i in range(self.thread_num): t = threading.Thread(target=self.worker) t.start() self.q.join() self.logger.info("Pinged {} ports".format(self.ping_counter)) return self.open_results def get_local_ip(): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: # doesn't even have to be reachable s.connect(('10.255.255.255', 1)) IP = s.getsockname()[0] except: IP = '127.0.0.1' finally: s.close() return IP IP_HELP_STR = """ [string] Positional Argument, if not provided, will default to your local IP address. Accepts Single IP (e.g 172.28.31.227) Multiple IP in a list delineated by "," and enclosed in [] (e.g. [172.28.31.227,172.28.31.228]) 24 IP BLOCK (e.g. 172.28.31.0/24) """ PORT_HELP_STR = """ [string] range of ports, default to 22,23,80,443 accept individual ports and ranges, delineated by "," e.g: 22,80,8000-8010 """ THREAD_HELP_STR = """ [int] maximum number of threads, default is 500, maxed at 2047 for safety. """ SHOW_REFUSED_HELP_STR = """ [boolean flag] Show connection that was responded to but returned a refusal. """ WAIT_HELP_STR = """ [float] Wait time for response, for local, this can be as low as 0.1, unit in second """ STOP_AFTER_HELP_STR = """ [int] Stopping after x many open port has been found, not on by default. Note that the numbers are not exact as threads will continue to finish the existing queue before exiting. """ def main(): parser = argparse.ArgumentParser() parser.add_argument('ip', nargs='?', default=None, help=IP_HELP_STR) parser.add_argument('-p', '--port', action='store', dest='port', help=PORT_HELP_STR) parser.add_argument('-t', '--threadnum', action='store', dest='threadnum', default=500, type=int, help=THREAD_HELP_STR) parser.add_argument('-e', '--show_refused', action='store_true', dest='show_refused', default=False, help=SHOW_REFUSED_HELP_STR) parser.add_argument('-w', '--wait', action='store', dest='wait_time', default=3, type=float, help=WAIT_HELP_STR) parser.add_argument('-s', '--stop_after', action='store', dest='stop_after_count', default=None, type=int, help=STOP_AFTER_HELP_STR) parser.add_argument('-x', '--dont_try_ssl', action='store_true', dest='dont_try_ssl', default=False, help='Disable option to establish a SSL connection to each port scanned before sending data') parser.add_argument('-d', '--no_ssl_data', action='store_true', dest='no_ssl_data', default=False, help='Dont require data received over established SSL connection to consider port open') parser.add_argument('-l', '--loglevel', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], default='INFO', help='Set logging level') parser.add_argument('-o', '--output', action='store', default=None, type=str, help='output file') parser.add_argument('-i', '--ips', action='store', default=None, type=str, help='read hosts from file') args = parser.parse_args() logger = create_logger(args.loglevel, 'PortScanner') if args.ip is None and not args.ips: print("No IP string found, using local address") ip = get_local_ip() print("Local IP found to be {}, scanning entire block".format(ip)) ipblocks = ip.split('.') ipblocks[-1] = '0/24' ipfinal = '.'.join(ipblocks) args.ip = ipfinal if args.ips: args.ip = '[{}]'.format(','.join([a for a in open(args.ips).read().split('\n') if a])) scanner = PortScan(ip_str=args.ip, port_str=args.port, thread_num=args.threadnum, show_refused=args.show_refused, wait_time=args.wait_time, stop_after_count=args.stop_after_count, dont_try_ssl=args.dont_try_ssl, logger=logger, outputfile=args.output, nossldata=args.no_ssl_data) logger.info("Threads will wait for ping response for {} seconds".format(args.wait_time)) scanner.run() if __name__ == '__main__': main()