SSH RCE via Race Condition (CVE-2024-6387)
1 июля, исследователи Qualys обнаружили как добиться удаленного исполнения кода в SSH 8.5p1 через состояние гонки в обработчике сигнала SIGALRM.
Состояние гонки - когда несколько частей программы могут одновременно изменять один и тот же ресурс, без какой-либо синхронизации в формате "сначала меняю значение Я, а потом ТЫ"
Что интересно, эта уязвимость уже была в 2006, получив CVE-2006-5051 и будучи исправленной патчем 4.4p1
но потом "случайно" вернулась в прод, от чего Qualys прозвали уязвимость regreSSHion.
Опасный обработчик сигнала
В конфиге для sshd есть опция LoginGraceTime, описывающая за сколько секунд пользователь должен успеть аутентифицироваться перед его отключением от сервера. По умолчанию, за 120 секунд.
Если пользователь аутентифицируется больше 120 секунд, то асинхронно вызывается SIGALRM обработчик для прерывания соединения.
Однако, этот обработчки также прерывает синхронные функции, небезопасные для асинхронных сигналов (non-async-signal-safe), такие как syslog()
В Qualys обнаружили, что во время состояния гонки возможно добиться повреждения кучи с FILE структурой и перенаправить исполнения программы.
Повреждение кучи
В управлении памятью есть два основных концепта
И первая проблема у атакующих, это добиться определенного расположения чанков в куче памяти. Добиваются этого через длительный обмен валидных и не очень сертификатов, при обмене ключами для аутентификации.
В итоговой куче должен быть свободный кусок памяти в 8КБ и после свободный мелкий кусок в 320байт, разделенные "чанк барьерами" (barrier chunk), что позволяют создать отдельные свободные куски памяти
Прыгаем на подключение к серверу.
- атакующий превышает время для аутентификации во время подключения, указанное в LoginGraceTime
- вызывается асинхронный SIGALRM обработчик, что вызывает синхронный
syslog()
Здесь атакующий встречается с новым испытанием, ведь теперь ему надо попасть в исполнение malloc()
внутри syslog()
, между 4327 и 4339 строкой, когда malloc() аллоцировал память, но не успел сдвинуть её.
Что приводит к делению доступного нам куска памяти в 8КБ на 2 чанка по 4КБ. В одном аллоцированная память, а другой становится "свободным остатком"
И тут происходит странная ситуация. Прервав исполнение malloc()
, "свободный чанк" привязывается к списку "свободной для перезаписи памяти", в то время как атакующий всё ещё имеет доступ к этой области памяти.
Зная, что эта память перезапишется, атакующий искусственно увеличивает размер "свободного для перезаписи" чанка, чтобы тот захватывал мелкий кусок свободной памяти в 320бит, который был подготовлен ещё в начале атаки.
Думаешь хватит сложностей для атаки? Не тут то было
Эти все операции должны произойти перед тем, как асинхронный обработчик SIGALRM вызовет небезопасные для асинхронного вызова - синхронные функции __tzfile_read()
и fopen()
fopen()
аллоцирует свою FILE структуру в наш мелкий кусок памяти на 320бит__fread_unlocked()
перезапишет "свободный кусок памяти" с 4КБ буфером на чтение, затрагивая часть FILE структуры от fopen()
3. атакующий, имея доступ к буферу, перезаписывает часть FILE структуры и дальше, ещё более душно, перенаправляет flow исполнение программы куда ему нужно для исполнения кода
Вот и всё. Всего-то пару операций, туда-сюда и ты исполнился от рута!
Ладно, давай откроем форточку и суммируем атаку по картинке ниже
Так ли страшно?
Кроме того, что на системе должен быть glibc, большую роль играет архитектура процессора.
Для 32-битных систем
- 3-4 часа для победы в гонке за ~10 000 попыток, при 100 соединений в окне 120 секунд
- 6-8 часов чтобы найти нужный адрес и 1 раз исполнится от рута
Для 64-битных систем
Теперь точно всё. Если твой голод подобных разборов не утолился, то прочитай мой более простой разбор нашумевшего бэкдора в библиотеке xz
Появились вопросы по уязвимости? Читай разбор от Qualys
Появились вопросы не по уязвимости? Пиши @w0ltage