5 ноября 2026 года вышел экстренный OpenSSH 10.6p2, закрывающий замечательную дыру: на Linux с musl libc и без PAM можно было войти в учётную запись с заблокированным паролем, предъявив практически любую строку длиной более 256 байт.
И особенно интересна тут история появления уязвимости. Не столько сам баг, сколько последовательность решений, в результате которых система вместо отказа в аутентификации радостно сообщала: Welcome!
1. Начнём с Unix и /etc/shadow.
Вместо настоящего хеша пароля может использоваться *. Это означает, что парольная аутентификация фактически запрещена: ни один настоящий хеш не должен совпасть с этим значением.
Причём сама учётная запись не заблокирована — например, вход по SSH‑ключу вполне допустим.
2. В 2012 году разработчики musl решили бороться с падениями программ.
По контракту crypt() при ошибке возвращает NULL. Но… некоторые программы результат не проверяли, после чего закономерно падали.
В musl решили, что возвращать NULL — нехорошо, невежливо практически. Лучше вернуть строку, которая заведомо не может соответствовать настоящему хешу пароля.
И выбрали… да — *.
В том же году в реализации SHA-512 crypt установили ограничение длины пароля в 256 байт, чтобы слишком длинными паролями нельзя было устраивать DoS через дорогостоящее хеширование.
Получилось следующее: crypt(long_password, “…”) // “*”
То есть ошибка вычисления теперь возвращает ровно то же значение, которым в файле паролей в Unix обозначен пароль «хрен подберёшь».
3. В 2016 году OpenSSH исправляет timing attack (CVE-2016-6210).
Проблема заключалась в возможности определить существование пользователя по времени проверки пароля.
Для учётных записей без пригодного хеша OpenSSH и раньше вызывал crypt(), используя фиксированную соль "xx".
Она выбирала старый DES crypt, использующий только первые восемь байт пароля. Хоть тысячу символов передавай — результатом будет обычный 13-символьный хеш, а не ошибка.
Но время вычисления DES и SHA-512 различается. Поэтому для устранения timing attack OpenSSH научили подбирать соль от другой учётной записи с настоящим хешем.
Например, $6$..., включающую SHA-512 crypt.
И вот здесь изменения 2012 и 2016 годов наконец встретились.
4. Добро пожаловать, лишний ляп!
Допустим, у пользователя в /etc/shadow стоит *.
Атакующий предъявляет пароль длиной 300 байт.
OpenSSH вызывает crypt() с солью SHA-512. musl обнаруживает превышение лимита и возвращает *.
OpenSSH сравнивает результат с сохранённым значением:
stored = "*"; result = crypt(long_password, "$6$..."); // result == "*" strcmp(result, stored) == 0; // Authentication successful!
Всё. Ошибка вычисления превратилась в успешную аутентификацию.
Заметьте, дело не в том, что атакующий подобрал пароль или каким‑то чудом воспроизвёл его хеш. Хеш вообще не был вычислен. Библиотека сообщила об ошибке, а вызывающая программа приняла её за совпадение.
Чем‑то напоминает знаменитый Apple goto fail, только там проверку пропустили, а здесь проверили результат ошибки — и остались довольны.
5. Кстати, механизм настоящей блокировки учётной записи в Unix тоже есть.
В том же /etc/shadow предусмотрено отдельное поле sp_expire — срок действия самой учётной записи.
И здесь принципиальная разница:
*в поле пароля запрещает парольную аутентификацию, но позволяет использовать другие способы входа.Истёкший
sp_expireзапрещает вход вообще, независимо от способа аутентификации.
OpenSSH при соответствующей поддержке shadow expiration проверяет это ещё до аутентификации. И через sshhole в такую учётную запись уже не попасть.
Так что заменить одно другим бездумно тоже нельзя: пользователю, которому разрешён вход исключительно по ключу, полная блокировка учётной записи не подходит.
То есть у системы существуют два разных механизма с разными задачами. И ошибка как раз оказалась в реализации одного из них.
6. Насколько всё плохо?
Для эксплуатации нужно сочетание условий: musl, OpenSSH без PAM, разрешённая парольная аутентификация, учётная запись с * и другая учётная запись с настоящим хешем подходящего алгоритма.
В частности, это затрагивало Alpine Linux. Более того, в некоторых официальных облачных образах Alpine пользователь alpine имел беспарольный doas. Так что успешный SSH‑вход мог означать практически немедленное получение root.
А вот OpenWrt, которым тоже пугают в новостях, штатно использует Dropbear, а не OpenSSH. Dropbear этой дыре не подвержен. Наличие musl само по себе уязвимости не создаёт.
Исправление — OpenSSH 10.6p2 или соответствующий патч дистрибутива. Пока не обновились — PasswordAuthentication no, если парольный вход не нужен.
7. И кто же это раскопал?
Özgün Kültekin из Wakefield. Причём не без помощи LLM: по словам исследователя, он использовал целые рои агентов, которые прогнали около 17 600 нестандартных вариантов аутентификации через разные сборки OpenSSH.
Все вели себя одинаково. Кроме musl без PAM, которая почему‑то соглашалась с тем, с чем соглашаться совершенно не следовало.
И вот тут, на мой взгляд, самое интересное.
Эту историю можно преподнести как цепочку вполне благоразумных инженерных решений, которые неожиданно дали плохой результат.
А можно посмотреть иначе.
Разработчики musl решили не возвращать ошибку crypt() предусмотренным способом — через NULL. А вдруг вызывающий код не умеет её обрабатывать? Вместо этого стали возвращать * — якобы заведомо несовпадающий хеш. Разработчики OpenSSH такой инклюзивности подставы попросту не ожидали: проверка на NULL у них есть.
Но отдельно прекрасное выяснилось ещё в декабре 2024 года. Разработчикам musl уже указывали, что их самодеятельность ломает passwd в Alpine: PAM пытался затереть возвращённую строку *, находящуюся в read‑only памяти, и таки падал — какая ирония! Rich Felker тогда даже признал, что решение не возвращать ошибку, возможно, стоит пересмотреть.
И вот, почти два года спустя, та же самая * уже не просто роняет программу, а позволяет обойти парольную аутентификацию OpenSSH.
В результате десять лет существовала возможность обхода аутентификации там, где сама система должна была категорически отказывать во входе.
Забавно, что в IT давным давно существует простой инженерный принцип: лишний стоп лучше, чем лишний ляп. И как наглядно показывает эта история, лишний стоп оказался бы куда полезнее лишней заботы о чужих ошибках.
Вот только, как становится очевидным, знаком он далеко не всем.
Если проверка не может быть выполнена корректно — она должна завершаться отказом. Иначе однажды очередной «безопасный» fallback сложится исключительно удачным образом, только не тем — и вместо отказа система радостно откроет дверь.
А потом мы будем читать очередную увлекательную историю о том, как несколько локальных исправлений сложились в одну глобальную дыру.
Источники: