Наше прохождение космического CTF от RUVDS и Positive Technologies

В обычный будничный день во время обеда, когда я просматривал последние новости на телеграм-каналах, мое внимание привлек увлекательный анонс в канале Positive Technologies. RUVDS совместно с ними объявили о проведении “космического” CTF (Capture The Flag), которое связано с запуском пико-спутника на орбиту Земли и созданием первого космического дата-центра.

Казалось, что посвятить несколько вечеров CTF в середине рабочей недели  —  не лучшая идея. Особенно после насыщенных выходных, которые были проведены в попытках решить задачи на IT’s Tinkoff CTF. Конечно, они выбрали не самое удобное время, однако любопытство взяло верх. Ведь интересно сравнить различные подходы к проведению CTF, получить новый опыт и расширить свои знания в этой и других областях  —  все-таки CTF “космический”.

Затем уже я наткнулся на статью на Хабре с заголовком “Хакните спутник и заработайте 0.1 BTC”. Там некий хакер Череп предлагает сыграть в игру с возможностью выиграть награду. Для получения подсказки, как к ней приступить, было достаточно всего лишь получить сигнал со спутника. И хорошо, что для этого не обязательно быть радиолюбителем с кучей мощных антенн. Но сама идея, что спутник передает подсказки для квеста,  —  это очень круто.

Прохождение

Итак, чтобы получить первую подсказку, можно было пойти сложным путем и воспользоваться ресурсом TinyGS, найти там интересующий нас спутник, название которого легко ищется в интернете, и смотреть, что он передает. Либо же можно было просто перейти на сайт спутника, где заботливо разместили терминал для отображения информации переданной со спутника, в том числе и на русском языке, когда передача идет на английском.

На данном этапе задача заключалась в поиске таинственного бота-ассистента, о котором имеется лишь приблизительное представление о его названии. С этим вопросом я вышел в интернет. Методом «гугления» было установлено, что речь идет о Юхане Андерссоне  —  бывшем сотруднике Funcom, а ныне менеджере “какого-то Парадокс” Development Studio. Далее можно изучить проекты Paradox и увидеть, что она выпустила стратегию Stellaris, что похоже на подсказку с названием от Черепа.

Следующим шагом был поиск бота. Поначалу не было понятно, где его искать  —  в интернете подходящих вариантов не находилось. Однако я решил попробовать поискать в Telegram, используя ключевые слова “stellaris bot test”. Таким образом был обнаружен @stellaris_test_bot, представляющий собой чат-бот с ИИ. С идеей поискать в Telegram мне повезло, а на следующий день подсказка обновилась, и в ней стало конкретно указано, где искать бота.

При обращении к боту встречало следующее сообщение:

На прямой вопрос бот не хотел отвечать. Также не хотел говорить, кто просил передать сообщение, и упоминать, какой репозиторий на GitHub нужен.

И после нескольких вариантов диалога с ботом был найден следующий простой способ получить автора послания @St3llaWh1z:

Ради интереса получим список ограничений, которые предстояло обойти.

А также узнаем версию GPT, используемую в боте,  —  скорее всего, это GPT-3.

Теперь нам предстоит отыскать пользователя St3llaWh1z на GitHub. При просмотре профиля пользователя, мы замечаем арт, а значит, движемся правильно. Изначально, когда я нашел пользователя, у него было два репозитория, однако со временем один из них пропал:

Приступим к внимательному изучению репозитория SatelliteReport с любопытным описанием: 

web app to generate PDF satellite status reports

И обнаруживаем интересный коммит:

delete deploy server for security reasons

В нем видно удаление строчки, содержащей ip-адрес деплой сервера:

Переходим по указанному IP, и по умолчанию идет обращение к 80 порту, где нас приветствует сервер Nginx. Значит, есть вероятность, что также существуют другие порты, на которых могут быть доступны дополнительные веб-ресурсы для изучения.

Поэтому дальше методом «научного тыка» был найден порт 8080, на котором запущен Report generator:

