Update: Tested it under Windows 7, Cygwin, openSUSE, and Ubuntu 16 on Python 2.6 and Python 2.7.
Works on all these platforms, but you cannot syn-scan from windows/cygwin

This is a scanner written in Python 2.X
I am almost positive it will work in 2.6+, but it has only been tested on 2.7

Most of what this does it found in the comments at the top of each function, but I will provide a brief overview here:

Ping scan using built-in *nix “ping” command. I *might* think about using sockets to send a ping later, so that the entire program is all python
Port scan using Python sockets. Performs a standard TCP connect.
Syn-scan using python raw sockets. Catches the SYN-ACK using a raw socket listener. The kernel will send the RST packet since the SYN-ACK is unexpected, so we don’t have to send it.

This will only work on Linux due to how I implemented raw sockets and ping, but you might be able to get away with performing the full connect port scan on Windows.
The ping function would only need a tiny rewrite to work under Windows as well.

#!/usr/bin/python
#Will operate under cygwin, windows, and linux environments.
#Tested in 2.6 and 2.7
#Cygwin/Windows might not be able to perform syn-scan due to use of raw sockets
import os, subprocess, sys, socket, itertools
from threading import Thread
from threading import active_count
from Queue import Queue
from optparse import OptionParser
from struct import *
from time import sleep

source_ips = {}
try:
    ips = sys.argv[1]
except Exception:
    print("IP must be first argument")
    sys.exit(1)

devnull = open(os.devnull,'w')
ip_queue = Queue()
socket_queue = Queue()
parser = OptionParser()
parser.add_option("-t", "--threads", type="int", dest="threads", default=64, help="Set number of threads")
parser.add_option("--timeout", type="int", dest="timeout", default=5, help="Set timeout value (for ping and TCP)")
parser.add_option("-s", type="string", dest="scan_type", default="T", help="Set the scan type\n\tT = TCP full connect\n\tS = TCP Syn Scan")
parser.add_option("--ping","--ping-scan", action="store_true", dest="ping_only",default=False, help="Perform a ping-only scan")
parser.add_option("-P", type="string",dest="ping", default="y",help="Use -PN or -Pn to skip pinging targets")
parser.add_option("-p","--ports",type="string",dest="ports", help="Ports to scan. Format is comma separated, and accepts ranges with a \"-\"")
parser.add_option("--source",type="string",dest="source",default="auto", help="Optional. Set a source IP if using -sS option. Skips sending UDP packets to auto-determine source IP")
parser.add_option("-v","--verbose",action="store_true",dest="verbose",default=False, help="Verbose output (for debugging)")
parser.add_option("--example",help="Example usage: ./scan 192.168.0-10.0-255 -sS -Pn -p 20-80,400-500\n./scan 1.1.1.1 --ping --threads 1 --timeout 10")
options,args = parser.parse_args()
print(options)

def tcp_listener():
    #Raw socket listener for when send_raw_syn() is used. This will catch return SYN-ACKs
    listen = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)
    ips_of_interest = [n for n in convert_ips(ips)]
    print("Starting Listener")
    while True:
        #packet = ('E \x00(\x1f\xaa@\x00w\x06\x99w2\xe0\xc8\xa2\xa2\xf3\xac\x18\xdf\xb3\x00\x16\xb6\x80\xc1\xa0/\xa6=$P\x10\xce\xab\xd1\xe4\x00\x00', ('50.XXX.200.162', 0))
        raw_packet = listen.recvfrom(65565)
        #Now we need to unpack the packet. It will be an IP/TCP packet
        #We are looking for SYN-ACKs from our SYN scan
        #Fields to check: IP - src addr; TCP - src port, flags
        #We want to pull out and compare only these three
        #Heres the math for unpacking: B=1, H=2, L=4, 4s=4  (those are bytes)
        packet = raw_packet[0]
        ip_header = unpack('!BBHHHBBH4s',packet[0:16]) #This is the IP header, not including any options OR THE DST ADDR. Normal length is 20!! Im parsing as little as possible
        ip_header_length = (ip_header[0] & 0xf) * 4         #If there are any options, the length of the IP header will be >20. We dont care about options
        src_addr = socket.inet_ntoa(ip_header[8])           #This is the source address (position 8, or the first "4s" in our unpack)
        
        tcp_header_raw = packet[ip_header_length:ip_header_length+14]   #We had to get the proper IP Header length to find the TCP header offset.
        tcp_header = unpack('!HHLLBB',tcp_header_raw)                         #TCP header structure is pretty straight-forward. We want PORTS and FLAGS, so we partial unpack it
        
        src_port = tcp_header[0]    #self-explanatory
        dst_port = tcp_header[1]    #self-explanatory
        flag = tcp_header[5]        #We only care about syn-ack, which will be 18 (0x12)
        
        if flag == 18:
            if src_addr in ips_of_interest and src_port in options.ports:
                sys.stdout.write("OPEN: \t{} : {}\n".format(src_addr,src_port))

