/Разборы задач
25 октября 2026
Райтап на задачу «Просмотр файлов»
Райтап feadal на задачу «Просмотр файлов»: проверка пути через startswith, поведение os.path.normpath и обход запрета на чтение файла.10
30

Web
Задача «Безвыигрышная лотерея»
Запускаем персональный инстанс. Нас встречает Пикси и просит представиться для участия в лотерее.
Сделаем это и сразу получим результаты лотереи.
Мы проиграли. Но не расстраиваемся и попробуем обновить страницу ещё несколько раз. Каждый раз мы получим уже новое число, но победы так и не дождёмся :(.
Вместо того чтобы делать это дальше, попробуем разобраться, как же работает сама лотерея. К задаче уже приложены исходники, изучим код напрямую.
Здесь и видно, как определяется победа:
$rnd = mt_rand(1, 100) + 1; // число от 2 до 101
$isLose = (!empty($user) && $user['is_win'] !== $rnd);
$isWin = !$isLose;Во-первых, выпавшее число всегда попадает в диапазон от 2 до 101, а наш is_win всегда равен 1. Совпасть они не могут в принципе.
Во-вторых, проигрыш засчитывается только при условии !empty($user). То есть если $user окажется пустым, первое условие рушится, $isLose становится false, и мы выигрываем. А $user — это результат json_decode(user_data). Значит, наша цель — сделать так, чтобы json_decode вернул null. Для этого изучим код index.php — той самой страницы, где мы вначале вводили свой юзернейм.
В index.php при регистрации наше имя попадает в JSON-строку и пишется в базу:
$userData = json_encode([ 'user' => $username, 'is_win' => 1 ], JSON_UNESCAPED_UNICODE);Остаётся понять, как испортить сохранённый JSON. Смотрим на структуру таблицы в init.sql:
Колонка user_data объявлена как VARCHAR(255). То есть строка не должна быть длиннее 255 символов, но что, если мы захотим ввести её больше этого лимита? Чтобы это узнать, изучим также файл настройки самой БД — custom.cnf:
В sql_mode перечислены:
ERROR_FOR_DIVISION_BY_ZERO (ошибка при делении на ноль)
NO_AUTO_CREATE_USER (не создавать пользователей через GRANT)
NO_ENGINE_SUBSTITUTION (не подменять недоступный движок таблиц)
Но ни один из них не включает строгую проверку длины: для этого нужен STRICT_TRANS_TABLES, а его здесь нет. Загуглим, что случится, если его нет.
Получается, слишком длинное значение не вызывает ошибку, а молча обрезается до 255 символов. Значит, если наше имя сделать достаточно длинным, JSON {"user":"...","is_win":1} перевалит за лимит и сохранится обрезанным посередине — без закрывающей кавычки и скобки. Такой JSON будет невалиден, и функция json_decode вернёт null, а $user окажется пустым, и лотерея засчитает выигрыш.
Возьмём любое большое имя, чтобы перейти через эту границу.
Клиентская валидация не даст сделать это так просто — ограничит примерно 50 символами. Но проверим, аналогична ли ей серверная. Для этого отправим запрос напрямую через Burp Repeater.
В ответ получим код 302, который говорит нам о перенаправлении. Нажмём на кнопку Follow Redirection, и нас перенаправит на страницу лотереи, на которой мы успешно победили.
Суть уязвимости — это доверие к клиентской валидации и отсутствие серверной проверки длины имени. Из-за нестрогого режима БД переполненная строка обрезается и ломает сохранённый JSON, а приложение на битых данных ошибочно засчитывает выигрыш. Если бы валидация сервера была аналогична клиентской, то выиграть в такой лотерее не получилось бы.
В OWASP Top 10 этой уязвимости соответствует A06:2025 — Insecure Design: доверие к клиентской валидации и логика, которая на повреждённых данных засчитывает выигрыш. По CWE это CWE-602 (перенос серверной проверки на клиент).