Create a Multithreaded Port Scanner with Python
Learn how to create a multithreaded port scanner with Python. We will go over TCP, how multiple threads speed up a program, and how port scanners work under the hood.
Table of Contents 📖
How Port Scanners Work
Port scanners search for processes that are listening out for connections on a target machine. The scanning is often done with the TCP protocol, but can be done with UDP as well. When using TCP, the scanner will attempt to perform a 3 way handshake to establish a connection. If this handshake fails, then the port is closed. If this handshake succeeds, then the port is open.
INFO: The port scanner we create in this example will use the TCP protocol.
Writing Code
First lets import the required libraries.
# Library for asynchronously executing functions.
# The ThreadPoolExecutor does the asynchronous execution with threads.
from concurrent.futures import ThreadPoolExecutor
# Library for working with sockets. We will use it to attempt to form a TCP connection.
import socket
# Library for working with time. We will use it to calculate how long the application took to run.
import time
Now, at the top of the file, lets create a MAX_WORKERS variable. This will be the max amount of threads to execute a function asynchronously. We will use it to assign a thread to different ranges of ports.
MAX_WORKERS = 100
WARNING: This variable can be changed to best suite the runtime environment.
Now lets create a function to divide up the provided port range into a number of ranges. Each of these ranges will be handled in a separate thread.
def generate_port_chunks(port_range):
# Get the min and max port numbers from the port range
port_ranges = port_range.split('-')
port_chunks = []
# Divide the port range into chunks
chunk_size = int((int(port_ranges[1]) - int(port_ranges[0])) / MAX_WORKERS)
# Create a nested list of port chunks to be handled by each worker
for i in range(MAX_WORKERS):
start = int(port_ranges[0]) + (chunk_size * i)
end = start + chunk_size
port_chunks.append([start, end])
return port_chunks
After creating the chunks, lets create the scan function to check for open ports. This is the function that will be ran asynchronously in the ThreadPoolExecutor.
def scan(ip_address, port_chunk):
print(f"[~] Scanning {ip_address} from {port_chunk[0]} to {port_chunk[1]}.")
# Loop through the min and max port chunks
for port in range(int(port_chunk[0]), int(port_chunk[1])):
# Attempt a TCP IPv4 connection to the provided port and IP address
try:
scan_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
scan_socket.settimeout(2)
# print(f"[~] Scanning {ip_address} on port {port}.")
scan_socket.connect((ip_address, port))
print(f"[!] Port {port} is open")
# If the port is closed an exception will be thrown, capture it here
except:
None
Now we just need to create the main function to run the application.
def main():
ip_address = '31.220.55.159'
port_range = '0-10000'
# Divide port range into chunks
port_chunks = generate_port_chunks(port_range)
# Start the timer
start_time = time.time()
# Submit tasks to be executed by the thread pool using map
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
executor.map(scan, [ip_address] * len(port_chunks), port_chunks)
# Finish the timer
end_time = time.time()
print(f"Scanned {port_range[1]} ports in {end_time - start_time} seconds.")
if __name__ == '__main__':
main()
Here we set the maximum amount of threads for the pool and then use them to scan their provided port range. We also make use of the with statement so we can close the pool when we are done.
Running the Program
Now we just need to run the program with Python.
python3 ./port_scanner.py
[~] Scanning 31.220.55.159 from 0 to 100.
[~] Scanning 31.220.55.159 from 100 to 200.
[~] Scanning 31.220.55.159 from 200 to 300.
[~] Scanning 31.220.55.159 from 300 to 400.
[~] Scanning 31.220.55.159 from 400 to 500.
[~] Scanning 31.220.55.159 from 500 to 600.
[~] Scanning 31.220.55.159 from 600 to 700.
[~] Scanning 31.220.55.159 from 700 to 800.
...
[~] Scanning 31.220.55.159 from 9500 to 9600.
[~] Scanning 31.220.55.159 from 9600 to 9700.
[~] Scanning 31.220.55.159 from 9700 to 9800.
[~] Scanning 31.220.55.159 from 9800 to 9900.
[~] Scanning 31.220.55.159 from 9900 to 10000.
[!] Port 22 is open
[!] Port 443 is open
[!] Port 80 is open
Scanned - ports in 32.10092091560364 seconds.
More on Threads
Multithreading and multiprocessing are two ways to execute a function asynchronously in Python. They both speed up the execution of code. To demonstrate, try toggling the MAX_WORKERS variable and take a look at the output for how long the scan takes.
WARNING: Multithreading executes multiple threads simultaneously within the same process.
Disclaimers
It should also be noted that this port scanner code is for educational purposes only. Do not go scanning IP addresses that do not belong to you. Also, firewalls or other security measures could make a port appear closed.