原视频:https://www.bilibili.com/video/BV16q4y1A74h
作者:智慧少年Xenny
简介:这是一个关于docker使用的教学。
包含
docker基础使用
dockerfile编写
docker-compose编写
docker在ctf中的实例
等内容进行介绍。

所需文件Xenny已上传至群里。如果不去一个个操作就看文章就行了,文章里给出了代码的。

为什么要写这篇文章

docker,简化配置、全平台、提高效率、方便共享、快速部署……
学习CTF的时候,无论是想用sqli-labs、upload-labs、pikachu、awvs。只需要打开docker,输入两行代码,即可完整“搭建”,在本地畅享服务。
制作动态靶机题的时候,使用docker那是再好不过的,但是怎么去实现自己想要达到的目的?这就是写这篇文章的主要原因。

Docker的使用

Docker基础操作

参考安装:百度
帮助:docker --help或者官方文档
在这里插入图片描述
要找资源、找帮助、找阿巴阿巴的东西、安装。都去dockerhub上找。

Dockerfile

举例1(test1)

Dockerfile:

FROM ubuntu:16.04

COPY run.sh /run.sh

VOLUME /share/data

RUN useradd nss

RUN sed -i s@/deb.debian.org/@/mirrors.aliyun.com/@g /etc/apt/sources.list && \
    sed -i s@/security.debian.org/@/mirrors.aliyun.com/@g /etc/apt/sources.list
    
RUN ["apt-get", "update", "-y"]

WORKDIR /home/nss

RUN chmod +x /run.sh

ENV FLAG=NSSCTF{test_flag}

EXPOSE 80/tcp

CMD [ "helloworld" ]

USER nss

ENTRYPOINT [ "/run.sh" ]

run.sh:

#! /bin/bash
echo "Run with user `whoami`"
echo "Run with directory `pwd`"
echo "Run with environment $FLAG"
echo "Run with parameter $1"
tail -f /dev/null

read v
while [ "$v" == "n" ];
do
    cat /share/data/data
    read v
done;

1.FROM ubuntu:16.04:从某一基础镜像来构建我们的docker。除前面可以添加注释语句以外,docker语句的第一行必须是此类语句。
当然也可以直接FROM ubuntu,不跟冒号docker会自动去选择最新版本,跟上冒号+版本号可以指定版本。

2.COPY run.sh /run.sh:把run.sh移动到/run.sh
COPY:Copy files or folders from source to the dest path in the image’s filesystem

3.VOLUME /share/data:创建挂载点(并未实际挂载,只是创建挂载点)

4.RUN useradd nss:在镜像中运行命令,此语句即useradd nss(创建nss用户)

5.RUN sed -i s@/deb.debian.org/@/mirrors.aliyun.com/@g /etc/apt/sources.list && \ sed -i s@/security.debian.org/@/mirrors.aliyun.com/@g /etc/apt/sources.list:单纯换源

6.RUN ["apt-get", "update", "-y"]:与4不同,此格式是exec格式,而第4条相当于是shell格式。

7.WORKDIR /home/nss:指定工作目录,指定后的所有操作的相对路径都是/home/nss

8.ENV FLAG=NSSCTF{test_flag}:设置环境变量

9.EXPOSE 80/tcp:指定某个端口要被打开而不是一定会打开,这里相当于标记信息,写不写都可以,但是为了养成良好的编写习惯以及告诉其他人哪些端口应该被打开,尽量写上此语句,这样方便运维人员在使用容器时,知道要打开哪些端口。不指定/tcp或者其他链接则默认开启所有链接。

10.CMD [ "helloworld" ]:启动的时候执行,格式有3种

shell CMD echo 1
exec CMD [“echo”,“1”]
param CMD [“xxxx”] (给一个参数,后面需要有ENTRYPOINT)

11.USER nss:后面的操作都用nss用户来运行

12.ENTRYPOINT [ "/run.sh" ]:进入点(挂载点),在进入容器的时候就会执行/run.sh
配合上面CMD [ "helloworld" ],这里在进入docker时执行了/run.sh helloworld

运行测试:
这是test1目录:
在这里插入图片描述
先build镜像

docker build . -t mumuzi7760/test1

第一次build有点慢,如果之前就已经build过而且没有改变,会有一个CACHED的标志并使用缓存,非常的人性。
(PS:第一次太慢啦)
在这里插入图片描述

运行一下镜像

docker run -it --rm mumuzi7760/test1

在这里插入图片描述
这里再去看run.sh,首先是开头几行

echo "Run with user `whoami`"
echo "Run with directory `pwd`"
echo "Run with environment $FLAG"
echo "Run with parameter $1"

