
Análise detalhada de um desafio de CTF: engenharia reversa de um SPA em React, parsing de imagens SVG, criação de um solucionador automatizado com correspondência gulosa e obtenção de uma pontuação perfeita.
Resultado: 200/200 (100%) — Token: WKX-9245FA70-200 Data: 24 de março de 2026
Uma publicação no LinkedIn anunciou um desafio especial para o Wix KickstartX — um programa para desenvolvedores juniores. As regras:
wix-kickstartx-challenge-2026.base44.app/ e wixkickstart.comO site principal wixkickstart.com é um site hospedado na Wix (renderizado no servidor pelo mecanismo
Thunderbolt da Wix). Um curl padrão retorna apenas o shell JavaScript — sem conteúdo real.
Usamos Chromium headless para renderizá-lo:
chromium --headless --disable-gpu --no-sandbox --virtual-time-budget=10000 \
--dump-dom "https://wixkickstart.com" > /tmp/wix_rendered.html
Do DOM renderizado extraímos um iframe embutido:
Isso acabou sendo uma animação de partículas p5.js (enfeite da seção hero), não o desafio em si. O desafio real está no app Base44.
O desafio roda na Base44 — uma plataforma de apps low-code. O app é uma aplicação React de página única com toda a lógica em um único bundle JS:
https://wix-kickstartx-challenge-2026.base44.app/assets/index-oTG160r9.js
Tamanho: 410.809 bytes (React minificado + lógica do app + SDK Base44)
Ao buscar no bundle JS, mapeamos toda a API:
| Função | Finalidade |
|---|---|
startGame | Cria uma sessão de jogo, retorna 200 imagens + 200 descrições |
scoreGame | Aceita {matches: {}, sessionId: ""}, retorna {correctCount: N} |
generateToken | Recebe sessionId, retorna token de conclusão |
getLeaderboard | Retorna as maiores pontuações |
saveNickname | Salva o nome de exibição para o leaderboard |
Entidades: GameSession, Participant
Padrão de URL da API:
POST /api/apps/{appId}/functions/{functionName}
App ID: 69aea07cbcb9a3dd1039a58d
const or = 1800; // Time limit: 1800 seconds (30 minutes)
const rm = 200; // Total items: 200 image-description pairs
Uma descoberta crítica — o app tem um modo de envio em massa por JSON. Do código minificado:
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
}
Texto de placeholder no modal:
{
"IMG-001": "DESC-042",
"IMG-002": "DESC-017",
...
}
Isso significa que não precisamos clicar 200 vezes na interface — podemos enviar um mapeamento JSON de todas as 200 correspondências programaticamente.
O SDK Base44 usa autenticação JWT:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
O payload do JWT contém:
{
"sub": "[email protected]",
"exp": 1782162460,
"iat": 1774386460
}
Cabeçalhos necessários para todas as chamadas de 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)
Estrutura da resposta:
{
"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
}
Cada imagem é um SVG 200x200 contendo:
Exemplo de SVG decodificado (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>
Cada descrição segue um padrão estrito:
{background} field[, overlaid with {overlay}]. {N} elements total:
{size} {opacity} {color} {shape} ({rotation}, at {position}) ·
{size} {opacity} {color} {shape} ({rotation}, at {position}) · ...
Exemplo:
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) · ...
Precisávamos mapear propriedades SVG para o vocabulário das descrições. Isso exigiu analisar todas as 200 imagens e todas as 200 descrições para encontrar correspondências exatas.
Ao contar ocorrências nos dois lados, estabelecemos um mapeamento 1:1:
| Cor Inicial do Gradiente SVG | Contagem | Palavra na Descrição | Contagem |
|---|---|---|---|
#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 |
Método: Contar as cores iniciais únicas do gradiente em todos os 200 SVGs, contar a primeira palavra única em todas as 200 descrições e fazer a correspondência pela contagem.
Os overlays SVG são implementados de forma diferente conforme o tipo:
| Padrão SVG | Método de Detecção | Contagem | Nome na Descrição | Contagem |
|---|---|---|---|---|
Elementos <line> horizontais (dy=0) | Verificar x1,y1,x2,y2 | 35 | striped overlay | 35 |
Elementos <line> verticais (dx=0) | Verificar x1,y1,x2,y2 | 34 | lattice pattern | 34 |
<circle> com opacity="0.06" (pontos) | Contar círculos de baixa opacidade | 30 | stippled layer | 30 |
| Nenhum elemento de overlay | Sem linhas, sem formas de fundo | 28 | (nenhum) | 28 |
Elementos <line> diagonais | Verificar a direção da inclinação | 27 | tilted cross-lines | 27 |
<polygon>/<path> pequenos com opacity="0.06" | Formas com atributo de baixa opacidade | 25 | arrow-band texture | 25 |
<circle> com fill="none" stroke="#888" | Círculos apenas com stroke | 21 | ringed pattern | 21 |
Insight-chave: Os elementos de padrão de fundo usam opacity como um atributo XML
(por exemplo, opacity="0.06"), enquanto as formas em primeiro plano usam opacity dentro do atributo
style (por exemplo, style="opacity:0.7"). Essa distinção foi crucial para separar
os padrões de fundo das formas reais.