从零搭建本地 AI 安全测试平台(一):Ubuntu2604_Docker四大漏洞靶场部署实战
适用场景:在隔离、获得授权的实验环境中搭建 Juice Shop、DVWA、WebGoat、crAPI,供另一台 Ubuntu 主机上的 AI 安全测试平台使用。
实战环境:2026 年 11 月,Ubuntu 26.04.1、Docker Engine、SSH、iptables-nft。
记录说明:文中“实测”部分来自本次实际命令及输出;“参考部署”是供重新搭建时使用的示例,并非声称当时逐条执行。具体镜像 tag、Compose 文件内容、安装来源未完整保留,重新部署时应以项目官方文档和实际镜像版本为准。
一、项目背景与目标
本次实验使用两台 Ubuntu 主机:
- A 主机:
javen03,Ubuntu 26.04,后续计划部署 Ollama、Qwen3 8B、Python 自动化测试控制器。 - B 主机:
yj2-Ubuntu2,Ubuntu 26.04.1,IP192.168.18.237,网卡enp0s31f6,运行 Docker 漏洞靶场。 - B 主机已有业务:Nginx 文件共享使用 TCP
443、8443;SSH 使用22;远程桌面使用3389。任何实验配置都不能破坏这些服务。 - 代理:局域网 SOCKS5
192.168.10.3:7892仅用于按需获取难以直接访问的国外资源,不是漏洞靶场的必要出口。
目标包括:四个靶场运行正常;A 主机能够访问靶场;靶场不主动访问不必要的互联网和其他内网;crAPI 内部数据库及微服务互通;不重启 Docker、不清空现有防火墙规则、不影响文件共享服务。
1.1 网络拓扑
A:javen03(AI 测试主机)
|
| SSH TCP/22 + 本地端口转发
v
B:192.168.18.237(Ubuntu 26.04.1)
|-- 127.0.0.1:3000 -> Juice Shop
|-- 127.0.0.1:8081 -> DVWA
|-- 127.0.0.1:8082 -> WebGoat
|-- 127.0.0.1:8888 -> crAPI Web
|
|-- Nginx 443/8443(既有文件共享,保持不变)
|-- SSH 22、RDP 3389(保持不变)
|
`-- Docker 独立网桥
172.18.0.0/16 Juice Shop
172.19.0.0/16 DVWA
172.20.0.0/16 WebGoat
172.21.0.0/16 crAPI 多容器
为什么不直接暴露漏洞靶场到局域网? 这些应用有意保留安全缺陷,绑定 127.0.0.1 后由 SSH 隧道提供受控访问,减少意外暴露。注意:这不是完整的隔离措施,仍须审计容器到宿主机、IPv6 和持久化防火墙策略。
二、准备工作与风险边界
2.1 先确认既有服务
在 B 主机执行:
hostnamectl
ip -br addr
sudo ss -lntp | grep -E ':(22|3389|443|8443)\b'
systemctl is-active docker
sudo docker version
sudo docker compose version
本次实测:Docker 为 active;Nginx 监听 0.0.0.0:443 和 0.0.0.0:8443;SSH 监听 22;GNOME 远程桌面监听 3389。
安全原则:不要执行 iptables -F、nft flush ruleset、iptables -P FORWARD ACCEPT、随意 ufw reload 或重启 Docker。尤其不要把靶场映射到 0.0.0.0。
2.2 安装 Docker(仅新机器参考)
本次 B 主机已经安装 Docker,下面不是本次安装历史,而是新机器的准备检查。安装时优先按照 Docker 官方 Ubuntu 指南配置 apt 仓库、安装 docker-ce、docker-ce-cli、containerd.io、docker-buildx-plugin、docker-compose-plugin。不要在已有业务主机上为完成教程而重新安装或重启 Docker。
sudo docker info
sudo docker compose version
sudo docker network ls
2.3 记录镜像版本与部署配置
为了让未来重建可复现,建议在当前 B 主机采集以下信息(只读):
sudo docker ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
sudo docker inspect lab-juice-shop --format '{{.Config.Image}}'
sudo docker inspect dvwa-dvwa-1 --format '{{.Config.Image}}'
sudo docker inspect lab-webgoat --format '{{.Config.Image}}'
sudo docker inspect crapi-web --format '{{.Config.Image}}'
重要:当前对话没有完整的原始 Compose YAML、镜像 digest 和所有环境变量,因此无法保证仅靠本文就能一字不差地重建原部署。特别是 crAPI 多服务配置需要保留官方 Compose 文件和对应版本。
三、四个靶场的部署设计
3.1 端口规划(实测)
| 应用 | B 宿主机绑定 | 容器端口 | 备注 |
|---|---|---|---|
| OWASP Juice Shop | 127.0.0.1:3000 |
3000 |
单容器 |
| DVWA | 127.0.0.1:8081 |
80 |
另有数据库容器 |
| WebGoat | 127.0.0.1:8082 |
8080 |
另有 127.0.0.1:9090 |
| crAPI Web | 127.0.0.1:8888 |
80 |
另有 30080、18443、30443 等映射 |
crAPI 曾与已有业务的 8443 端口冲突,因此最终使用 18443、30443 等不冲突的端口。实际运行时还存在 5500(chatbot)与 8025(MailHog)等回环映射。
3.2 Juice Shop(参考单容器部署)
在全新、专用的实验机上,可参考以下形式。实际使用前固定经过验证的镜像版本,不建议生产环境依赖 latest。
sudo docker network create juice-shop_default
sudo docker run -d \
--name lab-juice-shop \
--network juice-shop_default \
-p 127.0.0.1:3000:3000 \
bkimminich/juice-shop
这是一份参考重建示例,不是原始执行记录。现有主机上不要重复运行,否则会与已存在的容器/网络冲突。实测容器为 lab-juice-shop,IP 172.18.0.2。
3.3 DVWA(参考部署方式)
DVWA 在本次环境中通过 Compose 运行,应用容器为 dvwa-dvwa-1,数据库为 dvwa-db-1,网络 dvwa_dvwa。应用容器 IP 172.19.0.3,数据库 IP 172.19.0.2。
重新部署时建议获取 DVWA 官方仓库的 Compose 文件,只修改对外发布端口,把 Web 服务端口映射为:
# 仅示意需要修改的 ports 字段,不能替代完整 Compose 文件
ports:
- "127.0.0.1:8081:80"
然后在 Compose 项目目录执行:
sudo docker compose config
sudo docker compose up -d
现有部署无需重新执行。DVWA 入口返回 302 是正常的跳转现象,不能误判为服务失败。
3.4 WebGoat(参考单容器部署)
本次环境使用 lab-webgoat,网桥 webgoat_default,IP 172.20.0.2。宿主机 8082 映射到容器 8080,9090 映射到 9090。
可参考的发布端口形式:
# 仅展示端口规划,镜像、环境变量和健康检查需以实际版本为准
ports:
- "127.0.0.1:8082:8080"
- "127.0.0.1:9090:9090"
访问时应使用应用路径 /WebGoat,而不是仅检查 /。本次实测根路径返回 404,但 /WebGoat 经重定向后返回 200。
3.5 crAPI(多容器 Compose)
crAPI 是本次最复杂的靶场,包含 Web 前端、Identity、Community、Workshop、Chatbot、PostgreSQL、MongoDB、ChromaDB、MailHog 等组件。建议从官方 crAPI 项目获取与目标版本对应的完整 Compose 文件,不要根据零散端口表自行拼凑完整 YAML。
本次 docker_default 网桥中实测容器及地址:
| 容器 | IP |
|---|---|
| postgresdb | 172.21.0.2 |
| api.mypremiumdealership.com | 172.21.0.3 |
| chromadb | 172.21.0.4 |
| mailhog | 172.21.0.5 |
| mongodb | 172.21.0.6 |
| crapi-identity | 172.21.0.7 |
| crapi-community | 172.21.0.8 |
| crapi-chatbot | 172.21.0.9 |
| crapi-workshop | 172.21.0.10 |
| crapi-web | 172.21.0.11 |
部署时应特别检查是否占用了既有 Nginx 的 443、8443,并把需要测试的 Web 入口限制到 127.0.0.1。这些 IP 是本次实测值,不是持久的固定地址;容器重建后可能改变。
四、验证 B 主机本地四个靶场
sudo docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
for item in 'JuiceShop:3000:/' 'DVWA:8081:/' 'WebGoat:8082:/WebGoat' 'crAPI:8888:/'; do
IFS=: read -r name port path <<< "$item"
printf '%-12s ' "$name"
curl --noproxy '*' -sS --connect-timeout 3 --max-time 10 \
-o /dev/null -w 'HTTP=%{http_code}\n' \
"http://127.0.0.1:${port}${path}"
done
本次实际结果:
| 靶场 | 结果 | 说明 |
|---|---|---|
| Juice Shop | 200 |
HTTP 正常 |
| DVWA | 302 |
应用跳转 |
| WebGoat | /WebGoat 最终 200 |
入口正常 |
| crAPI | 200 |
Web 入口正常 |
HTTP 200/302 仅代表入口可达,不能替代注册登录、数据库写入、靶场练习等功能测试。
五、WebGoat unhealthy 的真实原因
这是本次排障中非常典型的一例:docker ps 显示 WebGoat Up (unhealthy),但浏览器和 curl 都可以访问。
排查命令:
sudo docker inspect lab-webgoat --format '{{json .State.Health}}'
curl --noproxy '*' -sS -L -o /dev/null \
-w 'HTTP %{http_code}\n' --max-time 10 \
http://127.0.0.1:8082/WebGoat
实际健康检查日志:
Status: unhealthy
FailingStreak: 7890
/bin/sh: 1: curl: not found
因此 unhealthy 的直接原因是健康检查命令依赖 curl,但容器里没有该程序;不能由此认定 WebGoat 应用故障。本次 /WebGoat 最终返回 HTTP 200。后续应根据镜像中实际可用的工具调整健康检查,或在构建镜像时补齐依赖;本文未执行重建,不将其写成已修复。
六、A 主机通过 SSH 隧道访问 B 靶场
所有靶场仅监听 B 的 127.0.0.1,A 主机直接访问 192.168.18.237:3000 不应成功。使用 SSH 本地端口转发,不必把有漏洞的服务暴露到局域网。
在 A 主机执行(SSH 账号根据 B 实际账号调整):
ssh -o ExitOnForwardFailure=yes -N \
-L 13000:127.0.0.1:3000 \
-L 18081:127.0.0.1:8081 \
-L 18082:127.0.0.1:8082 \
-L 18888:127.0.0.1:8888 \
yj2@192.168.18.237
保持该终端运行,在 A 主机另开终端验证:
curl --noproxy '*' -sS -o /dev/null -w 'JuiceShop HTTP %{http_code}\n' http://127.0.0.1:13000/
curl --noproxy '*' -sS -o /dev/null -w 'DVWA HTTP %{http_code}\n' http://127.0.0.1:18081/
curl --noproxy '*' -sS -o /dev/null -w 'WebGoat HTTP %{http_code}\n' http://127.0.0.1:18082/WebGoat
curl --noproxy '*' -sS -o /dev/null -w 'crAPI HTTP %{http_code}\n' http://127.0.0.1:18888/
A 主机实测结果:Juice Shop 200、DVWA 302、WebGoat 302、crAPI 200,说明 SSH 隧道及四个入口均可达。SSH 会话退出后隧道断开;后续若需常驻,应另行设计密钥、访问控制与服务守护。
七、Docker 网络和防火墙故障排查
7.1 Docker 网桥与子网(实测)
sudo docker network ls
sudo docker network inspect docker_default
ip -br link show type bridge
sysctl net.ipv4.ip_forward
sudo iptables -L FORWARD -n -v --line-numbers
sudo iptables -L DOCKER-USER -n -v --line-numbers
实际网桥:
br-0c45b8201d1f Juice Shop 172.18.0.0/16
br-bd58040c84aa DVWA 172.19.0.0/16
br-3c90cf5d4e3b WebGoat 172.20.0.0/16
br-81e33e196878 crAPI 172.21.0.0/16
本次 net.ipv4.ip_forward = 1;但 IPv4 FORWARD 默认策略是 DROP,Docker 的 DOCKER-USER 链显示 0 references,且没有常见的 Docker 转发跳转规则。不要因为 DOCKER-USER 链存在,就误以为写入该链的规则一定生效。
7.2 为什么最初 SOCKS5 测试不通?
最初从 Juice Shop 容器网络命名空间访问 192.168.10.3:7892 超时。抓包观察到 SYN 从容器进入 veth 和 Docker 网桥,但没有从物理网卡 enp0s31f6 发出;FORWARD 默认 DROP 且无放行规则。
当时为定位问题,临时添加了两条精确的转发规则:
sudo iptables -I FORWARD 1 \
-i br-0c45b8201d1f -o enp0s31f6 \
-s 172.18.0.2/32 -d 192.168.10.3/32 \
-p tcp --dport 7892 -j ACCEPT
sudo iptables -I FORWARD 2 \
-i enp0s31f6 -o br-0c45b8201d1f \
-d 172.18.0.2/32 \
-m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
随后,使用宿主机 curl 进入容器网络命名空间测试:
PID=$(sudo docker inspect -f '{{.State.Pid}}' lab-juice-shop)
sudo nsenter -t "$PID" -n \
curl --socks5-hostname 192.168.10.3:7892 \
--connect-timeout 10 --max-time 20 \
-I https://example.com
实测成功建立到代理的 TCP 连接,远端返回 HTTP/2 200。这证明转发链是先前故障的重要原因,不意味着漏洞靶场应该长期通过代理上网。
7.3 纠正设计:代理只用于按需下载资源
用户原本配置 SOCKS5,是因为部分国外网站或镜像资源无法直接访问。漏洞靶场作为被测目标,通常不需要主动访问互联网。更合理的设计是:宿主机或镜像下载按需使用代理;靶场容器的出站访问默认受限;A 主机通过 SSH 隧道进入靶场。
因此上述两条临时规则后来被精确删除,而不是清空整个防火墙:
sudo mkdir -p /root/pentest-lab-backup
sudo iptables-save > /root/pentest-lab-backup/before-isolation.rules
sudo iptables -D FORWARD \
-i br-0c45b8201d1f -o enp0s31f6 \
-s 172.18.0.2/32 -d 192.168.10.3/32 \
-p tcp --dport 7892 -j ACCEPT
sudo iptables -D FORWARD \
-i enp0s31f6 -o br-0c45b8201d1f \
-d 172.18.0.2/32 \
-m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo iptables -L FORWARD -n -v --line-numbers
本次实际执行后,FORWARD 显示:
Chain FORWARD (policy DROP ...)
num pkts bytes target prot opt in out source destination
即默认 DROP,无显式规则。此状态在当前环境下使测试的容器出站请求超时,但不能简单等同于完整隔离;也不能不经评估就把这种缺失 Docker 转发跳转的状态推广到其他 Docker 主机。
7.4 UFW 与 Docker 的关系:不能未经证实就归因
本次检查中 ufw 服务显示 active,iptables 使用 nf_tables 后端;Docker 的部分过滤链存在但未挂接。现有日志和规则不足以确定是谁删除或覆盖了 Docker 转发规则。不能断言 UFW 就是根因。后续若追查,应结合 UFW 配置、nftables 规则集、Docker 日志和重启时间线取证;但这不是完成靶场部署的前置条件。
八、验证靶场主动访问受限
使用 nsenter -n 调用宿主机 curl,进入目标容器网络命名空间,不依赖容器内是否安装 curl:
for c in lab-juice-shop dvwa-dvwa-1 lab-webgoat crapi-web; do
echo "===== $c ====="
PID=$(sudo docker inspect -f '{{.State.Pid}}' "$c")
for target in http://1.1.1.1 http://192.168.18.1 http://192.168.10.3:7892; do
echo "目标: $target"
sudo nsenter -t "$PID" -n \
curl --noproxy '*' -sS --connect-timeout 2 --max-time 4 \
-o /dev/null -w 'HTTP=%{http_code} TCP=%{time_connect}s\n' \
"$target" || echo '连接失败'
done
done
本次实测:四个靶场对以上三个目标均返回 curl (28) Connection timed out、HTTP=000。说明这些指定地址与端口在测试时不可达。特别注意:SOCKS5 端口不是 HTTP 服务,HTTP 协议错误与 TCP 连接失败不是一回事;本次观察到的是连接超时。
不能过度解读:这些结果并未验证所有目的端口、容器到 B 宿主机的 INPUT 路径、IPv6、DNS、跨网桥路径和重启后的策略。隔离目前属于初步验证通过,未完成正式安全审计。
九、crAPI 内部通信:四次 FAIL 到四次 PASS
9.1 第一次测试为什么全部失败?
最初采用以下思路:
PID=$(sudo docker inspect -f '{{.State.Pid}}' crapi-identity)
sudo nsenter -t "$PID" -n timeout 3 bash -c 'echo > /dev/tcp/postgresdb/5432'
结果 crAPI 四项测试全部 FAIL。但各容器多数显示 healthy,数据库及应用端口也在监听。
原因是:nsenter -n 仅切换网络命名空间,不会自动切换 /etc/resolv.conf 等文件系统环境。宿主机 Bash 无法保证能解析 Docker 内部的 postgresdb、mongodb 等容器名称。这类 FAIL 不能直接认定 TCP 网络故障。
9.2 改用实测容器 IP 测试
check_tcp() {
local source="$1" target_ip="$2" port="$3" pid
pid=$(sudo docker inspect -f '{{.State.Pid}}' "$source")
printf '%-22s -> %-16s : %-5s ' "$source" "$target_ip" "$port"
if sudo nsenter -t "$pid" -n timeout 3 bash -c \
"echo > /dev/tcp/$target_ip/$port" 2>/dev/null; then
echo PASS
else
echo FAIL
fi
}
check_tcp crapi-identity 172.21.0.2 5432
check_tcp crapi-community 172.21.0.6 27017
check_tcp crapi-web 172.21.0.7 8080
check_tcp crapi-web 172.21.0.8 8087
实测输出:
crapi-identity -> 172.21.0.2 : 5432 PASS
crapi-community -> 172.21.0.6 : 27017 PASS
crapi-web -> 172.21.0.7 : 8080 PASS
crapi-web -> 172.21.0.8 : 8087 PASS
这证明四条核心 TCP 路径畅通。它们位于同一 Docker 网桥中;本次 FORWARD 默认 DROP 并没有阻断这些实际测试的连接。但TCP 可连接不等于完整业务链路和数据库读写已验证。
十、最终验收结果与未完成事项
| 项目 | 本次结论 | 证据 |
|---|---|---|
| Juice Shop 部署 | 已完成基础部署 | HTTP 200;A 侧隧道 HTTP 200 |
| DVWA 部署 | 已完成基础部署 | HTTP 302;A 侧隧道 HTTP 302 |
| WebGoat 部署 | 已完成基础部署 | /WebGoat 最终 HTTP 200;A 侧 HTTP 302 |
| crAPI 部署 | 已完成基础部署 | Web HTTP 200;四条核心 TCP PASS |
| A → B 访问 | 已验证 | SSH 隧道四个入口均可达 |
| 出站限制 | 初步验证 | 四个容器对三个指定目标均超时 |
| 原有服务 | 监听正常 | 22、3389、443、8443 均在监听 |
| WebGoat 健康检查 | 待修正 | curl: not found |
| 全面网络隔离审计 | 未完成 | 宿主机 INPUT、IPv6、其他端口/路径未完整覆盖 |
| 防火墙规则持久化 | 未验证 | 未做重启后测试 |
| crAPI 全功能业务测试 | 未完成 | 仅核心 TCP 和 Web 入口验证 |
| A 主机 AI 测试平台 | 下一阶段 | 尚未部署 Ollama/Qwen3 8B 测试控制器 |
后续安全收尾建议
- 检查容器能否访问 B 宿主机的敏感端口和服务,必要时制定独立的容器到宿主机访问控制。
- 审计 IPv6 与容器 DNS 出口;检查 Docker 网桥及其他容器网络的实际策略。
- 评估 Docker 转发规则缺失的原因,明确重启 Docker/UFW/主机后的行为;实施持久化前准备回滚方案。
- 修正 WebGoat 的健康检查,但不要为了消除
unhealthy标签而贸然重建正常运行的靶场。 - 进行 crAPI 注册、登录、API 调用和数据库实际读写验证;对其他三个靶场执行最小业务冒烟测试。
- 为 SSH 隧道增加密钥、权限和运行状态管理;不要把漏洞靶场直接映射到公网。
十一、下一阶段:AI 自动化安全测试平台
计划在 A 主机部署:
Ollama + Qwen3 8B(CPU 推理)
|
v
Python 测试编排程序
|-- HTTP/API 基础检测
|-- Nmap 授权服务识别
|-- OWASP ZAP 被动扫描
|-- 证据与日志归档
v
本地 AI 分析、分类与 Markdown/HTML 报告
|
v
SSH 隧道 -> B 主机四个漏洞靶场
第一版建议采用白名单目标、只读/被动检测优先、主动测试人工审批的模式,不允许模型任意执行系统命令,也不把漏洞靶场扫描任务扩展到非授权网络。
十二、总结
这次实战的重点不只是“把四个 Docker 容器跑起来”,而是把漏洞靶场部署、最小暴露面、SSH 隧道访问、网络排障和验证证据串成完整流程。
最有价值的经验有四条:
- 代理用于解决资源访问,不代表被测靶场需要代理上网。
- HTTP 入口正常与 Docker 健康检查 healthy 是两个不同概念。 WebGoat 的
curl: not found就是典型案例。 - 进入网络命名空间不等于进入容器全部运行环境。 用宿主机 DNS 解析 Docker 容器名可能得到假阴性。
- 默认 FORWARD DROP 不等于完整网络隔离。 既要验证容器间业务,也要考虑宿主机访问、IPv6、规则持久化与既有服务保护。
截至本次记录,四个靶场的基础部署和 A → B 访问已经完成,可以开始 AI 自动化测试平台建设;网络隔离的全面安全验收仍需后续补齐。
关键词:Ubuntu 26.04、Docker、Juice Shop、DVWA、WebGoat、crAPI、iptables、FORWARD、SSH Tunnel、网络隔离、OWASP、AI 安全测试

浙公网安备 33010602011771号