执行命令whoami输出nss,因为dockerfile执行了USER nss

工作目录/home/nss因为执行了WORKDIR /home/nss

环境变量中的FLAG因为有ENV FLAG=NSSCTF{test_flag}

parameter那里是最后说的CMD [hellowolrd]

之前的CMD [helloworld]再ENTRYPOINT["/run.sh"]和ENTRYPOINT["/run.sh",“helloworld”]是等价的。为什么要用前者呢。等会说
然后看后面几行

tail -f /dev/null

read v
while [ "$v" == "n" ];
do
    cat /share/data/data
    read v
done;

读取v这个变量,如果$v=n,就会去读取/share/data/data里面的值
可以看到,在dockerfile开头,写了一个VOLUME /share/data作为挂载点。用上面的启动方式,找不到data文件,会返回cat: /share/data/data:No such file or directory。
此时用另一种方式启动

docker run -it --rm -v 路径:/share/data mumuzi7760/test1

像这样:
在这里插入图片描述再去执行,就会输出data里面的内容,当然此时再去改一下data内容再去输出,可以发现输出的内容也变了。
这里的挂载就相当于是映射,里面的文件内容改变了,物理机上面的内容也会改变;如果物理机上指定的文件内容改变了,容器里面的文件内容也会改变,这样有什么好处。比如当你在进行网站的维护时,可能要经常看日志文件,每次要看日志文件都要进入docker就变得很麻烦,但如果把这个文件给映射出来可以直接在物理机上面看和修改还有保存,就显得很方便。

如果不使用-v命令,那么会被执行到哪里呢?这里打开docker desktop

在这里插入图片描述
可以看到是挂载到了/var/lib/docker/volumes/阿巴阿巴/_data

回看,解释一下ENTRYPOINT,如果去掉这个再去执行会怎样呢?这里注释掉10和12,重新去启动一下(顺便就能看到CACHED)
在这里插入图片描述然后再docker run -it --rm mumuzi7760/test1
在这里插入图片描述
可以看到,是直接进入了docker,执行操作也是没有问题的。
那么现在退出来,使用后台运行的方式去操作

docker run -dt mumuzi7760/test1
docker ps

在这里插入图片描述
这里可以看到,COMMAND为"/bin/bash",这里是ubuntu自动设置的,自动挂载了一个/bin/bash,没有指定ENTRYPOINT就自动去用父目录的ENTRYPOINT(相当于继承)。
这里想说,如果没有ENTRYPOINT,一般就会直接退出或者用其他的ENTRYPOINT
那么现在取消掉之前的注释,run.sh只留#!和echo那几行,后面8行注释掉会发生什么呢
操作一下看看(build、run、后台、ps)
在这里插入图片描述
可以发现,刚刚run起来的镜像直接退出了。因为run.sh要执行的已经执行完了,所以执行完之后他就自己退出了。因此我们在编写的时候要进行一个阻塞(比如第6行的tail、后面的死循环)来保证容器能一直运行下去。

权限设置

有时候会遇到这种情况:
在这里插入图片描述

看图可以发现,现在已经是root用户了,但是还是会提示permission denied。
因为容器不是虚拟机,他用的是宿主机的资源,像mount之类的操作,容器管理时是不会分配这些权限(因为这些权限可能会存在一些安全问题)
如果实在想用这些,就需要给docker一个特权,可以看这里链接:https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilitie

一般的话是用不到这个,如果是出pwn题可能就会遇到

一些没人会告诉你的知识
  • ADD or COPY
  • CMD or ENTRYPOINT
  • *Shell or Exec
  • *ARG or ENV
  • docker run -it --rm的i和t

1.ADD:更加高级的"copy"
ADD命令:copy files,folders,or remote URLs from source to the dest path in the image’s filesystem
可以明显看到ADD命令可以从远处下载内容。
此外,ADD命令还可以打包和解包,比如
ADD flag.tar.gz /xxxx,add命令会自动解包这个到/xxxx
当然最好情况下就用COPY就行了

2.用dockerfile最后的举例,如果把ENTRYPOINT["/run.sh"]换成CMD ["/run.sh"],其实是一样的。和之前说的一样命令是相同的,但是ENTRYPOINT更加符合一个容器的挂载点,而CMD更加适合作为命令来执行,最好用ENTRYPOINT吧。

3.如果用shell,执行的并不是那个命令,实际执行的是sh -c "echo 1",但是万一某个精简版的没有sh呢?而且执行shell还会开启sh进程。而exec就直接执行/bin/echo 1。所以两者一般来说是没有什么区别的。

4.这里就是两种东西,ENV是给靶机环境变量,而ARG是给dockerfile在build过程中的环境变量,build完之后即消失。