def ping(ip_q):
    #ping function. Populates the socket queue for port scanning if ping and port scans are being performed
    #Since ping() is called from daemon threads, the threads are killed by passing the ip as "HALT"
    while True:
        ip = ip_q.get()
        if ip == "HALT":
            ip_q.task_done()
            return
        if "win" in sys.platform.lower():
            # *1000 since windows does millisecond timeout values
            command = ["ping","-n","1","-w",str(options.timeout*1000),ip]
        else:
            command = ["ping","-c 1","-W {}".format(options.timeout),ip]
        ret_value = subprocess.call(command, stdin=devnull, stdout=devnull, stderr=devnull)
        if ret_value == 0:
            sys.stdout.write("UP: \t{0}\n".format(ip))
            if options.ping_only != True:
                for port in options.ports:
                    if options.verbose: sys.stdout.write("Placing ({},{}) in socket queue\n".format(ip,port))
                    socket_queue.put((ip,port))
                    if options.verbose: print(socket_queue.queue)
        if options.verbose: sys.stdout.write("DONE: {} ping\n".format(ip))
        ip_q.task_done()

def port_check(socket_q):
    #If port scan type is "T", calls TCP full connect function
    #If the port scan type is "S", then it calls the raw socket syn-scan function
    while True:
        ip,port = socket_q.get()
        if options.scan_type.upper() == "T":
            send_full_connect_syn(ip,port)
        elif options.scan_type.upper() == "S":
            send_raw_syn(ip,port)
        else:
            sys.stderr.write("Invalid port scan type, doing nothing...\n")
        socket_q.task_done()

def convert_ips(ips):
    #This converts IPs from 10-12.50-60.1.2-5 format to a list of IPs formatted correctly
    #I found that itertools is slower than doing for loops by about 4 seconds for a /8 subnet
    #On my test system, itertools finishes generating the IPs in about 19 seconds for a /8, I consider this acceptable
    #DOES NOT ATTEMPT TO CORRECT MALFORMED IPS. input correctly! Malformed IPs wont crash the scanner, but its a waste of time
    octets = []
    for ip in ips.split("."):
        if "-" in ip:
            lo,hi = ip.split("-")
            octets.append(range(int(lo),int(hi)+1))
        else:
            octets.append([int(ip)])
    for ip in itertools.product(octets[0],octets[1],octets[2],octets[3]):
        yield "{}.{}.{}.{}".format(ip[0],ip[1],ip[2],ip[3])

