Last reviewed

Industrial Protocols

Enumeration

ICS environments use specialized protocols designed for reliability and real-time communication, often with minimal security features. Understanding these protocols is essential for effective testing.

Modbus

The most common ICS protocol. Simple, no authentication, widely supported. Used in nearly all industrial sectors.

Variants

  • Modbus RTU - Serial (RS-232/485)
  • Modbus ASCII - Serial, human readable
  • Modbus TCP - Ethernet, Port 502

Security Issues

  • • No authentication
  • • No encryption
  • • No integrity checking
  • • Easily spoofable

Modbus Function Codes

Code Function Risk
0x01 Read Coils Info disclosure
0x03 Read Holding Registers Info disclosure
0x05 Write Single Coil Control manipulation
0x06 Write Single Register Control manipulation
0x0F Write Multiple Coils Mass control manipulation
0x10 Write Multiple Registers Mass control manipulation
0x2B Read Device Identification Fingerprinting

Modbus Testing

bash
# Using pymodbus
pip install pymodbus

# Read holding registers
python3 << 'EOF'
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient('192.168.1.100', port=502)
client.connect()

# Read registers 0-9
# NOTE: pymodbus 3.x renamed unit= to slave= (now device_id= in 3.7+)
result = client.read_holding_registers(0, count=10, slave=1)
print(f"Registers: {result.registers}")

# Read coils
coils = client.read_coils(0, count=10, slave=1)
print(f"Coils: {coils.bits}")

# Device identification
device_id = client.read_device_information()
print(f"Device: {device_id}")

client.close()
EOF

# Using modbus-cli
pip install modbus-cli
modbus read 192.168.1.100 holding_register 0 10
modbus read 192.168.1.100 coil 0 10

# Nmap script
nmap -sV -p 502 --script modbus-discover 192.168.1.100
# Using pymodbus
pip install pymodbus

# Read holding registers
python3 << 'EOF'
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient('192.168.1.100', port=502)
client.connect()

# Read registers 0-9
# NOTE: pymodbus 3.x renamed unit= to slave= (now device_id= in 3.7+)
result = client.read_holding_registers(0, count=10, slave=1)
print(f"Registers: {result.registers}")

# Read coils
coils = client.read_coils(0, count=10, slave=1)
print(f"Coils: {coils.bits}")

# Device identification
device_id = client.read_device_information()
print(f"Device: {device_id}")

client.close()
EOF

# Using modbus-cli
pip install modbus-cli
modbus read 192.168.1.100 holding_register 0 10
modbus read 192.168.1.100 coil 0 10

# Nmap script
nmap -sV -p 502 --script modbus-discover 192.168.1.100

Write Operations

Never perform write operations on production ICS systems. Writing to registers/coils can cause physical damage, safety incidents, or process disruption.

Siemens S7comm

Proprietary protocol for Siemens S7 PLCs. Port 102 (ISO-TSAP). Very common in manufacturing and utilities.

bash
# S7 enumeration with Nmap
nmap -p 102 --script s7-info 192.168.1.100

# Returns:
# - Module type
# - Serial number  
# - Plant identification
# - Module name
# - Firmware version

# Using snap7 library
pip install python-snap7

python3 << 'EOF'
import snap7
from snap7.util import get_string

client = snap7.client.Client()
client.connect('192.168.1.100', 0, 1)  # IP, rack, slot

# Get CPU info
info = client.get_cpu_info()
print(f"Module: {info.ModuleTypeName}")
print(f"Serial: {info.SerialNumber}")

# Read data block
db_number = 1
data = client.db_read(db_number, 0, 100)
print(f"DB{db_number}: {data.hex()}")

client.disconnect()
EOF

# S7 password cracking (if protected)
# Use s7-brute-offline with captured traffic
# S7 enumeration with Nmap
nmap -p 102 --script s7-info 192.168.1.100

# Returns:
# - Module type
# - Serial number  
# - Plant identification
# - Module name
# - Firmware version

# Using snap7 library
pip install python-snap7

python3 << 'EOF'
import snap7
from snap7.util import get_string

client = snap7.client.Client()
client.connect('192.168.1.100', 0, 1)  # IP, rack, slot

# Get CPU info
info = client.get_cpu_info()
print(f"Module: {info.ModuleTypeName}")
print(f"Serial: {info.SerialNumber}")

# Read data block
db_number = 1
data = client.db_read(db_number, 0, 100)
print(f"DB{db_number}: {data.hex()}")

client.disconnect()
EOF

# S7 password cracking (if protected)
# Use s7-brute-offline with captured traffic

EtherNet/IP (CIP)

Common Industrial Protocol over Ethernet. Used by Allen-Bradley/Rockwell PLCs. Port 44818.

bash
# Using pycomm3 for Allen-Bradley PLCs
pip install pycomm3

python3 << 'EOF'
from pycomm3 import LogixDriver, CIPDriver

# Generic CIP info
with CIPDriver('192.168.1.100') as plc:
    info = plc.get_plc_info()
    print(f"Vendor: {info['vendor']}")
    print(f"Product: {info['product_name']}")
    print(f"Revision: {info['revision']}")

# For ControlLogix/CompactLogix
with LogixDriver('192.168.1.100') as plc:
    # Get all tags
    tags = plc.get_tag_list()
    for tag in tags[:10]:
        print(f"Tag: {tag['tag_name']} Type: {tag['data_type']}")
    
    # Read a tag
    result = plc.read('MyTag')
    print(f"Value: {result.value}")
EOF

# Nmap enumeration
nmap -p 44818 --script enip-info 192.168.1.100
# Using pycomm3 for Allen-Bradley PLCs
pip install pycomm3

python3 << 'EOF'
from pycomm3 import LogixDriver, CIPDriver

# Generic CIP info
with CIPDriver('192.168.1.100') as plc:
    info = plc.get_plc_info()
    print(f"Vendor: {info['vendor']}")
    print(f"Product: {info['product_name']}")
    print(f"Revision: {info['revision']}")

# For ControlLogix/CompactLogix
with LogixDriver('192.168.1.100') as plc:
    # Get all tags
    tags = plc.get_tag_list()
    for tag in tags[:10]:
        print(f"Tag: {tag['tag_name']} Type: {tag['data_type']}")
    
    # Read a tag
    result = plc.read('MyTag')
    print(f"Value: {result.value}")
EOF

# Nmap enumeration
nmap -p 44818 --script enip-info 192.168.1.100

DNP3 (Distributed Network Protocol)

Common in utilities (power, water). Port 20000. Supports authentication in DNP3-SA (Secure Authentication).

bash
# DNP3 enumeration with Nmap
nmap -p 20000 --script dnp3-info 192.168.1.100

# Using dnp3-python
pip install dnp3-python

# DNP3 Wireshark analysis
# Filter: dnp3
# Look for:
# - Function codes
# - Data objects
# - Unsolicited responses

# Common attacks:
# - Replay captured commands
# - Inject false data
# - Disable unsolicited reporting

# DNP3 Secure Authentication (SA)
# Check if authentication is enabled
# If not, protocol is vulnerable to command injection
# DNP3 enumeration with Nmap
nmap -p 20000 --script dnp3-info 192.168.1.100

# Using dnp3-python
pip install dnp3-python

# DNP3 Wireshark analysis
# Filter: dnp3
# Look for:
# - Function codes
# - Data objects
# - Unsolicited responses

# Common attacks:
# - Replay captured commands
# - Inject false data
# - Disable unsolicited reporting

# DNP3 Secure Authentication (SA)
# Check if authentication is enabled
# If not, protocol is vulnerable to command injection

OPC UA

Modern protocol with built-in security. Port 4840. Supports authentication and encryption.

bash
# asyncua is the current, maintained OPC UA library for Python
pip install asyncua

# Optional GUI client (opcua-client-gui) for manual browsing
pip install opcua-client

python3 << 'EOF'
import asyncio
from asyncua import Client

