利用Docker搭建CTF比賽動態靶機
"我的心田本空無一物,你來之后萬物生長,你走之后,一片荒蕪" --《隱入塵煙》
一、前言
最近有個朋友準備搭建一個CTF比賽靶場在學校搞一個CTF比賽,讓我幫他參考參考。說實話,自己也沒有整過,自己就先實驗唄,搜集了大量的資料最終是搞成了,就差朋友買個服務器,和域名掛上了,這期間經歷過了大量的報錯,因此,想記錄下來,為兄弟們指一條明道,避免出錯。另外本篇文章大部分是依賴網上這位兄弟的文章,因此我把原文章鏈接掛出來,
感謝這位兄弟:
https://www.yuque.com/docs/share/364ef08c-9405-45fe-b269-e9236de57242?#pBjVN,
自己這樣寫主要是整理出來好查詢,在加上自己遇到的問題和解決方案,以及漢化,題目發放的疑惑,值得寫一下。
二、環境準備
- 主機:服務器版本的CentOs7
- 鏈接:http://centos.nethub.com.hk/7.9.2009/isos/x86_64/CentOS-7-x86_64-Minimal-2009.iso
- Docker版本:20.10.2
- Docker-compose版本:1.25.0
- IP地址:公網地址或虛擬機地址
三、環境搭建
1.系統環境搭建配置:如果是阿里云服務器,直接更新yum源:
yum update
2.如果是虛擬機不能直接更新yum源的話就換阿里源
centos 7 換阿里源方法:
- 先下載 wget :一定要確保有wget
yum install wget
- 阿里源:
wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo yum clean all yum makecache yum update
注:一定要運行yum update 確保軟件的更新和下載
3.換源之后把openssh-server 進行安裝,主要是為了方便命令的輸入,和文件的傳輸
- 運行:
yum install openssh-server
- 下載vim 編輯工具:
yum install vim
- 配置/etc/sshd/sshd_config 不然xshell等工具不能連接,改過之后如圖所示:

- 用xshell連接就能復制命令,方便下面的操作:

4.安裝系統環境所需服務,主要是python,數據庫 運行完就ok了。
- 運行:
yum install -y git nginx mariadb mariadb-server Mysql-python python-pip gcc python-devel yum- utils device-mapper-persistent-data lvm2 epel-release

- 安裝docker之前換源
- 命令:
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
- 選擇合適的版本并安裝

- 安裝17.12.1.ce版本,可能很慢,耐心等待。
- 運行:
yum -y install docker-ce-17.12.1.ce

5.DaoCloud配置docker鏡像源加速
- 運行:
curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://f1361db2.m.daocloud.io
- 查看是否安裝成功并啟動
- 運行:
docker --version

6.安裝docker-compose
# 下載docker compose curl -L https://get.daocloud.io/docker/compose/releases/download/1.25.4/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose # 添加可執行權限 chmod +x /usr/local/bin/docker-compose # 將文件copy到 /usr/bin/目錄下 ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose # 查看版本 docker-compose --version

2.靶場環境
1.下載CTFd
- 運行:
git clone https://github.com/glzjin/CTFd.git
- 下載frp
- 運行:
wget https://github.com/fatedier/frp/releases/download/v0.29.0/frp_0.29.0_linux_amd64.tar.gz
- 下載完成后解壓備用:
tar -zxvf frp_0.29.0_linux_amd64.tar.gz
2.下載ctfd-whale
- 運行:
git clone https://github.com/glzjin/CTFd-Whale.git
- 將下載的CTFd-Whale文件夾重命名為小寫:
mv CTFd-Whale/ ctfd-whale
3.下載docker版本的frps
- 運行:
git clone https://github.com/glzjin/Frp-Docker-For-CTFd-Whale
- 將下載后的Frp-Docker-For-CTFd-Whale也重命名為小寫:
- 運行:
mv Frp-Docker-For-CTFd-Whale/ frp-docker-for-ctfd-whale
- 下載之后截圖:

注:如果不能下載,或者下載緩慢的話,可以掛代理,也可以下載下來在傳到服務器上

3.CTFd環境配置
1.Docker集群設置 初始化docker集群
- 運行:
docker swarm init
如果出現這種情況Cannot connect to the Docker daemon at unix:

- 運行:
systemctl start docker
- 然后再運行初始化命令:
docker swarm init

