
Compte-rendu détaillé d'un challenge CTF : rétro-ingénierie d'une SPA React, analyse d'images SVG, construction d'un solveur automatisé avec appariement glouton, et obtention d'un score parfait.
Résultat : 200/200 (100 %) — Jeton : WKX-9245FA70-200 Date : 24 mars 2026
Une publication LinkedIn annonçait un challenge spécial pour Wix KickstartX — un programme pour développeurs juniors. Les règles :
wix-kickstartx-challenge-2026.base44.app/ et wixkickstart.comLe site principal wixkickstart.com est un site hébergé par Wix (rendu côté serveur par le moteur Thunderbolt de Wix). Un curl standard ne renvoie que la coquille JavaScript — aucun contenu réel. Nous avons utilisé Chromium headless pour le rendre :
chromium --headless --disable-gpu --no-sandbox --virtual-time-budget=10000 \
--dump-dom "https://wixkickstart.com" > /tmp/wix_rendered.html
À partir du DOM rendu, nous avons extrait un iframe intégré :
Il s'est avéré que c'était une animation de particules p5.js (un artifice visuel de la section hero), et non le challenge lui-même. Le véritable challenge se trouve dans l'application Base44.
Le challenge s'exécute sur Base44 — une plateforme d'applications low-code. L'application est une application React à page unique avec toute la logique dans un seul bundle JS :
https://wix-kickstartx-challenge-2026.base44.app/assets/index-oTG160r9.js
Taille : 410 809 octets (React minifié + logique applicative + SDK Base44)
En utilisant grep sur le bundle JS, nous avons cartographié l'intégralité de l'API :
| Fonction | Rôle |
|---|---|
startGame | Crée une session de jeu, renvoie 200 images + 200 descriptions |
scoreGame | Accepte {matches: {}, sessionId: ""}, renvoie {correctCount: N} |
generateToken | Prend sessionId, renvoie le jeton de complétion |
getLeaderboard | Renvoie les meilleurs scores |
saveNickname | Enregistre le nom affiché pour le classement |
Entités : GameSession, Participant
Modèle d'URL d'API :
POST /api/apps/{appId}/functions/{functionName}
ID d'application : 69aea07cbcb9a3dd1039a58d
const or = 1800; // Time limit: 1800 seconds (30 minutes)
const rm = 200; // Total items: 200 image-description pairs
Une découverte critique — l'application dispose d'un mode de soumission groupée JSON. À partir du source minifié :
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
}
Texte d'espace réservé dans la modale :
{
"IMG-001": "DESC-042",
"IMG-002": "DESC-017",
...
}
Cela signifie que nous n'avons pas besoin de cliquer 200 fois dans l'interface — nous pouvons soumettre programmatiquement un mapping JSON des 200 correspondances.
Le SDK Base44 utilise l'authentification JWT :
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Le payload JWT contient :
{
"sub": "[email protected]",
"exp": 1782162460,
"iat": 1774386460
}
En-têtes requis pour tous les appels 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)
Structure de la réponse :
{
"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
}
Chaque image est un SVG de 200x200 contenant :
Exemple de SVG décodé (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>
Chaque description suit un modèle strict :
{background} field[, overlaid with {overlay}]. {N} elements total:
{size} {opacity} {color} {shape} ({rotation}, at {position}) ·
{size} {opacity} {color} {shape} ({rotation}, at {position}) · ...
Exemple :
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) · ...
Nous devions faire correspondre les propriétés SVG au vocabulaire des descriptions. Cela nécessitait d'analyser les 200 images et les 200 descriptions pour trouver des correspondances exactes.
En comptant les occurrences des deux côtés, nous avons établi un mapping 1:1 :
| Couleur de début de dégradé SVG | Nombre | Mot de description | Nombre |
|---|---|---|---|
#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éthode : compter les couleurs de début de dégradé uniques dans les 200 SVG, compter les premiers mots uniques dans les 200 descriptions, faire correspondre par nombre.
Les superpositions SVG sont implémentées différemment selon le type :
| Motif SVG | Méthode de détection | Nombre | Nom de description | Nombre |
|---|---|---|---|---|
Éléments <line> horizontaux (dy=0) | Vérifier x1,y1,x2,y2 | 35 | striped overlay | 35 |
Éléments <line> verticaux (dx=0) | Vérifier x1,y1,x2,y2 | 34 | lattice pattern | 34 |
<circle> avec opacity="0.06" (points) | Compter les cercles à faible opacité | 30 | stippled layer | 30 |
| Aucun élément de superposition | Aucune ligne, aucune forme d'arrière-plan | 28 | (aucun) | 28 |
Éléments <line> diagonaux | Vérifier la direction de la pente | 27 | tilted cross-lines | 27 |
Petits <polygon>/<path> avec opacity="0.06" | Formes avec attribut de faible opacité | 25 | arrow-band texture | 25 |
<circle> avec fill="none" stroke="#888" | Cercles avec contour uniquement | 21 | ringed pattern | 21 |