async def main():
    url = "opc.tcp://192.168.1.100:4840"
    async with Client(url=url) as client:
        # Get server info
        print(f"Server: {await client.get_server_node()}")
        
        # Browse root
        root = client.get_root_node()
        children = await root.get_children()
        for child in children:
            print(f"Node: {child}")
        
        # Read a variable
        node = client.get_node("ns=2;i=2")
        value = await node.read_value()
        print(f"Value: {value}")

asyncio.run(main())
EOF

# Security modes:
# - None (no security)
# - Sign (message signing)
# - SignAndEncrypt (full security)

# Common issues:
# - Anonymous access enabled
# - Self-signed certificates accepted
# - Weak cipher suites
# asyncua is the current, maintained OPC UA library for Python
pip install asyncua

# Optional GUI client (opcua-client-gui) for manual browsing
pip install opcua-client

python3 << 'EOF'
import asyncio
from asyncua import Client

async def main():
    url = "opc.tcp://192.168.1.100:4840"
    async with Client(url=url) as client:
        # Get server info
        print(f"Server: {await client.get_server_node()}")
        
        # Browse root
        root = client.get_root_node()
        children = await root.get_children()
        for child in children:
            print(f"Node: {child}")
        
        # Read a variable
        node = client.get_node("ns=2;i=2")
        value = await node.read_value()
        print(f"Value: {value}")

asyncio.run(main())
EOF

# Security modes:
# - None (no security)
# - Sign (message signing)
# - SignAndEncrypt (full security)

# Common issues:
# - Anonymous access enabled
# - Self-signed certificates accepted
# - Weak cipher suites

BACnet (Building Automation)

Building automation protocol. Port 47808 (UDP). Controls HVAC, lighting, access control.

bash
# Using BAC0 library
pip install BAC0

python3 << 'EOF'
import BAC0

# Start BACnet/IP network
bacnet = BAC0.lite()

# Discover devices (WhoIs)
devices = bacnet.whois()
for device in devices:
    print(f"Device: {device}")

# Read device properties
device_id = 1234
# Read Present Value of Analog Input 0
value = bacnet.read(f'{device_id} analogInput 0 presentValue')
print(f"Temperature: {value}")

# Read all points from device
# device = BAC0.device('192.168.1.100', device_id, bacnet)
# print(device.points)
EOF

# Nmap BACnet script
nmap -sU -p 47808 --script bacnet-info 192.168.1.100

# Common attacks:
# - Write to setpoints (temperature, schedules)
# - Disable alarms
# - Manipulate access control
# - Read sensitive building data
# Using BAC0 library
pip install BAC0

python3 << 'EOF'
import BAC0

# Start BACnet/IP network
bacnet = BAC0.lite()

# Discover devices (WhoIs)
devices = bacnet.whois()
for device in devices:
    print(f"Device: {device}")

# Read device properties
device_id = 1234
# Read Present Value of Analog Input 0
value = bacnet.read(f'{device_id} analogInput 0 presentValue')
print(f"Temperature: {value}")

# Read all points from device
# device = BAC0.device('192.168.1.100', device_id, bacnet)
# print(device.points)
EOF

# Nmap BACnet script
nmap -sU -p 47808 --script bacnet-info 192.168.1.100

# Common attacks:
# - Write to setpoints (temperature, schedules)
# - Disable alarms
# - Manipulate access control
# - Read sensitive building data

PROFINET

Siemens industrial Ethernet standard. Layer 2 protocol using Ethernet frames directly.

bash
# PROFINET operates at Layer 2
# Uses Ethernet frames with EtherType 0x8892

# Discovery with DCP (Discovery and Configuration Protocol)
# Sends broadcast to identify PROFINET devices

# Using Wireshark
# Filter: pn_dcp or pn_io

# PROFINET tools:
# - Siemens PRONETA
# - Wireshark with PROFINET plugins

# Common issues:
# - No authentication
# - Name spoofing
# - Configuration manipulation
# PROFINET operates at Layer 2
# Uses Ethernet frames with EtherType 0x8892

# Discovery with DCP (Discovery and Configuration Protocol)
# Sends broadcast to identify PROFINET devices

# Using Wireshark
# Filter: pn_dcp or pn_io

# PROFINET tools:
# - Siemens PRONETA
# - Wireshark with PROFINET plugins

# Common issues:
# - No authentication
# - Name spoofing
# - Configuration manipulation

IEC 60870-5-104 (IEC-104)

The dominant telecontrol protocol in European and international power utilities (transmission/distribution SCADA). TCP port 2404. Like Modbus, base IEC-104 has no authentication or encryption — IEC 62351 adds TLS and authentication but is inconsistently deployed. The Industroyer/CRASHOVERRIDE malware included a dedicated IEC-104 module used against the Ukrainian grid.

bash
# Enumeration / traffic analysis
nmap -p 2404 192.168.1.100        # IEC-104 listens on TCP 2404

# Wireshark filters
104apci                            # IEC 60870-5-104 APCI (frames)
104asdu                            # Application data units (ASDUs)
104asdu.typeid == 45              # C_SC_NA_1 single command (control!)
104asdu.typeid == 46              # C_DC_NA_1 double command

# Key ASDU type IDs to recognize
#  1  M_SP_NA_1   Single-point information (status)
# 13  M_ME_NC_1   Measured value, short float
# 45  C_SC_NA_1   Single command  (WRITE / control)
# 46  C_DC_NA_1   Double command  (WRITE / control)
# 100 C_IC_NA_1   Interrogation command (general station poll)

# Python testing (c104 library binds the IEC 60870-5-104 stack)
pip install c104

python3 << 'EOF'
import c104
client = c104.Client()
conn = client.add_connection(ip="192.168.1.100", port=2404)
client.start()
# A general interrogation (type 100) enumerates a station's data points
# conn.interrogation(common_address=1)  # read-only station poll
EOF

# Common attacks (LAB ONLY):
# - Replay captured C_SC/C_DC commands to trip breakers
# - Spoof measured values to mislead operators
# - General interrogation flooding (DoS)
# Enumeration / traffic analysis
nmap -p 2404 192.168.1.100        # IEC-104 listens on TCP 2404

# Wireshark filters
104apci                            # IEC 60870-5-104 APCI (frames)
104asdu                            # Application data units (ASDUs)
104asdu.typeid == 45              # C_SC_NA_1 single command (control!)
104asdu.typeid == 46              # C_DC_NA_1 double command

# Key ASDU type IDs to recognize
#  1  M_SP_NA_1   Single-point information (status)
# 13  M_ME_NC_1   Measured value, short float
# 45  C_SC_NA_1   Single command  (WRITE / control)
# 46  C_DC_NA_1   Double command  (WRITE / control)
# 100 C_IC_NA_1   Interrogation command (general station poll)

# Python testing (c104 library binds the IEC 60870-5-104 stack)
pip install c104

python3 << 'EOF'
import c104
client = c104.Client()
conn = client.add_connection(ip="192.168.1.100", port=2404)
client.start()
# A general interrogation (type 100) enumerates a station's data points
# conn.interrogation(common_address=1)  # read-only station poll
EOF

# Common attacks (LAB ONLY):
# - Replay captured C_SC/C_DC commands to trip breakers
# - Spoof measured values to mislead operators
# - General interrogation flooding (DoS)

Grid Control Commands

IEC-104 C_SC_NA_1 / C_DC_NA_1 commands operate breakers and switchgear on live power systems. Never issue control ASDUs outside an isolated lab — a single spoofed command can de-energize a substation.

IEC 61850 (Substation Automation)

The substation automation standard, ubiquitous in modern electrical substations. It is a family of protocols rather than one wire format: MMS (client/server over TCP 102, same port as S7comm), GOOSE (Layer 2 multicast, EtherType 0x88B8, for fast protection trips), and Sampled Values / SV (Layer 2, EtherType 0x88BA, digitized CT/VT measurements).

bash
# MMS (Manufacturing Message Specification) - TCP 102
nmap -p 102 192.168.1.100         # shared with S7comm; confirm via banner/MMS

# libiec61850 ships reference clients for MMS browsing
git clone https://github.com/mz-automation/libiec61850
# examples/ -> mms_client_example, iec61850_client_example

