Garde-fous intégrés :
- LAB_ONLY : chaque composant refuse de démarrer si
LAB_ONLY=1n'est pas présent. - Sous-réseau
172.20.0.0/24: le ver-jouet refuse toute cible hors de ce /24 de lab. - Anti-Internet : réseau Docker
internal: true(pas de route sortante) et contrôle applicatif qui arrête le programme si Internet est joignable. - Consentement des cibles : seuls les leurres qui répondent
X-Lab-Decoy-State: simulated-compromisesont comptés. - Exfiltration simulée : le « butin » (un canari local) est écrit dans un fichier local, jamais envoyé sur le réseau.
Théorie - deux failles mémoire (octobre 2026)
Ces deux CVE ne sont pas l'ancienne histoire du CGI PHP avec
-d auto_prepend_file=php://input (injection d'argument CGI). Ce sont des failles
de gestion mémoire au cœur de PHP, qui touchent presque chaque requête : la gestion
de la mémoire et la gestion des erreurs. C'est ce qui les rend graves et difficiles à corriger
sans casser la compatibilité ou les performances.
CVE-2026-109618 - RCE via corruption OOM
CritiqueCVSS 9.2 GHSA-w8hw-w9fx-j79w
Mécanisme : PHP utilise longjmp() pour interrompre le traitement
d'une requête lors d'une erreur out-of-memory. Si l'allocation échoue pendant
l'initialisation, un état de requête partiellement initialisé est déjà atteignable.
La routine d'arrêt (shutdown/cleanup) peut alors traiter cet état invalide comme valide,
d'où une corruption mémoire pouvant mener à l'exécution de code.
Quand ça frappe : au pire moment de l'init mémoire. Sous Apache mod_php, une requête non authentifiée peut déclencher la faille avant même que le code applicatif ne s'exécute (RCE démontrée en configuration par défaut). PHP-FPM est aussi concerné via des chemins applicatifs.
CVE-2026-109619 - RCE via état global obsolète (UAF)
CritiqueGHSA-6wmf-4rv5-p3rp
Mécanisme : une erreur fatale au mauvais endroit peut laisser un état global interne pointant vers de la mémoire appartenant à une requête déjà libérée. Une requête ultérieure traitée par le même worker réutilise cet état obsolète avant que l'initialisation normale ne le réinitialise : c'est un use-after-free entre requêtes.
Quand ça frappe : à la requête suivante sur un worker persistant (mod_php, FPM). Use-after-free et crash de worker confirmés ; RCE démontrée contre une application réelle sous Apache mod_php.
Le point commun : les deux failles vivent dans des parties que presque toutes les requêtes traversent - allocation mémoire et gestion des erreurs. L'attaquant n'a pas besoin d'authentification ; il lui suffit d'un accès réseau au service web et de requêtes HTTP spécialement forgées. Ce lab ne forge aucune de ces requêtes : il n'en imite que la phase comportementale observable (propagation, collecte, exfil) via un en-tête marqueur inoffensif.
Versions corrigées & mitigations provisoires (CERT-FR)
Correctifs publiés le 21 octobre 2026. La seule protection complète est la mise à jour : les mitigations ci-dessous ne font que réduire la surface d'attaque.
| Branche PHP | Versions vulnérables | Version corrigée |
|---|---|---|
| PHP 8.5.x | < 8.5.12 | 8.5.12 |
| PHP 8.4.x | < 8.4.27 | 8.4.27 |
| PHP 8.3.x | < 8.3.36 | 8.3.36 |
| PHP 8.2.x | < 8.2.35 | 8.2.35 |
| PHP 8.1 et antérieur | fin de vie | backports source minimaux (7.4/8.0/8.1) ou vendeur LTS |
Risque le plus élevé : Apache mod_php. Également concernés : PHP-FPM, php-cgi, FrankenPHP, LiteSpeed.
Contournements CVE-2026-109618 (OOM)
- Fixer
memory_limit=-1au niveau administrateur (ex.php_admin_valuedans le pool FPM ou la conf Apache) pour qu'aucune appli ne puisse le redéfinir. post_max_sizebas : 8M sans upload, 32M si uploads nécessaires ; garderupload_max_filesize < post_max_size.- Limiter au niveau serveur/proxy : taille du corps décodé, nombre de variables, de parties
multipart, d'en-têtes, profondeur d'imbrication (y compris sans
Content-Lengthexploitable). - Utiliser des limites conteneur/cgroup comme garde-fou de disponibilité (tuer seulement
le worker touché). Ne pas utiliser
RLIMIT_AS/RLIMIT_DATA(ils provoquent justement l'échec d'allocation vulnérable).
Contournements CVE-2026-109619 (UAF)
session.upload_progress.enabled=0puis redémarrer chaque worker (viaphp.ini/pool FPM/conf serveur appliquée avant le parsing - pasini_set()ni.user.ini).- Sur PHP 8.5 :
fatal_error_backtraces=0puis redémarrer les workers. - Supprimer les configurations incorrectes/obsolètes appliquées à chaque requête qui génèrent des diagnostics avant l'initialisation.
- Corriger les erreurs fatales et les dépassements de délai accessibles aux attaquants.
Sources officielles
- PHP Foundation - Today's PHP Security Release (21 oct. 2026)
- php.net - News Archive 2026 (avis de sécurité)
- CERT-FR - CERTFR-2026-ALE-014 (contournements provisoires)
- CERT-FR - CERTFR-2026-AVI-1227
- CERT/CC - VU#763488 - Memory corruption vulnerabilities in PHP
- oss-security - CVE-2026-109618 (CVSS 9.2)
- CERT-EU - Critical Vulnerabilities in PHP (2026-018)
Architecture du lab
FORGE_NET 172.20.0.0/24 (internal:true -> AUCUN Internet)
+-----------------------------------------------------------------------+
| |
| [orchestrateur .5] --header X-Lab-Php2shell-Sim--> [banque .11] |
| | \ [gouv .12] |
| | \--scan /24-------------------------------> [honeypot .20] |
| | |
| [suricata-ids] .....alertes sid:2600xxx.....> (journaux /logs) |
| ^ |
| banque/gouv --logs SIM-COMPROMISSION--> orchestrateur |
| |
+-----------------------------------------------------------------------+
X Internet <--BLOQUE (reseau internal + garde-fou)
Si le diagramme ne s'affiche pas (CDN bloqué), le schéma ASCII de secours apparaît automatiquement.
Prérequis
- Ta machine Kali avec Docker + docker compose v2 (
docker compose version). - Tout se passe en local, réseau isolé. Ne lance rien en dehors du lab.
- On ne lance pas Docker dans cette page : les commandes sont fournies à copier, tu décides quand exécuter.
Déploiement - étapes numérotées (dans l'ordre)
Fais les étapes dans l'ordre, de 1 à 12. Chaque chose à faire est un bloc avec un bouton Copier.
Étape 1 Créer l'arborescence du lab
Tout le lab tient dans un seul dossier de ton Bureau. Copie et exécute :
# Cree l'arborescence du lab dans ton Bureau
mkdir -p ~/Bureau/php2shell-lab/{banque,gouv,honeypot,orchestrateur,canaris,suricata}
mkdir -p ~/Bureau/php2shell-lab/logs/{banque,gouv,honeypot,suricata}
cd ~/Bureau/php2shell-lab
Étape 2 Écrire docker-compose.yml
À la racine ~/Bureau/php2shell-lab/. Note le réseau internal: true (garde-fou anti-Internet) et le sous-réseau 172.20.0.0/24.
# php2shell-lab - LAB_ONLY - reseau 100% isole (aucun acces Internet)
# Images PHP deliberement PATCHEES : ce lab n'exploite AUCUNE CVE reelle.
services:
banque-leurre:
build: ./banque
container_name: banque-leurre
environment: [ "LAB_ONLY=1" ]
env_file: ./canaris/banque.env
volumes: [ "./logs/banque:/var/log" ]
networks:
forge_net:
ipv4_address: 172.20.0.11
restart: "no"
gouv-leurre:
build: ./gouv
container_name: gouv-leurre
environment: [ "LAB_ONLY=1" ]
env_file: ./canaris/gouv.env
volumes: [ "./logs/gouv:/var/log" ]
networks:
forge_net:
ipv4_address: 172.20.0.12
restart: "no"
honeypot:
build: ./honeypot
container_name: honeypot
environment: [ "LAB_ONLY=1" ]
volumes: [ "./logs/honeypot:/logs" ]
networks:
forge_net:
ipv4_address: 172.20.0.20
restart: "no"
suricata:
image: jasonish/suricata:latest
container_name: suricata-ids
network_mode: "service:banque-leurre" # observe le meme namespace reseau que la banque
cap_add: [ NET_ADMIN, NET_RAW, SYS_NICE ]
volumes:
- ./suricata/lab.rules:/etc/suricata/rules/lab.rules:ro
- ./logs/suricata:/var/log/suricata
command: [ "-i", "eth0", "-S", "/etc/suricata/rules/lab.rules" ]
restart: "no"
orchestrateur:
build: ./orchestrateur
container_name: orchestrateur
environment: [ "LAB_ONLY=1" ]
volumes: [ "./logs:/logs" ]
networks:
forge_net:
ipv4_address: 172.20.0.5
restart: "no"
networks:
forge_net:
name: forge_net
driver: bridge
internal: true # GARDE-FOU anti-Internet : le /24 ne route PAS vers l'exterieur
ipam:
config:
- subnet: 172.20.0.0/24
Étape 3 Site leurre « banque » (consent)
Dans banque/. Le Dockerfile part d'une image PHP patchée : aucune vraie faille servie. index.php accepte volontairement le marqueur et journalise.
# banque/Dockerfile - site leurre "Forge Banque"
# Base PHP PATCHEE (>= 8.2.35) : le lab ne repose sur AUCUNE vulnerabilite reelle.
FROM php:8.2.35-apache
LABEL lab="php2shell" role="banque-leurre"
RUN mkdir -p /var/log && touch /var/log/leurre.log
COPY index.php /var/www/html/index.php
EXPOSE 80
<?php
// banque-leurre - SITE LEURRE DE LABORATOIRE ("Forge Banque").
// AUCUNE vulnerabilite reelle. Ce site ACCEPTE VOLONTAIREMENT le marqueur de
// simulation afin de jouer le role d'une cible "compromise" et de produire des
// traces pour l'entrainement a la detection. Il n'execute RIEN de ce que le
// "ver-jouet" envoie : il se contente de JOURNALISER l'evenement.
declare(strict_types=1);
$marqueur = $_SERVER['HTTP_X_LAB_PHP2SHELL_SIM'] ?? '';
$journal = '/var/log/leurre.log';
if ($marqueur !== '') {
// Consentement explicite du leurre : on NOTE, on n'EXECUTE pas.
$ligne = sprintf("[%s] SIM-COMPROMISSION src=%s marqueur=%s\n",
date('c'), $_SERVER['REMOTE_ADDR'] ?? '?', substr($marqueur, 0, 64));
@file_put_contents($journal, $ligne, FILE_APPEND);
header('Content-Type: application/json; charset=utf-8');
header('X-Lab-Decoy-State: simulated-compromise');
http_response_code(200);
echo json_encode([
'lab' => 'php2shell',
'role' => 'banque-leurre',
'etat' => 'compromission SIMULEE enregistree (aucun code execute)',
'canari' => getenv('CANARI_TOKEN') ?: 'absent',
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
return;
}
?>
<!doctype html>
<html lang="fr"><head><meta charset="utf-8"><title>Forge Banque - Espace client</title></head>
<body style="font-family:system-ui,sans-serif;background:#0b0f14;color:#eee;padding:2rem">
<h1>🏦 Forge Banque</h1>
<p><strong>LEURRE DE LABORATOIRE</strong> - aucune donnee reelle, aucun vrai service.</p>
<form onsubmit="return false">
<p><input placeholder="Identifiant"></p>
<p><input type="password" placeholder="Mot de passe"></p>
<button>Connexion</button>
</form>
</body></html>
Étape 4 Site leurre « gouv » (consent)
Dans gouv/. Même principe que la banque.
# gouv/Dockerfile - site leurre "Forge.gouv"
# Base PHP PATCHEE (>= 8.2.35) : aucune vulnerabilite reelle n'est servie.
FROM php:8.2.35-apache
LABEL lab="php2shell" role="gouv-leurre"
RUN mkdir -p /var/log && touch /var/log/leurre.log
COPY index.php /var/www/html/index.php
EXPOSE 80
<?php
// gouv-leurre - SITE LEURRE DE LABORATOIRE ("Forge.gouv - teleservices").
// Meme principe que la banque : il CONSENT a jouer la cible et JOURNALISE le
// marqueur de simulation. Aucune execution, aucune vulnerabilite reelle.
declare(strict_types=1);
$marqueur = $_SERVER['HTTP_X_LAB_PHP2SHELL_SIM'] ?? '';
$journal = '/var/log/leurre.log';
if ($marqueur !== '') {
$ligne = sprintf("[%s] SIM-COMPROMISSION src=%s marqueur=%s\n",
date('c'), $_SERVER['REMOTE_ADDR'] ?? '?', substr($marqueur, 0, 64));
@file_put_contents($journal, $ligne, FILE_APPEND);
header('Content-Type: application/json; charset=utf-8');
header('X-Lab-Decoy-State: simulated-compromise');
http_response_code(200);
echo json_encode([
'lab' => 'php2shell',
'role' => 'gouv-leurre',
'etat' => 'compromission SIMULEE enregistree (aucun code execute)',
'canari' => getenv('CANARI_TOKEN') ?: 'absent',
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
return;
}
?>
<!doctype html>
<html lang="fr"><head><meta charset="utf-8"><title>Forge.gouv - Teleservices</title></head>
<body style="font-family:system-ui,sans-serif;background:#10131a;color:#eee;padding:2rem">
<h1>🏛️ Forge.gouv</h1>
<p><strong>LEURRE DE LABORATOIRE</strong> - portail fictif, aucune donnee d'usager.</p>
<p>Demarches : etat civil (demo) · impots (demo) · carte grise (demo)</p>
</body></html>
Étape 5 Fichiers .env canaris (appâts)
Dans canaris/. Ces secrets sont faux et pistés : s'ils réapparaissent ailleurs, c'est une collecte détectée.
# canaris/banque.env - .env CANARI (appats de laboratoire).
# Ces "secrets" sont FAUX et pistes. S'ils apparaissent dans un log, une requete
# ou un fichier d'exfiltration, c'est la PREUVE qu'une collecte a eu lieu.
LAB_ONLY=1
CANARI_TOKEN=CANARI-BANQUE-7f3a9c2e-NE-PAS-UTILISER
DB_HOST=172.20.0.11
DB_USER=forge_bank_app
DB_PASSWORD=canari-faux-mdp-9931
API_KEY=sk_live_FORGE_CANARI_0000_FAKE
# canaris/gouv.env - .env CANARI (appats de laboratoire).
# Secrets FAUX et pistes. Toute apparition ailleurs = collecte detectee.
LAB_ONLY=1
CANARI_TOKEN=CANARI-GOUV-1b8d4f60-NE-PAS-UTILISER
DB_HOST=172.20.0.12
DB_USER=teleservice_ro
DB_PASSWORD=canari-faux-mdp-4472
JWT_SECRET=CANARI_GOUV_JWT_FAKE_DO_NOT_USE
Étape 6 Honeypot passif
Dans honeypot/. Il n'offre aucun service réel : toute connexion est un signal.
# honeypot/Dockerfile - pot de miel passif
FROM python:3.12-slim
LABEL lab="php2shell" role="honeypot"
COPY honeypot.py /honeypot.py
EXPOSE 8080
CMD ["python", "/honeypot.py"]
#!/usr/bin/env python3
"""honeypot.py - pot de miel PASSIF de laboratoire (LAB_ONLY).
Il n'offre AUCUN service reel : il ecoute, journalise QUI le contacte, repond un
204 vide, puis ferme. En fonctionnement normal, PERSONNE ne devrait lui parler ;
donc toute connexion est un signal fort (hote suspect / mouvement lateral)."""
import os, socket, datetime, sys
if os.getenv("LAB_ONLY") != "1":
sys.exit("Refus : LAB_ONLY != 1 (honeypot reserve au laboratoire).")
HOTE, PORT = "0.0.0.0", 8080
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind((HOTE, PORT))
srv.listen(16)
print(f"[honeypot] ecoute {HOTE}:{PORT} - LAB_ONLY", flush=True)
while True:
conn, addr = srv.accept()
try:
data = conn.recv(2048)
ts = datetime.datetime.now().isoformat()
ligne = f"[{ts}] CONTACT depuis {addr[0]}:{addr[1]} ({len(data)} octets)\n"
with open("/logs/honeypot.log", "a") as f:
f.write(ligne)
print(ligne.strip(), flush=True)
conn.sendall(b"HTTP/1.1 204 No Content\r\nX-Lab-Honeypot: 1\r\n\r\n")
finally:
conn.close()
Étape 7 Règles Suricata légères
Dans suricata/. Quatre règles pour détecter les phases du ver (marqueur, balayage, fuite de canari, contact honeypot).
# suricata/lab.rules - regles Suricata LEGERES de laboratoire (php2shell).
# Objectif PEDAGOGIQUE : apprendre a DETECTER le comportement du ver, jamais a
# l'exploiter. Les SID 2600xxx sont locaux au lab.
# 1) Marqueur du ver-jouet dans un en-tete HTTP
alert http any any -> $HOME_NET any (msg:"LAB php2shell - marqueur X-Lab-Php2shell-Sim vu"; flow:to_server,established; http.header; content:"X-Lab-Php2shell-Sim"; nocase; classtype:policy-violation; sid:2600001; rev:1;)
# 2) Balayage horizontal : une source parle a beaucoup d'hotes du /24
alert tcp 172.20.0.0/24 any -> 172.20.0.0/24 80 (msg:"LAB php2shell - balayage horizontal probable (scan lateral)"; flow:to_server; threshold:type both, track by_src, count 10, seconds 15; classtype:attempted-recon; sid:2600002; rev:1;)
# 3) Fuite de canari : un jeton appat part dans une requete (exfil simulee)
alert http any any -> any any (msg:"LAB php2shell - jeton canari dans une requete (exfil simulee)"; http.request_body; content:"CANARI-"; nocase; classtype:data-loss; sid:2600003; rev:1;)
# 4) Contact du honeypot : ne devrait jamais arriver en temps normal
alert tcp any any -> 172.20.0.20 8080 (msg:"LAB php2shell - contact du honeypot (hote suspect)"; flow:to_server; classtype:bad-unknown; sid:2600004; rev:1;)
Étape 8 Orchestrateur défensif (blue team)
Dans orchestrateur/. Il vérifie les garde-fous puis corrèle les journaux. Il n'attaque rien.
# orchestrateur/Dockerfile - superviseur DEFENSIF (blue team)
FROM python:3.12-slim
LABEL lab="php2shell" role="orchestrateur-defensif"
COPY orchestrateur.py /orchestrateur.py
CMD ["python", "/orchestrateur.py"]
#!/usr/bin/env python3
"""orchestrateur.py - superviseur DEFENSIF du lab php2shell (blue team, LAB_ONLY).
Role : (1) verifier les garde-fous d'isolation, (2) surveiller les journaux des
leurres, du honeypot et de Suricata, (3) produire un tableau de bord des
detections. Il n'attaque RIEN et ne porte aucune charge offensive : c'est l'outil
cote defense qui apprend a RECONNAITRE les phases du ver dans les traces."""
import os, sys, time, glob, json, socket, ipaddress
SOUS_RESEAU = ipaddress.ip_network("172.20.0.0/24")
DOSSIER_LOGS = "/logs"
MOTS_CLES = ("SIM-COMPROMISSION", "CANARI-", "CONTACT")
def garde_fous():
"""Refuse de tourner hors du lab, et verifie que le reseau est bien isole."""
if os.getenv("LAB_ONLY") != "1":
sys.exit("ARRET : LAB_ONLY != 1 (le lab doit rester confine).")
try:
socket.setdefaulttimeout(2)
socket.create_connection(("1.1.1.1", 53))
sys.exit("ARRET : Internet accessible - le reseau n'est PAS isole !")
except OSError:
print("[garde-fou] Internet injoignable : reseau correctement isole OK", flush=True)
def surveiller():
"""Lit les journaux montes dans /logs et remonte chaque detection une fois."""
vus = set()
print("[orchestrateur] surveillance des journaux... (Ctrl+C pour arreter)", flush=True)
while True:
for chemin in glob.glob(f"{DOSSIER_LOGS}/**/*.log", recursive=True):
try:
with open(chemin, encoding="utf-8", errors="replace") as f:
for i, ligne in enumerate(f):
cle = (chemin, i)
if cle in vus:
continue
vus.add(cle)
if any(m in ligne for m in MOTS_CLES):
print(json.dumps(
{"source": os.path.basename(chemin),
"detection": ligne.strip()},
ensure_ascii=False), flush=True)
except FileNotFoundError:
pass
time.sleep(3)
if __name__ == "__main__":
garde_fous()
surveiller()
Étape 9 Le ver-jouet BÉNIN
À la racine ~/Bureau/php2shell-lab/ver_jouet.py (il sera copié dans l'orchestrateur à l'étape 12). Aucune charge d'exploit : il envoie seulement le marqueur. Sa décomposition fonction par fonction est plus bas.
#!/usr/bin/env python3
"""ver_jouet.py - SIMULATEUR de comportement de ver, 100% BENIN (LAB_ONLY).
/!\ CE N'EST PAS UN EXPLOIT /!\
Il n'y a AUCUNE charge RCE / OOM / UAF, aucun PoC de CVE, aucun code servi a la
cible. Le "ver" se contente d'ENVOYER un en-tete marqueur (X-Lab-Php2shell-Sim)
a des sites LEURRES qui ACCEPTENT VOLONTAIREMENT de jouer la cible. But : montrer
et apprendre a DETECTER les phases d'un ver (amorcage, balayage, propagation,
collecte, exfiltration) SANS jamais compromettre quoi que ce soit.
Garde-fous : LAB_ONLY obligatoire ; cibles bornees a 172.20.0.0/24 ;
refus total si Internet est joignable."""
import os, sys, json, time, socket, ipaddress
from urllib import request, error
MARQUEUR = "X-Lab-Php2shell-Sim"
SIGNATURE = "ver-jouet/benin-1.0 (simulation-pedagogique)"
SOUS_RESEAU = ipaddress.ip_network("172.20.0.0/24")
CANARI_LOCAL = "/tmp/canari_local.txt"
JOURNAL_EXFIL = "/tmp/exfil_simulee.jsonl"
# ============================== boot ==============================
def boot():
"""AMORCAGE. S'execute en PREMIER, une seule fois, au lancement.
Dans un vrai ver : s'installe, verifie qu'il n'est pas deja la, prepare son
etat. Ici : il VERIFIE LES GARDE-FOUS. Sans LAB_ONLY=1, et si Internet est
joignable, il REFUSE de demarrer. C'est le seul "demarrage" du cycle."""
if os.getenv("LAB_ONLY") != "1":
sys.exit("ARRET : LAB_ONLY != 1 - simulateur reserve au laboratoire.")
try:
socket.setdefaulttimeout(2)
socket.create_connection(("1.1.1.1", 53))
sys.exit("ARRET : Internet joignable - le ver-jouet refuse de sortir du lab.")
except OSError:
pass
print(f"[boot] garde-fous OK - {SIGNATURE}", flush=True)
return {"demarre": time.time(), "infectes": []}
# ============================== scan ==============================
def scan():
"""BALAYAGE. Demarre JUSTE APRES boot. Enumere les hotes du /24 de lab et
teste le port 80. C'est la phase de reconnaissance : il CARTOGRAPHIE les
cibles vivantes mais n'envoie encore AUCUN marqueur. (Suricata reperera le
pic de connexions : regle sid:2600002.)"""
cibles = []
for ip in SOUS_RESEAU.hosts():
ip = str(ip)
try:
with socket.create_connection((ip, 80), timeout=0.3):
cibles.append(ip)
print(f"[scan] hote web vivant : {ip}", flush=True)
except OSError:
continue
return cibles
# =========================== infect_stub ===========================
def infect_stub(ip, etat):
"""PROPAGATION (STUB BENIN). C'est ICI qu'un VRAI ver "se propage" : il
enverrait une charge pour executer du code sur la cible. Ce simulateur
N'ENVOIE AUCUNE CHARGE : seulement l'en-tete marqueur. Le leurre, qui a
CONSENTI, repond X-Lab-Decoy-State: simulated-compromise. Rien n'est execute
sur la cible ; on note juste "propagation simulee". (Regle sid:2600001.)"""
if ipaddress.ip_address(ip) not in SOUS_RESEAU:
print(f"[infect] REFUS {ip} : hors du sous-reseau de lab", flush=True)
return
req = request.Request(f"http://{ip}/",
headers={MARQUEUR: "sim", "User-Agent": SIGNATURE})
try:
with request.urlopen(req, timeout=2) as r:
if r.headers.get("X-Lab-Decoy-State") == "simulated-compromise":
etat["infectes"].append(ip)
print(f"[infect] {ip} : compromission SIMULEE acceptee par le leurre", flush=True)
else:
print(f"[infect] {ip} : cible non consentante - ignoree", flush=True)
except error.URLError as e:
print(f"[infect] {ip} : injoignable ({e})", flush=True)
# ============================= harvest =============================
def harvest():
"""COLLECTE. Apres la "propagation". Un vrai ver fouille la machine pour des
secrets (mots de passe, cles, .env). Le simulateur lit UNIQUEMENT un fichier
canari LOCAL depose expres pour le lab - jamais de vrais fichiers systeme."""
if os.path.exists(CANARI_LOCAL):
with open(CANARI_LOCAL) as f:
data = f.read().strip()
print(f"[harvest] canari local collecte ({len(data)} octets)", flush=True)
return {"canari_local": data}
print("[harvest] aucun canari local - rien a collecter", flush=True)
return {}
# =========================== exfil_stub ===========================
def exfil_stub(butin, etat):
"""EXFILTRATION (STUB BENIN). DERNIERE phase. Un vrai ver enverrait le butin
vers un serveur externe (C2). Ici, l'anti-Internet l'INTERDIT : on ECRIT le
butin dans un fichier LOCAL pour que la defense (Suricata sid:2600003 /
orchestrateur) le repere. AUCUNE donnee ne quitte la machine."""
enregistrement = {
"horodatage": time.time(),
"infectes_simules": etat["infectes"],
"butin": butin,
"note": "EXFIL SIMULEE - ecriture locale uniquement, aucun envoi reseau",
}
with open(JOURNAL_EXFIL, "a") as f:
f.write(json.dumps(enregistrement, ensure_ascii=False) + "\n")
print(f"[exfil] butin ecrit localement dans {JOURNAL_EXFIL} (aucun envoi)", flush=True)
# ============================== main ==============================
def main():
etat = boot() # 1. amorcage + garde-fous
for ip in scan(): # 2. balayage du /24
infect_stub(ip, etat) # 3. propagation SIMULEE (header seul)
butin = harvest() # 4. collecte du canari local
exfil_stub(butin, etat) # 5. exfil SIMULEE (fichier local)
print(f"[fin] cycle termine - {len(etat['infectes'])} leurre(s) marque(s) en simulation", flush=True)
if __name__ == "__main__":
main()
Étape 10 Construire & démarrer le lab
# Depuis ~/Bureau/php2shell-lab
# 1) Construire les images (ne tire que les bases officielles patchees)
docker compose build
# 2) Demarrer le lab ISOLE (reseau interne, aucun acces Internet)
docker compose up -d
# 3) Verifier que tout tourne
docker compose ps
Étape 11 Lancer la simulation
Dépose un canari local, copie le ver-jouet dans l'orchestrateur et lance-le (confiné au /24).
# 1) Deposer un canari LOCAL dans l'orchestrateur (la "cible" de collecte)
echo "CANARI-LOCAL-$(date +%s)-NE-PAS-UTILISER" \
| docker compose exec -T orchestrateur tee /tmp/canari_local.txt
# 2) Copier le ver-jouet dans l'orchestrateur, puis le lancer (confine au /24)
docker compose cp ver_jouet.py orchestrateur:/ver_jouet.py
docker compose exec -e LAB_ONLY=1 orchestrateur python /ver_jouet.py
Étape 12 Observer la détection, puis nettoyer
Regarde les traces côté défense, puis arrête tout.
# Tableau de bord des detections cote defense
docker compose logs orchestrateur | tail -n 40 # correlation des traces
docker compose exec banque-leurre cat /var/log/leurre.log # SIM-COMPROMISSION
docker compose exec gouv-leurre cat /var/log/leurre.log
tail -n 40 logs/suricata/fast.log # alertes Suricata sid:2600xxx
cat logs/honeypot/honeypot.log 2>/dev/null || echo "honeypot : aucun contact (normal)"
# Tout arreter et effacer reseaux/volumes du lab
docker compose down -v
Décomposition du ver-jouet, fonction par fonction
Le cycle complet est dans main() : boot() → scan() →
infect_stub() → harvest() → exfil_stub(). Voici ce que fait
chaque aspect, quand il démarre et quand « ça se propage ».
boot() - amorçage
Quand : en tout premier, une seule fois au lancement.
Propagation : aucune.
Prépare l'état du ver et vérifie les garde-fous : si LAB_ONLY=1 est absent ou si Internet est joignable, il s'arrête. C'est l'équivalent bénin de la phase d'installation/persistance d'un vrai ver, mais réduit à un contrôle de sécurité.
scan() - balayage / reconnaissance
Quand : juste après boot().
Propagation : pas encore - il cartographie seulement.
Énumère les hôtes de 172.20.0.0/24 et teste le port 80 pour lister les cibles vivantes. Ce pic de connexions est détectable (Suricata sid:2600002). Aucun marqueur n'est encore envoyé.
infect_stub() - propagation (stub bénin)
Quand : pour chaque cible trouvée par scan().
Propagation : oui, ici - mais simulée.
C'est le cœur « propagation ». Un vrai ver enverrait une charge d'exploitation ; ici on envoie seulement l'en-tête X-Lab-Php2shell-Sim. Le leurre consent et répond X-Lab-Decoy-State: simulated-compromise : on compte alors une « infection simulée ». Rien n'est exécuté sur la cible, et toute cible hors du /24 est refusée (Suricata sid:2600001).
harvest() - collecte
Quand : après la phase de propagation.
Propagation : non (local).
Un vrai ver fouillerait la machine pour des secrets. Ici il lit uniquement un fichier canari local (/tmp/canari_local.txt) déposé exprès - jamais de vrais fichiers système.
exfil_stub() - exfiltration (stub bénin)
Quand : en dernier, une fois la collecte faite.
Propagation : non - écriture locale uniquement.
Un vrai ver enverrait le butin vers un C2. Ici l'anti-Internet l'interdit : le butin est écrit dans un fichier local (/tmp/exfil_simulee.jsonl) pour que la défense le repère (Suricata sid:2600003 sur le motif CANARI-). Aucune donnée ne quitte la machine.
Où « se propage »-t-il ? Uniquement dans infect_stub(), et seulement de façon
simulée : un en-tête, pas une charge. La « propagation » réelle d'un ver (exécuter du code
sur la cible via une faille comme les CVE ci-dessus) est volontairement absente. On garde la
forme du comportement pour l'entraîner à la détection, jamais le fond offensif.
Détection - ce qu'il faut repérer (IOC de lab)
| Phase du ver | Indice observable | Où le voir | Règle |
|---|---|---|---|
| Balayage | Pic de connexions TCP/80 d'une même source vers plusieurs hôtes du /24 | Suricata fast.log | sid:2600002 |
| Propagation (simulée) | En-tête X-Lab-Php2shell-Sim dans des requêtes HTTP | Suricata + leurre.log (SIM-COMPROMISSION) | sid:2600001 |
| Collecte | Lecture du canari local ; jetons CANARI-* | Journaux orchestrateur | - |
| Exfiltration (simulée) | Motif CANARI- dans un corps de requête / fichier exfil_simulee.jsonl | Suricata http.request_body | sid:2600003 |
| Mouvement latéral | Toute connexion au honeypot (anormale par nature) | honeypot.log | sid:2600004 |
Exercice : relance ver_jouet.py, puis entraîne-toi à retrouver
chaque phase dans les journaux. L'orchestrateur te sert de tableau de bord corrélé.