
Preuve de concept reproduisant la faille de clé codée en dur de CVE-2021-22681 et validant un correctif TLS mutuel/CRL par appareil sur EtherNet/IP simulé, avec mise en correspondance IEC 62443-4-2.
Six scripts exécutables — trafic réel du protocole EtherNet/IP (Test 1), mécanismes réels TLS/PKI (Tests 3–6) — zéro logiciel ou licence Rockwell dans toute la chaîne. Conçus pour tester une affirmation avant de la rédiger, pas pour la défendre par simple conviction.
Provenance : construit le 2026-07-31, en parallèle d'une enquête sur la station de traitement des eaux usées de Braham, MN — l'une des quatre installations publiquement divulguées lors de l'incident coordonné du secteur de l'eau du Minnesota des 26 et 27 juillet 2026. Le contexte de cet incident figure dans l'avis CISA AA26-097A (conjoint FBI/CISA/NSA/EPA/DOE/USCYBERCOM/Treasury ; publié le 2026-04-07, étendu le 2026-07-22), qui couvre la campagne en cours CyberAv3ngers affiliée à l'IRGC. Réserve d'attribution, tenue avec précision : aucune agence n'a formellement attribué l'incident du Minnesota spécifiquement à ce groupe — seule la campagne plus large en cours l'est. Ce dossier est le volet technique de la correction, volontairement séparé de l'enquête sur l'incident.
Rockwell PSIRT ([email protected]) et RA Secure Mail ([email protected]) ont été contactés le 2026-07-31, avant la mise en ligne de ce dépôt et de cette analyse (~30 minutes avant, selon les horodatages des fichiers). L'équipe d'architecture de sécurité de Rockwell a examiné le dépôt et a répondu le 2026-08-03. Citation directe, non reformulée en une affirmation plus forte que la leur :
Rockwell Automation n'approuve ni ne valide votre interprétation, votre correspondance avec l'IEC 62443-4-2, ni aucune conclusion tirée de la preuve de concept. Veuillez ne pas présenter ce travail comme ayant été examiné, approuvé ou cautionné par Rockwell Automation... les scripts démontrent des principes généraux de cryptographie et d'authentification plutôt que quoi que ce soit de spécifique à CIP Security ou à CVE-2021-22681.
Ce travail n'a pas été examiné, approuvé ni cautionné par Rockwell Automation, point final. Leur
caractérisation technique — principes généraux, pas spécifiques à CIP Security — est la même distinction
que le tableau « La frontière stricte » ci-dessous établit déjà concernant les affirmations de ce dépôt ;
leur examen la confirme indépendamment plutôt que de la contester. Pour les installations sur du matériel
qui ne peut pas atteindre CIP Security, Rockwell a renvoyé vers son propre guide de conception et de mise
en œuvre Converged Plantwide Ethernet (CPwE) (cité dans PHASED_ROLLOUT.md Phase 1) — la ressource
fournisseur existante vers laquelle ce projet tente d'orienter les gens plutôt que de la dupliquer.
La conclusion du Test 1 — aucune authentification dans l'état par défaut du protocole — n'est pas propre à EtherNet/IP. Modbus TCP, toujours l'un des protocoles les plus déployés dans les systèmes de contrôle de l'eau et des eaux usées, ne comporte aucune notion d'authentification dans la spécification du protocole ; il date des communications série de 1979 et n'a jamais été conçu avec la sécurité à l'esprit. La CISA a régulièrement cité exactement cette défaillance dans ses avis ICS (par ex. la série MELSEC iQ-F de Mitsubishi Electric : « MODBUS/TCP manque d'authentification appropriée », permettant des lectures/écritures/ arrêts non autorisés). La réponse propre de la Modbus Organization, Modbus/TCP Security, est une encapsulation TLS avec certificats X.509 — structurellement la même catégorie de correction que le Test 3 démontre ici, normalisée au niveau de l'organisation du protocole plutôt que chez un seul fournisseur. DNP3 dispose d'une extension optionnelle Secure Authentication (SAv5, normalisée en 2012) ; des analyses indépendantes et des témoignages d'intégrateurs décrivent tous deux qu'elle est rarement configurée en pratique, citant des écarts d'interopérabilité entre fabricants d'OT et une complexité protocolaire réelle — laissant la même exposition que le Test 1 démontre pour EtherNet/IP.
Le point architectural, énoncé avec précision pour ne pas être survendu : la correction du Test 3 — TLS mutuel, identité par périphérique liée dans le certificat (pas seulement la validité de l'AC), révocation via CRL — opère au niveau de la couche transport, pas au niveau du protocole applicatif ICS. Le principe est également applicable sous Modbus, DNP3 ou un protocole propriétaire ; ce qui change, c'est l'emballage, pas la forme de la correction. Ce dépôt n'a pas construit ni exécuté de PoC spécifique Modbus ou DNP3 — il s'agit d'une généralisation architecturale à partir de documentation publique, soumise au même classement « démontré vs sourcé » que tout le reste ici, pas d'une nouvelle affirmation testée.
Sources : Avis ICS de la CISA sur les lacunes d'authentification Modbus/TCP — Industrial Cyber · Vue d'ensemble de Modbus/TCP Security — Veridify · Défis d'adoption DNP3 SAv5/SAv6 — Step Function I/O
Un dossier de divulgation se joue ou se perd sur le fait de ne pas mélanger ces éléments, car chacun a une correction différente :
La correction démontrée ici — liaison d'identité par périphérique (Test 3) — répond à la défaillance de la clé de parc.
| Affirmation | Niveau | Pourquoi | |
|---|---|---|---|
| Le principe architectural | « un secret unique partagé sur un parc est compromis à l'échelle du parc par une seule fuite ; une authentification liée à l'identité par périphérique comble cette faille » | PROUVÉ | démontré avec du code réellement exécuté y compris le contrôle négatif qui prouve que le contrôle est nécessaire, pas seulement qu'il se déclenche : sur un point de terminaison strict, le certificat réellement valide CA du périphérique B est rejeté sur l'identité (Test 3 · cas 3), mais sur un point de terminaison à validité CA uniquement, le même certificat est accepté (cas 4 — le contrôle) → « une signature CA valide seule == accès à l'échelle du parc == Test 2 en habit TLS ». La liaison tient aussi dans le sens inverse : un serveur rogue présentant un certificat de parc valide est rejeté par le client (cas 5). Le Test 1 montre séparément la base plus large sans authentification. |
| L'implémentation spécifique de CIP Security chez Rockwell se comporte à l'identique | « activer CIP Security sur du matériel Rockwell réel corrige CVE-2021-22681 exactement de cette façon » | PISTE, sourcée non vérifiée | c'est le langage de l'avis propre de Rockwell (PN1550) — « Lorsqu'il est correctement déployé, CIP Security corrige cette vulnérabilité... n'utilise aucune clé codée en dur » — pas quelque chose que nous avons confirmé indépendamment contre du matériel Logix réel. Nous avons testé le principe que leur avis décrit, pas leur implémentation exacte au niveau du câble. |
Ne confondez pas les deux lignes. Le principe est prouvé. L'implémentation spécifique du fournisseur est crédible (c'est leur intention de conception déclarée) mais non testée par nous contre du matériel réel.
test1_baseline_vulnerable.py — la base sans authentification, en directLance un véritable simulateur PLC EtherNet/IP (cpppo, émulant un Allen-Bradley ControlLogix) et
lit + écrit une balise de contrôle avec zéro identifiant. (Portée : il s'agit de la base large
sans authentification sur laquelle s'appuyait la campagne — pas le mécanisme spécifique de clé codée
en dur de CVE-2021-22681. Gardée distincte volontairement ; voir « Trois défaillances différentes » ci-dessus.)
python test1_baseline_vulnerable.py
test2_shared_secret_fails.py — la forme de la faille de clé de parc (pont narratif, pas un test)Deux points de terminaison détiennent une clé statique ; un identifiant du périphérique A ouvre le périphérique B sans modification — l'analogue structurel le plus proche de la faille une-clé-pour-tous de CVE-2021-22681. Mais c'est tautologique : les deux gestionnaires sont construits pour accepter cette clé, donc il n'existe aucun chemin d'exécution où elle peut échouer. Il ne démontre rien que le code ne définisse pas par lui-même. Conservé comme pont narratif du Test 1 au Test 3 ; il ne porte aucun poids probatoire et n'est délibérément pas une jambe de preuve.
python test2_shared_secret_fails.py
test3_mutual_tls_fix.py — la correction, avec son contrôle négatif et ses deux directionsVraie AC, deux certificats de périphérique individuellement uniques — identité liée dans le SubjectAlternativeName, pas dans le CommonName obsolète. Cinq cas, tous exécutés :
device-a (check_hostname contre un vrai SAN). Mutuel — les deux extrémités lient l'identité.python test3_mutual_tls_fix.py
test4_revocation.py — la jambe du cycle de vie : la révocationUne vraie CRL signée CA. Un identifiant client (engineer-1) est accordé ; puis son numéro de série est
ajouté à la CRL, et le même identifiant toujours valide, non expiré, signé CA est refusé — le contenu
empirique de la clause de révocation CR 1.8 / 1.9. L'unicité (Test 3) ≠ la révocabilité ; cela montre qu'un
identifiant peut être retiré.
python test4_revocation.py
test5_rotation.py — la jambe du cycle de vie que le Test 4 n'a pas fermée : la rotationÉmet un identifiant de remplacement (v2) pour la même identité (engineer-1) détenant déjà un
identifiant valide (v1). LE CONTRÔLE (cas 3) : v1 est présenté à nouveau après l'existence de v2
mais avant le retrait explicite de v1 → toujours ACCORDÉ — prouvant que la seule réémission ne retire
pas l'ancien identifiant. Ce n'est qu'après l'ajout explicite de v1 à la CRL (cas 4) qu'il est refusé ; v2
n'est pas affecté tout au long (cas 5) — l'identité ne perd jamais l'accès pendant la transition. Le « émettre
un remplacement et retirer le précédent » de CR 1.8 est deux actions, et cela montre les deux, séparément.
python test5_rotation.py
test6_tamper_injection.py — la jambe que CR 3.1 n'a nommée que « par construction » : un test d'intégrité dédiéUn relais au niveau des enregistrements se place entre un vrai client et un vrai serveur TLS mutuel, transférant
les enregistrements TLS en analysant uniquement l'en-tête de 5 octets — il ne voit jamais le texte clair de la
charge utile chiffrée. Contrôle : chaque octet transféré sans modification → message délivré intact.
Altération : un bit inversé dans le texte chiffré d'un enregistrement Application Data en direct → le
contrôle AEAD de la pile TLS réceptrice échoue (SSLV3_ALERT_BAD_RECORD_MAC) et la connexion est déchirée —
des données corrompues ne sont jamais délivrées comme si elles étaient valides.
Quel octet, et pourquoi il est spécifié : un corps d'enregistrement AEAD TLS 1.2 est
explicit_nonce(8) ‖ ciphertext ‖ auth_tag(16), donc l'octet 0 est le nonce, pas la charge utile. Inverser
le nonce déclenche aussi le contrôle AEAD — mais en brouillant le déchiffrement plutôt qu'en faisant attraper
par la balise une charge utile altérée. CR 3.1 concerne la modification non autorisée des informations
transmises, donc l'inversion cible un octet au milieu du texte chiffré, et le test démontre alors exactement
la phrase qu'il revendique.
python test6_tamper_injection.py
maximum_version = TLSv1_2 pour deux raisons qui concernent l'observabilité, pas la
sécurité : sous TLS 1.2, un certificat client manquant ou rejeté échoue pendant la poignée de main, donc
le test obtient une erreur déterministe et attribuable plutôt que l'échec post-poignée de main de TLS 1.3 ;
et le type de contenu de l'enregistrement reste visible en clair, ce dont le relais du Test 6 a besoin pour
identifier un enregistrement Application Data. Déployez la version TLS la plus élevée que vos périphériques
supportent — TLS 1.3 là où c'est disponible. Rien dans ce dépôt ne doit être lu comme un conseil de plafonner
un système de production à 1.2.presented == KEY, identity in SAN) ne sont pas à temps constant. Non exploitables ici — les valeurs
comparées sont des chaînes d'identité quasi publiques et TLS a déjà effectué la véritable authentification
cryptographique avant que la comparaison ne s'exécute — mais signalées parce que le motif est copié dans des
endroits où cela compte.python -m venv venv
venv\Scripts\activate # ou : source venv/bin/activate
pip install -r requirements.txt
62443-4-2_SL2_MAPPING.md (CR 1.2 / 1.8 / 1.9 / 1.14 / 3.1,
honnêtement classée, chaque lacune nommée ; CR 1.2, CR 1.9 et la jambe émission/validation/révocation de CR 1.8
sont désormais démontrées). Avant PSIRT ([email protected] / [email protected]) :
vérifier le texte normatif de chaque CR contre un exemplaire acheté de l'IEC 62443-4-2:2019.PHASED_ROLLOUT.md (Phase 0 stopper l'hémorragie · 1 segmenter · 2
contrôles compensateurs · 3 CIP Security/PKI, selon le matériel · 4 exploiter). Dimensionné pour une petite
installation ; honnête sur le fait que CIP Security est limité par le matériel, donc les Phases 0–2 portent
la réduction des risques dans tous les cas.PHASE0_INVENTORY_WORKSHEET.md (un inventaire de périphériques à remplir, pas seulement l'instruction d'en
faire un), RESOURCES.md (assistance gratuite CISA / EPA / WaterISAC / AWWA, vérifiée en direct, non
reconstituée de mémoire), et INCIDENT_RESPONSE_QUICK_REFERENCE.md (une fiche des 60 premières minutes,
explicitement pas un plan complet de réponse aux incidents — la sécurité des opérations y prime toujours).test5_rotation.py fait passer
CR 1.8 de « activé » à pleinement démontré (rotation, avec son propre contrôle) ; test6_tamper_injection.py
fait passer CR 3.1 de « par construction » à démontré (une vraie inversion de bit, rejetée par le contrôle
AEAD de TLS). Les deux ont été réexécutés à plusieurs reprises sans aucune instabilité avant d'être ajoutés
ici. Également ajoutés : PHASE3_CA_QUICKSTART.md (l'AC « en quelques lignes de code », sous forme de vraies
commandes openssl testées) et un exemple concret de liste blanche dans PHASED_ROLLOUT.md Phase 1.GLOSSARY.md, INTEGRATOR_CHECKLIST.md,
PHASE3_CA_QUICKSTART.md) est désormais réellement lié depuis le plan au lieu de rester sans référence.
Rien sur cette liste n'est encore en BROUILLON — tout ce qui précède et suit est poussé et en ligne sur le
dépôt public.Chaque identifiant externe ici a été extrait d'une source en direct le 2026-07-31, pas reconstitué de
mémoire depuis l'entraînement : AA26-097A (multi-sources, y compris WaterISAC / Tenable / SecurityWeek),
Braham comme l'une des quatre victimes divulguées, CyberAv3ngers/IRGC, PN1550 confirmé comme le véritable
avis Rockwell avec sa citation de la ligne 2 vérifiée mot pour mot (« Lorsqu'il est correctement déployé,
CIP Security corrige cette vulnérabilité » + « n'utilise aucune clé codée en dur »), « ne peut pas être
corrigé par un correctif » mot pour mot, CVSS 10.0 / CRITIQUE (v3.1), suivi CISA ICSA-21-056-03, et
62443-4-2 CR 1.8 (PKI) + CR 3.1 (intégrité des communications) confirmés exacts.
Discipline pour le dossier : re-récupérer chaque identifiant auprès des sources primaires au moment de la
soumission. Les avis sont renumérotés, étendus et remplacés — AA26-097A montre déjà une extension — donc
« vérifié le 2026-07-31 » n'est pas « vérifié à la soumission ». La correspondance formelle complète CR par CR
62443-4-2 est désormais rédigée (62443-4-2_SL2_MAPPING.md) — ce qui reste est de re-récupérer son texte
normatif cité contre un exemplaire acheté de la norme avant soumission, pas de rédiger la correspondance.
l0gic — Patrick Crosby · 2026-07-31.
Journal de durcissement, 2026-08-03 : test5_rotation.py et test6_tamper_injection.py ajoutés, fermant les
deux éléments techniques nommés ouverts depuis le 2026-07-31 — chacun réexécuté trois fois sans aucune
instabilité avant d'être documenté ici. PHASE3_CA_QUICKSTART.md (de vraies commandes openssl testées), un
exemple concret de liste blanche de pare-feu pour la Phase 1, PHASE0_INVENTORY_WORKSHEET.md, RESOURCES.md,
INCIDENT_RESPONSE_QUICK_REFERENCE.md et la généralisation Modbus/DNP3 ont également été ajoutés le même jour.
Journal de durcissement, 2026-08-03 (suite) — deux passes de relecture indépendantes, toutes deux appliquées :
une équipe rouge de sécurité a trouvé et corrigé une vraie vulnérabilité PKI dans le démarrage rapide AC
(-copy_extensions copyall permettait à une demande de certificat malveillante de s'auto-déclarer CA:TRUE ;
corrigé via un -extfile explicite, vérifié contre une demande délibérément malveillante dans les deux sens)
et a corrigé le test6 pour inverser le texte chiffré spécifiquement plutôt que l'octet 0 (le nonce), plus une
vraie correction de sécurité des threads et l'épinglage des dépendances. Un audit structurel séparé — lisant
les phases comme un système dans le temps, pas comme une liste de contrôle — a trouvé et corrigé : le titre
« quelques lignes » de la Phase 3 masquait que la révocation/rotation ne sont pas simples ; le
fail-open/fail-closed de la CRL n'avait jamais été décidé (il l'est désormais, avec une valeur par défaut et
un raisonnement) ; l'expiration des certificats était un nouveau mode de panne non nommé (la Phase 4 porte
désormais la propre leçon de chevauchement sûr du Test 5) ; la Phase 3 aveugle silencieusement l'alerteur
d'écriture de la Phase 2 (désormais nommé, avec des remplacements suggérés) ; NTP était un prérequis non
énoncé ; CR 1.14 était formulé comme « non satisfait » là où « non applicable à la conception corrigée » est
la formulation exacte ; et INTEGRATOR_CHECKLIST.md/GLOSSARY.md étaient des documents orphelins auxquels
rien ne renvoyait (désormais liés depuis la Phase 3 et le haut de ce plan). Chaque correction vérifiée en
réexécutant les tests concernés, pas seulement en relisant le diff. Poussé et en ligne — les cinq phases du
plan de déploiement sont terminées, relues deux fois, et rien d'aujourd'hui ne reste en local.
Journal de durcissement : le Test 3 a été renforcé pour inclure le contrôle négatif (cas 4, prouvant la nécessité et pas seulement le déclenchement), le cas en direction inverse (cas 5, liaison mutuelle), et l'identité basée sur SAN (pas CN), puis re-vérifié en réexécutant les cinq cas ; le Test 4 (révocation CRL) a été ajouté. Les légendes des Tests 1/2 ont été redimensionnées pour garder les trois classes de défaillance distinctes ; le Test 2 a été rétrogradé de « test » à pont narratif ; les limites de portée sur la révocation et le temps constant ont été ajoutées. La chaîne de citations a été récupérée auprès des sources primaires et horodatée pour une re-récupération à la soumission.