Proof-of-concept для гонки TOCTOU в настройке Socket.IO при первом запуске Uptime Kuma, позволяющий неаутентифицированному злоумышленнику внедрить скрытого второго администратора и отключить аутентификацию.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · uptime-kuma-setup-toctou
Uptime Kuma 2.5.5 - Louis Lam
При первом запуске Socket.IO setup выполняет COUNT пользователей, bcrypt с cost 10, затем INSERT. Username имеет только ограничение UNIQUE. Транзакции, мьютекса или ограничения на одну строку нет. Два уже подключённых сокета, которые оба видят count === 0, оба хешируют, оба вставляют. Лишний администратор невидим: интерфейса списка пользователей нет.
Посторонний, который обратится к только что запущенной Kuma в том же опросе, что и оператор, может внедрить скрытого второго администратора, отключить аутентификацию и через авто-вход получить доступ к мониторам оператора.
| ID | CVE пока нет |
| CWE | CWE-362, CWE-367 |
| CVSS | High: 7.4 CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Продукт | Uptime Kuma |
| Затронуто | вплоть до 2.5.5 (c98982a), пустая таблица user при первом запуске |
| Аутентификация | без аутентификации |
| Лицензия | GNU Affero GPL v3.0 |
| Лаборатория | только 127.0.0.1 |
Обратиться к только что запущенной Kuma одновременно с тем, как оператор отправляет /setup. По умолчанию Docker привязывается к 0.0.0.0:3001. Вставить вторую строку user с другим именем пользователя. Войти под этим пользователем, установить disableAuth и через R.findOne("user") авто-вход получить доступ к мониторам, уведомлениям, API-ключам и настройкам оператора.
После фиксации первой строки последующий последовательный setup отклоняется. Это не «setup остаётся открытым». Это гонка в течение окна bcrypt. Уже инициализированные экземпляры с администратором не подвержены этой уязвимости.
Латентный IDOR включения/отключения API-ключа (UPDATE api_key SET active=? WHERE id=? без user_id) также становится активным, как только существуют два пользователя. Это следствие лишней строки, а не оракул лаборатории.
Сначала я прочитал SECURITY.md, затем список пропусков: восемнадцать опубликованных GitHub-рекомендаций, плюс публичный XSS в Matomo siteId, плюс SSRF к метаданным облака, помеченный вендором как wontfix. Затем двадцать два поиска по тегу 2.5.5 (c98982a): auth/setup/2FA, socket IDOR, HTTP/monitor SSRF, остатки SSTI, path/LFI, реальный браузер, plugin/tailscale, status-page, docker.sock, prototype pollution, push/prometheus, база данных, monitor exec, analytics XSS, настройки, websocket CSRF, frontend XSS, jobs, Apprise, maintenance, master-delta. Большая часть этой планки не дала ничего неопубликованного. Аутентификация — дала.
Настройка первого пользователя — это публичное событие Socket.IO. Без логина. setup выполняет COUNT(user), затем await passwordHash.generate, затем R.store. passwordHash.generate — это bcrypt.hash с cost 10. Это передышка, а не блокировка. needSetup находится в памяти и используется только для отправки "setup" при подключении. Путь записи игнорирует его и пересчитывает.
Схема имеет только UNIQUE для username. knex_init_db.js не обеспечивает одну строку. Одинаковый username конфликтует (SQLITE_CONSTRAINT). Разные username оба проходят.
Я поднял louislam/uptime-kuma:2.5.5 на loopback с UPTIME_KUMA_DB_TYPE=sqlite, чтобы пропустить мастер базы данных v2. bcrypt cost 10 — это гонка, а не гарантия с первой попытки. run.sh очищает том и повторяет. Exit 2 — промах. Далее свежая пустая таблица user.
Когда сработало: sqlite 1|labadmin и 2|UK-SETUP-TOCTOU-WITNESS. Последовательный поздний setup: Uptime Kuma has been initialized. Лишний пользователь вошёл, setSettings disableAuth=true. Авто-вход при переподключении — это R.findOne("user"): наименьший id, оператор. Идентификатор otpauth в prepare2FA — labadmin.
Близкая GHSA-23q2-5gf8-gjpp — это повторное включение disableAuth, не сбрасывающее сокеты. Другая уязвимость.
Уже зафиксированные неверные повороты: одна строка user после конкурентного setup (промах гонки, а не патч); отсутствие UK-SETUP-TOCTOU-WITNESS, потому что оба сокета использовали одно и то же имя; трактовка HTML 200 дашборда как SUCCESS; обратная оболочка. Театр. Оракул — это две строки sqlite, затем авто-вход с disableAuth как id 1.
cd lab
./run.sh
Цель только http://127.0.0.1:18141. Официальный образ louislam/uptime-kuma:2.5.5, sqlite, первый запуск. Не публикуйте порт за пределами loopback.
user-rows [('1', 'labadmin'), ('2', 'UK-SETUP-TOCTOU-WITNESS')]
race-count=2 witness=True
sequential-late-setup initialized=True
setSettings-disableAuth ack ok
reconnect autoLogin=True loginRequired=False
bonus-autologin-identity operator=True id1=labadmin auto_is_id1=True
SUCCESS uptime-kuma-setup-toctou
Сериализуйте setup: транзакция плюс ограничение на одну строку, или мьютекс вокруг COUNT+INSERT. Не рассматривайте авто-вход с disableAuth как findOne наименьшего id. До этого не выставляйте настройку первого запуска в общей сети.
server.js setup · password-hash.js · knex_init_db.jsGNU Affero GPL v3.0. См. LICENSE. Только лаборатория на loopback. Без гарантий.