2.將剛剛初始化的這個集群加入到節點當中
命令執行后,返回的就是節點ID了,暫時不用管這個節點ID
- 運行:
docker node update --label-add='name=linux-1' $(docker node ls -q)
3.ctfd-whale放入CTFd的插件配置
- 將ctfd-whale放入CTFd的插件目錄中
- 運行:
mv ctfd-whale/ CTFd/CTFd/plugins/
- 進入目錄并配置frps.ini文件
- 運行:
cd frp-docker-for-ctfd-whale/frp vim frps.ini

改成圖片所示:一般token不用更改,端口根據自己喜好更改,一般采用默認
4.修改完成后返回目錄啟動
注意:是 frp-docker-for-ctfd-whale目錄
- 運行:
cd .. docker-compose up -d

耐心等待
- 構建完成以后查看是否正在運行
docker ps

5.將frpc文件配置到CTFd中
首先進入CTFd目錄中,新建frpc文件夾
運行:
(1)cd CTFd/ (2) mkdir frpc
- 進入frpc的目錄(frp_0.29.0_linux_amd64)將里面的frpc,frpc.ini,frpc_full.ini,LICENSE這四個文件放在CTFd/frpc文件夾中
cd ../frp_0.29.0_linux_amd64 mv frpc.ini ../CTFd/frpc/ mv frpc_full.ini ../CTFd/frpc/ mv frpc ../CTFd/frpc/ mv LICENSE ../CTFd/frpc/
- 進入剛剛新建的CTFd/fprc目錄,配置frpc.ini文件,直接復制粘貼
[common] token = randomme server_addr = 172.1.0.4 server_port = 6490 pool_count = 200 tls_enable = true admin_addr = 172.1.0.3 admin_port = 7400
注意:這里的token要和frp-docker-for-ctfd-whale/frp/frps.ini的token一樣,前面沒改,這里就是randomme
- 進入CTFd目錄,將下方內容復制到Dockerfile中,直接復制粘貼
FROM python:3.6-alpine RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories &&\ apk update && \ apk add python3 python3-dev linux-headers libffi-dev gcc make musl-dev py-pip mysql-client git openssl-dev g++ RUN adduser -D -u 1001 -s /bin/bash ctfd WORKDIR /opt/CTFd RUN mkdir -p /opt/CTFd /var/log/CTFd /var/uploads RUN pip3 config set global.index-url https://pypi.doubanio.com/simple RUN pip3 config set install.trusted-host pypi.doubanio.com COPY requirements.txt . RUN pip install -r requirements.txt -i https://pypi.doubanio.com/simple COPY . /opt/CTFd RUN for d in CTFd/plugins/*; do \ if [ -f "$d/requirements.txt" ]; then \ pip install -r $d/requirements.txt -i https://pypi.doubanio.com/simple; \ fi; \ done; RUN chmod +x /opt/CTFd/docker-entrypoint.sh RUN chown -R 1001:1001 /opt/CTFd RUN chown -R 1001:1001 /var/log/CTFd /var/uploads USER 1001 EXPOSE 8000 ENTRYPOINT ["/opt/CTFd/docker-entrypoint.sh"]
- 在CTFd目錄配置docker-compose.yml文件,直接復制粘貼
version: '2.2' services: ctfd-nginx: image: nginx:1.17 volumes: - ./nginx/http.conf:/etc/nginx/nginx.conf user: root restart: always ports: - "443:443" networks: default: internal: depends_on: - ctfd cpus: '1.00' mem_limit: 150M ctfd: build: . user: root restart: always ports: - "8000:8000" environment: - UPLOAD_FOLDER=/var/uploads - DATABASE_URL=mysql+pymysql://root:ctfd@db/ctfd - REDIS_URL=redis://cache:6379 - WORKERS=1 - LOG_FOLDER=/var/log/CTFd - ACCESS_LOG=- - ERROR_LOG=- - REVERSE_PROXY=true volumes: - .data/CTFd/logs:/var/log/CTFd - .data/CTFd/uploads:/var/uploads - .:/opt/CTFd:ro - /var/run/docker.sock:/var/run/docker.sock depends_on: - db networks: default: internal: frp: ipv4_address: 172.1.0.2 cpus: '1.00' mem_limit: 450M db: image: mariadb:10.4 restart: always environment: - MYSQL_ROOT_PASSWORD=ctfd - MYSQL_USER=ctfd - MYSQL_PASSWORD=ctfd volumes: - .data/mysql:/var/lib/mysql networks: internal: command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --wait_timeout=28800, --log-warnings=0] cpus: '1.00' mem_limit: 750M cache: image: redis:4 restart: always volumes: - .data/redis:/data networks: internal: cpus: '1.00' mem_limit: 450M frpc: image: glzjin/frp:latest restart: always volumes: - ./frpc:/conf/ entrypoint: - /usr/local/bin/frpc - -c - /conf/frpc.ini networks: frp: ipv4_address: 172.1.0.3 frp-containers: cpus: '1.00' mem_limit: 250M networks: default: internal: internal: true frp: driver: bridge ipam: config: - subnet: 172.1.0.0/16 frp-containers: driver: overlay internal: true ipam: config: - subnet: 172.2.0.0/16
- 配置requirements.txt 這里主要是修改gevent版本號,gevent原本是1.4.0的,這邊經過多次嘗試,不指定版本號才能拉取,直接復制粘貼
Flask==1.1.1 Werkzeug==0.16.0 Flask-SQLAlchemy==2.4.1 Flask-Caching==1.4.0 Flask-Migrate==2.5.2 Flask-Script==2.0.6 SQLAlchemy==1.3.11 SQLAlchemy-Utils==0.36.0 passlib==1.7.2 bcrypt==3.1.7 six==1.13.0 itsdangerous==1.1.0 requests>=2.20.0 PyMySQL==0.9.3 gunicorn==19.9.0 normality==2.0.0 dataset==1.1.2 mistune==0.8.4 netaddr==0.7.19 redis==3.3.11 datafreeze==0.1.0 python-dotenv==0.10.3 flask-restplus==0.13.0 pathlib2==2.3.5 flask-marshmallow==0.10.1 marshmallow-sqlalchemy==0.17.0 boto3==1.10.39 marshmallow==2.20.2 gevent tzlocal==2.1
- 配置nginx,在CTFd的目錄下,新建一個nginx文件夾并進入
mkdir nginx cd nginx
- 在上面創建的nginx目錄下, 創建一個文件http.conf,輸入以下內容,直接復制粘貼
worker_processes 4;
events {
worker_connections 1024;
}
http {
# Configuration containing list of application servers
upstream app_servers {
server ctfd:8000;
}
server {
listen 80;
client_max_body_size 4G;
# Handle Server Sent Events for Notifications
location /events {
proxy_pass http://app_servers;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
# Proxy connections to the application servers
location / {
proxy_pass http://app_servers;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
}
}

6.開始構建,在CTFd目錄下開始構建,運行下面的代碼,耐心等待
docker-compose up -d
拉取完鏡像之后

- 拉取完成后配置docker網絡,先看一下現在的容器狀態
docker ps -a

看到ctfd_frpc_1這個容器的狀態是退出狀態。
- 查看docker的網絡
docker network ls

7.需要將ctfd_frpc_1,frp-docker-for-ctfd-whale_frps_1,ctfd_ctfd_1這三個容器加入到ctfd_frp網絡中
并且這三個容器的IP如下:
ctfd_ctfd_1:172.1.0.2
ctfd_frpc_1:172.1.0.3
frp-docker-for-ctfd-whale_frps_1:172.1.0.4
- 查看一下ctfd_frp網絡
docker network inspect ctfd_frp

我們需要把其他兩個加進去
- 只有ctfd_ctfd_1這個容器是在ctfd_frp網絡里的,此時我們指定IP將其他兩個容器加入ctfd_frp網絡里
docker network connect --ip 172.1.0.3 ctfd_frp ctfd_frpc_1 docker network connect --ip 172.1.0.4 ctfd_frp frp-docker-for-ctfd-whale_frps_1
- 再次查看一下ctfd_frp網絡
- 重啟一下這兩個容器
docker restart ctfd_frpc_1 frp-docker-for-ctfd-whale_frps_1
- 再次查看一下ctfd_frp網絡
docker network inspect ctfd_frp

發現已經可以看到,這個時候,ctfd_frpc_1,frp-docker-for-ctfd-whale_frps_1,ctfd_ctfd_1這三個容器已經加入到ctfd_frp網絡中了
- 再查看一下ctfd_frpc_1容器如果沒有一直重啟就算配好了

- 直接訪問http://你的ip:8000

至此,靶場已經搭建好了
4.網站基礎以及ctfd-whale配置
注:這個主要是配置題目分發時端口開啟情況,因為你做一道題肯定是訪問網站的一個端口
- CTF配置

- 管理員配置

- 樣式配置

- 配置比賽開始時間

- 直接finish 后

- 進入管理員模板配置進行ctfd-whale配置



- 設置方面,現給出下面的參數配置詳情
屬性配置Docker API URLunix://var/run/docker.sockFrp API IPfrpc的ip配置 172.1.0.3Frp API Portfrpc的端口配置 7400Frp Http Domain SuffixDocker API URL to connect(可填None)Frp Http Port80Frp Direct IP Address你的公網ip,本機即為127.0.0.1Frp Direct Minimum Port與之前frps最小端口呼應Frp Direct Minimum Port與之前frps最大端口呼應Max Container Count不超過最大-最小Max Renewal Times最大實例延時次數Frp config template填入frps的配置,只需填[common]Docker Auto Connect Containersctfd_frpc_1Docker Dns Setting可填機器內DNS,沒有可填個外網DNS114.114.114.114Docker Swarm Nodeslinux-1 與前面swarm集群呼應Docker Multi-Container Network Subnet內網題大子網ip配置/CIDRDocker Multi-Container Network Subnet New Prefix每個內網題實例的CIDRDocker Container Timeout單位為秒Docker Auto Connect Networkctfd_frp-containers
- 其中Frp config template配置內容如下,一會直接復制粘貼就行
[common] token = randomme server_addr = 172.1.0.4 server_port = 6490 pool_count = 200 tls_enable = true admin_addr = 172.1.0.3 admin_port = 7400
- 我的配置


5.漢化
- 網上按照教程一頓操作,弄一個,網站癱瘓一次。最后發現漢化版本不一樣,本次CTFd 的版本是2.3.1 因此,直接去github搜一下,果然找到了一個,只是一個半漢化。先湊合著用唄,其實你也可以自己漢化,就是把里面的樣式自己改一下,比如在標題那里英文改成中文。
1.漢化鏈接
https://github.com/cxaqhq/ctfd-2.3.1
漢化包:ctfd-2.3.1-master.zip
下載下來之后,直接把

這兩個替換為

這兩個
2.漢化效果

6.發放題目
注:因為是swam模式本次僅僅測試了dynamic_ docker來添加題目 ,其他形式,請自測
1.添加一道web題目,如圖這樣配置



2.完成之后測試如下
因為拉取鏡像需要時間,請耐性等待,多刷新瀏覽器


4.測試 網址

5.有的人可能就會問了,那我如何獲取其他的題目,接下來就講一講。
- 其實也沒有啥,比如ctftraining/qwb_2019_supersqli 就是拉取了ctftraining的一道題目。我在CTFd源碼里面沒有找到,去github找了一下,發現了這個。
https://github.com/CTFTraining/CTFTraining
- 然后我嘗試拉取其他的題目,比如 qwb_2019_upload 按照格式ctftraining/qwb_2019_upload進行拉取,果然成功了。大約6-8分鐘拉取完一個題目,請耐心等待。


至于其他方式拉取題目我也測試了下,因為是swam模式,只有docker-dynamic這種方式比較簡單。
- standard 模式拉取ctftraining的試題也是可行的
比如我在~目錄下創建一個CTFTraining目錄,git 一道題目
mkdir CTFTraining cd CTFTraining/ git clone https://github.com/CTFTraining/0ctf_2016_unserialize.git cd cd 0ctf_2016_unserialize/
- 然后修改docker-compose.yml

- 修改之后,啟動

- 這個時候你訪問靶機地址:http://你的ip:8302 就能訪問

- 同樣在CTFd中加入這道題目

[是兄弟就來打我](http://192.168.52.164:8302/)
- 按照你自己的需求進行更改

flag 就是docker-compose.yml里面的flag

但是這種方式不太好,你打完這道題之后提交flag ,這道題目依然開著,只能手動關閉
6.附錄:近年ctf writeup大全
https://github.com/ctfs/write-ups-2016 https://github.com/ctfs/write-ups-2015 https://github.com/ctfs/write-ups-2014 fbctf競賽平臺Demo https://github.com/facebook/fbctf ctf Resources https://github.com/ctfs/resources https://github.com/ctfwiki/ctf_game_history https://github.com/SecWiki/ctf-hub https://github.com/le31ei/ctf_challenges
7.問題報錯以及解決辦法
注釋:本人遇到的問題還算比較少,因此報錯以及解決辦法多是搬運他人寫的。
1.frpc日志報錯:

解決方案:
仔細配置網絡,網絡配置的要求如下:
- 將ctfd_frpc_1,frp-docker-for-ctfd-whale_frps_1,ctfd_ctfd_1這三個容器加入到ctfd_frp網絡中
- 這三個容器對應的IP如下:
- ctfd_ctfd_1:172.1.0.2
- ctfd_frpc_1:172.1.0.3
- frp-docker-for-ctfd-whale_frps_1:172.1.0.4
docker network connect --ip 172.1.0.3 ctfd_frp ctfd_frpc_1 docker network connect --ip 172.1.0.4 ctfd_frp frp-docker-for-ctfd-whale_frps_1 docker restart ctfd_frpc_1 frp-docker-for-ctfd-whale_frps_1
加入ctfd_frp網絡之后,重啟完成之后再查看一下ctfd_frp網絡,運行
docker network inspect ctfd_frp
如果還是不行,靈活使用docker 的logs功能,查看是什么問題報錯
docker logs <容器ID或名稱>
2.Dockerfile的gevent報錯
如果是以下報錯,可以通過修改版本號解決


解決方案:
其他Dockerfile在構建的時候可能會出現gevent構建不成功的問題,有以下幾種解決辦法:
- 將鏡像源替換為清華源
- 替換gevent版本
- 不指定gevent版本號
在CTFd目錄下,修改requirements.txt的gevent
然后 docker-compose down 再啟動 docker-compose up -d 不出意外應該就能解決問題
3.Dockerfile的world模塊報錯

解決方案:
將Dockerfile中的python和python-dev刪掉,或者修改成python2或python3,python2-dev或python3-dev都可以
4.構建的時候,ctfd鏡像無法啟動

解決方案:
將docker-entrypoint.sh第一行修改為
#!/bin/bash

修改后重新docker-compose up -d即可
5.ctfd_ctfd_1容器一直在重啟:
ctfd_ctfd_1日志報錯如下

解決方法:
出現時區問題,因此在requirements.txt中添加如下:
tzlocal==2.1
6.ctf_frpc_1容器一直重啟
解決方案:
仔細配置網絡,網絡配置的要求如下:
- 將ctfd_frpc_1,frp-docker-for-ctfd-whale_frps_1,ctfd_ctfd_1這三個容器加入到ctfd_frp網絡中
- 這三個容器對應的IP如下:
- ctfd_ctfd_1:172.1.0.2
- ctfd_frpc_1:172.1.0.3
- frp-docker-for-ctfd-whale_frps_1:172.1.0.4
docker network connect --ip 172.1.0.3 ctfd_frp ctfd_frpc_1 docker network connect --ip 172.1.0.4 ctfd_frp frp-docker-for-ctfd-whale_frps_1 docker restart ctfd_frpc_1 frp-docker-for-ctfd-whale_frps_1
重啟完成之后再查看一下ctfd_frp網絡
docker network inspect ctfd_frp
如果還是不行,靈活使用docker 的logs功能,查看是什么問題報錯
docker logs <容器ID或名稱>
如果是以下報錯

則重新配置frpc,編輯/CTFd/frpc/frpc.ini
[common] token = randomme server_addr = 172.1.0.4 server_port = 6490 pool_count = 200 tls_enable = true admin_addr = 172.1.0.3 #這里千萬千萬別忘加,之前要被搞氣死 admin_port = 7400
然后運行
docker restart ctfd_frpc_1
7.MySQL容器反復重啟

解決方法:
因為當前目錄下的.data文件中的MySQL日志與容器版本不匹配
解決方法是將.data文件刪除,重新構建鏡像
7.docker容器無法啟動或者frp端口無法映射
如果docker容器無法啟動或者frp端口無法映射可以進容器檢查,確保docker api填寫正確,如docker-compose.yml中寫的unix:///var/run/docker.sock你也可以使用端口形式的api如官方示例:可以用IP:端口指定API
docker容器無法啟動問題
進入容器檢查:
docker exec -it <ctfd容器id> sh /opt/CTFd# python >>>import docker >>>client=docker.DockerClient(base_url="unix:///var/run/docker.sock") >>>client.images.list()
如果api正確會列出所有鏡像
frp端口無法映射
其實檢查可以順便檢查一下上面的,因為都在ctfd容器內 docker exec -it <ctfd容器id> sh
docker exec -it <ctfd容器id> sh
/opt/CTFd# python
>>>import requests
>>>requests.get("http://172.1.0.3:7400/api/reload")//即frp api的地址
返回
<Response [200]> #表示成功
如果frpc還是出現如下問題
requests.exceptions.ConnectionError: HTTPConnectionPool(host='172.1.0.3', port=7400): Max retries exceeded with url: /api/reload (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f8df919f850>: Failed to establish a new connection: [Errno 111] Connection refused'))
則重新配置frpc,編輯/CTFd/frpc/frpc.ini
[common] token = randomme server_addr = 172.1.0.4 server_port = 6490 pool_count = 200 tls_enable = true admin_addr = 172.1.0.3 #這里千萬千萬別忘加,之前要被搞氣死 admin_port = 7400
然后運行docker restart ctfd_frpc_1(這里再看frpc.ini會發現內容更新了,admin配置沒了,不用擔心)
再進容器檢查requests.get("http://172.1.0.3:7400/api/reload")應該就可以