5.i:构建标准输入,t:进入一个默认的终端

举例2(test2)

build by build
举例:有一个需要编译的语言要放在docker中,这个时候不是所有的语言都可以如同python一样去动态执行。那如果提前编译再放进容器里面可以吗?答案是不行,因为操作系统的不同会导致动态链接库等不一样,所以需要一个本地的去编译。
银杏化的解释是 从一个工具人那里编译,编译完之后再从工具人那里拿过来。
dockerfile:

FROM golang:1.13.4-stretch AS worker
COPY src /src

ENV KEY=NSS{test_key}

WORKDIR /src

RUN go build

FROM ubuntu:16.04

COPY --from=worker /src/main main

ENV KEY=NSS{test_key2}

RUN chmod +x /main

CMD ["/main"]

main.go:

package main

import (
	"os"
	"fmt"
)

func main() {
	var key string = os.Getenv("KEY")
	fmt.Println(key)
}

在这里插入图片描述

1.FROM golang:1.13.4-stretch AS worker:拿了个golang的镜像,取名叫worker

后面的操作都同test1讲的一样了。
copy文件、设置环境变量、设置工作目录、执行编译、第二台、从worker拿文件、再次设置一个环境变量、加权限、CMD。

build看看

docker build . -t mumuzi7760/test2

然后run起来

docker run -it --rm mumuzi7760/test2

在这里插入图片描述
可以看到只输出了最后的key,是来自于ubuntu的。

清理缓存:清理build中所有的CACHED、所有的

docker duilder prune
docker image prune
docker system prune

Docker compose(基本使用)

yaml:自己看文档

举例3(test3)

在这里插入图片描述

docker-compose.yml:

version: '3'
# Linux + Nginx + Mysql + PHP
services:
  nginx:
    build: ./nginx
    container_name: "nginx"
    ports:
      - 8080:80
    restart: always
    cap_add:
        - SYS_ADMIN
    depends_on:
      - php1
      - php2
    networks:
      default:
      my_net:
        ipv4_address: 172.2.0.3
  php1:
    build: ./php1
    container_name: nssctf
  php2:
    build: ./php2
    container_name: xenny
  mysql:
    build: ./mysql
    container_name: mysql
    environment:
      - MYSQL_ROOT_PASSWORD=password
      - MYSQL_DATABASE=app
    command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --sql-mode='' --max-execution-time=1000

networks:
  my_net:
    driver: bridge
    internal: true
    ipam:
      config:
        - subnet: 172.2.0.0/16

构造了一个LNMP(Linux + Nginx + Mysql + PHP)

1.version: '3':指定操作系统的版本

2.services:指定有多少服务,这里有nginx、php1、php2、mysql

3.build: ./nginx:从nginx目录里面去build,如果是已经存在的镜像,可以用image:imageName去拿

4.container_name: "nginx":运行起来的容器名字、主机名

5.ports: - 8080:80:指定映射,dockerfile就不能指定启动的操作,dockerfile如果要映射在run的时候必须自己指定,如docker run -p xxxx:xxxx,而docker-compose就可以像这样直接在里面指定8080:80

6.restart: always:服务挂了自动重启

7.cap_add:特权

8.depends_on::等php1和php2启动了再启动Nginx

9.networks:如果没有配置network,会把所有镜像单独配置在default网络中,这里还给Nginx单独配置了my_net网络,并配置了ip为172.0.0.3

10.environment: - MYSQL_ROOT_PASSWORD=password - MYSQL_DATABASE=app:指定环境变量

11.command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --sql-mode='' --max-execution-time=1000:跟参数就可以了。这里就相当于之前说的param CMD ["xxxx"] (给一个参数,后面需要有ENTRYPOINT)的用法

12.网络配置:名字my_net、网桥、internal:true不出网(默认出网)、配置ip地址

下面是文件夹中的部分文件内容:
nginx:Dockerfile:

FROM nginx:latest

COPY default.conf /etc/nginx/conf.d/default.conf

nginx:default.conf配置文件:

server {
	listen 80 default_server;
	listen [::]:80 default_server;

	server_name _;

	location /nssctf {
		proxy_pass http://nssctf/;
	}

	location /xenny {
		proxy_pass http://xenny/;
	}
}

location那里设置了反代、nssctf和xenny就是主机名。解析nssctf就会被解析到http://nssctf/中

php1:Dockerfile:

FROM php:7.2-apache

RUN docker-php-ext-install mysqli

COPY src/ /var/www/html

php1:src/index.php

<?php

$conn = mysqli_connect('mysql', 'root', 'password', 'app');
$query = "select * from user";
$result = mysqli_query($conn, $query);
$row = mysqli_fetch_array($result);

