Logo
Offensive Python

Python Automation for Penetration Testing

Python Automation for Penetration Testing

Short answer: Python automation in penetration testing means wrapping existing tools (Nmap, Gobuster, Hydra), chaining their output into subsequent steps, and scripting any repetitive task — credential spraying, directory enumeration, screenshot capture — that would otherwise take hours of manual effort.

TL;DR: A Python automation script is a pipeline: gather input, run a tool or socket operation, parse the output, make a decision, move to the next step. Master subprocess, requests, and output parsing and you can automate most of a pentest's reconnaissance and enumeration phases.

Key Takeaways - Automation is not cheating — it is time management. Manual testing of 20 hosts × 100 service checks takes days; automated pipelines take minutes. - The three core Python automation primitives for pentesters: subprocess (tool wrappers), requests (HTTP), and socket (raw network). - Parse structured output (XML, JSON) from tools rather than scraping their text output — Nmap's XML and -oN formats are far more reliable than regex on terminal output. - Rate limiting and error handling are not optional in automation — targets break, networks drop packets, services slow down under load. - All examples in this post are for authorized testing and lab use only.

What pentest automation actually looks like

What pentest automation actually looks like

Automation in a real engagement is not a single "auto-hack" script. It is a collection of targeted scripts that remove the manual tedium from specific phases:

  • Recon pipeline: resolve a list of hostnames, run Nmap, parse results, categorize by service type
  • Web enumeration: discover directories and parameters across a list of web targets
  • Credential testing: apply a wordlist against a login endpoint with proper rate limiting and lockout detection
  • Post-scan triage: pull all hosts running specific services (SMB, RDP, unpatched Apache) from a large scan result

Python is the glue between these phases. Tools produce output; Python reads it, makes decisions, and feeds the next tool. Python consistently appears in the top required technical skills for penetration testing roles, reflecting its role as the primary automation and scripting language across the offensive security profession.

Python penetration testing automation pipeline Five pipeline stages connected by arrows: Input (target list), Recon (Nmap wrapper), Parse (XML parser), Enumerate (HTTP/SMB scripts), Report (findings collector). Python sits as the orchestration layer between each stage. Python pentest automation pipeline Input Target list Recon Nmap wrapper Parse XML / JSON Enumerate HTTP / SMB Report Findings CSV Python orchestration layer (subprocess · requests · socket · xml.etree)
Figure 1. Python as the orchestration layer connecting discrete pentest tools into a unified pipeline.

Wrapping tools with subprocess

Wrapping tools with subprocess

subprocess.run() executes a system command and captures its output. This is the primary way you integrate existing tools (Nmap, Gobuster, Nikto, whatweb) into Python pipelines.

import subprocess
import shlex


def run_nmap(target: str, flags: str = "-sV -T4 --open") -> str:
    """
    Run Nmap against a target and return stdout as a string.
    Uses shlex.split to safely handle flag strings.
    For authorized targets only.
    """
    cmd = f"nmap {flags} {target}"
    result = subprocess.run(
        shlex.split(cmd),
        capture_output=True,
        text=True,
        timeout=300   # 5-minute hard timeout
    )
    if result.returncode != 0:
        raise RuntimeError(f"Nmap failed: {result.stderr}")
    return result.stdout


# Usage
output = run_nmap("192.168.1.10", flags="-sV -T4 -p 80,443,22,21,3306 --open")
print(output)

shlex.split() is important — it handles quoted arguments correctly and avoids shell injection if you construct commands from variables. Never use shell=True with user-controlled input.

Parsing Nmap XML output

Parsing Nmap XML output

Text output from tools is fragile to parse with regex. Nmap's XML output (-oX) is structured and reliable:

import subprocess
import shlex
import xml.etree.ElementTree as ET
from dataclasses import dataclass, field
from typing import List


@dataclass
class PortResult:
    port: int
    protocol: str
    state: str
    service: str
    version: str


@dataclass
class HostResult:
    ip: str
    hostname: str
    ports: List[PortResult] = field(default_factory=list)


def nmap_xml_scan(target: str, flags: str = "-sV -T4") -> List[HostResult]:
    """
    Run Nmap with XML output and parse results into structured objects.
    Returns a list of HostResult objects.
    """
    cmd = ["nmap", "-oX", "-"] + shlex.split(flags) + [target]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)

    hosts = []
    root = ET.fromstring(result.stdout)

    for host_elem in root.findall("host"):
        # Get IP address
        addr_elem = host_elem.find("address[@addrtype='ipv4']")
        if addr_elem is None:
            continue
        ip = addr_elem.get("addr", "")

        # Get hostname
        hostname_elem = host_elem.find(".//hostname")
        hostname = hostname_elem.get("name", "") if hostname_elem is not None else ""

        host = HostResult(ip=ip, hostname=hostname)

        # Parse ports
        for port_elem in host_elem.findall(".//port"):
            state_elem = port_elem.find("state")
            service_elem = port_elem.find("service")

            if state_elem is None or state_elem.get("state") != "open":
                continue

            host.ports.append(PortResult(
                port=int(port_elem.get("portid", 0)),
                protocol=port_elem.get("protocol", "tcp"),
                state="open",
                service=service_elem.get("name", "") if service_elem is not None else "",
                version=service_elem.get("version", "") if service_elem is not None else "",
            ))

        hosts.append(host)

    return hosts


