使用 GZCTF 结合 GitHub 仓库搭建独立容器与动态 Flag 的 CTF 靶场+基于 Docker 的 Web 出题与部署+容器权限控制

Myon⁶ 2024-09-15 17:33:04 阅读 73

写在前面

关于 CTF 靶场的搭建(使用 CTFd 或者 H1ve)以及 AWD 攻防平台的搭建,勇师傅在前面博客已经详细写过,可以参考我的《网站搭建》专栏,前段时间玩那个 BaseCTF,发现它的界面看着挺不错的,了解到也是一个开源项目-GZCTF,网上未见有这方面的详细介绍,看了下网上都只是简单说了下怎么搭出 GZCTF 这个靶场的界面,然而对于后续题目的部署并未进行详细的介绍与说明,因此这里分享下自己的部署经验以及可能遇到的问题、注意点与解决方案。

文章开始前给大家分享一个人工智能学习网站,通俗易懂,风趣幽默

人工智能教程

icon-default.png?t=N7T8

https://www.captainbed.cn/myon/

目录

一、靶场基础搭建

二、靶场基础配置与功能使用

三、Web 题目示例说明

四、Web 题目制作与部署

五、docker 镜像制作、上传、拉取

补充完善《容器权限控制》


一、靶场基础搭建

首先我们肯定得把基础的框架搭好,也就是靶场的界面,之前的一般都是直接 git 克隆 github 的项目,这个的话我们是需要先准备两个文件。

参照官方手册:快速上手 - GZ::CTF Docs (gzti.me)

1、文件1:appsettings.json

内容:

<code>{

"AllowedHosts": "*",

"ConnectionStrings": {

"Database": "Host=db:5432;Database=gzctf;Username=postgres;Password=<Your POSTGRES_PASSWORD>"

},

"EmailConfig": {

"SendMailAddress": "a@a.com",

"UserName": "",

"Password": "",

"Smtp": {

"Host": "localhost",

"Port": 587

}

},

"XorKey": "<Your XOR_KEY>",

"ContainerProvider": {

"Type": "Docker", // or "Kubernetes"

"PortMappingType": "Default", // or "PlatformProxy"

"EnableTrafficCapture": false,

"PublicEntry": "<Your PUBLIC_ENTRY>", // 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"]

}

}

该文件中必须修改的参数:

(1)数据库密码

Password=<Your POSTGRES_PASSWORD>"

(2)用于加密比赛私钥的随机字符串(随便输入一串字符就行了)

"XorKey": "<Your XOR_KEY>"

(3)外部访问地址,可以是 IP 或域名,用于提供给选手访问题目容器的地址

"PublicEntry": "<Your PUBLIC_ENTRY>"

2、文件2:compose.yml

内容:

<code>services:

gzctf:

image: registry.cn-shanghai.aliyuncs.com/gztime/gzctf:develop

restart: always

environment:

- "GZCTF_ADMIN_PASSWORD=<Your GZCTF_ADMIN_PASSWORD>"

# 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

restart: always

environment:

- "POSTGRES_PASSWORD=<Your POSTGRES_PASSWORD>"

volumes:

- "./data/db:/var/lib/postgresql/data"

该文件中必须修改的参数:

(1)初始管理员密码

"GZCTF_ADMIN_PASSWORD=<Your GZCTF_ADMIN_PASSWORD>" 

特别注意:该密码需要满足要求的密码复杂度,至少应包括大小写字母加数字,如果配置文件中设置的密码太简单是无法成功创建管理员这个账户的,默认管理员登录用户名是 Admin。

(2)数据库密码

"POSTGRES_PASSWORD=<Your POSTGRES_PASSWORD>"

文件改好后,都放到某个目录下,对了你需要提前装好 docker 和 docker-compose 。

在该目录下执行如下命令拉取并启动容器:

<code>docker-compose up -d

确保你的 80 端口未被占用,如果你之前开了中间件 nginx、Apache 这些可能就会报错端口被占用,需要停掉再拉取。

