手把手教你搭建GZCTF比赛平台+动态靶机部署,超详细教程,看这一篇就够了
GZCTF平台搭建
GZCTF是一个开源平台,我们可以去github上面下载平台源码
https://github.com/GZTimeWalker/GZCTF
然后官方也给了详细的搭建教程
https://gzctf.gzti.me/zh/guide/start/quick-start
这里我就记录一下搭建过程,供后来的师傅参考
一、安装docker(CentOS)
1.卸载旧版本docker
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
2.安装依赖包
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
3.添加Docker官方仓库
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
4.安装Docker引擎
sudo yum install docker-ce docker-ce-cli containerd.io
5.启动Docker服务
sudo systemctl start docker
sudo systemctl enable docker
6.验证安装
sudo docker --version
sudo docker run hello-world

出现图中所示version即代表docker配置成功
7.配置国内源(镜像加速)
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://docker.mirrors.ustc.edu.cn",
"https://hub-mirror.c.163.com"
]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
换源后我们查看docker信息,验证是否成功换源
docker info

至此,我们就完成了docker的搭建
二、搭建平台
前往官网下载源码(release)
解压后修改目录名,放在服务器的目录中

然后针对我们自己的需要修改配置文件。
appsettings.json修改如下
{
"AllowedHosts": "*",
"ConnectionStrings": {
"Database": "Host=db:5432;Database=gzctf;Username=postgres;Password=Admin1234." //Password是你自己的数据库密码
},
"EmailConfig": {
"SendMailAddress": "a@a.com",
"UserName": "",
"Password": "",
"Smtp": {
"Host": "localhost",
"Port": 587
}
},
"XorKey": "Admin1234.", //同样的,你自己的密码
"ContainerProvider": {
"Type": "Docker", // or "Kubernetes"
"PortMappingType": "Default", // or "PlatformProxy"
"EnableTrafficCapture": false,
"PublicEntry": "你自己的服务器ip", // or "xxx.xxx.xxx.xxx"
// optional
"DockerConfig": {
"SwarmMode": false,
"Uri": "unix:///var/run/docker.sock"
}
},
"RequestLogging": false,
"DisableRateLimit": true,
"RegistryConfig": {
"UserName": "",
"Password": "",
"ServerAddress": ""
},
"CaptchaConfig": {
"Provider": "None", // or "CloudflareTurnstile" or "GoogleRecaptcha"
"SiteKey": "<Your SITE_KEY>",
"SecretKey": "<Your SECRET_KEY>",
// optional
"GoogleRecaptcha": {
"VerifyAPIAddress": "https://www.recaptcha.net/recaptcha/api/siteverify",
"RecaptchaThreshold": "0.5"
}
},
"ForwardedOptions": {
"ForwardedHeaders": 5,
"ForwardLimit": 1,
"TrustedNetworks": ["192.168.12.0/8"]
}
}
docker-compose.yml修改如下
version: "3.0"
services:
gzctf:
image: gztime/gzctf:latest #gzctf镜像源
restart: always
environment:
- "GZCTF_ADMIN_PASSWORD=Admin1234." #平台管理员账户密码,修改为你自己的
# choose your backend language `en_US` / `zh_CN` / `ja_JP`
- "LC_ALL=zh_CN.UTF-8"
ports:
- "80:8080"
volumes:
- "./data/files:/app/files"
- "./appsettings.json:/app/appsettings.json:ro"
# - "./kube-config.yaml:/app/kube-config.yaml:ro" # this is required for k8s deployment
- "/var/run/docker.sock:/var/run/docker.sock" # this is required for docker deployment
depends_on:
- db
db:
image: postgres:alpine #postgres镜像源
restart: always
environment:
- "POSTGRES_PASSWORD=Admin1234." #数据库密码
volumes:
- "./data/db:/var/lib/postgresql/data"
然后就可以在当前目录中
docker compose up -d
启动GZCTF,随后我们就可以通过80端口访问平台了。
可能遇到的问题
1.docker compose up时网络超时
大概率是gzctf或者postgres的镜像源访问不到,导致网络超时,更换其他镜像源加速即可。
2.端口占用
报错如下:
Error response from daemon: driver failed programming external connectivity on endpoint gzctf-gzctf-1 (d22fc1ae61f51d4aa3265a4e0060bb4244b2ca9861d263proxy: listen tcp4 0.0.0.0:80: bind: address already in use
这里可以看到我们的80端口被占用了。两种解决方法
1.查找占用80端口占用的程序,把他停掉
netstat -tunlp | grep :80

可以看到httpd正在占用80端口,把他停掉
systemctl stop httpd
重新查看端口情况

80端口占用情况已解决,重新compose up即可。
2.更换平台搭建端口
修改docker-compose.yml文件中
ports:
- "80:8080"
将80改为其他未占用端口,重新compose up,搭建完成后访问对应端口即可。
搭建成功示例


顺便吐槽一句,GZCTF真的比A1CTF好搭多了,A1虽然很帅,但是搭建教程几乎为零,配置环境和文件也很麻烦,而GZCTF只需要一个docker就够了。
动态靶机&动态flag
作为一名合格的web手,出题肯定是要出动态环境的。作为一名初探出题的小萌新,我也是刚刚学会了如何实现动态靶机和动态flag。这里也是记录一下,一方面防止自己忘了,另一方面为后继web出题的师傅提供一个有力可参考的教程。
首先我们要明白docker容器和镜像的含义和区别
docker容器&docker镜像
Docker 镜像
作用
- 只读的模板
Docker 镜像是一个静态的、只读的文件包。它包含了运行某个软件所需的一切:代码、运行时环境、系统工具、系统库和设置。你可以把它想象成一个面向对象的“类”(Class),或者一个安装程序的“.iso”文件。 - 创建容器的基础
镜像的唯一目的就是用于创建容器。一个镜像可以创建出多个相互独立、互不干扰的容器实例。 - 分层结构与共享
镜像采用联合文件系统(UnionFS),由一系列只读层组成。每一层代表 Dockerfile 中的一条指令。这种分层结构带来了巨大优势:- 共享性:不同的镜像可以共享相同的基础层(例如,Ubuntu 基础层)。当你拉取一个基于 Ubuntu 的新镜像时,如果你的系统里已经有 Ubuntu 层,就无需重复下载,节省了磁盘空间和网络带宽。
- 高效性:构建新镜像时,只需添加或修改变化的层,而不需要重建整个文件系统。
- 版本控制与分发
镜像可以被版本化、存储和分发。你可以使用docker commit来创建新镜像,也可以用docker push将镜像上传到镜像仓库(如 Docker Hub),其他人可以通过docker pull下载并使用。这保证了环境的一致性——在任何地方运行的同一个镜像,其内部内容都是完全相同的。
简单比喻:
Docker 镜像就像是房屋的蓝图(Blueprint)或者软件的安装光盘。 蓝图本身不能住人,但它包含了建造房屋所需的所有信息和规格。
Docker 容器
作用
-
镜像的运行实例
容器是镜像的一个动态的、可运行的实例。当你执行docker run命令时,Docker 会从镜像创建一个容器。继续上面的比喻,如果镜像是蓝图,那么容器就是根据蓝图建造出来的、可以实际入住的房子。 -
隔离的进程
一个容器代表一个独立的、轻量级的运行时环境。它包含:- 一个独立的进程空间:容器内通常运行一个主进程。
- 一个独立的文件系统:基于镜像提供,但额外有一个可写的薄层。
- 一个独立的网络配置:拥有自己的 IP 地址、端口映射等。
- 一个独立的资源限制:可以限制其 CPU、内存的使用。
这些隔离性是通过 Linux 的命名空间(Namespaces)和控制组(Cgroups)技术实现的。
-
可写层
当容器启动时,Docker 会在镜像的只读层之上添加一个薄薄的可写层。所有对运行中容器的修改(如创建新文件、修改现有文件、安装新软件)都发生在这个可写层中。这使得容器变得“动态”。 -
应用的生命周期
容器是应用真正运行的地方。你可以启动、停止、重启或删除容器。它的状态是瞬时的(ephemeral),默认情况下,当容器被删除时,其可写层中的数据也会一并丢失。
简单比喻:
Docker 容器就是根据蓝图(镜像)建造并正在运行的房子(Running Instance)。 你可以在房子里活动(运行应用),添置家具(修改文件),但房子的基本结构(镜像)是不变的。
核心区别与关系总结
| 特性 | Docker 镜像 | Docker 容器 |
|---|---|---|
| 本质 | 静态的、只读的模板 | 动态的、可运行的实例 |
| 状态 | 不可变 | 可变(通过可写层) |
| 存储 | 一系列只读的层 | 镜像的只读层 + 一个可写层 |
| 创建方式 | 通过 Dockerfile 使用 docker build 构建 |
通过 docker run 从镜像创建 |
| 生命周期 | 无状态,除非被更新或删除 | 有状态,可以被启动、停止、重启、删除 |
| 数量关系 | 一个镜像可以创建多个容器 | 一个容器基于一个镜像 |
| 类比 | 软件的安装程序(.exe/.iso) 或 房屋的蓝图 | 正在运行的软件进程 或 建好并入住的房子 |
它们之间的关系流程
- 构建镜像:开发者编写
Dockerfile,使用docker build命令构建出一个镜像。这个镜像被存储在本地或推送到远程仓库。 - 运行容器:用户或运维人员使用
docker run命令,指定一个镜像来创建并启动一个容器。 - 容器运行:在容器运行期间,所有数据修改都发生在容器自己的可写层。
- 持久化数据:如果需要数据持久化,可以使用 Docker 卷(Volumes)或绑定挂载(Bind Mounts),将数据存储在宿主机上,而不是容器的可写层。
- 停止与删除:当容器完成任务后,可以被停止和删除。删除容器时,其可写层也会被清除,但底层的镜像保持不变,随时可以用来创建新的、干净的容器。
所以我们出题的思路:配置题目环境,制作为镜像文件,放到平台制作容器,完毕。
看起来很简单是不是,其实也是比较简单的,只不过之前捋不清什么是容器,什么是镜像,分别有什么作用。当我们理解了他们,思路就清晰了。下面我们就根据这个思路,一步一步实现动态靶机。
配置题目环境
这里给大家推荐一个github上面的项目,存放着各种docker模板,可以根据需要自行修改使用
https://github.com/CTF-Archives/ctf-docker-template
我们以web-nginx-php73为例,看一下模板中各文件到底实现了什么功能
config/nginx.conf
这个文件放在哪、叫什么名字都没有关系,只要后缀是conf,内容符合环境需要即可。在dockerfile中会手动引用。(比如在另一道题目中这个文件就叫做default.conf)
# daemon off;
worker_processes auto;
events { # 定义每个工作进程可以处理的最大并发连接数为1024
worker_connections 1024;
}
http { # 基础http设置
include /etc/nginx/mime.types; # 引入MIME类型定义文件
default_type application/octet-stream; # 默认Content-Type
sendfile on; # 启用高效文件传输
keepalive_timeout 65; # 保持连接超时时间65秒
server { # 虚拟主机配置
listen 80; # 监听80端口
server_name localhost; # 服务器名:localhost
root /var/www/html; # 网站根目录:/var/www/html
index index.php index.html index.htm; # 默认索引文件顺序:先找php,再找html文件
location / { # 根路径处理
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ { # php处理
try_files $uri =404;
fastcgi_pass 127.0.0.1:9000; # 将php请求转发给PHP-FPM处理(监听在9000端口)
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; # 告诉PHP-FPM要执行的脚本完整路径
}
}
}
#整体工作流程
# 用户访问 http://localhost/about.php
# Nginx 接收请求,匹配到 PHP location
# Nginx 将请求转发给 127.0.0.1:9000 的 PHP-FPM
# PHP-FPM 执行 /var/www/html/about.php
# PHP-FPM 返回执行结果给 Nginx
# Nginx 将结果返回给用户
service/docker-entrypoint.sh
同样的,这个文件的路径、名称均非固定,只要后缀是sh,内容符合环境需要即可。在另一道题目中,这个文件和init.sh作用相似。
#!/bin/sh
rm -f /docker-entrypoint.sh # 删除自身脚本,防止选手通过查看入口点脚本获取解题线索
# Get the user
user=$(ls /home)
# Check the environment variables for the flag and assign to INSERT_FLAG
# 需要注意,以下语句会将FLAG相关传递变量进行覆盖,如果需要,请注意修改相关操作
if [ "$DASFLAG" ]; then
INSERT_FLAG="$DASFLAG"
export DASFLAG=no_FLAG
DASFLAG=no_FLAG
elif [ "$FLAG" ]; then
INSERT_FLAG="$FLAG"
export FLAG=no_FLAG
FLAG=no_FLAG
elif [ "$GZCTF_FLAG" ]; then
INSERT_FLAG="$GZCTF_FLAG"
export GZCTF_FLAG=no_FLAG
GZCTF_FLAG=no_FLAG
else
INSERT_FLAG="flag{TEST_Dynamic_FLAG}"
fi # 检查多种常见的CTF平台环境变量名,将动态传入的flag保存到变量中,然后立即清空环境变量,防止通过环境变量泄露flag。如果没有传入flag,使用测试flag
# 将FLAG写入文件 请根据需要修改
echo $INSERT_FLAG | tee /flag
chmod 744 /flag
php-fpm & nginx & # 启动PHP-FPM和Nginx服务(在后台运行)
echo "Running..."
tail -F /var/log/nginx/access.log /var/log/nginx/error.log # 日志监控,在前台持续输出Nginx日志,保持容器运行
Dockerfile
这个名字通常不变
FROM php:7.3-fpm-alpine
# 制作者信息
LABEL auther_template="CTF-Archives"
# 安装必要的软件包
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories &&\
apk add --update --no-cache nginx bash # 这里的repositories也可手动更换
# 拷贝容器入口点脚本
# 可用COPY,可用ADD
# 这里就是前面说文件路径和名称不固定的原因,路径和名称在这里保持一致即可。
COPY ./service/docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
# 复制nginx配置文件
COPY ./config/nginx.conf /etc/nginx/nginx.conf
# 复制web项目源码
COPY src /var/www/html
# 重新设置源码路径的用户所有权
RUN chown -R www-data:www-data /var/www/html
# 设置shell的工作目录
WORKDIR /var/www/html
EXPOSE 80
# 设置nginx日志保存目录
VOLUME ["/var/log/nginx"]
# 设置容器入口点
ENTRYPOINT [ "/docker-entrypoint.sh" ]
docker/docker-compose.yaml
这个文件在出题时通常是用不到的,他一般用于多容器协同,具体作用我也不太清楚,待我再沉淀沉淀
src/
存放题目源码的位置
综上,在这些配置文件中我们需要修改的文件很少,基本只要修改题目源码即可。
当我们修改好源码,出好签到题之后(最好真的是签到题),就可以制作镜像文件了。
在此之前,我们需要一个自己的镜像仓库来存放镜像文件。
配置镜像仓库
市面上普遍推荐dockerhub,但是我换了各种源依旧ping不通,也无法login,无奈只好换国内镜像仓库。
因为我的服务器是阿里云的,所以我这里推荐阿里云镜像仓库,
https://cr.console.aliyun.com/cn-hangzhou/instances
具体使用也很简单,我们创建一个个人实例,选择本地仓库(划重点),然后创建镜像仓库,仓库类型设为公开(划重点!!否则容器无法创建)

进入镜像仓库管理,根据操作指南进行操作即可。这里选取我们需要用到的几条命令进行说明
从Registry中拉取镜像
docker pull 你的镜像仓库地址:[镜像版本号]
将镜像推送到Registry
docker login --username=你的阿里云镜像仓库账户 你的镜像仓库链接
docker tag [ImageId] 你的镜像仓库地址:[镜像版本号]
docker push 你的镜像仓库地址:[镜像版本号]
我们需要做的,就是在本地制作镜像,然后push到镜像仓库中。
制作本地镜像
在dockerfile目录中,执行
docker build -t 镜像名 .

等待镜像创建完成,我们执行
docker images

找到刚才制作的镜像id,执行
docker login --username=你的阿里云镜像仓库账户 你的镜像仓库链接
docker tag [ImageId] 你的镜像仓库地址:[镜像版本号]
docker push 你的镜像仓库地址:[镜像版本号]

刷新镜像仓库页面,就可以看到镜像已经成功上传了

实现动态靶机
到目前为止题目镜像已经全部配置完毕,我们可以开始上题了
来到题目管理,新增题目,选择动态靶机,在容器镜像一栏中填写
镜像地址:版本号

点击创建测试容器

如果你也像图中一样显示实例已创建,那么恭喜你!你已经完全学会如何搭建一个CTF平台,并在平台上部署动态靶机了!
无法创建测试容器
当我们在容器镜像一栏中填写远程镜像仓库地址时,此时点击创建测试容器,可能会出现弹窗:服务器内部错误,无法创建容器。经过测试,想要解决这个问题也很简单:
1.在搭建平台的服务器上创建镜像,直接使用本地镜像
2.在搭建平台的服务器上把镜像仓库中的镜像pull到本地使用
可能会有朋友觉得第二种方法多此一举:在本地创建镜像,push到远程仓库,然后再pull到本地?
其实有的师傅的服务器可能会出现无法在本地创建镜像的问题,就比如我们学校的服务器,执行docker build -t name .就是会报错,就是无法创建镜像。到处搜、到处找办法解决也没招,最后只能在我自己的服务器上创建镜像,push到我的远程仓库,然后再在学校服务器上把我远程仓库的镜像pull下来,算是一个中转吧。
具体原因不清楚,至少能用了

浙公网安备 33010602011771号