Industrial Protocols
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
# 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.100Write Operations
Siemens S7comm
Proprietary protocol for Siemens S7 PLCs. Port 102 (ISO-TSAP). Very common in manufacturing and utilities.
# 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 trafficEtherNet/IP (CIP)
Common Industrial Protocol over Ethernet. Used by Allen-Bradley/Rockwell PLCs. Port 44818.
# 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.100DNP3 (Distributed Network Protocol)
Common in utilities (power, water). Port 20000. Supports authentication in DNP3-SA (Secure Authentication).
# 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 injectionOPC UA
Modern protocol with built-in security. Port 4840. Supports authentication and encryption.
# 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 suitesBACnet (Building Automation)
Building automation protocol. Port 47808 (UDP). Controls HVAC, lighting, access control.
# 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 dataPROFINET
Siemens industrial Ethernet standard. Layer 2 protocol using Ethernet frames directly.
# 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 manipulationIEC 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.
# 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 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).
# 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 enabledProtocol 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
# 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