
Подробный разбор CTF-задания: реверс-инжиниринг React SPA, парсинг SVG-изображений, создание автоматического солвера с жадным сопоставлением и достижение идеального результата.
Результат: 200/200 (100%) — Токен: WKX-9245FA70-200 Дата: 24 марта 2026 года
В посте LinkedIn был анонсирован специальный челлендж для Wix KickstartX — программы для начинающих разработчиков. Правила:
wix-kickstartx-challenge-2026.base44.app/ и wixkickstart.comОсновной сайт wixkickstart.com размещён на Wix (серверный рендеринг движком Thunderbolt от Wix). Обычный curl возвращает только JavaScript-оболочку — без фактического содержимого. Чтобы отрендерить его, мы использовали headless Chromium:
chromium --headless --disable-gpu --no-sandbox --virtual-time-budget=10000 \
--dump-dom "https://wixkickstart.com" > /tmp/wix_rendered.html
Из отрендеренного DOM мы извлекли встроенный iframe:
Это оказалось p5.js-анимацией частиц (декоративный элемент hero-секции), а не сам челлендж. Настоящий челлендж находится в приложении Base44.
Челлендж запущен на Base44 — low-code платформе для приложений. Приложение представляет собой одностраничное React-приложение, вся логика которого находится в одном JS-бандле:
https://wix-kickstartx-challenge-2026.base44.app/assets/index-oTG160r9.js
Размер: 410 809 байт (минифицированный React + логика приложения + SDK Base44)
С помощью grep по JS-бандлу мы составили карту всего API:
| Функция | Назначение |
|---|---|
startGame | Создаёт игровую сессию, возвращает 200 изображений + 200 описаний |
scoreGame | Принимает {matches: {}, sessionId: ""}, возвращает {correctCount: N} |
generateToken | Принимает sessionId, возвращает токен завершения |
getLeaderboard | Возвращает лучшие результаты |
saveNickname | Сохраняет отображаемое имя для таблицы лидеров |
Сущности: GameSession, Participant
Шаблон URL API:
POST /api/apps/{appId}/functions/{functionName}
ID приложения: 69aea07cbcb9a3dd1039a58d
const or = 1800; // Time limit: 1800 seconds (30 minutes)
const rm = 200; // Total items: 200 image-description pairs
Критическая находка — в приложении есть режим пакетной JSON-отправки. Из минифицированного исходника:
function Ik({onSubmit:r, onClose:n}) {
// ...
h = JSON.parse(s) // Parse JSON input
// Validation: must be object like { "IMG-001": "DESC-042", ... }
r(h) // Submit all matches at once
}
Текст-заглушка в модальном окне:
{
"IMG-001": "DESC-042",
"IMG-002": "DESC-017",
...
}
Это значит, что нам не нужно кликать 200 раз в интерфейсе — мы можем программно отправить JSON-сопоставление всех 200 пар.
SDK Base44 использует JWT-аутентификацию:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Полезная нагрузка JWT содержит:
{
"sub": "[email protected]",
"exp": 1782162460,
"iat": 1774386460
}
Обязательные заголовки для всех API-вызовов:
Content-Type: application/json
Authorization: Bearer {jwt_token}
X-App-Id: 69aea07cbcb9a3dd1039a58d
Base44-Functions-Version: prod
X-Origin-URL: https://wix-kickstartx-challenge-2026.base44.app/
POST /api/apps/69aea07cbcb9a3dd1039a58d/functions/startGame
Status: 200
Transferred: 62.87 kB compressed (2.20 MB decompressed)
Content-Encoding: br (Brotli)
Структура ответа:
{
"status": "active",
"sessionId": "69c2fd218e5b26f307c941c9",
"startedAt": "2026-03-24T21:07:45.101Z",
"imageOrder": ["IMG-154", "IMG-102", ...], // 200 items (display order)
"descOrder": ["DESC-125", "DESC-109", ...], // 200 items (display order)
"imagesData": { "IMG-001": "data:image/svg+xml;base64,...", ... }, // 200 SVGs
"descriptionsData": { "DESC-105": "milky field, overlaid with...", ... } // 200 texts
}
Каждое изображение — это SVG размером 200x200, содержащий:
Пример декодированного SVG (IMG-001):
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
<defs>
<linearGradient id="bg" ...>
<stop offset="0%" stop-color="#f5f0eb"/>
<stop offset="100%" stop-color="#e8e0d5"/>
</linearGradient>
</defs>
<rect width="200" height="200" fill="url(#bg)"/>
<!-- Optional overlay lines/dots/rings here -->
<polygon points="..." fill="#E2725B" transform="translate(128.6,185.7) rotate(90)"
style="opacity:0.5;filter:drop-shadow(...)"/>
<!-- More shapes... -->
</svg>
Каждое описание следует строгому шаблону:
{background} field[, overlaid with {overlay}]. {N} elements total:
{size} {opacity} {color} {shape} ({rotation}, at {position}) ·
{size} {opacity} {color} {shape} ({rotation}, at {position}) · ...
Пример:
milky field, overlaid with tilted cross-lines. 10 elements total:
minuscule nearly solid dim gray pike (steeply angled, at center inner-left) ·
substantial solid azure pike (diagonal, at top center) · ...
Нам нужно было сопоставить свойства SVG со словарём описаний. Для этого потребовалось проанализировать все 200 изображений и все 200 описаний, чтобы найти точные соответствия.
Подсчитав вхождения с обеих сторон, мы установили взаимно-однозначное соответствие:
| Начальный цвет градиента SVG | Кол-во | Слово в описании | Кол-во |
|---|---|---|---|
#0a1628 | 28 | pitch | 28 |
#eef2f7 | 20 | frosted | 20 |
#0d0d0d | 19 | tenebrous | 19 |
#fef9f0 | 18 | pearlescent | 18 |
#1e0a2e | 18 | nocturnal | 18 |
#0a1a0a | 17 | midnight | 17 |
#1a0a0a | 15 | inky | 15 |
#f5f0eb | 14 | milky | 14 |
#f7f3ee | 14 | ethereal | 14 |
#f0f0f0 | 14 | radiant | 14 |
#f0f7f4 | 13 | glowing | 13 |
#1a1a2e | 10 | somber | 10 |
Метод: Посчитать уникальные начальные цвета градиентов во всех 200 SVG, посчитать уникальные первые слова во всех 200 описаниях и сопоставить по количеству.
Оверлеи SVG реализованы по-разному в зависимости от типа:
| SVG-узор | Метод определения | Кол-во | Название в описании | Кол-во |
|---|---|---|---|---|
Элементы <line>, горизонтальные (dy=0) | Проверка x1,y1,x2,y2 | 35 | striped overlay | 35 |
Элементы <line>, вертикальные (dx=0) | Проверка x1,y1,x2,y2 | 34 | lattice pattern | 34 |
<circle> с opacity="0.06" (точки) | Подсчёт окружностей с низкой прозрачностью | 30 | stippled layer | 30 |
| Нет элементов оверлея вообще | Нет линий, нет фоновых фигур | 28 | (none) | 28 |
Элементы <line>, диагональные | Проверка направления наклона | 27 | tilted cross-lines | 27 |
Небольшие <polygon>/<path> с opacity="0.06" | Фигуры с атрибутом низкой прозрачности | 25 | arrow-band texture | 25 |
<circle> с fill="none" stroke="#888" | Окружности только со stroke | 21 | ringed pattern | 21 |
Ключевая идея: элементы фоновых узоров используют opacity как XML-атрибут (например, opacity="0.06"), тогда как фигуры переднего плана используют opacity внутри атрибута style (например, style="opacity:0.7"). Это различие было критически важным для отделения фоновых узоров от настоящих фигур.