def convert_ports(ports):
    #Converts ports from form 20-40,100-900,40000-70000
    #It will automatically prune off non-existent ports (<1 >65535)
    if ports == None: return [21,22,23,25,80,443,110,111,135,139,445,8080,8443,53,143,989,990,3306,1080,5554,6667,2222,4444,666,6666,1337,2020,31337]
    else:
        if "-" not in ports:
            tports = ports.split(",")
            print(tports)
        else:
            ports = ports.split(",")
            tports = []
            for port in ports:
                if "-" not in port: tports.append(int(port))
                else: tports.extend(range(int(port.split("-")[0]),int(port.split("-")[1])+1)) #I made this one line because I wanted to
    ports = [int(n) for n in tports if int(n) > 0 and int(n) < 65536]
    if options.verbose: print("Converted ports: {}".format(ports))
    return ports

class TCPHeader():
    #TCP header class. Thanks to Silver Moon for the flags calculation and packing order
    #This was designed to be re-used. You might want to randomize the seq number
    #get_struct performs packing based on if you have a valid checksum or not
    def __init__(self,src_port=47123,dst_port=80,seqnum=1000,acknum=0,data_offset=80,fin=0,syn=1,rst=0,psh=0,ack=0,urg=0,window=5840,check=0,urg_ptr=0):
        self.order = "!HHLLBBHHH" #!=network(big-endian), H=short(2), L=long(4),B=char(1) 
        self.src_port = src_port
        self.dst_port = dst_port
        self.seqnum = seqnum
        self.acknum = acknum
        self.data_offset = data_offset #size of tcp header; size is specified by 4-byte words; This is 80 decimal, which is 0x50, which is 20bytes (5words*4bytes).
        self.fin = fin
        self.syn = syn
        self.rst = rst
        self.psh = psh
        self.ack = ack
        self.urg = urg
        self.window = socket.htons(window)
        self.check = check
        self.urg_ptr = urg_ptr
    def flags(self):
        return self.fin + (self.syn << 1) + (self.rst << 2) + (self.psh <<3) + (self.ack << 4) + (self.urg << 5)
    def get_struct(self,check=False,checksummed=False):
        if check != False: self.check = check
        if checksummed:
            return pack('!HHLLBBH',self.src_port,self.dst_port,self.seqnum,self.acknum,self.data_offset,self.flags(),self.window)+pack('H',self.check)+pack('!H',self.urg_ptr)
        else:
            return pack(self.order,self.src_port,self.dst_port,self.seqnum,self.acknum,self.data_offset,self.flags(),self.window,self.check,self.urg_ptr)

def checksum(msg):
    #Shoutout to Silver Moon @ binarytides for this checksum algo.
    sum = 0
    for i in range(0,len(msg),2):
        w = ord(msg[i]) + (ord(msg[i+1]) << 8 )
        sum = sum + w
    
    sum = (sum>>16) + (sum & 0xffff)
    sum = sum + (sum >> 16)
    sum = ~sum & 0xffff
    return sum

def tcp_checksum(source_ip,dest_ip,tcp_header,user_data=''):
    #Calculates the correct checksum for the tcp header
    tcp_length = len(tcp_header) + len(user_data)
    ip_header = pack('!4s4sBBH',socket.inet_aton(source_ip),socket.inet_aton(dest_ip),0,socket.IPPROTO_TCP,tcp_length) #This is an IP header w/ TCP as protocol.
    packet = ip_header + tcp_header + user_data #Assemble the packet (IP Header + TCP Header + data, and then send it to checksum function)
    return checksum(packet)

def send_raw_syn(dest_ip,dst_port):
    #Use raw sockets to send a SYN packet.
    #If you want, you could use the IP header assembled in the tcp_checksum function to have a fully custom TCP/IP stack
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP) #Using IPPROTO_TCP so the kernel will deal with the IP packet for us. Change to IPPROTO_IP if you want control of IP header as well
    except Exception:
        sys.stderr.write("Error creating socket in send_raw_syn\n")
    if options.source == "auto":
        src_addr = get_source_ip(dest_ip) #This gets the correct source IP. Just in case of multiple interfaces, it will pick the right one
    else:
        src_addr = options.source
    src_port = 54321
    make_tcpheader = TCPHeader(src_port,dst_port)
    tcp_header = make_tcpheader.get_struct()
    packet = make_tcpheader.get_struct(check=tcp_checksum(src_addr,dest_ip,tcp_header),checksummed=True)
    if options.verbose: sys.stdout.write("SEND: SYN packet {} {}\n".format(dest_ip,dst_port))
    try: s.sendto(packet,(dest_ip,0))
    except Exception: sys.stderr.write("Error utilizing raw socket in send_raw_syn\n")

def send_full_connect_syn(ip,port):
    #Normal scan using socket to connect. Does 3-way handshack, then graceful teardown using FIN
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(options.timeout)
    except Exception:
        sys.stdout.write("Error creating socket in send_full_connect_syn\n")
    try:
        if options.verbose: sys.stdout.write("START: {} {} port\n".format(ip,port))
        s.connect((ip,port))
        sys.stdout.write("OPEN: \t{0} : {1}\n".format(ip,port))
        s.close()
    except Exception:
        pass
    if options.verbose: sys.stdout.write("DONE: {} {} port\n".format(ip,port))

def populate_queues():
    #Note: If pinging happens, the ping() function populates the socket queue
    #This function just fills up ip_queue with all the IPs that will need pinging/port scanning
    #If only doing a port scan, fills up socket_queue with the sockets (ip,port)
    options.ports = convert_ports(options.ports)
    for ip in convert_ips(ips):
        if options.ping.lower() == "n":
            for port in options.ports:
                if options.verbose: sys.stdout.write("Placing ({},{}) in socket queue\n".format(ip,port))
                if options.verbose: print(socket_queue.queue)
                socket_queue.put((ip,port))
        else:
            ip_queue.put(ip)

def start_ping_threads():
    #Starts up the threads responsible for sending pings
    #If youre unfamiliar, ip_queue.join() is a blocking line, meaning code will stop there until the queue is finished
    for index in range(options.threads):
        if options.ping == "y":
            ping_worker = Thread(target=ping, args=(ip_queue,))
            ping_worker.setDaemon(True)
            ping_worker.start()
        
    if options.verbose: print("THREADS inside ping: {}".format(active_count()))
    ip_queue.join()
    print("********* IP QUEUE BLOCK RELEASED ************")

def start_port_threads():
    #Starts up threads to perform port scanning.
    #If youre doing raw syn-scanning, starts up the listener for syn-acks
    if options.scan_type.lower() == "s":
        listen_thread = Thread(target=tcp_listener)
        listen_thread.setDaemon(True)
        listen_thread.start()
    for index in range(options.threads):
        port_worker = Thread(target=port_check, args=(socket_queue,))
        port_worker.setDaemon(True)
        port_worker.start()
    
    if options.verbose: print("THREADS inside port: {}".format(active_count()))
    socket_queue.join()
    if options.scan_type.lower() == "s": sleep(options.timeout)
    print("********* SOCKET QUEUE BLOCK RELEASED *************")

def halt_ping_threads():
    #Stops all the threads for pinging by sending them "HALT". They have an if: clause that tells them to return
    if options.ping == "y":
        for i in range(options.threads):
            ip_queue.put("HALT")

def get_source_ip(dst_addr):
    #Credit: 131264/alexander from stackoverflow. This gets the correct IP for sending. Useful if you have multiple interfaces
    #NOTE: This will send an additional packet for every single IP to confirm the route. (but just one packet)
    global source_ips
    try:
        if dst_addr in source_ips:
            return source_ips[dst_addr]
        else:
            source_ips[dst_addr] = [(s.connect((dst_addr, 53)), s.getsockname()[0], s.close()) for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1]
            return source_ips[dst_addr]
    except Exception:
        sys.stderr.write("Something went wrong in get_source_ip, results might be wrong\n")

populate_queues()
start_ping_threads()
halt_ping_threads()
if options.ping_only != True:
    start_port_threads()
Raw Socket Port Scanner in Python

Leave a Reply

Your email address will not be published.