Foundations of Ethical Security Testing with Python: A Beginner's Laboratory Guide
Chapter 2 of 11 · updated Jul 08, 2026
Python Security Toolkit: Essential Syntax and Patterns
Python Security Toolkit: Essential Syntax and Patterns
This page assumes you've worked through the first steps in Page 1 — variables, running a .py file, and the idea that Python executes top-to-bottom. Now we need patterns you'll actually use when scripting against Lab Wintermute: pulling data from DVWA at 192.0.2.30, parsing nmap output for 192.0.2.20, handling errors without your script dying mid-scan. Keep this open in a second terminal while you work.
Core Control Flow: Decisions and Repetition
Conditionals — branch based on a test.
# Basic if/elif/else — checking a port state from scan data
port_state = "open" if port_state == "open": print(f"Port is accessible — investigate service")
elif port_state == "filtered": print(f"Firewall or ACL blocking — port may be open behind filter")
else: print(f"Port closed or host unreachable") # Common comparison operators: ==, !=, <, >, <=, >=, 'in' for membership
Why this matters: A port marked
filteredis not a clean result — it is an unanswered question. Your script must handle all three states, not justopen.
Loops — repeat work across targets or data collections.
# for loop — iterate over a known collection
targets = ["192.0.2.20", "192.0.2.30"]
for ip in targets: print(f"Scheduling scan for {ip}") # range() — generate number sequences, useful for port ranges
for port in range(1, 1025): # 1 to 1024 inclusive # scan logic here pass # while loop — repeat while condition holds, with explicit exit
attempt = 0
max_attempts = 3
while attempt < max_attempts: # retry logic here attempt += 1 # increment to avoid infinite loop
enumerate() — get index and value together, essential for numbered output.
services = ["ssh", "http", "ftp"]
for idx, svc in enumerate(services, start=1): print(f"{idx}. {svc}")
# Output: 1. ssh, 2. http, 3. ftp
Storing and Structuring Data: Lists and Dictionaries
Lists — ordered, mutable sequences. Use for ordered scan results or target queues.
# Creating and modifying
open_ports = [22, 80, 443]
open_ports.append(3306) # add to end
open_ports.insert(0, 21) # insert at position # Accessing by position (zero-indexed)
first_port = open_ports[0] # 21
last_port = open_ports[-1] # 3306 # Slicing — extract subsets
low_ports = open_ports[:3] # first three elements
Dictionaries — key:value pairs. Use for structured host data: IP maps to findings.
# Wintermute scan result storage
host_findings = { "192.0.2.20": { "os": "Linux 2.6", "open_ports": [21, 22, 23, 80], "services": {21: "ftp", 22: "ssh", 23: "telnet", 80: "http"} }, "192.0.2.30": { "os": "Unknown", "open_ports": [80, 443], "services": {80: "http", 443: "https"} }
} # Accessing nested data
metasploitable_services = host_findings["192.0.2.20"]["services"]
print(metasploitable_services[80]) # "http" # Safe access with .get() — returns None instead of crashing if key missing
dvwa_os = host_findings.get("192.0.2.30", {}).get("os")
In plain terms: lists are for "here's a sequence of things," dictionaries are for "here's what I know about this specific thing."
File I/O: Reading and Writing Script Data
# Reading a file of target IPs — one per line
with open("wintermute_targets.txt", "r") as f: targets = f.read().splitlines() # removes trailing newlines # Writing structured output — append mode preserves previous runs
with open("scan_results.txt", "a") as f: f.write(f"192.0.2.20: ports {open_ports}\n") # CSV-style parsing for tool output
line = "192.0.2.20,80,open,Apache httpd 2.2.8"
parts = line.split(",") # ['192.0.2.20', '80', 'open', 'Apache httpd 2.2.8']
ip, port, state, banner = parts # unpack into variables
String Handling: Parsing and Encoding Security Data
Formatting — three approaches, f-strings preferred for readability.
target = "192.0.2.20"
port = 80 # f-string (Python 3.6+) — most readable for security scripts
url = f"http://{target}:{port}/dvwa/login.php" # .format() — older, still seen in inherited code
url = "http://{}:{}/dvwa/login.php".format(target, port) # % formatting — legacy, avoid in new code
url = "http://%s:%s/dvwa/login.php" % (target, port)
Encoding and Decoding — network data arrives as bytes; you need text.
import base64 # Bytes to text — specify encoding explicitly
raw_bytes = b"user:pass"
text = raw_bytes.decode("utf-8") # "user:pass"
back_to_bytes = text.encode("utf-8") # b"user:pass" # Base64 — common in HTTP Basic Auth headers
credentials = "admin:password"
encoded = base64.b64encode(credentials.encode("utf-8"))
# Result: b'YWRtaW46cGFzc3dvcmQ=' decoded = base64.b64decode(encoded).decode("utf-8")
# Back to: "admin:password"
⚠️ Authorized, defensive use only. Encoding transforms data; it does not protect it. Base64 is not encryption — anyone can reverse it. You'll see it in HTTP headers during legitimate traffic analysis in Lab Wintermute.
Error Handling: Keeping Scripts Alive Mid-Engagement
A script that crashes on the third host of fifty wastes your lab time and leaves gaps in documentation.
import requests # Basic try/except — catch specific exceptions, not all
try: r = requests.get("http://192.0.2.30/dvwa/", timeout=5) print(r.status_code)
except requests.exceptions.ConnectionError: print("Host unreachable — check VM network or snapshot state")
except requests.exceptions.Timeout: print("Request timed out — target may be overloaded or filtered")
except requests.exceptions.RequestException as e: print(f"Unexpected error: {e}") # finally block — runs regardless, useful for cleanup
finally: print("Scan attempt logged for report")
Common pattern: wrap the entire per-target loop body in try/except, so one bad target doesn't kill the batch.
Running External Tools: subprocess
The subprocess module (standard library since Python 2.4, no installation needed) calls system tools and captures output. It replaces older, less safe functions like os.system().
import subprocess # SAFE pattern: list of arguments, shell=False (default)
# Runs nmap against Metasploitable2; returns bytes
result = subprocess.check_output( ["nmap", "-sV", "-p", "22,80", "192.0.2.20"], stderr=subprocess.DEVNULL, # suppress error noise timeout=60 # don't hang indefinitely
) # Decode bytes to string for parsing
output_text = result.decode("utf-8")
print(output_text) # If command fails, CalledProcessError is raised with .returncode and .output
Why this matters:
shell=Truelets you writenmap -sV 192.0.2.20as a single string, but it also lets shell metacharacters in variables execute arbitrary commands. In Lab Wintermute that's low risk; in production with user input, it's catastrophic. Build the list-of-arguments habit now.
| Parameter | Purpose | Safe Default |
|---|---|---|
shell=False | Execute via OS directly, not shell interpreter | ✓ Keep False |
stderr | Where to send error stream; DEVNULL discards, STDOUT merges | Varies by need |
timeout | Seconds before aborting hung command | Always set |
universal_newlines / text=True | Return string not bytes; text=True is Python 3.7+ | Check your Python version |
HTTP Requests with the requests Library
Not in standard library — install once per environment:
python -m pip install requests
import requests # GET request to DVWA — basic call
r = requests.get("http://192.0.2.30/dvwa/login.php", timeout=10) # Response inspection
print(r.status_code) # HTTP result: 200, 404, 500, etc.
print(r.headers.get("Server")) # Web server banner, if disclosed # Text vs. binary content
print(r.text[:200]) # Decoded string using r.encoding
print(r.content[:50]) # Raw bytes — check before decoding # Encoding control — force if detection fails
r.encoding = "utf-8" # or "ISO-8859-1" for legacy apps
print(r.text) # JSON parsing — DVWA API endpoints, or tool output
# r.json() raises exception if response isn't valid JSON
try: data = r.json()
except ValueError: data = None # Not JSON, handle accordingly
Quick Reference: Built-ins and Type Conversions
| Function | Typical Security Use |
|---|---|
len(collection) | Count open ports, response length anomalies |
range(start, stop) | Iterate port numbers, generate test sequences |
enumerate(iterable, start=1) | Numbered output for reports |
int(string) | Convert port string "80" to integer 80 |
str(number) | Convert port integer to string for concatenation |
bytes(string, encoding) | Prepare data for network transmission |
help(object) | Inspect module or function in interactive Python |
# help() in practice — when you forget a method
help(requests.get) # shows signature and docstring
help(subprocess.check_output) # verify parameters before running
Putting It Together: Minimal Wintermute Recon Script Skeleton
#!/usr/bin/env python3
import subprocess
import sys TARGET = "192.0.2.20"
PORTS = [21, 22, 23, 25, 80, 443] def scan_port(target, port): """Run minimal TCP probe, return parsed result or None.""" try: # -Pn: skip host discovery (we know it's up in lab) # -p: single port cmd = ["nmap", "-Pn", "-p", str(port), "--open", target] result = subprocess.check_output(cmd, timeout=30) return result.decode("utf-8") except subprocess.CalledProcessError: return None # port closed or filtered except subprocess.TimeoutExpired: return "TIMEOUT" # Main loop with structured storage
findings = {}
for port in PORTS: output = scan_port(TARGET, port) findings[port] = output print(f"Port {port}: {'OPEN' if output else 'closed/filtered'}") # findings dictionary now holds everything for your report
Cross-Reference to Coming Pages
- Page 5 (Reconnaissance): Expands the nmap wrapper above into full host discovery.
- Page 6 (Web Applications): Deepens
requestsusage for DVWA form interaction and session handling. - Page 9 (Malware): Encoding patterns here apply to payload analysis — safely, without execution.
Samuel's note from the bench: I keep a
wintermute_utils.pyin every lab session folder with thescan_port()pattern above, modified per exercise. Reusing your own tested code beats rewriting from scratch — but only if you understand what it does. Copy-paste without comprehension produces scripts that fail silently at 2 AM when your snapshot rollback doesn't work.