Build_Your_Own_CTF_puzzle_with_Docker

用Docker搭建CTF题的环境

安装Docker

略,参考CentOS Docker 安装来操作即可

1
curl -sSL https://get.daocloud.io/docker | sh

卸载旧版本

1
2
3
4
5
6
7
8
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine

注意:需要开启iptables

1
service iptables restart

重启docker

1
service docker restart

编写Dockerfile

寻找初始环境

Dockerfile里的第一行,就是你CTF题的初始环境。如下所示,代表的就是从这里拉的镜像作为初始环境

1
FROM drupalci/php-5.5.38-apache:dev

只需要在https://hub.docker.com里搜索合适的版本即可。此处要注意必须写出`tag`,也就是上面冒号后面的`dev`,它往往指明了镜像里中间件的版本。

更改环境参数

有时候,我们需要更改Apache的配置文件apache2.conf、PHP的配置文件php.ini,那么就需要在Dockerfile里进行相应的编写。下面给出常用的命令写法,更多细节请移步=>如何用Dockerfile构建镜像

Dockerfile样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 第一行写跟第1点中一样的基本镜像
FROM drupalci/php-5.5.38-apache:dev

# 这里可以写上你的姓名/昵称
MAINTAINER unc1e

# 这里可以写上你的制作时间
ENV REFRESHED_AT 202081

# 使用utf-8编码
ENV LANG C.UTF-8

# 先写 修改源/更新 【如果必须的话】
# 替换源(这里可用sed或者直接COPY一个完整的sources.list来替换)
RUN sed -i 's/http:\/\/archive.ubuntu.com\/ubuntu\//http:\/\/mirrors.163.com\/ubuntu\//g' /etc/apt/sources.list

# 进行更新
RUN apt-get update -y

# 将环境变量设置为非交互的 【这个看个人】,在运行apt-get命令的时候格外有用,因为它会不停提示用户进行到了哪步并且需要不断确认
# 非交互模式会选择默认的选项并以最快的速度完成构建。
# 注意:ENV命令在整个容器运行过程中都会生效,在你通过BASH和容器进行交互时,可能因此出问题
ENV DEBIAN_FRONTEND noninteractive

# 修改一些配置
# 对于文件中的替换字符串,多使用sed命令
# 例如:去掉php响应头里的X-Powered-By
RUN sed -i 's/expose_php = On/expose_php = Off/' /usr/local/etc/php/php.ini

# 然后才是复制文件
# 不推荐挂载卷,因为常常需要将镜像导出为tar包文件
# ADD会自动解压压缩包,而COPY不会
ADD html.tgz /var/www

# 剩下一些操作(权限要控制好)
# 例如修改某个文件的所有者
RUN chown root:root /var/www/html/x.php

# WORKDIR:设置CMD指明的命令的运行目录。
WORKDIR /var/www/html/

# 最后处理flag以及开机启动项
# flag的规范格式为flag{uuid格式} (如:flag{8ba868f2-71b6-477b-bc7a-255302c881e1}
# 如有特殊情况请说明,但flag的格式至少需要有flag{},不接受其他类型格式。
# 默认flag的值存在flag.txt,
# 如果flag是在数据库里,请记得把flag所在的字段长度设置为大于42
# 把flag.txt复制到/root/flag.txt
COPY flag.txt /root/flag.txt

# start.sh为开机启动脚本,里面包含容器开启后要启动的命令
COPY start.sh /root/start.sh

# 加上执行权限
RUN chmod +x /root/start.sh

# ENTRYPOINT:配置容器启动时的执行命令(不会被忽略,一定会被执行)
# 建议使用ENTRYPOINT而不使用CMD,因为CMD容易受最后的RUN命令影响
ENTRYPOINT cd /root; ./start.sh

# WEB开放端口默认为80,一般为一个,若有特殊情况,请写明
EXPOSE 80

start.sh样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 第一行默认用bash
#!/bin/bash
# 至少sleep 1,但也不要太久
sleep 1

# 启动服务,例如apache2
# 具体的启动命令,视系统环境而定
# 一般的apache2
/etc/init.d/apache2 start
# 一般的nginx
# 为了适应大多数环境,修改下nginx配置
sed -i 's/listen 80 default_server;/listen 80;/' /etc/nginx/sites-enabled/default
sed -i 's/listen \[::\]:80.*;/#\0/' /etc/nginx/sites-enabled/default
nginx -c /etc/nginx/nginx.conf
/etc/init.d/nginx start
# 为了适应各种docker版本,mysql的启动命令建议如下(mysqld除外)
find /var/lib/mysql -type f -exec touch {} \; && service mysql start

# ctf.sql为数据库的sql文件,在mysql启动后才导入。
# 如果flag不是存在数据库里,那么这里写上存在flag的文件(如flag.php)
# 存在flag的文件里的flag值请用flag{xxxxxx}表示(这里这样的设置是为了动态替换)
flagfile=/var/www/html/ctf.sql
if [ -f $flagfile ]; then
# 这里就是替换flag值为/root/flag.txt里的值(/root/flag.txt为动态flag自动下发的位置)
# 这里的flag{x*}对应了flag{xxxxxx},因为sed不支持扩展正则语法
# 如果原来文件里的flag值并不是flag{xxxxxx},那么下面这一句请自己改写
sed -i "s/flag{x*}/$(cat /root/flag.txt)/" $flagfile
# 修改mysql的root密码(如果有使用mysql且必须修改的话)
mysqladmin -u root password "newpasswd"
# mysql导入sql文件(newwpasswd只是示例密码)
mysql -uroot -pnewpasswd < $flagfile
# 删除sql文件(一般是要删除的) / 如果不是sql文件这里不需要删除
rm -f $flagfile
fi
/bin/bash

