
Отчет об анализе CVE-2023-4911 (Looney Tunables) и лаборатория воспроизведения в Docker
| Параметр | Значение |
|---|---|
| CVE ID | CVE-2023-4911 |
| Тип атаки | Переполнение буфера в куче → локальное повышение привилегий (Local Privilege Escalation) |
| CVSS 3.1 | 7.8 (Высокий) |
| Дата публикации | 2023-10-03 |
| Уязвимая точка | Парсер GLIBC_TUNABLES динамического загрузчика glibc (ld.so) |
| Уязвимые версии | glibc 2.34~2.38 |
CVE-2023-4911 — это уязвимость переполнения буфера в куче, возникающая при парсинге переменной окружения GLIBC_TUNABLES динамическим загрузчиком GNU C Library (glibc). Используя это переполнение, атакующий может манипулировать путём поиска библиотек (RPATH) динамического загрузчика, чтобы при выполнении SUID root-бинарника (su, sudo и т.д.) загружалась вредоносная разделяемая библиотека, подготовленная атакующим, что позволяет выполнить произвольный код с правами root. Поскольку glibc является ключевым компонентом практически всех основных дистрибутивов Linux, данная уязвимость затронула большинство дистрибутивов на основе glibc, выпущенных после апреля 2021 года.
while (true)
{
char *name = p;
size_t len = 0;
/* Нахождение длины имени (name) */
while (p[len] != '=' && p[len] != ':' && p[len] != '\0')
len++;
/* Если строка заканчивается без '=', завершаем */
if (p[len] == '\0')
{
if (__libc_enable_secure)
tunestr[off] = '\0';
return;
}
/* Если первой встретилась ':', значит неверная запись */
if (p[len] == ':')
{
p += len + 1;
continue;
}
/* Встретили '=', перемещаемся к началу значения */
p += len + 1;
/* Вычисляем значение из исходной строки */
char *value = &valstring[p - tunestr];
len = 0;
/* Нахождение длины значения */
while (p[len] != ':' && p[len] != '\0')
len++;
...
/* Копирование в tunestr */
...
if (p[len] != '\0')
p += len + 1;
}
__tunables_init() находит GLIBC_TUNABLES в списке переменных окружения.tunables_strdup() выделяет буфер с помощью __minimal_malloc() и копирует исходную строку (на этом этапе malloc — это очень ранняя реализация, ещё не полностью инициализированная).parse_tunables() обходит этот буфер, разделяя его по символу : (двоеточие) и присваивает значения соответствующим tunables для каждой пары key=value.parse_tunables() обрабатывает один tunable в последовательности: разбор имени → перемещение p → разбор значения → перемещение p. При нормальном вводе после обработки значения p перемещается на начало следующего tunable для его парсинга.
Однако если подать входные данные вида name=name=value:
GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=AAAA...(длинная строка)
В первом проходе парсинга вся строка glibc.malloc.mxfast=AAAA... интерпретируется как одно значение и копируется в буфер tunestr. После этого, поскольку за значением не следует двоеточие (:), разделяющее следующий tunable, указатель парсинга (p) не перемещается на следующий элемент, а снова указывает на начало уже скопированного значения.
Проблема в том, что само значение имеет форму name=value. На следующей итерации парсер ошибочно распознаёт это как новый tunable и повторно записывает данные в буфер. Так как tunestr выделен только под размер исходной строки, повторная запись приводит к переполнению буфера в куче.
Возникшее переполнение буфера в куче перезаписывает соседние области кучи, выделенные последовательно с помощью __minimal_malloc(). Используя это, атакующий может изменить указатель l_info[DT_RPATH] внутренней структуры link_map динамического загрузчика (ld.so) на контролируемый им стековый адрес.
В этой области стека заранее размещается поддельная структура Elf64_Dyn, которая указывает каталог, заданный атакующим, в качестве нового пути поиска библиотек (RPATH). В результате ld.so загружает вредоносную разделяемую библиотеку, подготовленную атакующим, вместо системных.
GLIBC_TUNABLES вида name=name=value.su) с правами обычного пользователя.parse_tunables() ld.so происходит переполнение буфера в куче.l_info[DT_RPATH] структуры link_map на стековый адрес, где находится поддельная структура Elf64_Dyn.libc.so.6, подготовленной атакующим.libc.so.6 выполняется с правами root, производя setuid(0), setgid(0) и запуская /bin/sh.PoC использует метод брутфорса, повторяя execve() до тех пор, пока не сформируется нужный макет памяти из-за влияния рандомизации (ASLR). Таким образом, успех атаки и время её выполнения зависят от окружения; обычно требуется от нескольких сотен до нескольких тысяч попыток.
Сначала клонируем содержимое из git в директорию
git clone https://github.com/baeseungwon1010/CVE-2023-4911

Собираем docker-образ с помощью следующей команды
cd C* && docker compose run --rm cve-2023-4911-lab

После входа в контейнер запускаем эксплойт
cd /home/student/exploit && ./exp
После запуска и ожидания видно, что обычный пользователь изменился на sudo(0)

Обновить версию glibc с уязвимой на исправленную. После обновления желательно перезагрузить систему или перезапустить службы, чтобы предыдущая версия glibc не осталась в памяти. Если немедленное обновление невозможно, в качестве временной меры можно удалить ненужные процессы SUID, SGID.
su, поэтому получение root-оболочки происходит без проверки пароля.