print_r($row);

去链接mysql。
当然php1的dockerfile里RUN docker-php-ext-install mysqli是可以不要的,docker-php-ext-install是去安装了一个拓展,这个命令在手册里面有讲解,需要用的时候再去看就可以了。

php2就和php1一样,但是少了RUN docker-php-ext-install mysqli,然后index.php的内容为echo “hello”;

mysql:dockerfile:

FROM mysql:5

COPY app.sql /docker-entrypoint-initdb.d/app.sql

mysql:app.sql:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(400) NOT NULL,
  `password` varchar(400) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `username` (`username`) USING HASH
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

BEGIN;
INSERT INTO `user` VALUES (1, 'nss', 'nssctf');
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

启动来看一下吧
进入test3文件夹,然后输入docker-compose up
中途遇到报错自行百度。
在这里插入图片描述
然后去访问一下8080,之前说过了有反代,所以访问那个地址去试试
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

没有问题啊。就通过这样的compose,就完成了还算毕竟复杂的操作。之前提到了网络,看一下他的网络如何。
首先是docker ps,记得开个新的终端
在这里插入图片描述
通过bin bash去Nginx看看

docker exec -it ID /bin/bash

比如我就要执行docker exec -it 91e /bin/bash
在这里插入图片描述
那么可以看到有两个ip,第一个172.18.0.5就是default的ip,而172.2.0.3就是自己配置的my_net

除了上面的指令以外,还有个指令

docker attach 91e

之前的docker exec -it ID /bin/bash其实并没有进入这个容器,而是弹了一个bash出来让我们能在容器里操作,而attach才算进入了容器内部。

CTF中的使用(不进行讲解,自行看代码)

举例4(test4)(PWN)

flag.sh:

#! /bin/bash

echo $FLAG > /home/ctf/flag.txt
FLAG=flag_not_here
export FLAG=flag_not_here
rm /home/ctf/flag.sh

service:题目

xingtd_config:

service ctf
{
	disable = no
	socket_type = stream
	protocol = tcp
	wait = no
	user = ctf
	type = UNLISTED
	bind = 0.0.0.0
	port = 9999
	server = /home/ctf/service
}

Dockerfile:

FROM ubuntu:18.04
RUN dpkg --add-architecture i386

ENV FLAG=NSSCTF{test_flag}

RUN apt update
RUN yes Y | apt-get install libc6:i386 libncurses5:i386 libstdc++6:i386
RUN apt-get install multiarch-support

RUN apt install -y xinetd

RUN groupadd -r ctf && useradd -r -g ctf ctf
RUN mkdir -p /home/ctf/

COPY service /home/ctf/
COPY flag.sh /home/ctf/
COPY xinetd_config /etc/xinetd.d/

RUN chown -R root:ctf /home/ctf/
RUN chmod -R 750 /home/ctf/
RUN chmod +x /home/ctf/flag.sh

EXPOSE 9999

CMD /home/ctf/flag.sh && service xinetd restart && /bin/sleep infinity

举例5(test5)(crypto)

Dockerfile:

FROM python:2-slim

COPY main.py /

ENV FLAG=NSSCTF{123456}

RUN sed -i s@/deb.debian.org/@/mirrors.aliyun.com/@g /etc/apt/sources.list && \
    sed -i s@/security.debian.org/@/mirrors.aliyun.com/@g /etc/apt/sources.list && \
    apt-get update -y && \
    apt-get install socat -y && \
    pip install pycryptodome

EXPOSE 9999
ENTRYPOINT socat TCP4-LISTEN:9999,tcpwrap=script,reuseaddr,fork EXEC:"python -u /main.py"

main.py:题目,这里不放了。想要完整打包代码可以进Xenny的群里看看。

一些docker指令操作

docker version:查看docker版本
docker images:查看docker镜像
docker run -it ubuntu /bin/sh:交互运行
docker exec -it ID /bin/bash:进入一个后台启动的容器
docker ps:查看运行的docker
docker log ID:查看docker输出
docker stop ID:停止容器,如果安装了docker desktop也可以在这里面关闭
docker start ID:开启一个已经停止的容器
docker export ID > 容器名称.tar:导出某个容器到压缩包

docker port ID:查看端口映射
docker top ID:查看内部进程
docker run -d -p 外部:内部 容器名称:映射端口运行容器 (这里就体现了docker-compose的好处了吧)

然后一般要用某些东西,比如想要安装pikachu可以这样:
docker search pikachu:搜索镜像
docker pull xxx/xxxx:获取镜像
docker rmi xxx:删除本地镜像

就像这样:
在这里插入图片描述
其他要用的到时候再去找百度吧,总之docker忒好用了。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