English version of this write-up is here

Условие

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

Форма входа

Начнем решение с небольшого осмотра: запустим dirsearch на этом хосте. В выводе видим package.json, sessions, что намекает нам на то, что приложение написано на node.js.

Со страницы входа находим JS-файл js/main.js, который обращается к API. Обнаруживаем, что на сервере подключен autoindex и мы можем посмотреть файлы в директории js/, где находим исходники серверной части приложения.

index: js

Сохраненная копия директории js/

Выясняем, что приложение написано на фреймворке Express и доступно на порту 4000, куда запросы проксируются с порта 80 с помощью nginx. Имеет три метода: /api/login и /api/logout «не реализованы» и редиректят на /login, а последний проверяет сессию, и для юзера admin показывает флаг.

В config.js обнаруживаем, что приложение проксирует все неизвестные запросы на порт 5000 (правда, по всей видимости, этот порт не висит наружу), а также находим секретный ключ для подписи сессий. Выглядит как короткий путь к победе, однако, тут же выясняется, что сессии хранятся в файликах, файлик никак не создать, а существующих сессий в директории sessions/ нет. Что ж, всё равно запомним.

index: sessions

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

/api/images: 403

Возвращаясь к исходникам приложения, видим файл auth.js, в котором реализована проверка наличия сессии для всех путей, кроме /api/login и /api/logout. Также заметим, что /api/images не реализовано в Express-приложении, значит эта страница должна проксироваться на порт 5000.

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

OPTIONS /api/login

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

/api/images: 200

/api/images?year=2018

/api/images?year=2018/..

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

Null byte injection

/var/www

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

/api/image: fail #1

/api/image: fail #2

/api/image: fail #3

/api/image: fail #4

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

Applications

volga_gallery

volga_auth

volga_adminpanel

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

adminpanel: sessions

Поскольку все пакеты в 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.

Успех

:triangular_flag_on_post: VolgaCTF{31c2ac53d4101a01264775328797d424}