offensive-tools/exploits/pwntools/SKILL.md
Auth/lab ref: Python CTF/exploitation framework for interacting with remote services, local processes, and binary exploitation.
npx skillsauth add aeondave/malskill pwntoolsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Service-interaction and binary exploitation framework. The central abstraction is the tube — a uniform API for TCP sockets, local processes, and SSH channels. Write your solver once; switch between local and remote with one flag.
pip install pwntools
Every CTF solver follows this pattern:
from pwn import *
import sys
# Connection switch: python solver.py host:port
# Local: python solver.py → process(cmd)
# Remote: python solver.py host:1337 → remote(host, port)
# Debug: python solver.py host:1337 debug → remote(..., level='debug')
if len(sys.argv) > 1:
host, port = sys.argv[1].split(':')
level = sys.argv[2] if len(sys.argv) > 2 else 'info'
io = remote(host, int(port), level=level)
else:
io = process(['python3', 'challenge/server.py'])
# --- solver logic ---
io.close()
This template appears verbatim in HTB Cyber Apocalypse 2025 (Twin Oracles, Kewiri, Traces) and is the de-facto standard.
All receive calls block until the delimiter/count is met, or raise EOFError on close.
| Method | Description |
|--------|-------------|
| io.recv(n) | Up to n bytes, returns as soon as any data is available |
| io.recvn(n) | Exactly n bytes — blocks until complete |
| io.recvline() | Until \n — includes newline by default |
| io.recvline(drop=True) | Until \n — strips newline |
| io.recvuntil(b'delim') | Until delimiter — includes delimiter |
| io.recvuntil(b'delim', drop=True) | Until delimiter — strips it |
| io.recvregex(b'pattern') | Until regex match |
| io.recvregex(b'pattern', capture=True) | Returns re.Match object |
| io.recvall() | Until EOF |
| io.recvrepeat(timeout) | Until timeout or EOF |
| io.clean() | Drain buffer (discard pending data) |
# Read a line, strip newline, decode to str, extract last token
value = int(io.recvline().decode().strip().split()[-1])
# Read until marker, drop marker, decode, split on ' = '
value = int(io.recvuntil(b'\n', drop=True).decode().split(' = ')[1])
# Read multiple named values from consecutive lines
# Server sends: "n = 12345\ne = 65537\n"
n = int(io.recvline().decode().split(' = ')[1])
e = int(io.recvline().decode().split(' = ')[1])
# Receive until prompt, then parse all hex values from the output
raw = io.recvuntil(b'> ').decode()
import re
values = list(map(bytes.fromhex, re.findall(r'[0-9a-f]{6,}', raw)))
| Method | Description |
|--------|-------------|
| io.send(data) | Raw bytes, no newline |
| io.sendline(data) | Bytes + \n |
| io.sendafter(b'delim', data) | recvuntil(delim) then send(data) |
| io.sendlineafter(b'delim', data) | recvuntil(delim) then sendline(data) |
| io.sendlinethen(b'delim', data) | sendline(data) then recvuntil(delim) |
# Send integer as decimal string
io.sendline(str(value).encode())
# Send integer as hex string (no 0x prefix)
io.sendline(hex(value)[2:].encode())
# Send after prompt: recvuntil(b'> ') then sendline(answer)
io.sendlineafter(b'> ', str(answer).encode())
# Send comma-separated list
io.sendlineafter(b'> ', ','.join(map(str, my_list)).encode())
# Send underscore-separated factorization (HTB Kewiri pattern)
answer = '_'.join([f'{f},{e}' for f, e in factors])
io.sendlineafter(b' > ', answer.encode())
Server sends a prompt, client sends an answer, repeat.
for _ in range(100):
io.recvuntil(b'Question: ')
q = io.recvline(drop=True).decode()
answer = solve(q)
io.sendline(str(answer).encode())
flag = io.recvline().decode()
print(flag)
Server offers a menu. Client selects an option, sends ciphertext, receives result. Classic pattern in RSA/AES oracle challenges.
def query_oracle(ciphertext):
io.sendlineafter(b'> ', b'2') # select oracle option
io.recvuntil(b': ')
io.sendline(hex(ciphertext)[2:].encode())
return int(io.recvline().decode().split()[-1])
# Binary search using oracle (parity/LSB oracle)
lo, hi = 1, n - 1
for _ in range(n.bit_length()):
mid = (lo + hi) // 2
result = query_oracle(pow(2, e, n) * ciphertext % n)
if result:
lo = mid + 1
else:
hi = mid
Server requires: connect → auth/setup → challenge phase → flag retrieval.
# Phase 1: receive server parameters
io.recvuntil(b'n = ')
n = int(io.recvline())
io.recvuntil(b'e = ')
e = int(io.recvline())
# Phase 2: interact with challenge API
io.sendlineafter(b'option > ', b'1')
ct = int(io.recvline().decode().split()[-1])
# Phase 3: send solution
io.sendlineafter(b'answer > ', str(solution).encode())
flag = io.recvline().decode()
def interact(option, data=None):
io.sendlineafter(b'> ', str(option).encode())
if data is not None:
io.recvuntil(b': ')
io.sendline(data if isinstance(data, bytes) else str(data).encode())
return io.recvline().decode().strip()
# Use case
ct = interact(1) # option 1: encrypt
pt = interact(2, ct) # option 2: decrypt with ciphertext
# Service emits encrypted channel logs on connect
io.sendlineafter(b'> ', b'join #general')
raw = io.recvuntil(b'guest > ').strip().decode()
enc_data = list(map(bytes.fromhex, re.findall(r'[\da-f]{6,}', raw)))
context.log_level = 'debug' # show all send/recv bytes
context.log_level = 'info' # default — show sent/received sizes
context.log_level = 'warning' # suppress routine output
# Architecture (needed for ELF/shellcraft)
context.arch = 'amd64'
context.bits = 64
context.os = 'linux'
# Pass log level from command line
io = remote(host, port, level=sys.argv[2] if len(sys.argv) > 2 else 'info')
from pwn import *
# Pack integers to bytes (little-endian by default)
p64(0xdeadbeef) # → b'\xef\xbe\xad\xde\x00\x00\x00\x00'
p32(0xdeadbeef) # → b'\xef\xbe\xad\xde'
u64(b'\xef\xbe\xad\xde\x00\x00\x00\x00') # → 0xdeadbeef
# XOR (operates on bytes, handles different lengths)
xor(b'key', b'plaintext')
xor(b'\x41\x42', b'\x01\x02') # → b'CB'
xor(ciphertext, key, plaintext) # three-way XOR
# Hex helpers
enhex(b'hello') # → '68656c6c6f'
unhex('68656c6c6f') # → b'hello'
# Number ↔ bytes (Crypto.Util.number equivalent)
from pwn import *
# pwntools does not ship long_to_bytes/bytes_to_long
# use pycryptodome for these:
from Crypto.Util.number import long_to_bytes, bytes_to_long
cyclic(100) # generate 100-byte De Bruijn sequence
cyclic_find(0x61616168) # find offset of 4-byte sequence
# Load ELF
elf = ELF('./vuln')
libc = ELF('./libc.so.6')
# Address lookup
elf.symbols['main']
elf.got['printf']
elf.plt['system']
# Leak libc base from a leak
libc.address = leaked_puts - libc.symbols['puts']
# ROP chain
rop = ROP(elf)
rop.call('system', [next(elf.search(b'/bin/sh\x00'))])
payload = flat({offset: rop.chain()})
# Shellcode
shellcode = asm(shellcraft.sh())
shellcode = asm(shellcraft.amd64.linux.sh())
# GDB attach (local process)
gdb.attach(io)
gdb.attach(io, gdbscript='b *main+0x42\ncontinue')
# Option 1: debug flag to process
io = process('./vuln', level='debug')
# Option 2: attach GDB mid-exploit
io = process('./vuln')
gdb.attach(io)
pause() # wait for gdb to attach before continuing
# Option 3: pause at specific point
io.sendline(b'A' * 40)
pause() # press enter to continue
# Option 4: inspect tube buffer state
io.unrecv(b'debug data') # push data back into recv buffer
io.recvline() when the service does not send a newline — use io.recvuntil(b'prompt') instead.int directly — always .encode() or wrap with str().drop=True on recvuntil when the delimiter should not be in the parsed value.io.recv() (non-blocking, may return partial) instead of io.recvn(n) when an exact byte count is needed.from Crypto.Util.number import long_to_bytes but assuming pwntools provides it — it does not.development
Auth/lab ref: Unicorn Engine CPU-only emulation for shellcode, decryptors, custom VM handlers, instruction tracing, memory hooks, and register-level experiments.
development
Auth/lab ref: Renode board and SoC simulation for MCU/RTOS firmware, UART/GPIO/peripheral modeling, GDB remote debugging, REPL platforms, and RESC scripts.
development
Auth/lab ref: Qiling OS-layer binary emulation for PE/ELF/Mach-O/UEFI/shellcode with rootfs, syscall/API hooks, filesystem mapping, and runtime patching.
databases
Auth/lab ref: QEMU user-mode and full-system emulation for cross-arch binaries, firmware, kernels, disks, serial consoles, networking, and GDB stubs.