Share
## 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.