CTFd+CTFd_Whale_1.0
上次搭建 CTFd 还是好久好久之前(2 年叭),前阵子由于院校比赛需要又捡起来了,这次顺便将动态靶场搭建(Whale)和动态 Flag 都搞上,经过漫长的百度流程,还是总结出了蛮多东西的。
1 引言
之前有按照网上的文章从零开始搭建 CTFd + Whale,经过了两天的努力侥幸成功了两次(淦),其它都会出现动态靶场出现地址,但是 CTFd 不会拉取镜像的问题(QAQ)。
本来已经放弃,又看到 VaalaCat 师傅的文章可以一键启动,尝试了两遍没问题,写一份文档备用。
VaalaCat 师傅文章地址:传送门
Linux 系统版本:Ubuntu 18.04(众多师傅的反馈发现 Ubuntu 20 会出现不可预测的 bug,我之前也是用 Ubuntu 20.04)
2 Ubuntu 靶机安装
VM 安装过程略。
登录后修改下 root 密码:
sudo passwd root
进行 SSH 安装,目前靶机用不到 VMware Tools:
apt install -y ssh
因为要后续要进行多个软件安装,更新下 apt 源:
apt install -y vim
vim /etc/apt/sources.list
## aliyun
deb https://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse
deb-src https://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse
deb https://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse
deb-src https://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse
deb https://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse
deb-src https://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse
deb https://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse
deb-src https://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse
更新 apt 源:
apt-get update -y
由于需要 curl 下载文件,安装一下:
apt-get install -y curl
3 Docker 环境安装
由于动态靶机是使用 docker 实现的,所以首先要准备安装一下 docker(这一步耗时较长):
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
然后还要准备 docker-compose(建议使用 proxychains 代理下载):
apt-get install -y python3-pip
pip3 install docker-compose -i https://pypi.tuna.tsinghua.edu.cn/simple
这时会出现报错信息:
Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-build-5ydi0mj7/cryptography/
百度了一下,需要更新 pip:
python3 -m pip install --upgrade --force pip
再次安装 docker-compose 即可。
4 Docker 集群配置
Whale 插件使用的是集群的模式,所以这里要创建一个单个服务器的集群:
docker swarm init
然后将这个服务器加入集群:
docker node update --label-add='name=linux-1' $(docker node ls -q)
最后要准备的是给 docker 更换镜像源,不然会很龟速,修改文件 /etc/docker/daemon.json:
{
"registry-mirrors": ["https://y5u7p3c7.mirror.aliyuncs.com"]
}
重启 docker 生效:
systemctl daemon-reload
systemctl restart docker
5 CTFd + Whale 安装
使用 VaalaCat 师傅整合好的 ctfd:
git clone https://github.com/Un1kTeam/CTFd --depth=1
修改 frp 配置文件(看自己需求,不变就不动):
## frps.ini
[common]
bind_port = 7000
vhost_http_port = 9123
token = your_token
subdomain_host = node.vaala.ink
## frpc.ini
[common]
token = your_token
server_addr = 172.1.0.3
server_port = 7000
admin_addr = 172.1.0.4
admin_port = 7400
插件初始化、更新或检查子模块:
git submodule update --init
启动 CTFd(巨慢,我用了半小时):
docker-compose up -d
查看下 5 个容器是否正常运行:
docker ps -a
root@docker:/opt/CTFd# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
47daf4dfd0b4 glzjin/frp:latest "/usr/local/bin/frpc…" 3 minutes ago Up 3 minutes ctfd_frpc_1
07dc9cfaab10 ctfd_ctfd "/opt/CTFd/docker-en…" 3 minutes ago Up 3 minutes 0.0.0.0:9124->8000/tcp, :::9124->8000/tcp ctfd_ctfd_1
501401fb07ff mariadb:10.4.12 "docker-entrypoint.s…" 3 minutes ago Up 3 minutes ctfd_db_1
39163c2ad93e glzjin/frp "/usr/local/bin/frps…" 3 minutes ago Up 3 minutes 0.0.0.0:9123->9123/tcp, :::9123->9123/tcp, 0.0.0.0:9125-9129->9125-9129/tcp, :::9125-9129->9125-9129/tcp ctfd_frps_1
0cc9acac2e4c redis:4 "docker-entrypoint.s…" 3 minutes ago Up 3 minutes ctfd_cache_1
6 Whale 配置
新版 CTFd 的 Whale 和旧版不同,做了板块区分,这里 VaalaCat 师傅已经配好了。
左侧 Docker 菜单栏如果没有特殊需求无需更改,只需更改左侧 Frp 菜单栏中的内容:
只需要填入 Direct IP Address 即可。
最后点一下更新就可以保存配置了,填写完成后新建题目测试是否成功,按下表新建题目,表中没有提到的保持默认就好:
新建题目过后点击启动,然后等待靶机创建,可以在服务器中 docker ps -a 查看是否启动,若启动成功则搭建完成。


7 端口修改
VaalaCat 师傅仓库中所使用的端口一共有三类,需要修改平台信息请按照以下文件位置修改:
9123(一般用不到,都采用直连):
默认 http 模式靶机访问端口,需要修改
frps.ini、后台中的Http Port、docker-compose.yml
9124:
默认为 ctfd 端口,需要修改
docker-compose.yml
9125-9129:
动态靶机 Direct 模式端口,需要修改
docker-compose.yml、后台中的端口范围
8 后记
8.1 Docker Swarm
在一段时间不使用 CTFd 后,使用 docker-compose up -d 会出现如下报错:
root@docker:/opt/CTFd# docker-compose up -d
......
ERROR: This node is not a swarm manager. Use "docker swarm init" or "docker swarm join" to connect this node to swarm and try again.
解决方法如下:
docker swarm leave
docker swarm init
docker node update --label-add='name=linux-1' $(docker node ls -q)
8.2 前端汉化
由于该大佬的 CTFd 版本是 3.5.0,市面上没有完美适配的汉化包,为了避免意外,将前端汉化一下就好:
项目地址:https://github.com/Gu-f/CTFd_chinese_CN
下载完成后,我们只需要 CTFd-3.4.1/CTFd/themes/core 文件夹即可,重命名为 core-cn。
将该文件放到 Ubuntu 的 CTFd/CTFd/themes 即可,在后台进行切换:

8.3 端口开放
由于公司映射的端口比较特殊,且访问量较大,这里直接修改开放端口:
frps:
image: glzjin/frp
restart: always
volumes:
- ./conf/frp:/conf
entrypoint:
- /usr/local/bin/frps
- -c
- /conf/frps.ini
ports:
- 40001-41000:40001-41000 # 映射direct类型题目的端口
- 9123:9123 # 映射http类型题目的端口
networks:
default:
frp_connect:
ipv4_address: 172.1.0.3
8.4 路由删减
默认打开大致会产生几个路由表项:
root@docker:/opt/CTFd# route
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
default _gateway 0.0.0.0 UG 100 0 0 ens32
10.10.8.0 0.0.0.0 255.255.255.0 U 100 0 0 ens32
link-local 0.0.0.0 255.255.0.0 U 1000 0 0 ens32
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker_gwbridge
172.19.0.0 0.0.0.0 255.255.0.0 U 0 0 0 br-04dc3f4f5258
172.20.0.0 0.0.0.0 255.255.0.0 U 0 0 0 br-ac6c0698e9e8
经过简单的尝试,发现若是删除 172.17,就会生成 172.21,所以哪个网段冲突就删除哪个,不影响使用:
route del -net 172.17.0.0 netmask 255.255.0.0
route del -net 172.18.0.0 netmask 255.255.0.0
route del -net 172.19.0.0 netmask 255.255.0.0
route del -net 172.20.0.0 netmask 255.255.0.0
使用 docker-compose up -d 后重新生成:
root@docker:/opt/CTFd# route
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
default _gateway 0.0.0.0 UG 100 0 0 ens32
10.10.8.0 0.0.0.0 255.255.255.0 U 100 0 0 ens32
link-local 0.0.0.0 255.255.0.0 U 1000 0 0 ens32
172.21.0.0 0.0.0.0 255.255.0.0 U 0 0 0 br-202551e5b1ed
172.22.0.0 0.0.0.0 255.255.0.0 U 0 0 0 br-2d92069faa0f
8.5 CTFd Update Del
每次进后台都会看见更新提示,很烦,直接删了。

该文件需要进入 CTFd/CTFd/themes/admin/templates/base.html 进行修改,把下面这段内容删除:
<div class="container-fluid bg-warning text-center py-3">
<div class="row">
<div class="col-md-12">
<a class="btn btn-warning" href="{{ get_config('version_latest') }}">
A new CTFd version is available!
</a>
</div>
</div>
</div>
世界清净了:

8.6 容器题目
建议创建题目时不要直接填镜像 ID,先拉取不然创建很慢。
8.7 开启自启动
创建一个 ctfd.sh 放在根目录,内容如下:
#!/bin/bash
docker-compose -f /opt/CTFd/docker-compose.yml up -d
给个执行权限:
chmod +x /root/ctfd.sh
创建服务单元:
vim /etc/systemd/system/ctfd.service
内容如下:
[Unit]
Description=CTFd Start
Requires=docker.service
After=docker.service
[Service]
Restart=always
ExecStart=/root/ctfd.sh
[Install]
WantedBy=default.target
重载一下:
systemctl daemon-reload ; systemctl enable ctfd.service ; systemctl start ctfd.service
验证一下:
root@docker:~# systemctl start ctfd.service
root@docker:~# systemctl status ctfd.service
● ctfd.service - CTFd Start
Loaded: loaded (/etc/systemd/system/ctfd.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2023-11-09 23:57:35 CST; 212ms ago
Main PID: 9098 (ctfd.sh)
Tasks: 2 (limit: 2281)
CGroup: /system.slice/ctfd.service
├─9098 /bin/bash /root/ctfd.sh
└─9099 /usr/bin/python3 /usr/local/bin/docker-compose -f /opt/CTFd/docker-compose.yml up -d
11月 09 23:57:35 docker systemd[1]: Started CTFd Start.
11月 09 23:57:35 docker ctfd.sh[9098]: /usr/local/lib/python3.6/dist-packages/paramiko/transport.py:32: CryptographyDeprecationWarning: Python 3.6 is no longer supported by the Python core team. Therefore
11月 09 23:57:35 docker ctfd.sh[9098]: from cryptography.hazmat.backends import default_backend
11月 09 23:57:35 docker ctfd.sh[9098]: The Docker Engine you're using is running in swarm mode.
11月 09 23:57:35 docker ctfd.sh[9098]: Compose does not use swarm mode to deploy services to multiple nodes in a swarm. All containers will be scheduled on the current node.
11月 09 23:57:35 docker ctfd.sh[9098]: To deploy your application across the swarm, use `docker stack deploy`.
8.8 Web 动态链接
CTFd_Whale 插件默认情况下是没有动态链接的,非常难受,这里直接修改文件。
文件路径:/opt/CTFd/CTFd/plugins/ctfd-whale/assets/view.js
原文内容:
'<div class="card" style="width: 100%;">' +
'<div class="card-body">' +
'<h5 class="card-title">Instance Info</h5>' +
'<h6 class="card-subtitle mb-2 text-muted" id="whale-challenge-count-down">Remaining Time: ' + response.remaining_time + 's</h6>' +
'<h6 class="card-subtitle mb-2 text-muted">Lan Domain: ' + response.lan_domain + '</h6>' +
'<p class="card-text">' + response.user_access + '</p>' +
'<button type="button" class="btn btn-danger card-link" id="whale-button-destroy" onclick="CTFd._internal.challenge.destroy()">Destroy this instance</button>' +
'<button type="button" class="btn btn-success card-link" id="whale-button-renew" onclick="CTFd._internal.challenge.renew()">Renew this instance</button>' +
'</div>' +
'</div>'
现修改为:
'<div class="card" style="width: 100%;">' +
'<div class="card-body">' +
'<h5 class="card-title">Instance Info</h5>' +
'<h6 class="card-subtitle mb-2 text-muted" id="whale-challenge-count-down">Remaining Time: ' + response.remaining_time + 's</h6>' +
'<p class="card-text">' + '<a target="_blank" href="http://' + response.user_access + '">' + 'http://' + response.user_access + '</a>' + '</p>' +
'<button type="button" class="btn btn-danger card-link" id="whale-button-destroy" onclick="CTFd._internal.challenge.destroy()">Destroy this instance</button>' +
'<button type="button" class="btn btn-success card-link" id="whale-button-renew" onclick="CTFd._internal.challenge.renew()">Renew this instance</button>' +
'</div>' +
'</div>'
如果是比较新的版本,那修改成如下的样子:
$("#user-access").html(
'<a target="_blank" href="http://' + response.user_access + '">' + "http://" + response.user_access + "</a>"
8.9 CTFd 用户批量导入
最近也是有个比赛,组队人员也是比较多的,百度了个脚本改了改:
import requests
import json
url = 'url'
Cookie = input('Input Session:')
CSRF = input('Input CSRF-Token:')
if not Cookie:
Cookie = 'Session'
if not CSRF:
CSRF = 'CSRF-Token'
headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
'Cookie': Cookie,
'Referer': url + '/admin/users/new',
'CSRF-Token': CSRF
}
user = []
passwd = []
file = open('user.txt', 'r', encoding="utf-8")
file1 = open('passwd.txt', 'r', encoding="utf-8")
for i in file.readlines():
user.append(i[:-1])
for j in file1.readlines():
passwd.append(j[:-1])
for k in range(len(user)):
data = {
"name": user[k],
"email": str(k) + "1@qq.com",
"password": passwd[k],
"type": "user",
"verified": "false",
"hidden": "false",
"banned": "false"
}
response = requests.post(
url = url + "/api/v1/users",
headers=headers,
data=json.dumps(data)
)
if "true" in response.text:
print("User:" + user[k] + " -- Pass:" + passwd[k] + " -- Email:" + str(k) + "1@qq.com")
else:
print("用户创建失败!")
file.close()
file1.close()