/Разборы задач
31 октября 2026
Райтап на задачу «Бесконечный бензин»
Изучаем настройки АЗС BugOil и используем prototype pollution, чтобы обойти лимит выдачи топлива.4
5

Web
Задача «YOFA 1.0 – grep challenge»
Скачиваем исходники таски и запускаем собственный инстанс.
Рассмотрим структуру проекта с помощью tree. Сразу можно обратить внимание на папку config, там есть файлы, связанные с инициализацией базы данных. Зачастую там бывает пароль администратора.
Открываем любым текстовым редактором config/init-db.js и смотрим:
Оказывается adminPassword лежит в исходниках. Открываем инстанс и входим под пользователем admin с этим паролем.
Теперь мы администратор.
Мы можем пойти по простому пути и сразу получить флаг: для этого надо немножко почитать исходники и понять, где он вообще должен лежать. Откроем controllers/adminController.js, routes/admin.js.
И видим, что только администратору откроется /admin/flag, перейдём туда.
Но не кажется ли это слишком просто? Рассмотрим решение по задумке автора.
Полазив по сайту и исходникам, можем найти, что есть некий бот (/report), который переходит по localhost-ссылкам с админской кукой.
Значит, надо как-то заставить бота открыть страницу сайта, чтобы бот сделал что-то пакостное, ну или сделал обычного пользователя админом. Как хорошо, что есть метод /admin/addAdmin (см. userController.js)
Теперь фокусируемся на анализе самих страниц сайта. В большинстве EJS-файлов в POST-запросах URL и параметры фиксированы, кроме одного — profile.ejs.
Рассмотрим этот код. Видим urlParams.getAll(“id”) – параметр возвращает массив. Также видим, что берётся последний элемент и подставляется в URL без проверки. В теле уже есть _csrf токен и userId.
Теперь рассмотрим серверную часть. В routes/user.js мы видим, что нам нужен userController.js, откроем его.
Пятая строка на скриншоте является ключевой. Если передать два параметра id: /user/profile?id=2&id=../admin/addAdmin то express.js разберёт этот дубликат как массив ["2", "../admin/addAdmin"]
Далее parseInt приводит массив к строке и прочитает число до первого нецифрового символа: parseInt("2,../admin/addAdmin") – то есть вернёт 2 в этом примере.
Получается, что сервер берёт первый id, а клиент в profile.ejs — последний. Это client-side parameter pollution.
Какой же теперь у нас план? Регистрируем своего пользователя и запоминаем его id. Он виден сразу после входа.
Запомним, что наш id = 3. Теперь идём в /report, чтобы отправить боту свою очень злую ссылку.
Как мы ее составим? Мы знаем, что кука админа привязана к localhost, но не знаем даже порта. Смотрим docker-compose.yaml в корне и видим.
Оно работает на порту 3000. Составляем URL:
http://localhost:3000/user/profile/?id=3&id=../admin/addAdminНе забудьте только поменять первый id на свой собственный.
Логинимся под своим пользователем, переходим на /report, вводим туда эту ссылку и ждём.
После отправки бот с админской кукой открывает страницу. Сервер рендерит наш профиль по первому id, а в body наш userId + валидный _csrf. Клиент берёт последний id, делает fetch("/log/"+profileId), браузер нормализует путь в POST /admin/addAdmin.
/admin/addAdmin видит админский JWT бота, берёт userId из body и делает нас админом. Поле action ему пофиг.
Теперь остаётся только перезайти, а то старый токен ещё обычного пользователя, — и вуаля, вы – админ. Остаётся перейти на /admin/flag, чтобы получить его.
Таким образом, цепочка такая:
SSRF (Server Side Request Forgery) - /report открывает любую http localhost ссылку.
Client-side parameter pollution – клиент и сервер берут разные id.
Path traversal в клиентском fetch (..)
Privilege Escalation - /admin/addAdmin принимает userId из тела и делает его админом.
А CSRF никак и не нужно обходить, страницы сами всё за нас делают.