# Wireshark filters
mms                               # MMS (61850 client/server)
goose                             # GOOSE protection messages
sv                                # Sampled Values

# GOOSE poisoning (LAB ONLY) - protection trips ride on GOOSE.
# Frames carry a stNum/sqNum counter; an attacker who replays or
# injects a higher stNum with a spoofed trip dataset can cause a
# false breaker operation or suppress a real one.
# Tools: scapy (custom Ether()/GOOSE), goose-tools, plus IEDScout for MMS.

# Common weaknesses:
# - GOOSE/SV have NO authentication at L2 by default
# - MMS often allows unauthenticated reads of the data model
# - IEC 62351-6 (auth/signing) rarely enabled
# MMS (Manufacturing Message Specification) - TCP 102
nmap -p 102 192.168.1.100         # shared with S7comm; confirm via banner/MMS

# libiec61850 ships reference clients for MMS browsing
git clone https://github.com/mz-automation/libiec61850
# examples/ -> mms_client_example, iec61850_client_example

# Wireshark filters
mms                               # MMS (61850 client/server)
goose                             # GOOSE protection messages
sv                                # Sampled Values

# GOOSE poisoning (LAB ONLY) - protection trips ride on GOOSE.
# Frames carry a stNum/sqNum counter; an attacker who replays or
# injects a higher stNum with a spoofed trip dataset can cause a
# false breaker operation or suppress a real one.
# Tools: scapy (custom Ether()/GOOSE), goose-tools, plus IEDScout for MMS.

# Common weaknesses:
# - GOOSE/SV have NO authentication at L2 by default
# - MMS often allows unauthenticated reads of the data model
# - IEC 62351-6 (auth/signing) rarely enabled

Protocol Comparison

Protocol Port Auth Encryption Primary Use
Modbus TCP 502 Universal ICS
S7comm 102 ⚠️ Optional Siemens PLCs
EtherNet/IP 44818 Allen-Bradley PLCs
DNP3 20000 ⚠️ SA optional ⚠️ SA optional Utilities
OPC UA 4840 ✅ Built-in ✅ Built-in Modern ICS
BACnet 47808 ⚠️ SC optional ⚠️ SC optional Building automation
IEC-104 2404 ⚠️ 62351 optional ⚠️ 62351 optional Power utilities (EU/intl)
IEC 61850 MMS 102 ⚠️ 62351 optional ⚠️ 62351 optional Substation automation
GOOSE / SV L2 (0x88B8/0x88BA) Substation protection

Protocol Analysis with Wireshark

bash
# Wireshark ICS protocol filters
modbus                    # Modbus TCP
s7comm                    # Siemens S7
enip                      # EtherNet/IP
cip                       # Common Industrial Protocol
dnp3                      # DNP3
bacnet                    # BACnet
opcua                     # OPC UA
profinet                  # PROFINET (pn_io, pn_dcp)
104apci                   # IEC 60870-5-104 (power utilities)
mms                       # IEC 61850 MMS
goose                     # IEC 61850 GOOSE (protection)

# Useful display filters
modbus.func_code == 6     # Write Single Register
s7comm.param.func == 0x05 # S7 Write Var
enip.command == 0x006f    # EtherNet/IP Send RR Data

# Extract interesting data
# Statistics > Protocol Hierarchy
# Statistics > Conversations
# File > Export Objects
# Wireshark ICS protocol filters
modbus                    # Modbus TCP
s7comm                    # Siemens S7
enip                      # EtherNet/IP
cip                       # Common Industrial Protocol
dnp3                      # DNP3
bacnet                    # BACnet
opcua                     # OPC UA
profinet                  # PROFINET (pn_io, pn_dcp)
104apci                   # IEC 60870-5-104 (power utilities)
mms                       # IEC 61850 MMS
goose                     # IEC 61850 GOOSE (protection)

# Useful display filters
modbus.func_code == 6     # Write Single Register
s7comm.param.func == 0x05 # S7 Write Var
enip.command == 0x006f    # EtherNet/IP Send RR Data

# Extract interesting data
# Statistics > Protocol Hierarchy
# Statistics > Conversations
# File > Export Objects