Writeup - firststeps
Цель: Найти флаг на CTF-задаче «Первые шаги»
Декодирование: «The early bee gets the honey» (Кто рано встаёт — того мёд ждёт) — leetspeak
Категория: Web/JavaScript reverse engineering (client-side CTF challenge)
Разведка
- Открываем сайт https://firststeps-5w7im53y.avitoctf.ru
- Видим игру «Первые шаги» — лабиринт на сотах, где нужно провести барсука-медоеда к флагу, обходя ловушки
- Изучаем исходный код страницы — находим подключённый JavaScript-файл
/assets/firststeps.js
Анализ JavaScript
В файле firststeps.js обнаруживаем ключевую функцию levelReward(level), которая генерирует часть флага для каждого уровня сложности:
var FIXED_COLS = 15;
var FIXED_ROWS = 10;
var LEVEL_ORDER = ['easy', 'medium', 'hard'];
var LEVEL_REWARD_LENGTHS = [10, 9, 9]; // длина каждой части флага
// Массив зашифрованных данных для всех частей флага
var LEVEL_REWARD_DATA = [
99,179,125,189,59,220,227,52,178,148,
156,34,201,91,248,251,14,73,54,212,
178,230,102,64,45,182,145,14,3,160
];
// Координаты "дырок" на игровом поле (используются для seed'а)
var FIXED_HOLE_COORDS = [[2,1],[7,1],[11,1],[4,2],[9,3],[13,3],
[1,4],[6,5],[11,5],[3,7],[8,8],[12,8]];function levelReward(level){
var index = LEVEL_ORDER.indexOf(level); // 0, 1, 2
// Начальное seed-значение на основе индекса уровня и размеров поля
var stream = (0x6d2b79f5 ^ Math.imul(index + 1, 0x9e3779b1)
^ (FIXED_COLS << 24) ^ (FIXED_ROWS << 16)) >>> 0;
// Примешиваем координаты "дырок" в seed
FIXED_HOLE_COORDS.forEach(function(pos, holeIndex){
stream ^= Math.imul(cellKey(pos[0], pos[1], FIXED_COLS)
+ holeIndex + 1, 0x045d9f3b);
stream = ((stream << 11) | (stream >>> 21)) >>> 0;
});
var reward = '';
// lane определяет, какие байты из LEVEL_REWARD_DATA брать
var lane = (index * 2 + 1) % LEVEL_ORDER.length;
for (var i = 0; i < LEVEL_REWARD_LENGTHS[index]; i++){
// xor-шифр: псевдослучайная последовательность на основе stream
stream ^= stream << 13;
stream ^= stream >>> 17;
stream ^= stream << 5;
stream >>>= 0;
// Берём байт из LEVEL_REWARD_DATA и xor'им с младшим байтом stream
reward += String.fromCharCode(
LEVEL_REWARD_DATA[i * LEVEL_ORDER.length + lane] ^ (stream & 255)
);
}
return reward;
}- Алгоритм генерации:
Вычисляется seed (stream) на основе номера уровня (0, 1, 2), размера поля (15×10) и координат «дырок» - Для каждого символа флага применяется простой xor-шифр: берётся байт из предопределённого массива
LEVEL_REWARD_DATAи xor'ится с псевдослучайной последовательностью, генерируемой через младший байтstream - Lane (дорожка) определяет, какие элементы массива относятся к какому уровню:
easy (index 0) → lane 1— элементы 1, 4, 7, 10, 13, 16, 19, 22, 25, 28medium (index 1) → lane 0— элементы 0, 3, 6, 9, 12, 15, 18, 21, 24hard (index 2) → lane 2— элементы 2, 5, 8, 11, 14, 17, 20, 23, 26
Эксплуатация
Флаг генерируется полностью на клиентской стороне. Нет необходимости проходить игру — достаточно скопировать функцию levelReward() и выполнить её в любой JavaScript-среде (браузерная консоль, Node.js).
Способ 1:Через консоль браузера
Открыть DevTools (F12) на странице https://firststeps-5w7im53y.avitoctf.ru
Выполнить в консоли:
var flag = '';
for (var l of ['easy', 'medium', 'hard']) flag += levelReward(l);
console.log('avito{' + flag + '}');Способ 2: Node.js репликация
Создать файл compute_flag.js со скопированной функцией levelReward() и выполнить:
node compute_flag.js
Вывод
Уязвимость: Клиентская генерация флага — вся логика вычисления флага выполняется на стороне клиента в JavaScript. Любой пользователь может прочитать и воспроизвести эту логику, получив флаг без прохождения игры.
Рекомендация по исправлению: Флаг должен генерироваться и проверяться на серверной стороне. Клиент должен отправлять серверу доказательство прохождения уровня (например, найденный путь в лабиринте), а сервер — возвращать часть флага только при валидном решении.
Потенциальное улучшение: API-эндпоинт /api/flag?level=easy&proof=<path_hash>, который проверяет, действительно ли игрок прошёл уровень до конца, и возвращает только соответствующую часть флага.