- 📊 报告类型:技术方案报告
- 👤 适用项目:Pikachu(PHP + MySQL 漏洞练习平台)
- 🕐 编写时间:2026-04-26
- 🏷️ 当前状态:实施级设计完成,未修改源码
- 📎 设计边界:Windows / PowerShell、Docker Desktop for Windows、PHP 受控调用 Docker CLI、静态白名单模板、单机本地编排
本设计将 Docker Lab 落地为 Pikachu 的新增独立模块,目录位于 vul/dockerlab/。第一版不引入数据库表,不引入独立 Controller 服务,不开放任意 Docker 参数,只允许用户在页面中管理白名单模板对应的本地容器。
第一版 MVP 仅保留 3 个模板:
redis-unauthmysql-weakflask-ssti
平台只允许以下动作:
- 查看模板
- 检查 Docker 环境
- 启动
- 停止
- 重启
- 删除
- 查看最近日志
- 打开本地访问入口
flowchart TD
A([用户进入 Docker Lab]) --> B[加载 templates/*.json]
B --> C[校验模板字段与白名单]
C --> D[展示环境状态与模板状态]
D --> E{用户 POST 动作}
E -->|start/stop/remove/restart| F[校验 CSRF Token 与 lab_id]
F --> G[按模板构造固定 Docker 参数]
G --> H[调用 docker CLI]
H --> I[仅操作带 pikachu.lab=true 的容器]
I --> J([返回结果并刷新页面])
Pikachu 当前是典型的“单体 PHP 页面 + 公共头尾 + 模块目录”结构:
- 根目录:
index.php、header.php、footer.php、install.php - 公共配置:
inc/config.inc.php、inc/mysql.inc.php、inc/function.php - 模块目录:
vul/<module>/ - 菜单入口:集中在
header.php
- 适合按
vul/dockerlab/形式新增模块 - 适合新增少量本地库文件,不适合大规模分层重构
- 适合继续使用同步表单提交和结果回显
- 不适合第一版引入异步任务、消息队列、数据库状态表或独立编排服务
最稳妥的落地方式是:
- 新增
vul/dockerlab/目录 - 使用静态 JSON 模板作为白名单配置源
- 使用
dockerlab_lib.php封装受控命令 - 所有状态变更动作走
dockerlab_action.php的 POST 请求
第一版只支持 3 个实验环境:
redis-unauthmysql-weakflask-ssti
以下模板推迟到第二批扩展:
postgres-weaknginx-misconfigtomcat-weakflask-debugspring-actuatorfastapi-docsnode-proto-pollution
vul/dockerlab/
├── dockerlab.php
├── dockerlab_center.php
├── dockerlab_action.php
├── dockerlab_logs.php
├── dockerlab_check.php
├── dockerlab_lib.php
└── templates/
├── redis-unauth.json
├── mysql-weak.json
└── flask-ssti.json
| 路径 | 职责 |
|---|---|
dockerlab.php |
模块概述、使用说明、安全边界 |
dockerlab_center.php |
模板列表、状态展示、操作入口 |
dockerlab_action.php |
仅处理 POST 状态变更动作 |
dockerlab_logs.php |
查看指定模板对应容器最近日志 |
dockerlab_check.php |
展示 Docker 环境检查详情 |
dockerlab_lib.php |
模板、状态、命令、校验的公共函数 |
templates/*.json |
白名单模板定义 |
必须包含以下函数:
dockerlab_load_templates()dockerlab_get_template($id)dockerlab_validate_lab_id($id)dockerlab_validate_template($template)dockerlab_build_run_command($template)dockerlab_run_command($args, $timeout = 30)dockerlab_get_container_status($template)dockerlab_start_lab($id)dockerlab_stop_lab($id)dockerlab_remove_lab($id)dockerlab_get_logs($id, $tail = 200)dockerlab_check_environment()
职责:
- 读取
templates/*.json - 逐个 JSON 解析
- 调用
dockerlab_validate_template() - 返回以
id为键的模板数组
返回建议:
array(
'redis-unauth' => array(...),
'mysql-weak' => array(...),
'flask-ssti' => array(...)
)职责:
- 先校验
lab_id - 从
dockerlab_load_templates()中返回对应模板 - 不存在则返回
false
职责:
- 校验
lab_id是否匹配^[a-z0-9-]+$ - 严格限制为小写字母、数字、连字符
返回:
true/false
职责:
- 校验模板字段完整性
- 校验字段类型和值域
- 拒绝不支持字段
- 拒绝危险配置
校验失败时:
- 返回
array('ok' => false, 'error' => '...')
成功时:
- 返回
array('ok' => true)
职责:
- 根据模板构造固定的
docker run参数数组 - 不接收用户自定义覆盖
- 强制包含 label、container name、127.0.0.1 端口绑定
返回示例:
array(
'docker', 'run', '-d',
'--name', 'pikachu-redis-unauth',
'--label', 'pikachu.lab=true',
'--label', 'pikachu.template=redis-unauth',
'-p', '127.0.0.1:16379:6379',
'redis:7-alpine'
)职责:
- 用受控方式执行 Docker CLI
- 超时控制
- 捕获 stdout / stderr / exit code
- 统一返回结构化结果
返回建议:
array(
'ok' => true,
'exit_code' => 0,
'stdout' => '...',
'stderr' => ''
)职责:
- 基于模板中的
container_name查询状态 - 只允许查询带
pikachu.lab=truelabel 的容器
建议状态枚举:
not_createdrunningexitederror
职责:
- 获取模板
- 检查现有容器状态
- 不存在则执行
docker run - 已存在但停止则执行
docker start - 统一返回结果
职责:
- 仅停止目标模板对应且带目标 label 的容器
职责:
- 仅删除目标模板对应且带目标 label 的容器
- 推荐内部使用
docker rm -f
职责:
tail上限固定为200- 只允许查询白名单模板对应容器
- 只允许读取目标 label 容器日志
职责:
- 检查
docker命令是否存在 - 检查 Docker Desktop / Engine 是否运行
- 检查当前用户是否可执行 Docker CLI
返回建议:
array(
'docker_found' => true,
'docker_version_ok' => true,
'daemon_reachable' => true,
'message' => 'Docker Desktop is running'
)dockerlab_action.php 只允许以下动作:
startstopremoverestart
所有状态改变操作必须是 POST:
- 禁止通过
GET启动、停止、删除、重启容器
- 校验请求方法必须为
POST - 校验 CSRF Token
- 校验
lab_id匹配^[a-z0-9-]+$ - 校验
lab_id必须存在于templates - 校验
action必须在白名单中
以下字段即使用户提交,也必须忽略或拒绝:
imagecommandcmdvolumemountenvportcontainer_namenetworkprivilegedcap_adddevice
| action | 内部调用 |
|---|---|
start |
dockerlab_start_lab($id) |
stop |
dockerlab_stop_lab($id) |
remove |
dockerlab_remove_lab($id) |
restart |
先 dockerlab_stop_lab($id),再 dockerlab_start_lab($id) |
idnamecategoryimagecontainer_namelabelsportsenvcmdentry_urlnotesenabled
| 字段 | 类型 | 规则 |
|---|---|---|
id |
string | 必填,匹配 ^[a-z0-9-]+$ |
name |
string | 必填,显示名称 |
category |
string | 必填,建议如 database、middleware、web |
image |
string | 必填,只能来自模板,用户不可改 |
container_name |
string | 必填,必须以 pikachu- 开头 |
labels |
object | 必填,必须包含 pikachu.lab=true |
ports |
array | 必填,至少 1 项 |
env |
array | 可为空,格式为 KEY=VALUE |
cmd |
array | 可为空,仅来自模板 |
entry_url |
string | 可为空,用于 UI “打开” |
notes |
string | 可为空,用于教学说明 |
enabled |
bool | 必填,控制模板是否在 UI 展示 |
每一项必须包含:
host_iphost_portcontainer_portprotocol
校验规则:
host_ip必须等于127.0.0.1host_port必须是 1-65535 的整数container_port必须是 1-65535 的整数protocol仅允许tcp
第一版模板中明确不支持:
volumesmountsprivilegednetwork_mode--network hostcap_adddevicepidipcdocker.sock
| 动作 | 命令 | 说明 |
|---|---|---|
check |
docker version |
检查 CLI 可用 |
check |
docker info |
检查 daemon 可达 |
status |
`docker ps -a --filter "label=pikachu.lab=true" --filter "name=<container_name>" --format "{{.Names}} | {{.Status}}"` |
start(首次) |
docker run -d --name <container_name> --label pikachu.lab=true ... -p 127.0.0.1:host:container <image> |
首次创建并启动 |
start(已存在) |
docker start <container_name> |
启动已存在容器 |
stop |
docker stop <container_name> |
停止容器 |
remove |
docker rm -f <container_name> |
删除容器 |
logs |
docker logs --tail 200 <container_name> |
查看最近日志 |
第一版 docker run 至少固定包含:
-d--name <container_name>--label pikachu.lab=true--label pikachu.template=<id>-p 127.0.0.1:<host_port>:<container_port>
- 禁止任意 Docker 命令执行
- 禁止自定义
image - 禁止自定义
command - 禁止自定义
volume - 禁止
privileged - 禁止挂载
/var/run/docker.sock - 禁止
--network host - 禁止
--cap-add - 禁止
--device - 只操作带
pikachu.lab=truelabel 的容器 - 日志输出必须经过
htmlspecialchars - 日志最多 200 行
- 端口只绑定
127.0.0.1
- 页面只能提交
action和lab_id - 运行参数只来自静态模板
- 所有删除和状态检查都要同时满足“容器名匹配 + label 匹配”
- 即使存在同名异常输入,也不能覆盖模板配置
展示字段:
- 模块标题
- 模块用途
- MVP 支持的 3 个模板
- 使用前提:Docker Desktop 已启动
- 安全边界说明
- 进入
dockerlab_center.php的入口链接
展示字段:
docker version检查结果docker info检查结果- Docker daemon 是否可达
- 当前错误消息
- 返回编排中心入口
展示字段:
- 环境状态摘要
- 模板名称
- 分类
- 镜像名
- 容器名
- 本地端口
- 当前状态
- 访问入口
- 操作按钮:启动 / 停止 / 重启 / 删除 / 日志
- 当前操作结果提示
职责字段:
- 接收
POST - 接收
action - 接收
lab_id - 接收
csrf_token - 返回执行结果后重定向或回显
该页不作为独立浏览页使用,只作为动作处理入口。
展示字段:
- 模板名称
- 容器名
- 当前状态
- 最近日志文本
- 日志截断提示
- 返回编排中心入口
- Docker 未安装时,
dockerlab_check.php能明确提示不可用 - Docker 未运行时,
dockerlab_check.php能明确提示 daemon 不可达 - 模板目录正常时,3 个模板能被正确加载
- Redis:可启动 / 停止 / 删除
- MySQL:可启动 / 停止 / 删除
- Flask SSTI:可启动 / 停止 / 删除
- 运行中模板可查看日志
- 删除后状态回到
not_created
- 非白名单
lab_id被拒绝 - 非
pikachu.lab=true的容器不能被删除 - 用户无法通过请求参数覆盖
image - 用户无法通过请求参数覆盖
command - 用户无法通过请求参数覆盖
port - 日志输出经过 HTML 转义
- 日志输出不超过 200 行
- 所有端口仅绑定
127.0.0.1 - UI 中“打开”链接能正确指向本地入口
docker version
docker infodocker ps -a --filter "label=pikachu.lab=true"
docker ps -a --filter "label=pikachu.lab=true" --filter "name=pikachu-redis-unauth"
docker ps -a --filter "label=pikachu.lab=true" --filter "name=pikachu-mysql-weak"
docker ps -a --filter "label=pikachu.lab=true" --filter "name=pikachu-flask-ssti"docker logs --tail 200 pikachu-redis-unauth
docker logs --tail 200 pikachu-mysql-weak
docker logs --tail 200 pikachu-flask-sstidocker stop pikachu-redis-unauth
docker rm -f pikachu-redis-unauth
docker stop pikachu-mysql-weak
docker rm -f pikachu-mysql-weak
docker stop pikachu-flask-ssti
docker rm -f pikachu-flask-sstidocker port pikachu-redis-unauth
docker port pikachu-mysql-weak
docker port pikachu-flask-ssti
netstat -ano | findstr 127.0.0.1redis-cli -h 127.0.0.1 -p 16379 ping
mysql -h 127.0.0.1 -P 13306 -u root -p
curl http://127.0.0.1:15000/- 新增
vul/dockerlab/全部页面骨架 - 新增 3 个 JSON 模板
- 完成模板加载、环境检查、状态展示
- 完成
start / stop / remove / restart / logs
- 增加
postgres-weak - 增加
nginx-misconfig - 增加
flask-debug - 增加
fastapi-docs
- 强化错误提示与边界处理
- 优化日志页展示
- 增加更完整的模板校验错误反馈
这份实施级设计将 Docker Lab 第一版严格限制在“3 个白名单模板 + 受控 Docker CLI + 本地单机容器管理”的范围内,已经可以直接指导后续开发。该方案与当前 Pikachu 仓库结构兼容,不要求改数据库,不要求引入独立服务,且把高风险的 Docker 能力压缩在固定动作和固定参数之内。
redis-unauthmysql-weakflask-ssti
postgres-weaknginx-misconfigtomcat-weakflask-debugspring-actuatorfastapi-docsnode-proto-pollution
进入开发前,建议再补一份更细的开发任务单,至少包含:
- 菜单索引分配方案
- 每个页面的伪代码流程
- 3 个模板的完整 JSON 实例
- Docker CLI 输出解析格式