Die Nutzung von Virtualisierung, insbesondere durch Docker, hat die Art und Weise, wie Software entwickelt, bereitgestellt und betrieben wird, revolutioniert und ist insbesondere aus Cloud-Umgebungen nicht mehr wegzudenken. Die Effizienz und Portabilität, die Docker bietet, sind jedoch nicht ohne ihre Herausforderungen, speziell im Hinblick auf die Sicherheit.
Zwei Einstellungen für Docker-Container sind besonders verbreitet: die Einbindung des Docker-Sockets und das Starten von Containern mit erhöhten Rechten. In diesem Blogartikel werfen wir einen genaueren Blick auf diese zwei kritischen Container-Einstellungen, und wie sie von Angreifern genutzt werden können, um von einem kompromittierten Container auf das Host-System überzugehen. Diese potenziellen Sicherheitslücken stellen nicht nur eine Bedrohung für die Integrität und Vertraulichkeit der Containerumgebung dar, sondern können auch erhebliche Risiken für das gesamte Host-System mit sich bringen.
Container mit erhöhten Rechten (“–privileged”)
Oftmals werden Docker Container mit dem Flag „–privileged“ gestartet, wodurch der Container „root“-Rechte auf dem Host-System erhält. Das kann zum Beispiel notwendig sein, wenn er direkten Zugriff auf Geräte des Host-Systems benötigt oder Änderungen an der Systemkonfiguration bewerkstelligen muss.
Eine Kompromittierung des Containers stellt dadurch jedoch ein unmittelbares Risiko für das Host-System dar, da Angreifer dadurch aus dem Container auf das Host-System eskalieren bzw. sich auch auf dem Host-System „root“-Rechte verschaffen können.
Dies wollen wir im Folgenden veranschaulichen. Dazu haben wir einen Container, der auf dem Docker-Image alpine von der offiziellen Docker-Registry basiert und auf dem wir OpenSSH nachinstalliert haben, mit dem “–privileged”-Flag gestartet. In einer interaktiven Shell im Container zeigt uns der Befehl „ls /dev“ einige Dateien:
In einem nicht-privilegierten Container wäre diese Anzahl deutlich geringer:
Jetzt möchten wir das Dateisystem des Hosts mounten. Dazu listen wir uns zunächst die verfügbaren Dateisysteme auf:
Die „sda“-Einträge sind für uns interessant, wovon bereits 3 Dateien gemountet sind. Wir erstellen einen neuen Ordner als Mount-Punkt und mounten „/dev/sda1“.
Jetzt können wir auf das gesamte Dateisystem des Host-Systems zugreifen:
Um nun eine Shell auf dem Host-System zu bekommen, ändern wir temporär den Kontext bzw. das Root-Verzeichnis auf das gemountete in /mnt/mymount, erstellen einen neuen User und fügen ihn als „sudoer“ hinzu, um ihm root-Rechte zu geben. Angemerkt sei, dass es ebenfalls möglich wäre, sich direkt einen root-User anzulegen, doch viele SSH-Server, mitunter unser Host-System, erlauben keinen root-Login mit Passwort. Deswegen erstellen wir einen normalen User und geben ihm “sudo”-Rechte.
Nun können wir uns über SSH auf das Host-System verbinden und Kommandos mit root-Rechten ausführen:
Docker-Socket “docker.sock”
Eine andere gern genutzte Einstellung ist das Einbinden des Docker-Sockets in Container. Das Socket ermöglicht die direkte Kommunikation mit dem Docker-Daemon (API), der auf dem Host-System läuft. Container, denen das Socket zur Verfügung gestellt wird, können darüber Docker-Management-Befehle ausführen. Das macht sie zu nützlichen Zielen für Angreifer, da damit z.B. Informationen über laufende Container ausgelesen, Container gestartet/gestoppt und nicht zuletzt Code auf dem Host-System ausgeführt werden kann.
Letzteres wollen wir im Folgenden demonstrieren und gehen dazu davon aus, dass wir nun ein Angreifer sind, der die Kontrolle über einen (nicht-privilegierten) Container ergreifen konnte. Wie im vorherigen Beispiel haben wir dazu einen alpine-Container gestartet und dabei das Argument „-v /var/run/docker.sock:/var/run/docker.sock“ mitgegeben, das dem Container das Docker-Socket bereitstellt (zudem haben wir die Tools socat und curl nachinstalliert).
Innerhalb des Containers kann die Docker-API nun über das Socket im HTTP-Protokoll angesprochen werden. Lassen wir uns zum Beispiel die auf dem Host-System verfügbaren Docker-Images anzeigen:
Im JSON-Format sehen wir zwei Images, „ubuntu“ und „alpine“. Prüfen wir die Ergebnisse mit dem korrespondierenden Docker-Befehl auf dem Host, sehen wir, dass es in der Tat die gleichen Ergebnisse sind:
Um Code-Ausführung auf dem Host-System zu erlangen, können wir mithilfe der API nach laufenden Containern suchen, die das Host-System bereits gemountet haben oder privilegiert laufen:
Wie der Output im Screenshot zeigt, laufen aktuell keine anderen Container außer unser Test-Container. Mithilfe der API können wir aber einen eigenen neuen Container starten, den wir so konfigurieren, dass das Host-Dateisystem eingebunden wird und als Kontext genutzt wird:
Die API erlaubt es außerdem, Befehle in Containern auszuführen. Um dies zu testen, initiieren wir einen Befehl, der den Namen des aktuellen Users abfragt und diesen in eine Datei schreibt:
Auf dem Host-System können wir sehen, dass die Datei angelegt und der Befehl unter „root“ ausgeführt worden ist:
Wir können über die API aber auch eine Shell herstellen, z.B. mit socat:
Wie der Befehl „ls /tmp“ zeigt, ist im „/tmp“-Verzeichnis auch die eben erstellte Testdatei zu finden, was zeigt, dass wir uns auf dem Host-Dateisystem befinden.
Auch ein Blick in die Datei /etc/passwd zeigt, dass es sich um die passwd-Datei des Host-Systems handelt, da wir darin einen Eintrag für den im vorherigen Beispiel erstellten User (“nside”) vorfinden:
Anzumerken ist, dass wir in unserem Beispiel zwar auf alle Dateien des Hosts zugreifen können, aber aufgrund von Prozessisolation nicht direkt mit den Host-Prozessen interagieren können, da wir uns noch im PID Namespace des Containers befinden. Diese letzte Hürde könnten wir nehmen, indem wir z.B. einen Cronjob anlegen, der dann vom Host in seinem eigenen PID Namespace ausgeführt wird.
Fazit
In diesem kurzen Überblick wurden 2 gängige Techniken gezeigt, wie man aus Docker-Container-Umgebungen ausbrechen kann.
Hier gibt es weitere unerwähnte Möglichkeiten für Docker oder z.B. auch Kubernetes-Cluster.
Auf Grund der Menge an vorhandenen Images, dem einfachen und schnellen Einstieg in die Materie und dem Standard-Vorgehen Container-Images mit root-Rechten laufen zu lassen, bieten Container-Umgebungen oft diverse Angriffspunkte und sollten mit Bedacht aufgesetzt und regelmäßig geprüft werden.
NSIDE bietet folgende Dienstleistungen, um Sie bei der Sicherheit Ihrer Container-Umgebungen zu unterstützen:
– Host-Audit mit Fokus auf Container-Sicherheit
– Cloud-Security-Audit mit Fokus auf Container-Sicherheit
Sprechen Sie uns an!