# Usage
results = nmap_xml_scan("192.168.1.0/24", flags="-sV -T4 --open -p 22,80,443,445,3389")
for host in results:
    print(f"\n{host.ip} ({host.hostname})")
    for p in host.ports:
        print(f"  {p.port}/{p.protocol}  {p.service}  {p.version}")

This pattern — structured output from tools, parsed into Python objects — is the foundation of any repeatable automation pipeline.

HTTP enumeration with requests

The requests library handles web interaction. Here is a directory enumeration pattern that respects rate limits:

import requests
import time
from typing import List, Optional


def enumerate_directories(
    base_url: str,
    wordlist_path: str,
    extensions: List[str] = ["", ".php", ".html", ".txt"],
    delay: float = 0.1,
    timeout: float = 5.0,
    valid_codes: List[int] = [200, 201, 204, 301, 302, 401, 403]
) -> List[dict]:
    """
    Enumerate directories/files on a web target using a wordlist.
    delay: seconds between requests (be a good citizen on shared labs)
    Returns list of dicts with url, status_code, content_length.

    For authorized web application testing only.
    """
    session = requests.Session()
    session.headers.update({"User-Agent": "Mozilla/5.0 (Security Assessment)"})

    found = []

    try:
        with open(wordlist_path, "r", encoding="utf-8", errors="replace") as f:
            words = [line.strip() for line in f if line.strip() and not line.startswith("#")]
    except FileNotFoundError:
        print(f"[!] Wordlist not found: {wordlist_path}")
        return found

    base_url = base_url.rstrip("/")
    total = len(words) * len(extensions)
    checked = 0

    for word in words:
        for ext in extensions:
            url = f"{base_url}/{word}{ext}"
            try:
                resp = session.get(url, timeout=timeout, allow_redirects=False)
                if resp.status_code in valid_codes:
                    result = {
                        "url": url,
                        "status": resp.status_code,
                        "length": len(resp.content),
                    }
                    found.append(result)
                    print(f"  [{resp.status_code}] {url}  ({len(resp.content)} bytes)")
            except requests.RequestException:
                pass

            checked += 1
            if checked % 100 == 0:
                print(f"  ... {checked}/{total} requests sent, {len(found)} found")

            time.sleep(delay)

    return found


# Usage on an authorized lab target
results = enumerate_directories(
    "http://192.168.1.10",
    "/usr/share/wordlists/dirb/common.txt",
    delay=0.05
)
print(f"\nFound {len(results)} paths")

The delay parameter matters. Aggressive enumeration against a shared lab environment can cause service instability. A 50–100ms delay keeps scans reasonably fast while avoiding flooding.

Credential testing patterns

Credential testing (password spraying, brute-forcing) requires careful implementation to avoid account lockouts:

import requests
import time
from typing import List, Optional


def spray_http_login(
    url: str,
    usernames: List[str],
    password: str,
    user_field: str = "username",
    pass_field: str = "password",
    success_indicator: str = "dashboard",
    lockout_indicator: str = "locked",
    delay: float = 2.0,
) -> Optional[str]:
    """
    Password spray: try ONE password against MANY usernames.
    Spraying avoids lockout by staying under per-account attempt limits.
    Returns the first username that succeeds, or None.

    For authorized testing only. Verify lockout policy before running.
    """
    session = requests.Session()

    for username in usernames:
        try:
            resp = session.post(
                url,
                data={user_field: username, pass_field: password},
                timeout=10,
                allow_redirects=True
            )

            if lockout_indicator.lower() in resp.text.lower():
                print(f"  [!] Lockout detected for {username} — stopping")
                return None

            if success_indicator.lower() in resp.text.lower() or resp.status_code == 302:
                print(f"  [+] Valid credentials: {username} / {password}")
                return username

        except requests.RequestException as e:
            print(f"  [!] Request failed for {username}: {e}")

        time.sleep(delay)  # pause between attempts

    return None

Password spraying (one password, many users) is the correct approach when account lockout policies exist. Brute force (many passwords, one user) will trigger lockouts. Always confirm the lockout threshold from the scoping document before running any credential testing. Default Active Directory lockout policy is typically 3–5 failed attempts; spraying a single password across all accounts counts as one attempt per account, but any misconfigured policy or unknown threshold can still cause lockouts. Always verify before running.

Building a reusable scan pipeline

