
Write-up dettagliato di una challenge CTF: reverse engineering di una SPA React, parsing di immagini SVG, creazione di un solver automatizzato con greedy matching e raggiungimento di un punteggio perfetto.
Risultato: 200/200 (100%) — Token: WKX-9245FA70-200 Data: 24 marzo 2026
Un post su LinkedIn annunciava una sfida speciale per Wix KickstartX — un programma per sviluppatori junior. Le regole:
wix-kickstartx-challenge-2026.base44.app/ e wixkickstart.comIl sito principale wixkickstart.com è un sito ospitato su Wix (renderizzato lato server dal motore
Thunderbolt di Wix). Una curl standard restituisce solo lo shell JavaScript — nessun contenuto reale.
Abbiamo usato Chromium headless per renderizzarlo:
chromium --headless --disable-gpu --no-sandbox --virtual-time-budget=10000 \
--dump-dom "https://wixkickstart.com" > /tmp/wix_rendered.html
Dal DOM renderizzato abbiamo estratto un iframe incorporato:
Questa si è rivelata essere un'animazione di particelle p5.js (semplice effetto visivo della hero section), non la sfida stessa. La vera sfida vive nell'app Base44.
La sfida gira su Base44 — una piattaforma app low-code. L'app è una applicazione React a pagina singola con tutta la logica in un unico bundle JS:
https://wix-kickstartx-challenge-2026.base44.app/assets/index-oTG160r9.js
Dimensione: 410.809 byte (React minificato + logica app + SDK Base44)
Cercando nel bundle JS, abbiamo mappato l'intera API:
| Funzione | Scopo |
|---|---|
startGame | Crea una sessione di gioco, restituisce 200 immagini + 200 descrizioni |
scoreGame | Accetta {matches: {}, sessionId: ""}, restituisce {correctCount: N} |
generateToken | Prende sessionId, restituisce il token di completamento |
getLeaderboard | Restituisce i punteggi migliori |
saveNickname | Salva il nome visualizzato per la classifica |
Entità: GameSession, Participant
Pattern URL API:
POST /api/apps/{appId}/functions/{functionName}
App ID: 69aea07cbcb9a3dd1039a58d
const or = 1800; // Limite di tempo: 1800 secondi (30 minuti)
const rm = 200; // Elementi totali: 200 coppie immagine-descrizione
Un risultato fondamentale — l'app ha una modalità di invio JSON in blocco. Dal sorgente minificato:
function Ik({onSubmit:r, onClose:n}) {
// ...
h = JSON.parse(s) // Analizza l'input JSON
// Validazione: deve essere un oggetto come { "IMG-001": "DESC-042", ... }
r(h) // Invia tutte le corrispondenze in una volta
}
Testo segnaposto nella modale:
{
"IMG-001": "DESC-042",
"IMG-002": "DESC-017",
...
}
Questo significa che non dobbiamo cliccare 200 volte nell'interfaccia — possiamo inviare una mappatura JSON di tutte le 200 corrispondenze in modo programmatico.
L'SDK Base44 usa autenticazione JWT:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Il payload JWT contiene:
{
"sub": "[email protected]",
"exp": 1782162460,
"iat": 1774386460
}
Header richiesti per tutte le chiamate 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 compresso (2.20 MB decompresso)
Content-Encoding: br (Brotli)
Struttura della risposta:
{
"status": "active",
"sessionId": "69c2fd218e5b26f307c941c9",
"startedAt": "2026-03-24T21:07:45.101Z",
"imageOrder": ["IMG-154", "IMG-102", ...], // 200 elementi (ordine di visualizzazione)
"descOrder": ["DESC-125", "DESC-109", ...], // 200 elementi (ordine di visualizzazione)
"imagesData": { "IMG-001": "data:image/svg+xml;base64,...", ... }, // 200 SVG
"descriptionsData": { "DESC-105": "milky field, overlaid with...", ... } // 200 testi
}
Ogni immagine è un SVG 200x200 contenente:
Esempio di SVG decodificato (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)"/>
<!-- Linee/punti/anelli di overlay opzionali qui -->
<polygon points="..." fill="#E2725B" transform="translate(128.6,185.7) rotate(90)"
style="opacity:0.5;filter:drop-shadow(...)"/>
<!-- Altre forme... -->
</svg>
Ogni descrizione segue uno schema rigoroso:
{campo} field[, overlaid with {overlay}]. {N} elementi totali:
{dimensione} {opacità} {colore} {forma} ({rotazione}, at {posizione}) ·
{dimensione} {opacità} {colore} {forma} ({rotazione}, at {posizione}) · ...
Esempio:
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) · ...
Dovevamo mappare le proprietà SVG al vocabolario delle descrizioni. Questo ha richiesto l'analisi di tutte le 200 immagini e tutte le 200 descrizioni per trovare le corrispondenze esatte.
Contando le occorrenze su entrambi i lati, abbiamo stabilito una corrispondenza 1:1:
| Colore Inizio Gradiente SVG | Conteggio | Parola Descrizione | Conteggio |
|---|---|---|---|
#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 |
Metodo: Contare i colori di inizio gradiente unici in tutti i 200 SVG, contare la prima parola unica in tutte le 200 descrizioni, abbinare in base al conteggio.
Gli overlay SVG sono implementati in modo diverso a seconda del tipo:
| Pattern SVG | Metodo di Rilevamento | Conteggio | Nome Descrizione | Conteggio |
|---|---|---|---|---|
Elementi <line> orizzontali (dy=0) | Controlla x1,y1,x2,y2 | 35 | striped overlay | 35 |
Elementi <line> verticali (dx=0) | Controlla x1,y1,x2,y2 | 34 | lattice pattern | 34 |
<circle> con opacity="0.06" (punti) | Conta i cerchi a bassa opacità | 30 | stippled layer | 30 |
| Nessun elemento di overlay | Nessuna linea, nessuna forma di sfondo | 28 | (nessuno) | 28 |
Elementi <line> diagonali | Controlla la direzione della pendenza | 27 | tilted cross-lines | 27 |
Piccoli <polygon>/<path> con opacity="0.06" | Forme con attributo a bassa opacità | 25 | arrow-band texture | 25 |
<circle> con fill="none" stroke="#888" | Cerchi solo con stroke | 21 | ringed pattern | 21 |