拉取后会多出一个叫 data 的文件夹:

访问你前面配置文件 appsettings.json 里填的 "PublicEntry" 那个地址:

没什么问题的话,就可以看到靶场的页面了。

至此,靶场的基本搭建完成。

二、靶场基础配置与功能使用

使用 Admin 管理员账号和 compose.yml 中设置的密码进行登录(必须满足密码的复杂度,否则会提示用户不存在或者密码错误,其实是数据库里压根就没创建这个用户)。

首先肯定是需要新建一个比赛 

里面可以修改的内容如下: 

下面我自己创建的用户或者叫朋友创建的用户进行测试 

都是图形界面化的操作,很容易理解,到处点点就熟悉了,这里不过多赘述。

三、Web 题目示例说明

新建一个题目

选择动态容器,填好一些基础的信息。

最主要的是:容器镜像 * 这里,下面给出一些可以用于测试的镜像:

<code>vaalacat/push_f12

glzjin/hctf_2018_warmup

ctftraining/hbctf_2017_dameixian

ctftraining/qwb_2019_smarthacker

ctftraining/buuctf_2018_online_tool

ctftraining/qwb_2019_upload

ctftraining/qwb_2019_supersqli

比如我们使用 vaalacat/push_f12 进行测试:

填好镜像名后,点击创建测试容器。

创建成功后就可以访问题目环境了

题目容器的端口是 32791(随机生成的) 

此时在终端 docker ps 可以看到这个被新创建的容器:

将题目设置为可见的

我们再创建一个用户对这个题目进行测试,创建好后需要先报名参赛,如果没有开启免审核则还需要管理员账号通过下,然后就可以看到比赛题目了。

(这里似乎一个 ip 只能登录一个用户,因为我在 Admin 和 test 用户间切换会影响到另一个用户)

可以看到,新建容器的端口是 32792

终端会发现又多出了一个新建的容器,这些容器间是相互独立的,也就是说每个选手访问的地址不同,题目环境也是相互独立的,flag 也是不同的。 

四、Web 题目制作与部署

写这篇文章最重要的就是我们如何上 web 题目呢?如何生成不同容器的动态 flag?

ok,来到最重要的部分,说白了也就是我们题目该怎么制作?

下面我将带大家一起来制作 docker 镜像,以及将 docker 镜像上传到自己的镜像仓库,最后拉取镜像到自己的靶场。

先给大家看一下至少需要准备哪些文件,文件的位置:

废话少说,上文件:

(1)docker-compose.yml

相比之前的 docker-compose.yml 文件,我们不需要再单独暴露某个端口作为题目的访问端口,同时我们新增了镜像名称和标签信息。

<code>version: "3"

services:

web1:

build:

context: ./web1 # 指定 Dockerfile 所在的构建上下文目录

dockerfile: Dockerfile # 指定 Dockerfile 的名称

image: myon6/testweb1:latest # 为构建的镜像指定名称和标签,需要改成你自己 github 的名字

restart: always

我不知道大家对于 github 仓库是否熟悉,我最开始测试的其实是 docker hub 仓库,但是在执行 push 命令的时候一直无法解决超时的问题,无论是开了加速器还是换了 docker 源还是开全局代理都没有解决,解决 pull 命令无法拉取倒是完全可以的,主要是 push 命令有问题,最后还是选择使用 github 的仓库。

首先我们来看看 github 上自己的仓库在哪儿

下面这个 myon6/testweb1 就是我制作好的 docker 镜像,然后 push 到了 github 上的仓库。

好,你大概知道 github 的仓库在哪儿了,我们继续说出题需要的其他文件。

(2)Dockerfile

<code># 指定基础镜像

FROM php:7.0-fpm-alpine

# 删除默认的 web 根目录中的所有内容