Он в формате pdf выдает репорты со случайными данными в зависимости от переданной в него строки. Видимо, функция на сервере seed-based, зависящая только от переданной строки.

Видим форму <input>, пробуем проверить на наличие уязвимости XSS (межсайтового скриптинга). Применяя различные методы, мне удалось получить вывод, который дает нам информацию о пути хранения файла (до определенного момента она просто отображалась снизу возвращаемого файла PDF, но потом организаторы это пофиксили):

Каталог имеет говорящее название и после безуспешных попыток перейти в него, предположим, что раз каталог *_unzip, значит в корне есть файл *.zip. Обращаемся к /daab2a45fd5c44bca7b6.zip и начинается загрузка архива.

Ради удовлетворения любопытства получим код. Сделаем это с помощью XSS следующего вида (предположим, что там есть файл index.js, в противном случае стоило бы попробовать другие файлы):

<script>x = new XMLHttpRequest(); x.open(‘GET’, ‘file:///daab2a45fd5c44bca7b6_unzip/index.js’, false); x.send(); document.write(‘<pre>’ + x.responseText + ‘</pre>’);</script>

Получим:

let express = require("express")
let app = express()
var seedrandom = require('seedrandom');
let markdownpdf = require("markdown-pdf")
var opts = {
remarkable: {
html: true
}
}

app.set('view engine', 'ejs');
app.use(express.static('public'));
app.disable('x-powered-by');
app.use(express.json());
app.get("/", (req, res) => {
res.render("index")
});

app.post("/report", (req, res) => {
let rng = seedrandom(req.body.satelliteName);
console.log(req.body.satelliteName + "
["+req.socket.remoteAddress+"]");
let data = {
satelliteName: req.body.satelliteName,
orbitalPosition: {
latitude: (rng() * 100).toPrecision(4),
longitude: (rng() * 100).toPrecision(4)
},
altitude: (rng() * 100).toPrecision(5),
orbitalVelocity: (rng() * 100).toPrecision(5),
inclination: (rng() * 100).toPrecision(4),
eccentricity: (rng() * 10).toPrecision(3),
period: (rng() * 100).toPrecision(2),
lifetime: 0,
launchDate: "2023–01–01",
batteryVoltage: (rng() * 100).toPrecision(3),
communicationLink: "Good",
dataTransmission: (rng() * 100).toPrecision(4),
payloadStatus: "Operational",
temperature: (rng() * 100).toPrecision(4),
gyroscopeStatus: "Operational",
thrusterStatus: "Operational",
solarFlux: (rng() * 100).toPrecision(2),
radiationLevel: "Low",
antennaAlignment: "Good",
onboardStorage: (rng() * 100).toPrecision(4),
cpuUsage: (rng() * 100).toPrecision(4),
memoryUsage: (rng() * 100).toPrecision(4),
starTracker: "Operational",
attitudeControl: "Active",
payloadPower: (rng() * 100).toPrecision(4),
payloadDataRate: (rng() * 100).toPrecision(4),
solarArrayPower: (rng() * 100).toPrecision(4)
}
let initialMarkdown = `| **Parameter** | **Value** |
|--------------------|--------------------------|
| Satellite Name | ${data.satelliteName} |
| Orbital Position | Latitude:
${data.orbitalPosition.latitude}° |
| | Longitude:
${data.orbitalPosition.longitude}° |
| Altitude | ${data.altitude} kilometers |
| Orbital Velocity | ${data.orbitalVelocity} km/h |
| Inclination | ${data.inclination}° |
| Eccentricity | ${data.eccentricity} |
| Period | ${data.period} hours |
| Lifetime | ${data.lifetime} years |
| Launch Date | ${data.launchDate} |
| Solar Panel Status | Operational |
| Battery Voltage | ${data.batteryVoltage} volts |
| Communication Link | ${data.communicationLink} link |
| Data Transmission | ${data.dataTransmission} Mbps |
| Payload Status | ${data.payloadStatus} |
| Temperature | ${data.temperature} °C |
| Gyroscope Status | ${data.gyroscopeStatus} |
| Thruster Status | ${data.thrusterStatus} |
| Solar Flux | ${data.solarFlux} units |
| Radiation Level | ${data.radiationLevel} |
| Antenna Alignment | ${data.antennaAlignment} |
| Onboard Storage | ${data.onboardStorage} GB |
| CPU Usage | ${data.cpuUsage} % |
| Memory Usage | ${data.memoryUsage} % |
| Star Tracker | ${data.starTracker} |
| Attitude Control | ${data.attitudeControl} |
| Payload Power | ${data.payloadPower} kW |
| Payload Data Rate | ${data.payloadDataRate} kbps |
| Transponder Status | Operational |
| Solar Array Power | ${data.solarArrayPower} kW |`

markdownpdf(opts).from.string(initialMarkdown).to.buffer(function
(err, pdf) {
if (err) return console.log(err)
res.contentType("application/pdf")
pdf = pdf.toString('base64')
res.send(pdf)
})
})
app.all('/daab2a45fd5c44bca7b6.zip', function (req, res) {
// Send the zip file to the user
res.download('./daab2a45fd5c44bca7b6.zip',
'daab2a45fd5c44bca7b6.zip', function (err) {
if (err) {
console.log(err)
} else {
console.log("Sent to "+req.socket.remoteAddress )
}
})
})
app.listen(8080)

И в коде видим файл, который нужно получить для следующего этапа. В данном zip-архиве находится следующий zip-архив, разбитый на части, которые, в свою очередь, также содержат вложенные части архивов:

Engrampa и другие архиваторы не смогли корректно распаковать вложенные архивы. В конце разархивирования цепочки архивов оставалась только часть архива с заголовком следующего архива, а другие его части с содержимым терялись.

После раздумий было принято решение использовать unzip  —  возможно, утилита фигурирует в названии каталога не просто так. Тут бы стоило написать скрипт для автоматизации действий, но, так как был вопрос к целостности архива, разархивация проводилась вручную путем соединения частей архива в один простым склеиванием с помощью утилиты cat и распаковки результата. После многих итераций был получен файл с IP-адресом следующей машины:

После проведения сканирования данного хоста с помощью nmap без ключей обнаружим следующие открытые порты:

Подключение по ssh и попытка перебрать учетки не дали никакого результата.

До определенного момента на порту 3307/tcp при подключении через telnet была Satellite Control Panel с формой для авторизации, но в дальнейшем порт был закрыт организаторами.

А вот на порту 9000 при подключении через http происходил редирект на 9001 порт, на котором, в свою очередь, доступна веб-страница облачного объектного хранилища MINIO:

Попытки перебора стандартных учетных данных для Minio не дали результатов. Но также на странице была ссылка на GitHub проекта, изучив который, стало понятно, что в веб-версии отображаются доступные хранилища по пути /browser/*. Небольшим перебором каталогов был найден каталог data:

В нем находился файл flag.txt со следующим содержимым:

IP{213.108.TO_BE_CONTINUED

Видимо, в хранилище есть еще какие-то части флага, поэтому поищем, не было ли уязвимостей для Minio, которые в данной версии не были закрыты. Таким образом в результате поиска уязвимостей хранилища была найдена CVE-2023–28432. 

CVE связана с возвратом Minio всех переменных окружения, включая MINIO_SECRET_KEY и MINIO_ROOT_PASSWORD. Данная уязвимость проявляется в функции VerifyHandler, расположенной в файле bootstrap-peer-server.go исходного кода Minio. В этом коде производится проверка развертывания кластера, и при возврате конфигурации системы возвращаются все переменные окружения, включая те, которые содержат чувствительную информацию.

Кроме того, удалось найти несколько репозиториев, содержащих POC (Proof of Concept) для данной уязвимости. Был взят питоновский скрипт из этого репозитория и немного модифицирован путем добавления выводов в терминал полученного ответа от сервера Minio в открытом виде. В результате были получены следующие данные конфигурации, содержащие логин и пароль рута, который также является частью ранее полученного флага:

“MINIO_ROOT_USER”:”admin”

“MINIO_ROOT_PASSWORD”:”THE_PART_YOU_HAVE_ALREADY_RECEIVED.4.16}”

С этими учетными данными осуществлен вход в облачное хранилище, однако ничего полезного не было найдено.

Следующей задачей было изучение хоста 213.108.4.16, полученного сложением частей флага на прошлом этапе. Чтобы более детально изучить этот хост, выполним сканирование с помощью инструмента Nmap, чтобы выявить открытые порты. В результате сканирования было обнаружено всего два порта:

При подключении к порту 6666 обнаружился irc-сервер, который автоматически отправлял триграмму в ответ на установленное соединение и затем закрывал это соединение:

Я собрал некоторое количество триграмм и обнаружил, что они представляют собой случайные части строки. Путем сопоставления некоторых из них можно было увидеть слово PASSWORD. После удаления повторяющихся триграмм и анализа их взаимосвязи я смог собрать исходную строку:

WINNER_PASSWORD{TheW1nn3rT4k3sItAl1}

Далее возникло предположение, что к порту 22 можно установить соединение по протоколу SSH и попробовать войти в систему под пользователем “winner” с полученным паролем. После этого было получено следующее сообщение:

После осталось подготовить и отправить баг-репорт с указанной темой-ключом. Таким образом, за пару вечеров я успешно выполнил основной этап космического CTF, в котором мне даже удалось занять одно из призовых мест.

На следующий день Череп опубликовал второй пост на Хабре, в котором в более расширенном виде объяснил, что обманул всех, а также оставил в конце интересное послание:

У вас 110 дней. Время пошло!

Помимо этого послания, в приложенном видео также присутствовала подсказка для дополнительного этапа, которая указывала на домен civilsky.ru. К сожалению, начало дополнительного этапа я пропустил, поэтому выполнял вне рамок соревнования.

Перейдя на него, видим мемную картинку, что-то связанное с поддоменами и какой-то хеш.

Из нижней строки возникает предположение, что у этого сайта есть админка, и поэтому пробуем перейти на admin.civilsky.ru. И у нас открывается YouTube с песней бразильского мальчика

Что делать с хешем, так и не решил. Удалось установить только то, что первые два числа  —  это константы SHA-256 или BLAKE и RIPEMD-160. Пробовал подбирать поддомен по формуле и подобным:

RIPEMD-160(BLAKE(subdomain)) = hash

И ничего не получилось. 

Для тех, кому интересно, как правильно решить задание с хешем и не только: организаторы от RUVDS выпустили свой райтап.

После неудачных попыток решить данную задачу с хешем, было найдено альтернативное решение путем запроса в онлайн-поисковик поддоменов. Так были получены два поддомена. Один из них  —  door.civilsky.ru  —  оказался не действующим.

А вот на поддомене lidsa.civilsky.ru я обнаружил песочницу Go. Дальше у меня было два варианта: прокинуть обратный shell или написать простой код, чтобы проверить, имею ли я доступ к файловой системе. Я выбрал второй вариант и обнаружил файл flag.txt в корневой директории. Этот файл содержал строку, закодированную в формате base64. Затем с помощью небольшого кода на Go декодировал содержимое файла и получил флаг:

Интересно отметить, что в полученном флаге содержится id рикролла  —  “dQw4w9WgXcQ”.

Заключение

Этот космический CTF оказался очень интересным и увлекательным соревнованием, которое прошло с хорошей организацией и разнообразными заданиями, охватывающими различные тематики: от простого задания с поиском в открытых источниках и хака чат-бота до более сложных задач, связанных с поиском и эксплуатацией уязвимостей в веб-сервисах.

Однако среди заданий была и такая, как задача с хешем, которая вызвала затруднения и была непонятной для меня и других участников, судя по отсутствию решивших ее до публикации официального разбора.

Тем не менее квест оказался крайне увлекательным и захватывающим, и я прошел его практически на одном дыхании.

Читайте также:

Читайте нас в TelegramVK и Дзен

Предыдущая статьяЛучшие практики написания кода в Spring Boot
Следующая статьяВведение в Page Visibility API