配置启动命令

有时,由于各种特殊的需要:如实现动态flag,需要在docker外部指定flag,因此就需要了解docker-compose.yml的写法。

要实现动态flag,docker-compose.yml必须和Dockerfile相互配合。此处我以赵师傅做的bytectf_2019_babyblog的镜像来做说明。

docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
version: "2"

services:

web:
build: .
image: ctftraining/bytectf_2019_babyblog
restart: always
ports:
- "127.0.0.1:8302:80"
environment:
- FLAG=flag{glzjin_wants_a_girl_firend}

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
FROM orsolin/docker-php-5.3-apache

LABEL Author="glzjin <[email protected]>" Blog="https://www.zhaoj.in"

COPY ./files /tmp/files

RUN mv -f /tmp/files/sources.list /etc/apt/sources.list \
&& rm -rf /var/www/html/* \
&& mv -f /tmp/files/init.sql /tmp/db.sql \
&& mv -f /tmp/files/html/* /var/www/html/ \
&& apt update \
&& echo "debconf mysql-server/root_password password root\ndebconf mysql-server/root_password_again password root" >> /tmp/mysql-passwd \
&& debconf-set-selections /tmp/mysql-passwd && apt install mysql-server -y && rm -rf /tmp/mysql-passwd \
&& mysql_install_db --user=mysql --datadir=/var/lib/mysql \
&& sh -c 'mysqld_safe &' \
&& sleep 5s \
&& mysql -e "source /tmp/db.sql;" -uroot -proot \
&& echo "magic_quotes_gpc = Off\nopen_basedir = /var/www/html/:/tmp/:/proc/\ndisable_functions = pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,ini_set,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,dl,mail " >> /etc/php5/apache2/php.ini && \
touch /flag && \
mv /tmp/files/readflag /readflag && \
chmod 555 /readflag && \
chmod u+s /readflag && \
chmod 500 /flag

WORKDIR /var/www/html/

CMD echo $FLAG >> /flag && export FLAG=not_flag && FLAG=not_flag && find /var/lib/mysql -type f -exec touch {} \; && service mysql start && apache2-foreground

相信你已经看出来了——两个文件都只需要看最后一行。首先在docker-compose.yml 里设定一个环境变量(environment),叫做FLAG,这个其实就是我们的动态flag;而下一步Dockerfile中的命令CMD echo $FLAG >> /flag,将这个FLAG写入了/flag文件。

对于运维者来说,要在某个CTF题中实现动态flag的效果,只需要对docker-compose.yml进行调整,再运行docker-compose up -d即可,完全不需要改动Dockerfile. 这在某些场景(CTF靶场、AWD)中是很有用的

如果你还是不明白docker-composedocker的关系,那么请你移步=>[Docker 微服务教程 - 阮一峰的网络日志]https://ruanyifeng.com/blog/2018/02/docker-wordpress-tutorial.html),大意如下:

Compose 是 Docker 公司推出的一个工具软件,可以管理多个 Docker 容器组成一个应用。你需要定义一个 YAML 格式的配置文件docker-compose.yml,写好多个容器之间的调用关系。然后,只要一个命令,就能同时启动/关闭这些容器。

1
2
3
4
# 启动所有服务
$ docker-compose up
# 关闭所有服务
$ docker-compose stop

打包并运行测试

打包镜像

创建完上面的Dockerfile后,在当前目录执行以下命令进行镜像的构建

1
2
# 构建镜像
docker build -t web_ctf_puzzle:test1 .

稍等片刻,运行docker images指令就能看到名为web_ctf_puzzle,TAG为test1的镜像了。

但有些机构收题,考虑到安装时下的东西不一定跟制作者制作时的东西一模一样,所以需要制作好一个可用的镜像压缩包(tar包),以备后用,所以有了以下的命令

1
2
# 导出tar包
docker save web_xxx_name > web_xxx_name.tar

运行测试

启动镜像

1
2
3
4
5
6
7
8
# -d: 后台运行容器,并返回容器ID;
# -P: 随机端口映射
# -h: 指定容器的hostname
# --name: 指定容器的名字
# -p: 指定端口映射, 格式为:外部宿主端口:内部容器端口
# -v: 绑定一个卷(--volume),例如 -v /opt/ctf/src:/var/www/html/ 表示将主机的目录 /opt/ctf/src 映射到容器的 /var/www/html/

docker run -p 8088:80 --name="web_ctf_puzzle_docler" -d web_ctf_puzzle

进入docker的bash。需要将019dfb3e357b改为你运行docker ps后看到的那个 CONTAINER ID

1
2
# 进入docker的bash shell
docker exec -it 019dfb3e357b bash

reference