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()