
Analyse détaillée et preuve de concept d'exploitation pour CVE-2021-4034 (élévation de privilèges locale PolKit pkexec), incluant un environnement de laboratoire Docker pour une analyse et un débogage pratiques.
[toc]
Identifiant de la vulnérabilité : CVE-2021-4034
Score de la vulnérabilité :
Produit concerné : linux PolKit (pkexec)
Versions affectées : toutes les versions de 2009 à aujourd'hui (actuellement 0.105) Référence : http://its.dlut.edu.cn/info/1054/78309.htm
Conditions d'exploitation : linux local ; pkexec est un fichier suid avec droit d'exécution
Obtention du code source : apt source policykit-1
ou https://launchpad.net/ubuntu/bionic/+package/policykit-1
Environnement docker : chenaotian/cve-2021-4034
J'ai monté moi-même ce docker, il fournit :
pkexec compilé moi-même, débogable au niveau du code sourceTout se trouve dans le répertoire /root/ :

su test puis l'exécuterDémarrage de docker :
docker run -d -ti --rm -h cvedebug --name cvedebug --cap-add=SYS_PTRACE chenaotian/cve-2021-4034:latest /bin/bash
Test de l'exp :
cd ~/exp/CVE-2021-4034/
./run.sh
su test
./exp
whoami
Le produit concerné par la vulnérabilité est la commande pkexec de polkit. pkexec, comme sudo, est un outil qui nous permet d'exécuter des commandes en tant qu'un autre utilisateur (généralement root). La commande dpkg permet de voir le paquet auquel appartient pkexec :
dpkg -S /usr/bin/pkexec

Ensuite, on récupère le paquet source (il est aussi présent dans mon docker), puis on compile à partir du paquet source une version débogable pour faciliter le débogage.
Le principe de déclenchement de la vulnérabilité est très simple
/polkit-0.105/src/programs/pkexec.c : 386 main
int
main (int argc, char *argv[])
{
··· ···
··· ···
/* 这段的意思就是,循环遍历用户输入参数,根据输入的不同参数设置值
* 但问题在于,他循环遍历的起点是1,没有考虑用户没有输入任何参数的情况
*/
for (n = 1; n < (guint) argc; n++)
{
if (strcmp (argv[n], "--help") == 0)
{
opt_show_help = TRUE;
}
··· ···
else //如果是无法识别的参数则跳出循环,这里意味着该参数是想要执行的命令
{
break;
}
}
··· ···
g_assert (argv[argc] == NULL);
path = g_strdup (argv[n]); //获取执行命令具体字符串
if (path == NULL)
{
···
}
if (path[0] != '/')
{
/* g_find_program_in_path() is not suspectible to attacks via the environment */
//该函数会根据PATH环境变量寻找要执行命令的绝对地址
s = g_find_program_in_path (path);
if (s == NULL)
{
···
}
g_free (path);
argv[n] = path = s;//把获取到的绝对地址修改回命令行参数
}
··· ···
··· ···
Selon l'analyse de mes commentaires dans le code :
pkexec)-- est rencontré, il est considéré comme la commande à exécuter avec pkexec, et on sort de la boucle pour passer à la logique suivante.g_find_program_in_path est appelée pour rechercher le chemin absolu de la commande. Cette fonction cherche le chemin absolu de l'argument (commande) passé en utilisant la variable d'environnement PATH. Par exemple, si cat est passé, elle renvoie /bin/cat.C'est assez facile à comprendre, mais le problème est le suivant :
Lorsqu'un binaire Linux s'exécute, les arguments de ligne de commande argv[] et les variables d'environnement environ[] sont placés en bas de la pile, et argv[] et environ[] sont contigus. Le dernier élément de argv[] est null.

Si pkexec est lancé depuis la ligne de commande sans aucun autre argument, alors argv[0] vaut "pkexec" et argv[1] vaut \x00, aucun problème. Mais si pkexec est lancé via la fonction execve sans aucun autre argument, alors argv[0] vaut \x00, et argv[1] pointe vers les variables d'environnement ! Lors de la lecture de argv[1], on lit hors limites environ[0].
Lors d'un lancement direct de pkexec depuis la ligne de commande, argc vaut 1 et argv[0] est le chemin de pkexec :

Avec la fonction execve pour lancer pkexec, argc vaut 0 :

Quel est alors l'impact ? Lors du lancement avec execve, sans aucun autre argument, la longueur de argv[] est 0, donc argv[1] correspond à environ[0]. La logique analysée ci-dessus devient alors : récupérer la valeur de la première variable d'environnement et rechercher son chemin absolu dans la variable PATH. S'il est trouvé, l'écrire dans la première variable d'environnement. La méthode d'exploitation est alors la suivante :
Tout d'abord, il faut préciser que pkexec est un fichier privilégié (suid) :

Comment utiliser les variables d'environnement dans un fichier privilégié pour faire quelque chose ? Commençons par comprendre un petit détail :
Le linker dynamique de Linux ld-linux-x86-64.so.2 supprime les variables d'environnement sensibles lors de l'exécution d'un programme privilégié :
Fonction _dl_non_dynamic_init : glibc-2.27/elf/dl-support.c : 307
void
_dl_non_dynamic_init (void)
{
··· ···
··· ···
if (__libc_enable_secure) //特权模式的情况下
{
static const char unsecure_envvars[] =
UNSECURE_ENVVARS
#ifdef EXTRA_UNSECURE_ENVVARS
EXTRA_UNSECURE_ENVVARS
#endif
;
const char *cp = unsecure_envvars;
//循环将危险环境变量列表中的环境变量全部清空(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 liste des variables d'environnement dangereuses UNSECURE_ENVVARS est définie comme suit :
glibc-2.27/sysdeps/generic/unsecvars.h : 10