
Write-up dettagliato ed exploit proof-of-concept per CVE-2021-4034 (escalation dei privilegi locali PolKit pkexec), incluso un ambiente di laboratorio Docker per analisi e debug pratici.
[toc]
ID vulnerabilità: CVE-2021-4034
Punteggio vulnerabilità:
Prodotto vulnerabile: linux PolKit (pkexec)
Versione interessata: versioni dal 2009 a oggi (attuale 0.105) riferimento http://its.dlut.edu.cn/info/1054/78309.htm
Condizioni di sfruttamento: linux locale; pkexec è un file suid con permessi di esecuzione
Ottenere il sorgente: apt source policykit-1
oppure https://launchpad.net/ubuntu/bionic/+package/policykit-1
Ambiente docker: chenaotian/cve-2021-4034
Ho creato io stesso questo docker, che fornisce:
pkexec compilato da me e con sorgenti per il debugTutto si trova nella directory /root/:

su test e poi eseguirloAvvio di docker:
docker run -d -ti --rm -h cvedebug --name cvedebug --cap-add=SYS_PTRACE chenaotian/cve-2021-4034:latest /bin/bash
Test dell'exp:
cd ~/exp/CVE-2021-4034/
./run.sh
su test
./exp
whoami
Il prodotto interessato dalla vulnerabilità è il comando pkexec di polkit. pkexec, come sudo, è uno strumento che ci consente di eseguire comandi come un altro utente (di solito root). Con il comando dpkg possiamo vedere a quale pacchetto appartiene pkexec:
dpkg -S /usr/bin/pkexec

Poi si ottiene il pacchetto sorgente (è presente anche nel mio docker), quindi, a partire dal pacchetto sorgente, si compila una versione debuggabile per facilitare il debug.
Il principio che innesca la vulnerabilità è molto semplice
/polkit-0.105/src/programs/pkexec.c : 386 main
int
main (int argc, char *argv[])
{
··· ···
··· ···
/* Questo blocco significa: scorre in ciclo i parametri inseriti dall'utente e imposta i valori in base ai diversi parametri ricevuti.
* Ma il problema è che il ciclo parte da 1 e non considera il caso in cui l'utente non abbia inserito alcun parametro.
*/
for (n = 1; n < (guint) argc; n++)
{
if (strcmp (argv[n], "--help") == 0)
{
opt_show_help = TRUE;
}
··· ···
else //se è un parametro non riconosciuto esce dal ciclo; questo significa che quel parametro è il comando che si vuole eseguire
{
break;
}
}
··· ···
g_assert (argv[argc] == NULL);
path = g_strdup (argv[n]); //ottiene la stringa specifica del comando da eseguire
if (path == NULL)
{
···
}
if (path[0] != '/')
{
/* g_find_program_in_path() non è attaccabile tramite l'ambiente */
//questa funzione cerca il percorso assoluto del comando da eseguire in base alla variabile d'ambiente PATH
s = g_find_program_in_path (path);
if (s == NULL)
{
···
}
g_free (path);
argv[n] = path = s;//riscrive il percorso assoluto ottenuto nel parametro della riga di comando
}
··· ···
··· ···
In base ai commenti che ho aggiunto nel codice, l'analisi è:
for parte da 1, cioè si presuppone che venga fornito almeno un parametro (il comando da eseguire con pkexec)--, lo considera come il comando da eseguire e quindi esce dal ciclo per passare alla logica successiva.g_find_program_in_path per cercare il percorso assoluto del comando. La funzione g_find_program_in_path cerca il percorso assoluto del parametro ricevuto (comando) in base alla variabile d'ambiente PATH. Ad esempio, se riceve cat restituisce /bin/cat.È comunque facile da capire, ma il problema è:
quando un programma binario linux viene eseguito, i parametri della riga di comando argv[] e le variabili d'ambiente environ[] vengono posti in cima allo stack, e argv[] e environ[] sono adiacenti. L'ultimo elemento di argv[] è null.

Se pkexec viene avviato dalla riga di comando senza altri parametri, allora argv[0] è "pkexec" e argv[1] è \x00: nessun problema. Ma se pkexec viene avviato con la funzione execve senza altri parametri, allora argv[0] è \x00 e argv[1] punta già alle variabili d'ambiente! Quando viene letto argv[1], si esce dai limiti e si legge environ[0].
Avvio diretto di pkexec dalla riga di comando: argc è 1 e argv[0] è il percorso di pkexec:

Avvio di pkexec con la funzione execve: argc è 0:

Allora che effetto produce tutto questo? Quando si avvia con execve senza passare altri parametri, la lunghezza di argv[] è 0, quindi argv[1] corrisponde a environ[0]. In questo modo la logica analizzata sopra diventa: otterrebbe il valore della prima variabile d'ambiente e ne cercherebbe il percorso assoluto nella variabile d'ambiente PATH. Se lo trova, riscrive il valore nella prima variabile d'ambiente. Il metodo di sfruttamento è quindi il seguente:
La prima cosa da chiarire è che pkexec è un file privilegiato (suid):

Come si può usare una variabile d'ambiente per fare qualcosa in un file privilegiato? Per prima cosa, vediamo un piccolo dettaglio:
Il linker dinamico di linux ld-linux-x86-64.so.2, quando viene eseguito un programma privilegiato, elimina le variabili d'ambiente sensibili:
funzione _dl_non_dynamic_init: glibc-2.27/elf/dl-support.c : 307
void
_dl_non_dynamic_init (void)
{
··· ···
··· ···
if (__libc_enable_secure) //nel caso di modalità privilegiata
{
static const char unsecure_envvars[] =
UNSECURE_ENVVARS
#ifdef EXTRA_UNSECURE_ENVVARS
EXTRA_UNSECURE_ENVVARS
#endif
;
const char *cp = unsecure_envvars;
//scorre in ciclo tutte le variabili d'ambiente nella lista delle variabili pericolose e le azzera (unset)
while (cp < unsecure_envvars + sizeof (unsecure_envvars))
{
__unsetenv (cp);
cp = (const char *) __rawmemchr (cp, '\0') + 1;
}
#if !HAVE_TUNABLES
if (__access ("/etc/suid-debug", F_OK) != 0)
__unsetenv ("MALLOC_CHECK_");
#endif
}
··· ···
··· ···
}