English version of this write-up is here

Заходим на сайт, кроме формы входа ничего не видим:

Начнем решение с небольшого осмотра: запустим dirsearch на этом хосте. В выводе видим package.json, sessions, что намекает нам на то, что приложение написано на node.js.
Со страницы входа находим JS-файл js/main.js, который обращается к API. Обнаруживаем, что на сервере подключен autoindex и мы можем посмотреть файлы в директории js/, где находим исходники серверной части приложения.

Сохраненная копия директории
js/
Выясняем, что приложение написано на фреймворке Express и доступно на порту 4000, куда запросы проксируются с порта 80 с помощью nginx. Имеет три метода: /api/login и /api/logout «не реализованы» и редиректят на /login, а последний проверяет сессию, и для юзера admin показывает флаг.
В config.js обнаруживаем, что приложение проксирует все неизвестные запросы на порт 5000 (правда, по всей видимости, этот порт не висит наружу), а также находим секретный ключ для подписи сессий. Выглядит как короткий путь к победе, однако, тут же выясняется, что сессии хранятся в файликах, файлик никак не создать, а существующих сессий в директории sessions/ нет. Что ж, всё равно запомним.

На фронтэнде — в файле main.js — есть обращение к /api/images. Судя по всему, этот метод работает с файлами, что возможно поможет нам в решении таска. Обращаемся к данному методу, но получаем ошибку 403.

Возвращаясь к исходникам приложения, видим файл auth.js, в котором реализована проверка наличия сессии для всех путей, кроме /api/login и /api/logout. Также заметим, что /api/images не реализовано в Express-приложении, значит эта страница должна проксироваться на порт 5000.
Пока мы не можем обойти данную проверку, попробуем понять, что же нас ждёт внутри: случайно решаем обратиться с методом OPTIONS на /api/logout — здесь авторизация не требуется, но и у Express такого метода нет, поэтому нас прокидывает внутрь, где мы видим ошибку PHP-фреймворка Laravel.

Тем временем вспоминаем или, как мы, случайно обнаруживаем, что Express не нормализует пути, и обходим авторизацию, используя URL //api/images. Получаем 200 и пустой список картинок. Далее вспоминаем параметр year. Понимаем, что нам дан функционал листинга директорий, и находим уязвимость Directory traversal.



Благодаря включенному debug-режиму, понимаем, что после года подставляется /img/, что накладывает определенные ограничения на список доступных нам директорий. Однако, попробуем применить классический баг в PHP — Null byte injection, суть которого заключается в том, что функции на C воспринимают %00 за конец строки. Это работает, и теперь мы видим директории без /img/, что даёт нам листинг всей файловой системы.


Искомый флаг, как мы могли понять и ранее, находится в /var/www/flag, однако мы вам его не дадим получить его с помощью метода /api/image невозможно, поскольку перед открытием файл передается в функцию file_exists, которая выдает ошибку при наличии %00 в строке. Расстраиваемся и продолжаем искать.




Замечаем в директории /var/www/apps три приложения. С двумя из них мы уже знакомы: volga_gallery на PHP, а volga_auth на JS. А вот третье приложение под названием volga_adminpanel нам незнакомо.




В директории видим файл app.js и директорию sessions, которая, в отличие от такой же директории в volga_auth, содержит какую-то сессию. Очень подозрительно. Применив смекалочку, догадаемся, что в этой сессии может быть name=admin. Мы какое-то время пытались обнаружить, где же крутится этот самый app.js, надеясь в нём найти что-то ещё, но таск оказался не таким простым.

Поскольку все пакеты в npm попадают только после тщательнейшего анализа ведущими специалистами по информационной безопасности, количество зеродеев в модулях лишь немногим больше бесконечности. Вспоминаем, что сессии хранятся в самой лучшей базе данных — в JSON-файликах. Всё это происходит с помощью модуля session-file-store. Потратив немного времени, находим функцию, с помощью которой из имени куки составляется имя JSON-файла. Казалось бы, что может быть более безопасным. Однако, как вы уже догадываетесь, path.join просто сконкатенирует строки, вставив слеш между ними.
Внимательный читатель вспомнит, что сессий-то у нас нет, и в силу того, что сессии подписываются секретным ключом, ничего страшного подставить мы не сможем. Однако, сессия-то у нас есть — лежит она в файлике sessions/../../volga_adminpanel/sessions/euzb7bMKx-5F29b2xNobGTDoWXmVFlEM.json относительно корня приложения volga_auth. Чтобы открыть этот файлик, идентификатор сессии должен быть равен ../../volga_adminpanel/sessions/euzb7bMKx-5F29b2xNobGTDoWXmVFlEM. Кроме этого, вспоминаем, что и секретный ключ у нас тоже есть, а значит можно идти и подписывать.
Раскуривать, как работает подпись сессии в Express нам не очень-то хотелось, поэтому мы применили грязный хак — подняли то же самое приложение у себя, добавили прямо в модуль express-session подписание нужной нам сессии, и ещё один метод API, который дергает создание сессии:

В ответ получили куку, с которой можно попробовать достать флаг методом /api/flag.

VolgaCTF{31c2ac53d4101a01264775328797d424}