tom@home.htb:~$

Blog o HTB

2 January 2021

Container boundary mistakes: bind mounty, `docker exec`, `runc`, `privileged`

Úvod a kontext

Kontejner se často popisuje jako bezpečnostní hranice. V praxi je to ale jen část pravdy. Kontejner umí dobře oddělit procesy a filesystem, pokud je kolem něj rozumně navržené celé prostředí. Jakmile se do hry přidají bind mounty z hosta, přehnaná práva runtime nebo delegovaná správa přes docker exec, začne se hranice rozpadat.

Ready, TheNotebook a Registry ukazují tři různé způsoby, jak k tomu dochází. Jednou je problém přímo v privileged: true, podruhé v hostem mountnuté cestě a právu spouštět docker exec, potřetí v tom, že image registry a build artefakty vydají tajemství, která přesahují samotný kontejner. Nejde tedy jen o klasické “escape z Dockeru”. Jde o širší chybu návrhu: systém se tváří, že kontejner odděluje role, ale okolní provozní vrstva to sama popře.

Co vlastně kontejner odděluje a co už ne

Samotný kontejner typicky odděluje:

To ale ještě neznamená, že odděluje:

Když se řekne “kontejnerová hranice”, vyplatí se proto ptát ve čtyřech vrstvách:

  1. co je uvnitř image,
  2. co je mountnuté z hosta,
  3. co umí dělat runtime a kdo ho smí ovládat,
  4. s jakými právy kontejner skutečně běží.

Nejčastější chyby, které hranici bortí

1. Bind mount mění zápis v kontejneru na efekt na hostu

To, že aplikace běží v kontejneru, ještě neznamená, že zapisuje jen do kontejnerového filesystemu. Pokud je upload adresář nebo jiná cesta bind mountnutá z hosta, může útočníkův zápis uvnitř kontejneru skončit jako host-level artefakt.

2. privileged z kontejneru dělá téměř hosta

privileged: true není běžná optimalizace. Je to zásadní oslabení hranice. Root uvnitř takového kontejneru má často velmi krátkou cestu k host filesystemu, zařízením nebo dalším nízkoúrovňovým primitivům.

3. docker exec pod sudo je správa hosta, ne “jen kontejner”

Kdo smí přes sudo spouštět docker exec, nedostává jen omezené právo “podívat se do kontejneru”. Dostává přístup k vysoce privilegované runtime vrstvě hosta. Na starších verzích může jít o přímý exploit chain, ale i bez něj je to stále velmi silné oprávnění.

4. Image a registry patří do stejného trust modelu

Hranice se může rozpadnout ještě před spuštěním kontejneru. Jakmile registry nebo image vrstvy vydají shell historii, privátní klíče nebo provozní skripty, izolace runtime už situaci nezachrání. Útočník dostane tajemství, která platí mimo samotný kontejner.

Jak tenhle vzorec vypadal v praxi

TheNotebook: bind mount a host-level vykonání PHP

TheNotebook je velmi dobrý příklad toho, že mountovaná cesta je součást bezpečnostní hranice. Aplikace běžela v Dockeru, ale upload adresář byl bind mountnutý z hosta a nginx na hostu přímo zpracovával .php soubory z této cesty. To změnilo význam uploadu: nahraný rev.php se nespouštěl uvnitř kontejneru, ale na hostitelském systému.

To je důležitý detail. Nešlo o “escape z kontejneru” v tradičním smyslu. Šlo o to, že hranice byla sama navržená tak, že zápis v kontejneru okamžitě získal efekt na hostu.

TheNotebook podruhé: docker exec a starý runc

Stejný stroj přidal i druhou variantu. Uživatel noah mohl přes sudo spouštět:

(ALL) NOPASSWD: /usr/bin/docker exec -it webapp-dev01*

Na hostu běžela stará verze Dockeru a runc, takže šlo zneužít CVE-2019-5736. Podstatná lekce ale není samotné číslo CVE. Podstatné je, že docker exec je správní operace nad runtime hosta. Jakmile k ní má běžný uživatel přístup, je hranice mezi hostem a kontejnerem už velmi tenká.

Ready: root v kontejneru + privileged: true

Ready ukazuje nejpřímočařejší rozpad hranice. Foothold vznikl v GitLab kontejneru, poté se z gitlab.rb získalo heslo a útočník se stal rootem uvnitř kontejneru. Samotný root v kontejneru by ještě automaticky neznamenal host kompromis. Rozhodující byl až obsah docker-compose.yml:

privileged: true

To otevřelo přímou cestu k host filesystemu, například přes mount disku:

mount /dev/sda2 /mnt

Na tomhle příkladu je krásně vidět, že problém neležel jen v aplikaci. Skutečný dopad určila až provozní konfigurace kontejneru.

Registry: image vrstvy a tajemství přesahující kontejner

Registry není klasický container escape, ale je důležité ho do této rodiny zahrnout. Veřejně dostupná Docker registry vydala image vrstvu, v níž byly:

Tím se ukázalo, že hranice kontejneru nezačíná až při běhu procesu. Začíná už při build pipeline, image layer a tom, co se do ní dostane. Pokud image obsahuje host-relevantní tajemství nebo provozní klíče, izolace runtime je už jen částečná obrana.

Právě proto dává smysl číst Registry jako třetí typ container boundary chyby: nikoli únik z runtime, ale únik z container lifecycle.

Jak podobné chyby hledat po footholdu

Po shellu v aplikaci nebo v kontejneru se vyplatí systematicky projít čtyři vrstvy.

1. Ověřit, zda opravdu běžíte v kontejneru

Pomohou obvyklé indicie:

2. Zjistit, co je mountnuté z hosta

Právě bind mount často rozhoduje, zda zápis v kontejneru zůstane lokální, nebo zasáhne hosta. Hodnotné jsou hlavně:

3. Projít runtime oprávnění

Kritické jsou hlavně:

4. Podívat se na image a build artefakty

Když je dostupná registry, image nebo lokální cache vrstev, má smysl číst i je. Často vydají:

Jak si to nesplést s příbuznými tématy

Tento okruh se přirozeně překrývá s jinými články, ale je dobré držet rozdíly.

Obrana a bezpečnější návrh

1. Bind mounty používat minimálně a cíleně

Každý hostem mountnutý adresář je součást bezpečnostní hranice. Pokud aplikace zapisuje do mountu a host stejnou cestu interpretuje jiným runtime, je to vysoce rizikový návrh.

2. privileged: true brát jako výjimku nejvyšší závažnosti

Privilegovaný kontejner nemá být pohodlný default. Má být výjimečný, zdokumentovaný a pravidelně auditovaný stav.

3. Nedávat běžným účtům správu Docker runtime

docker exec, docker run, přístup k docker.sock a podobné operace mají bezpečnostní význam blízký rootovi. Obranně je potřeba auditovat je stejně přísně.

4. Image vrstvy chránit jako produkční tajemství

Registry, build cache a image historie nesmějí obsahovat klíče, hesla, shell historii ani jiné provozní artefakty, které překračují roli samotné aplikace.

5. Threat model stavět přes celý container lifecycle

Bezpečnost kontejneru nezačíná při docker run. Začíná už v image, pokračuje přes mounty a orchestrace a končí až u runtime práv na hostu.

Shrnutí klíčových poznatků

Co si odnést do praxe

tags: containers - docker - runc - bind-mount - privileged - privesc