Pulling the pieces together into a reusable pipeline. This script is meant to be combined with the helper functions defined earlier in this post — nmap_xml_scan() from the "Parsing Nmap XML output" section and enumerate_directories() from the "HTTP enumeration with requests" section. Save those two functions (and their imports/dataclasses) in a module named recon_helpers.py, then the pipeline imports them as shown:

#!/usr/bin/env python3
"""
recon_pipeline.py — Automated recon and enumeration pipeline.
For authorized use only. Requires a scope file and signed ROE.
"""
import argparse
import json
import os
from pathlib import Path

# Requires nmap_xml_scan() and enumerate_directories() from the sections above.
# Save those earlier snippets in recon_helpers.py, then import them here:
from recon_helpers import nmap_xml_scan, enumerate_directories


def load_targets(scope_file: str) -> list[str]:
    """Load targets from a simple text file (one IP/CIDR per line)."""
    with open(scope_file) as f:
        return [l.strip() for l in f if l.strip() and not l.startswith("#")]


def run_pipeline(target: str, output_dir: Path) -> dict:
    """Run full recon pipeline against a single target."""
    output_dir.mkdir(parents=True, exist_ok=True)
    results = {"target": target, "ports": [], "web": []}

    # Stage 1: Port scan
    print(f"\n[*] Stage 1: Port scan → {target}")
    hosts = nmap_xml_scan(target, flags="-sV -T4 --open -p-")
    for host in hosts:
        for port in host.ports:
            results["ports"].append({
                "ip": host.ip, "port": port.port,
                "service": port.service, "version": port.version
            })

    # Stage 2: Web enumeration on discovered HTTP(S) ports
    web_ports = [p for p in results["ports"] if p["service"] in ("http", "https", "http-alt")]
    for entry in web_ports:
        scheme = "https" if entry["service"] == "https" else "http"
        url = f"{scheme}://{entry['ip']}:{entry['port']}"
        print(f"\n[*] Stage 2: Web enum → {url}")
        web_results = enumerate_directories(url, "/usr/share/wordlists/dirb/common.txt")
        results["web"].extend(web_results)

    # Save results
    out_file = output_dir / f"{target.replace('/', '_')}_results.json"
    with open(out_file, "w") as f:
        json.dump(results, f, indent=2)
    print(f"\n[+] Results saved: {out_file}")

    return results


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Recon pipeline (authorized use only)")
    parser.add_argument("scope", help="Path to scope file (one target per line)")
    parser.add_argument("-o", "--output", default="./output", help="Output directory")
    args = parser.parse_args()

    targets = load_targets(args.scope)
    output_path = Path(args.output)

    print(f"[*] Pipeline starting: {len(targets)} targets")
    for t in targets:
        run_pipeline(t, output_path)

    print("\n[+] Pipeline complete.")
Automation component Python tool What it automates
Tool execution subprocess Nmap, Gobuster, Nikto, whois
XML/JSON parsing xml.etree, json Structured tool output
HTTP interaction requests Web enum, login testing, API fuzzing
Network primitives socket Custom port scanning, banner grabbing
Concurrency concurrent.futures Parallel scans across multiple targets
Output storage json, csv, pathlib Findings files for reporting

Frequently asked questions

What is the difference between Python automation and just running Nmap?

Running Nmap gives you output to read. Python automation turns that output into decisions and actions. A Python pipeline reads Nmap XML, extracts hosts running HTTP, passes those to a directory enumeration script, and collects findings into a report — without manual intervention at each step. The gain is speed and repeatability across large scopes.

How do I avoid triggering intrusion detection systems (IDS) with automated scans?

Rate limiting is the primary lever — add time.sleep() between requests. Use -T2 or -T3 in Nmap instead of -T4/-T5. Randomize port order. Vary User-Agent strings. Note that evading detection on a live production system during an authorized engagement is a separate scope decision — discuss it with the client in the rules of engagement before building evasive automation. For lab practice, tools like Snort on a local VM help you understand what triggers alerts.

Can I use Python to automate Active Directory enumeration?

Yes — Impacket is a Python library that implements SMB, MSRPC, NTLM, Kerberos, and LDAP natively. Scripts like GetUserSPNs.py, secretsdump.py, and smbclient.py from the Impacket suite are all Python and can be imported into your own pipelines. This is advanced offensive Python territory covered in the offensive Python course.

Where should I practice Python pentest automation safely?

Use a local lab: VirtualBox or VMware with Metasploitable, DVWA, or VulnHub VMs. Platforms like Hack The Box, TryHackMe, and PentesterLab provide structured lab environments with explicit authorization. Always confirm scope in writing before pointing any automation at a target. Start with building a port scanner in Python as the first lab exercise before building pipelines.


ThreatBlock's offensive Python course takes you from Python basics to building real security tools hands-on.

About the author: Samaksh is Lead Instructor at ThreatBlock and holds the OSCP, CRTP, CRTO, and CEH certifications, with hands-on experience in penetration testing and red teaming. Read more on the author page · Editorial policy.

← Back to all articles
{{ toast }}