RUN rm -rf /var/www/html/*

# 将本地的 html 目录复制到容器中

COPY html /var/www/html

# 将初始化脚本复制到容器的 /etc 目录中

COPY init.sh /etc/init.sh

# 设置权限

RUN chown -R root:root /var/www/html && chmod -R 755 /var/www/html

# 设置初始化脚本为可执行

RUN chmod +x /etc/init.sh

# 暴露 web 服务器的端口

EXPOSE 80

# 使用初始化脚本来启动容器

ENTRYPOINT ["/etc/init.sh"]

这里其实应该做一个权限控制,因为直接给 root 还是有一定风险在里面,比如 docker 逃逸。

 (但是我在测试中给 www-data 权限会导致 flag 无法成功写入根目录,后面再继续研究)

(3)初始化脚本 init.sh

#!/bin/sh

# Write the value of GZCTF_FLAG environment variable to /flag

echo "$GZCTF_FLAG" > /flag

chmod 444 /flag

# Unset the GZCTF_FLAG environment variable

unset GZCTF_FLAG

# Start the PHP built-in web server

php -S 0.0.0.0:80 -t /var/www/html

这个脚本是最容易出问题,也是当时折腾我最久的,最好不要使用 Windows 相关的东西去编辑这个初始化脚本,否则可能会导致 init.sh 文件中包含 Windows 风格的换行符 ^M:

这些字符会导致 Unix 系统无法正确解释脚本文件的内容,那么你在创建容器的时候就会遇到容器一直处于 restarting 的状态,无法看到映射的端口,实际就是容器刚启动就崩溃掉了。

当然我们也可以在 Dockerfile 中使用工具来处理这个问题:

<code># 安装 dos2unix 工具以转换脚本格式

RUN apk add --no-cache dos2unix

# 使用 dos2unix 转换脚本格式

RUN dos2unix /etc/init.sh

只要你操作得当,就不会遇到这个容器崩溃的问题,就可以不用加上面的内容。

(4)html 文件夹

这个就是大家很熟悉的题目环境文件了,我这里只做测试,因此就放了一个 index.php。

五、docker 镜像制作、上传、拉取

准备好上面说的文件后,在 web 目录下,使用 docker-compose build 来构建镜像:

docker-compose build

构建成功 

使用 docker images 查看镜像:

可以看到 myon6/testweb1

但是这是我们本地的镜像,我们需要将镜像标记为 GitHub Container Registry 的格式:

使用 docker tag 命令,注意一定要修改成你自己 github 账户的名字

<code>docker tag myon6/testweb1:latest ghcr.io/myon5/myon6/testweb1:latest

可以看到现在就有了一个叫 ghcr.io/myon5/myon6/testweb1 的镜像 

我们将这个镜像 push 到 github 仓库,首先你需要先登录 github 账号:

关于这个 key 在哪里获取,就是在 github 的扩展设置里生成你自己账户的 token 用于验证。

<code>echo <your_key> | docker login ghcr.io -u <your_username> --password-stdin

注意生成的 token 权限需要允许上传 packages,勾上。

登录成后就可以上传镜像到自己的仓库了:注意替换成自己账号

<code>docker push ghcr.io/myon5/myon6/testweb1:latest

为了给你们再演示一遍我是删掉了原来仓库的东西重新构建上传的 

默认是私有的,如果你要所有人都能直接拉取到,需要设为 public:

至此,我们成功将镜像从本地上传到了 github 公开镜像。

接下来我们就可以在靶场-题目管理-容器镜像里填上我们题目镜像的名字了,然后创建容器。

这个是我自己做的一个简单题目

直接截断就可以 RCE ,命令执行成功

可以看到根目录下已经成功创建了 flag

为什么端口不一样了,因为我前面测试的是 www-data 用户,发现 flag 无法成功写入根目录,下面是 root 权限:

这也是我前面说的对用户权限的控制其实我还没有调整好,一般我们给 nobody 是最安全的,或者给 www-data,但是我试了这两个都有点问题,权限给高倒不是怕它影响到其他选手,因为每个容器是独立的,无论它怎么玩,就算把 flag 删了都没事,但是就担心它从 docker 容器里逃逸到服务器本地,所以还需要继续调整吧。

下面是测试不同用户选手访问不同端口,读取到的 flag 都是不同的:

经测试,两个 flag 都可以正确提交。

关于 flag 的随机生成规则我们也是可以进行设置的:

至此,我们实现了独立容器、动态 flag 的 web 题目制作与部署。

关于 GZCTF 那些界面的操作、功能点、设置等都是图形化的,我也不做过多介绍,大家到处点点就熟悉了。关于那种直接放附件的题,静态 flag 的,比如杂项、逆向这些和 CTFd、H1ve 方法一样,那么对于 web 题目的部署,我上面讲的是自己部署的方法以及注意事项,其实也就是新增了一个初始化 flag 的脚本,对于 pwn 题我目前还没试过,看了下官方给的示例是结合 xinetd 来部署的,应该和 web 题类似,大家自行摸索,如果不知道 xinetd 是什么,那么还是建议你先去看一下我前面介绍使用 xinted 部署 pwn 题的文章,也在专栏 《网站搭建》 里面。

补充完善《容器权限控制》

在前面的基础上,勇师傅又对 dockerfile 和 初始化脚本进行了调整。

附上修改后的文件:

Dockerfile

<code># 指定基础镜像

FROM php:7.0-fpm-alpine

# 删除默认的 web 根目录中的所有内容

RUN rm -rf /var/www/html/*

# 创建空的 flag 文件并设置权限

RUN touch /flag && chmod 666 /flag

# 将本地的 html 目录复制到容器中

COPY html /var/www/html

# 将初始化脚本复制到容器的 html 目录中

COPY init.sh /var/www/html/init.sh

# 设置权限(此时仍为 root 用户)

RUN chown -R www-data:www-data /var/www/html && chmod -R 755 /var/www/html

# 设置初始化脚本为可执行

RUN chmod +x /var/www/html/init.sh

# 暴露 web 服务器的端口

EXPOSE 80

# 以 root 用户启动脚本

ENTRYPOINT ["/var/www/html/init.sh"]

# 切换到 www-data 用户执行后续命令

USER www-data

init.sh

#!/bin/sh

# 将环境变量 GZCTF_FLAG 的值写入 /flag 文件

echo "$GZCTF_FLAG" > /flag

# 清除 GZCTF_FLAG 环境变量

unset GZCTF_FLAG

# 启动 PHP 服务器并替换当前 shell 进程

exec php -S 0.0.0.0:80 -t /var/www/html &

# 等待 PHP 服务器启动

sleep 5

# 删除 init.sh 脚本

rm -f "$0"

# 保持脚本运行

wait

简单说一下改进的优缺点:

(1)最终容器的用户是 www-data,而非 root,更安全。

(2)容器成功启动后,删掉初始化脚本,避免敏感信息泄露。

这里没有选择将 init.sh 放到 etc 目录,而是直接放在 html 目录,确保 www-data 用户可以成功删除掉,可以看到容器启动后已经不存在初始化脚本这个文件了。

(3)flag 文件可以被更改

我们在 Dockerfile 中 chmod 666 /flag 目的是为了后续让 www-data 用户可以成功执行 init.sh,写入环境变量到根目录下的 flag,但是这也会导致 flag 这个文件可以被 www-data 用户更改,因为我们使用 www-data 用户后就没有权限再执行 chmod 444 到根目录下的 flag。不过影响不大,因为每个选手分配的都是独立容器,就算更改也是改的自己的 flag,不会影响到其他用户的容器,如果确实把正确的 flag 改了,重启容器即可获取到新的 flag。

总的来说,相对于前面使用 root 用户,改成 www-data 用户还是会更好一些的,后面如果有新的改进和完善再与大家分享。

创作不易,期待大家的关注与支持!



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。