
Analisi tecnica dettagliata ed exploit funzionante per CVE-2022-0492, fuga dal container del kernel Linux tramite cgroup release_agent, con configurazione lab passo-passo e indicazioni di mitigazione.
[toc]
ID Vulnerabilità: CVE-2022-0492
Prodotto Vulnerabile: kernel linux - cgroup
Versioni Interessate: ~linux kernel 5.17-rc3
Impatto Vulnerabilità: Quando il container non ha misure di sicurezza aggiuntive attive, ottenere i permessi di root all'interno del container permette di fuggire all'host.
Su una macchina Linux con una versione del kernel vulnerabile, utilizzare Docker.
# Avvia Docker disabilitando tutte le protezioni di sicurezza
docker run --rm -it -h cve --name cve --security-opt="seccomp=unconfined" --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash
Questo articolo utilizza Docker come ambiente sperimentale.
Il metodo di sfruttamento di questa vulnerabilità è già ben noto, ma il punto chiave è la mancanza di verifica dei permessi per la modifica di release_agent in cgroup, che abbassa ulteriormente la soglia per lo sfruttamento (prima era necessaria la capacità CAP_SYS_ADMIN, con questa vulnerabilità non serve). Per le differenze specifiche nelle precondizioni di sfruttamento, vedere la sezione "Condizioni di Sfruttamento" di seguito.
Analizzando la patch, è stata modificata la funzione cgroup_release_agent_write, aggiungendo l'autenticazione. Ciò significa che release_agent di cgroup non può più essere modificato da utenti senza i permessi necessari:

Quindi la vulnerabilità è determinata da un controllo di accesso inefficace.
cgroup (Control Group) è una funzionalità del kernel Linux utilizzata per limitare, controllare e separare le risorse (come CPU, memoria, I/O su disco) di un gruppo di processi.
cgroup ha i seguenti sottosistemi:
devices – permessi sui dispositivi a livello di processocpuset – assegna le CPU e i nodi di memoria che un processo può utilizzarecpu – controlla l'utilizzo della CPUcpuacct – statistiche sull'uso della CPU, ad esempio tempo di esecuzione, tempo throttledmemory – limita l'utilizzo massimo di memoriafreezer – sospende i processi in un cgroupnet_cls – insieme a tc (traffic controller) limita la larghezza di banda di retenet_prio – imposta la priorità del traffico di rete dei processihuge_tlb – limita l'utilizzo di HugeTLBperf_event – consente a Perf di eseguire misurazioni basate sui gruppi cgroupI cgroup dell'host si trovano tutti in /sys/fs/cgroup, dove si possono vedere i vari sottosistemi cgroup:

I cgroup corrispondenti in Docker sono nodi figli di quelli dell'host. Visualizzazione del cgroup di memoria in Docker:

Il nodo corrispondente al nome del contenitore nella directory Docker dell'host è identico:

cgroup viene utilizzato tramite un file system. Montando cgroup in una directory tramite mount, cgroup interagisce con noi attraverso il filesystem virtuale VFS. Le interfacce di cgroup sono presentate sotto forma di file, quindi si utilizzano le normali operazioni sui file per impostare i parametri di cgroup.
mount -t cgroup -o memory cgroup /tmp/testcgroup

È possibile creare nodi figli di cgroup creando sottodirectory nella directory: mkdir /tmp/testcgroup/x.
Ogni sottosistema di cgroup ha il parametro notify_on_release, di tipo Boolean (1 o 0). Può rispettivamente abilitare o disabilitare l'esecuzione del comando di rilascio. Se notify_on_release è abilitato (1), quando il cgroup non contiene più alcun task (cioè quando l'ultimo processo nel cgroup termina e il PID nel file tasks del cgroup è vuoto), il kernel esegue il contenuto del file specificato dal parametro release_agent. Il valore di notify_on_release viene modificato attraverso il file notify_on_release.

La vulnerabilità risiede nella modifica di release_agent. In origine, chiunque potesse operare su cgroup poteva modificare release_agent, ma per usare cgroup era necessaria la capacità CAP_SYS_ADMIN. Successivamente, i ricercatori hanno scoperto che creando un nuovo namespace con il comando unshare si ottengono tutte le capabilities, quindi la restrizione CAP_SYS_ADMIN viene aggirata, abbassando notevolmente la soglia di sfruttamento.
unshareIl comando unshare ha la funzione di rimuovere la condivisione di namespace specificati dal processo padre, per poi eseguire il programma specificato nel nuovo namespace creato. Rilevante per il nostro sfruttamento: il nuovo namespace creato da unshare possiede tutte le capabilities, inclusa CAP_SYS_ADMIN.

Lo sfruttamento di questa vulnerabilità è uguale al tradizionale metodo di fuga con CAP_SYS_ADMIN + release_agent di cgroup, ma le condizioni di sfruttamento differiscono.
Le differenze tra le condizioni di sfruttamento di questa vulnerabilità e quelle del tradizionale release_agent sono:
Tradizionale release_agent: Il container deve avere CAP_SYS_ADMIN e non deve avere apparmor o selinux attivi.
CVE-2022-0492: Il container deve essere "nudo" (più precisamente: seccomp non deve disabilitare unshare, apparmor non deve impostare cgroup in sola lettura, selinux disattivato), e si ottiene l'accesso root all'interno del container. Non è necessario ottenere CAP_SYS_ADMIN.
È interessante notare che il profilo apparmor predefinito di Docker imposta cgroup in sola lettura, e il profilo seccomp predefinito di Docker disabilita unshare quando non si ha CAP_SYS_ADMIN. Kubernetes di default di solito esegue container "nudi". In ogni caso, poiché lo sfruttamento è relativamente semplice, si può provare in scenari specifici.
Dopo la correzione della vulnerabilità: Secondo il codice della patch:

Per modificare il file release_agent sono necessarie due condizioni:
CAP_SYS_ADMINPertanto, dopo la correzione, la capacità CAP_SYS_ADMIN ottenuta tramite unshare non può più essere utilizzata per modificare release_agent, perché il nuovo namespace creato da unshare non è il namespace radice. Tuttavia, se il container possiede già la capacità CAP_SYS_ADMIN, può ancora utilizzare questo metodo di fuga.
CAP_SYS_ADMINSe il container è stato avviato con il parametro --cap-add=SYS_ADMIN o --privileged (container privilegiato), possiede già CAP_SYS_ADMIN e non è necessario ottenerlo ulteriormente. Ad esempio con il comando di avvio: