Python for Security Tooling: A Port Scanner and a Hash Cracker Sketch
A practical, beginner-friendly intro to writing small security tools in Python — a concurrent port scanner and a dictionary hash cracker, with ethics baked in.
Ethics & responsibility: Only ever scan systems you own or have explicit, written permission to test. A port scanner pointed at the wrong target is, in many jurisdictions, treated the same as any other unauthorized access attempt. The tools below are for learning, for defending your own infrastructure, and for use inside authorized engagements. If you're not sure whether you're allowed to point a tool at a host, the answer is no.
Note: This is sample placeholder content created to demonstrate the blog. Replace it with your own writing.
There's a moment, early in any security career, when you realize that most of the tools you depend on — nmap, hashcat, dirb, sqlmap — are themselves just programs. Once that clicks, the obvious next question is: "Could I write a small version of this myself?" The answer is yes, and doing so is one of the fastest ways to actually understand what the real tools are doing. This post walks through two tiny tools in Python: a concurrent port scanner and a dictionary hash cracker.
Tool 1 — A Concurrent Port Scanner
The goal is modest: given a host and a range of ports, report which ports are accepting TCP connections. The naive version — open a socket, try to connect, repeat — works, but it's painfully slow because it does everything one port at a time. The fix is concurrency, and the cleanest modern Python API for this is concurrent.futures.ThreadPoolExecutor.
"""portscanner.py — a tiny concurrent TCP port scanner for authorized use."""
from __future__ import annotations
import argparse
import socket
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
@dataclass
class PortResult:
port: int
open: bool
def probe(host: str, port: int, timeout: float = 1.0) -> PortResult:
"""Attempt a TCP connect() to (host, port). Returns a PortResult."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(timeout)
# connect_ex returns 0 on success, an errno otherwise.
result = s.connect_ex((host, port))
return PortResult(port=port, open=(result == 0))
def scan(host: str, ports: range, workers: int = 100) -> list[PortResult]:
results: list[PortResult] = []
with ThreadPoolExecutor(max_workers=workers) as pool:
futures = {pool.submit(probe, host, p): p for p in ports}
for future in as_completed(futures):
results.append(future.result())
results.sort(key=lambda r: r.port)
return results
def main() -> None:
parser = argparse.ArgumentParser(description="Tiny TCP port scanner.")
parser.add_argument("host", help="target host (only scan what you own)")
parser.add_argument("--start", type=int, default=1)
parser.add_argument("--end", type=int, default=1024)
parser.add_argument("--workers", type=int, default=100)
args = parser.parse_args()
print(f"Scanning {args.host} ports {args.start}-{args.end} "
f"with {args.workers} workers...")
for r in scan(args.host, range(args.start, args.end + 1), args.workers):
if r.open:
print(f" {r.port:>5}/tcp open")
if __name__ == "__main__":
main()
A few notes on the choices:
connect_exis used instead ofconnectso a refused connection returns an error code instead of raising an exception. Exceptions across 1,000 ports add up.- The socket is opened with a
withblock so file descriptors are released promptly. On Linux the default fd limit is 1024 per process; a careless scanner can hit it fast. - Threads are fine here because the work is I/O-bound — we're waiting for connect timeouts, not burning CPU. For CPU-bound work,
ProcessPoolExecutorwould be the right tool. - 100 workers is a reasonable default. Much higher and you start tripping IDS rules or hitting ephemeral port exhaustion on the scanner side.
A quick rule of thumb for the threads-vs-processes choice:
| Workload is… | Use | Why |
|---|---|---|
| I/O-bound | ThreadPoolExecutor |
Threads release the GIL during I/O syscalls |
| CPU-bound | ProcessPoolExecutor |
Processes sidestep the GIL across cores |
| Mixed | Threads, with care | Most security tooling is I/O-heavy |
| CPU + native | multiprocessing or C ext |
When Python hashing throughput is the bottleneck |
Run it against your own machine:
# Scan localhost ports 1-1024 with 100 workers.
python3 portscanner.py 127.0.0.1
# Scan a wider range with more workers.
python3 portscanner.py 127.0.0.1 --start 1 --end 65535 --workers 500
A nicer version of this tool would let you load a list of targets from a YAML file:
# targets.yaml — list of authorized scan targets.
targets:
- host: 127.0.0.1
ports: [22, 80, 443, 8080]
- host: 192.168.1.10
ports: [1, 1024]
I'm leaving the YAML-loading code as an exercise because it's the kind of small, satisfying task that makes a tool feel like yours.
What This Tool Doesn't Do (Yet)
A real scanner — nmap, masscan, zmap — does a lot more. Some things to consider if you want to extend it:
- Service fingerprinting. After a port is open, send a protocol-specific probe and parse the banner. This is how
nmap -sVworks. - SYN scanning. This scanner does a full
connect(), which is logged by the target. SYN scanning (nmap -sS) sends only a SYN and reads the SYN+ACK, then RSTs. It's faster and stealthier but requires raw sockets and usually root. - Rate limiting. A polite scanner caps its packets per second so it doesn't accidentally DoS the target. For a learning tool this is overkill; for anything pointed at real infrastructure it's mandatory.
Tool 2 — A Dictionary Hash Cracker Sketch
The second tool is a dictionary attack against a single unsalted hash. The setup: you have a hash (say, from a CTF challenge or from a forensic image of your own system) and a wordlist, and you want to find the plaintext that produced the hash.
"""hashcracker.py — dictionary attack against a single unsalted hash.
For learning only. Real password cracking tools (hashcat, john) are
orders of magnitude faster because they run on GPUs and use SIMD.
"""
from __future__ import annotations
import argparse
import hashlib
import sys
from concurrent.futures import ProcessPoolExecutor, as_completed
def hash_word(word: str, algorithm: str) -> str:
"""Return the hex digest of `word` under the given algorithm."""
h = hashlib.new(algorithm)
h.update(word.encode("utf-8"))
return h.hexdigest()
def crack(target: str, wordlist: str, algorithm: str = "sha256") -> str | None:
"""Return the first word whose hash matches `target`, or None."""
with open(wordlist, "r", encoding="utf-8", errors="ignore") as f:
words = [line.rstrip("\n") for line in f]
with ProcessPoolExecutor() as pool:
futures = {
pool.submit(hash_word, w, algorithm): w for w in words
}
for future in as_completed(futures):
word = futures[future]
if future.result() == target.lower():
# Cancel the rest as best we can; they're already running.
return word
return None
def main() -> None:
parser = argparse.ArgumentParser(description="Dictionary hash cracker.")
parser.add_argument("hash", help="target hash (hex)")
parser.add_argument("wordlist", help="path to wordlist")
parser.add_argument("--algo", default="sha256", help="hashlib algorithm name")
args = parser.parse_args()
print(f"Cracking {args.algo} hash with {args.wordlist}...")
result = crack(args.hash, args.wordlist, args.algo)
if result is None:
print("No match found.")
sys.exit(1)
print(f"Match: {result}")
if __name__ == "__main__":
main()
Generate a target hash from a known word and try it:
# Generate a sha256 of "letmein" — the password we're trying to recover.
echo -n "letmein" | sha256sum
# 9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee6845628c12b338f3b8c4b8c4b8c (illustrative)
# Run the cracker against a wordlist.
python3 hashcracker.py \
9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee6845628c12b338f3b8c4b8c4b8c \
/usr/share/wordlists/rockyou.txt \
--algo sha256
A few things to notice:
- This uses
ProcessPoolExecutor, not threads, because hashing is CPU-bound. Threads wouldn't help here due to the GIL. - The matching is case-insensitive on the hex digest because some tools emit uppercase and some lowercase.
- There's no salt handling. If the hash you're targeting was produced with a salt, you'd need to know the salt and incorporate it into
hash_word. Salting defeats precomputation (rainbow tables) but not dictionary attacks per se — it just makes each guess more expensive. - This is dramatically slower than
hashcat.hashcatuses GPUs and can do billions of SHA-256 hashes per second on consumer hardware. The point of this script is understanding, not performance.
Why Bother Writing These Yourself?
There are a few good reasons to write tiny versions of well-known tools, even though better versions already exist:
- You learn what the tool is actually doing. A port scanner stops being magic the moment you've written
connect_exagainst a port and watched the timeout. - You learn to read other people's code. Once you've written a hash cracker, reading
hashcat's source becomes tractable. The vocabulary is shared. - You build the habit of small, sharp tools. Most real security work is glue — a script that takes output from one tool, transforms it, and feeds it to another. Writing tiny tools from scratch is how you get good at glue.
Where to Go Next
If these two were interesting, the natural next projects are:
- A directory brute-forcer for HTTP, similar to
gobuster. The structure is the same: a target, a wordlist, a worker pool, and a probe function that returns open/closed. - A log parser that reads an Apache/Nginx access log and flags suspicious patterns (anomalous user agents, 4xx spikes, single-IP scans).
- A simple IDS that watches a pcap and alerts on SYN scans — a great exercise in reading TCP flags.
The OWASP Python Security project and the Real Python concurrency guides are good places to keep digging. And, as always, the hashlib and socket module docs are worth reading end-to-end at least once — they're not long, and they demystify a lot.