
Подробное описание и эксплойт для доказательства концепции для 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, и argv[1] указывает на первую переменную окружения! При чтении argv[1] происходит выход за границы массива — читается environ[0].
При запуске pkexec из командной строки argc равно 1, argv[0] — путь к pkexec:

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

К чему это приводит? Когда 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 через эти переменные, что могло бы привести к выполнению вредоносного кода и повышению привилегий.