## https://sploitus.com/exploit?id=9BA50CFA-865A-54C0-8135-D67559A5262F
# cip-security-poc โ proving the fix *principle* behind CVE-2021-22681 (not just its flaw)
Four runnable scripts. Real EtherNet/IP protocol traffic, real cryptography, zero Rockwell
software or licensing anywhere in the chain. Built to test a claim *before* writing it up, not
to argue for it on faith.
Provenance: built 2026-07-31, alongside a parallel investigation into the Braham, MN
WWTF โ one of the **four utilities publicly disclosed** in the coordinated Minnesota water-sector
incident of July 26โ27, 2026. Context for that incident lives in CISA advisory **AA26-097A**
(joint FBI/CISA/NSA/EPA/DOE/USCYBERCOM/Treasury; issued 2026-04-07, expanded 2026-07-22), which
covers the ongoing IRGC-affiliated **CyberAv3ngers** campaign. **Attribution caveat, held
precisely:** no agency has formally attributed the *Minnesota incident specifically* to that
group โ only the broader ongoing campaign is. This folder is the technical-fix side, kept
separate from the incident investigation on purpose.
## Three different failures โ keep them distinct
A disclosure packet lives or dies on not smearing these together, because each has a different fix:
- **No / absent authentication** *(Test 1)* โ a device exposed with no credential layer at all. The
broad baseline the CyberAv3ngers campaign leaned on (many victims were reachable with absent or
default creds).
- **One hardcoded / shared key across a fleet** *(the specific shape of CVE-2021-22681, modeled by
Test 2)* โ extract the one key once, forge fleet-wide. **This is the actual CVE.**
- **Default credentials** โ factory creds never changed. Not modeled here; named so it isn't
conflated with the two above.
The fix demonstrated here โ **per-device identity binding** *(Test 3)* โ addresses the fleet-key failure.
## The hard boundary (read this first)
| | Claim | Tier | Why |
|---|---|---|---|
| **The architectural principle** | "a single shared secret across a fleet is compromised fleet-wide by one leak; per-device identity-bound authentication closes that" | **PROVEN** | demonstrated with real running code **including the negative control that proves the check is *necessary*, not merely that it fires**: on a strict endpoint Device B's genuinely CA-valid cert is *rejected* on identity (Test 3 ยท case 3), but on a CA-validity-**only** endpoint the *same* cert is *accepted* (case 4 โ **the control**) โ "validly CA-signed alone == fleet-wide access == Test 2 in TLS clothing." The binding also holds in reverse: a rogue server presenting a valid fleet cert is rejected by the client (case 5). Test 1 separately shows the broader no-auth baseline. |
| **Rockwell's specific CIP Security implementation behaves identically** | "enabling CIP Security on real Rockwell hardware remediates CVE-2021-22681 exactly this way" | **LEAD, sourced not verified** | this is Rockwell's own advisory (PN1550) language โ *"When properly deployed, CIP Security remediates this vulnerability... does not make use of any hardcoded keys"* โ not something we've independently confirmed against real Logix hardware. We tested the *principle* their advisory describes, not their exact wire-level implementation. |
Do not conflate the two rows. The principle is proven. The vendor's specific implementation of it
is credible (it's their own stated design intent) but untested by us against real equipment.
## Tools
### `test1_baseline_vulnerable.py` โ the no-auth baseline, live
Spins up a real EtherNet/IP PLC simulator (`cpppo`, emulating an Allen-Bradley ControlLogix) and
reads + writes a control tag with **zero credentials**. *(Scope: this is the broad
**no-authentication baseline** the campaign leaned on โ **not** the specific hardcoded-key mechanism
of CVE-2021-22681. Kept distinct on purpose; see "Three different failures" above.)*
```
python test1_baseline_vulnerable.py
```
### `test2_shared_secret_fails.py` โ the *shape* of the fleet-key flaw (narrative bridge, not a test)
Two endpoints hold one static key; a credential from Device A opens Device B unchanged โ the closest
structural analog to CVE-2021-22681's one-key-fits-all flaw. **But it is tautological:** both handlers
are *constructed* to accept that key, so there is no execution path in which it can fail. It
demonstrates nothing the code doesn't define into existence. Kept as the **narrative bridge** from
Test 1 to Test 3; it carries **no evidential weight** and is deliberately not a proof-leg.
```
python test2_shared_secret_fails.py
```
### `test3_mutual_tls_fix.py` โ the fix, with its negative control and both directions
Real CA, two individually-unique device certs โ **identity bound in the SubjectAlternativeName, not
the deprecated CommonName**. Five cases, all executed:
- **[1]** Device A's own cert โ GRANTED ยท **[2]** no cert โ rejected at the TLS handshake ยท **[3]** Device B's CA-valid cert โ DENIED on identity (strict endpoint).
- **[4] THE CONTROL** โ the *same* Device B cert against a CA-validity-**only** endpoint โ **GRANTED**. This is what makes [3] mean anything: without the identity check, any fleet cert opens any device (== Test 2, in TLS clothing).
- **[5] REVERSE** โ a rogue server presenting Device B's cert is rejected by a client that binds `device-a` (`check_hostname` against a real SAN). Mutual โ both ends bind identity.
```
python test3_mutual_tls_fix.py
```
### `test4_revocation.py` โ the lifecycle leg: revocation
A real CA-signed **CRL**. A client credential (`engineer-1`) is granted; then its serial is added to
the CRL, and the *same* still-valid, unexpired, CA-signed credential is **denied** โ the empirical
content of the CR 1.8 / 1.9 revocation clause. Uniqueness (Test 3) โ revocability; this shows a
credential can be taken *back*.
```
python test4_revocation.py
```
## Scope boundaries (what is NOT claimed)
- **Revocation โ now demonstrated (`test4_revocation.py`); rotation โ not yet.** Per-device
*uniqueness* (Test 3) is not the same as *revocability*; **Test 4 closes that gap** โ a still-valid,
unexpired, CA-signed credential is granted before revocation and **denied after**, solely because the
CA-signed CRL now lists its serial. **Rotation** (re-issue a replacement credential and retire the
old one) is closely related and *enabled* by the same PKI, but is not separately demonstrated here โ
so "per-device identity binding" still must not silently expand into "rotation solved."
- **Constant-time (low severity, named for hygiene).** The identity string compares
(`presented == KEY`, `identity in SAN`) are not constant-time. Not exploitable here โ the compared
values are public-ish identity strings and TLS has already done the real cryptographic
authentication before the compare runs โ but flagged because the pattern gets copied into places
where it *does* matter.
## Setup
```
python -m venv venv
venv\Scripts\activate # or: source venv/bin/activate
pip install -r requirements.txt
```
## Next steps (one real item left, plus two small optional ones)
- **62443-4-2 SL 2 mapping โ DRAFTED** โ `62443-4-2_SL2_MAPPING.md` (CR 1.2 / 1.8 / 1.9 / 1.14 / 3.1,
honestly tiered, every gap named; CR 1.2, CR 1.9, and CR 1.8's issuance/validation/revocation leg
are now **demonstrated**). Before PSIRT (psirt@rockwellautomation.com / secure@ra.rockwell.com):
verify each CR's normative text against a purchased copy of IEC 62443-4-2:2019.
- **Phased rollout โ DRAFTED** โ `PHASED_ROLLOUT.md` (Phase 0 stop-the-bleeding ยท 1 segment ยท 2
compensating controls ยท 3 CIP Security/PKI, hardware-permitting ยท 4 operate). Right-sized for a small
utility; honest that CIP Security is hardware-gated, so Phases 0โ2 carry the risk reduction regardless.
- **Still open โ the actual remaining item:** responsible-disclosure sequencing. PSIRT contact
first, public writeup / LinkedIn after, so the chain of provenance is documented in order. Not yet
drafted: the PSIRT email itself.
- **Two small technical items, named not smuggled** (per the mapping doc's own summary): a
*rotation* test (re-issue + retire) to move CR 1.8 from "enabled" to fully demonstrated, and a
*tamper-injection* test to move CR 3.1 from "by construction" to demonstrated. Neither is
load-bearing for the central claim; both are small if picked up.
## Citations โ retrieved, not recalled (and re-pull before submission)
Every external identifier here was **pulled from a live source on 2026-07-31**, not recalled from
training: `AA26-097A` (multi-source, incl. WaterISAC / Tenable / SecurityWeek), Braham as one of the
four disclosed victims, CyberAv3ngers/IRGC, `PN1550` confirmed the real Rockwell advisory with its
Row-2 quote checked **verbatim** ("When properly deployed, CIP Security remediates this vulnerability"
+ "does not make use of any hardcoded keys"), "cannot be mitigated with a patch" verbatim, **CVSS
10.0 / CRITICAL (v3.1)**, CISA tracking `ICSA-21-056-03`, and `62443-4-2` CR 1.8 (PKI) + CR 3.1
(comm integrity) confirmed exact.
**Discipline for the packet:** re-pull every identifier from primary sources *at submission time*.
Advisories get renumbered, expanded, and superseded โ `AA26-097A` already shows one expansion โ so
"verified on 2026-07-31" is not "verified at submit." The full formal CR-by-CR `62443-4-2` mapping is
now written (`62443-4-2_SL2_MAPPING.md`) โ what's left is re-pulling its cited normative text against
a purchased standard copy before submission, not writing the mapping itself.
---
*l0gic โ Patrick Crosby ยท 2026-07-31.*
*Hardening log:* Test 3 was strengthened to include the **negative control** (case 4, proving necessity
not just firing), the **reverse-direction** case (case 5, mutual binding), and **SAN-based identity**
(not CN), then re-verified by re-running all five cases; Test 4 (CRL revocation) was added. Test 1/2
captions were right-sized to keep the three failure classes distinct; Test 2 was demoted from "test"
to narrative bridge; revocation and constant-time scope boundaries were added. The citation chain was
retrieved from primary sources and stamped for re-pull at submission.