Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
Outils/GitHubGitHub/d0rb/wix-kickstartx-challenge-writeup
Rétro-ingénierieCTFApprentissage et Éducation
GitHubd0rb/wix-kickstartx-challenge-writeup

wix-kickstartx-challenge-writeup

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.

Voir le dépôt

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager
il y a 3 moisPas encore vérifié

Wix KickstartX Challenge 2026 — Writeup complet

Résultat : 200/200 (100 %) — Jeton : WKX-9245FA70-200 Date : 24 mars 2026


1. Découverte et reconnaissance

L'annonce du challenge

Une publication LinkedIn annonçait un challenge spécial pour Wix KickstartX — un programme pour développeurs juniors. Les règles :

  • 30 minutes, une seule tentative
  • Les 10 meilleurs scores passent directement à l'étape de l'examen
  • Deux liens fournis : wix-kickstartx-challenge-2026.base44.app/ et wixkickstart.com

Trouver le point d'entrée du challenge

Le 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 :

root@kitploit:~
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é :

root@kitploit:~

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.


2. Ingénierie inverse de l'application du challenge

Plateforme : 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 :

root@kitploit:~
https://wix-kickstartx-challenge-2026.base44.app/assets/index-oTG160r9.js

Taille : 410 809 octets (React minifié + logique applicative + SDK Base44)

Extraction de la surface d'API

En utilisant grep sur le bundle JS, nous avons cartographié l'intégralité de l'API :

Entités : GameSession, Participant

Modèle d'URL d'API :

root@kitploit:~
POST /api/apps/{appId}/functions/{functionName}

ID d'application : 69aea07cbcb9a3dd1039a58d

Constantes clés du bundle

root@kitploit:~
const or = 1800;   // Time limit: 1800 seconds (30 minutes)
const rm = 200;    // Total items: 200 image-description pairs

Découverte de la soumission JSON

Une découverte critique — l'application dispose d'un mode de soumission groupée JSON. À partir du source minifié :

root@kitploit:~
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 :

root@kitploit:~
{
  "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.

Flux d'authentification

Le SDK Base44 utilise l'authentification JWT :

root@kitploit:~
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Le payload JWT contient :

root@kitploit:~
{
  "sub": "[email protected]",
  "exp": 1782162460,
  "iat": 1774386460
}

En-têtes requis pour tous les appels API :

root@kitploit:~
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/

3. Comprendre les données du jeu

Réponse de startGame

root@kitploit:~
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 :

root@kitploit:~
{
  "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
}

Format d'image : SVG inline (encodé en base64)

Chaque image est un SVG de 200x200 contenant :

  1. Un dégradé d'arrière-plan (thème clair ou sombre)
  2. Un motif de superposition optionnel (lignes, points, anneaux, etc.)
  3. Des formes géométriques avec des couleurs, tailles, opacités, rotations et positions spécifiques

Exemple de SVG décodé (IMG-001) :

root@kitploit:~
<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>

Format des descriptions : langage naturel structuré

Chaque description suit un modèle strict :

root@kitploit:~
{background} field[, overlaid with {overlay}]. {N} elements total:
{size} {opacity} {color} {shape} ({rotation}, at {position}) ·
{size} {opacity} {color} {shape} ({rotation}, at {position}) · ...

Exemple :

root@kitploit:~
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) · ...

4. Construction du solveur automatisé

Étape 1 : cartographier toutes les constantes

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.

Mapping des arrière-plans (12 types)

En comptant les occurrences des deux côtés, nous avons établi un mapping 1:1 :

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.

Mapping des superpositions (7 types)

Les superpositions SVG sont implémentées différemment selon le type :

Point clé : les éléments de motif d'arrière-plan utilisent opacity comme attribut XML (par exemple, opacity="0.06"), tandis que les formes de premier plan utilisent opacity dans l'attribut style (par exemple, style="opacity:0.7"). Cette distinction a été cruciale pour séparer les motifs d'arrière-plan des formes réelles.

Mapping des couleurs (28 couleurs)

Les 200 SVG utilisent exactement 28 couleurs de remplissage hexadécimales uniques. Les 200 descriptions utilisent exactement 28 noms de couleurs uniques.

Mapping des types de formes (10 types)

Mapping des tailles

Basée sur le rayon maximal des sommets de la forme par rapport à l'origine :

Mapping des opacités

Valeur d'opacité SVGNom de description
1.0solid
0.85nearly solid
0.7semi-transparent

Mapping des rotations

Grille de position

Les formes sont placées sur une grille 7x7 aux coordonnées : [14.3, 42.9, 71.4, 100.0, 128.6, 157.1, 185.7] pour X et Y.

Celles-ci correspondent aux noms de colonnes : far-left, left, inner-left, center, inner-right, right, far-right Et aux noms de lignes : top, upper, upper-mid, center, lower-mid, lower, bottom

Étape 2 : analyser tous les SVG

Pour chacune des 200 images SVG :

  1. Décoder le base64 en XML SVG brut
  2. Extraire l'arrière-plan — lire la couleur du premier <stop> dans <linearGradient id="bg">
  3. Classifier la superposition — rechercher <line>, <circle fill="none" stroke>, les points/formes à faible opacité
  4. Extraire les formes — trouver tous les éléments <polygon>, <circle>, <path>, <rect> qui :
    • Ne sont PAS le rect d'arrière-plan (ignorer width="200")
    • Ne sont PAS le rect de vignette (ignorer fill="url(#vig)")
    • Ne sont PAS des éléments de motif de superposition (ignorer opacity="0.0x" comme attribut)
    • ONT bien un style="opacity:..." (toutes les formes de premier plan en ont un)

Étape 3 : analyser toutes les descriptions

Pour chacune des 200 descriptions textuelles :

  1. Arrière-plan : premier mot (par exemple, « milky », « pitch », « tenebrous »)
  2. Superposition : texte après « overlaid with » dans la première phrase
  3. Nombre de formes : regex (\d+) elements total
  4. Formes individuelles : modèle regex :
    root@kitploit:~
    (minuscule|modest|mid-sized|substantial|massive)
    (solid|nearly solid|semi-transparent|faint)
    (color name)
    (shape type)
    ((rotation), at (position))
    

Étape 4 : correspondance par score

L'algorithme de correspondance utilise une approche de score gloutonne :

root@kitploit:~
def score_match(img, desc):
    # Hard constraints — must match exactly
    if img['num'] != desc['num']:      return -10000  # Shape count
    if img['bg'] != desc['bg']:        return -10000  # Background type
    if img['overlay'] != desc['overlay']: return -10000  # Overlay type

    score = 100  # Base score for matching hard constraints

    # Soft scoring — color overlap
    for color in img_colors:
        if color in desc_colors:
            score += 10

    # Soft scoring — type overlap
    for type in img_types:
        if type in desc_types:
            score += 8

    # Per-shape detail matching (Hungarian-style greedy)
    for each image_shape:
        find best matching desc_shape by:
            +20 if color matches
            +15 if type matches
            +10 if size matches
            +8  if opacity matches
            +5  if rotation matches
        score += best_match_score

    return score

Ensuite, on attribue de manière gloutonne les paires ayant le score le plus élevé en premier :

root@kitploit:~
all_scores.sort(reverse=True)
for score, img_id, desc_id in all_scores:
    if img_id not in matched and desc_id not in used:
        matches[img_id] = desc_id

5. Exécution et soumission

Appels API effectués

1. Démarrer le jeu

root@kitploit:~
POST /api/apps/69aea07cbcb9a3dd1039a58d/functions/startGame
Body: {"email": "[email protected]"}
Response: 200 OK (2.2 MB — all game data)

2. Noter le jeu

root@kitploit:~
POST /api/apps/69aea07cbcb9a3dd1039a58d/functions/scoreGame
Body: {
  "matches": {"IMG-032": "DESC-182", "IMG-198": "DESC-134", ...},
  "sessionId": "69c2fd218e5b26f307c941c9"
}
Response: 200 OK
{"correctCount": 200, "scoreSeal": "3d1ac8a7e7be4856"}

3. Générer le jeton

root@kitploit:~
POST /api/apps/69aea07cbcb9a3dd1039a58d/functions/generateToken
Body: {"sessionId": "69c2fd218e5b26f307c941c9"}
Response: 200 OK
{"token": "WKX-9245FA70-200", "score": 200}

Chronologie

ÉtapeAction
21:07:45 UTCJeu démarré (startGame appelé)
21:07–21:15Développement du script solveur (analyse + correspondance)
~21:15scoreGame soumis —

Temps total de résolution : ~8 minutes sur une fenêtre de 30 minutes.


6. Résumé de la pile technique


7. Enseignements clés

Pourquoi cela a fonctionné

  1. Le point de terminaison de soumission JSON a été le facteur clé — sans lui, il aurait fallu automatiser le navigateur pour cliquer 400 fois (sélectionner l'image + sélectionner la description x 200).

  2. Le mapping par comptage a été la percée pour les couleurs, les arrière-plans et les superpositions. Au lieu de deviner à quelle couleur anglaise correspond le hex #cd7f32, nous avons compté qu'il apparaît 65 fois dans les SVG et que « brass » apparaît 65 fois dans les descriptions — un nombre unique = correspondance garantie.

  3. Les contraintes strictes éliminent rapidement les candidats. Chaque image a une combinaison unique de (background_type, overlay_type, shape_count). Avec 12 arrière-plans x 7 superpositions x des nombres de formes variables, la plupart des images n'ont qu'une poignée de correspondances de description possibles, pas 200.

  4. Le SVG est une donnée structurée. Contrairement aux images matricielles (PNG/JPG), les SVG sont du XML — chaque forme, couleur, position et rotation est explicitement encodée sous forme de texte. Aucune vision par ordinateur nécessaire.

Ce qui l'a rendu difficile

  1. La détection des superpositions était délicate. Les motifs d'arrière-plan utilisaient 5 techniques SVG différentes : des éléments <line>, des <circle> avec contour uniquement, des <circle> avec attribut de faible opacité, des <polygon> avec attribut de faible opacité, et l'absence de tout cela. La distinction clé était opacity comme attribut XML (arrière-plan) par rapport à opacity dans style (formes de premier plan).

  2. Le vocabulaire des couleurs n'était pas évident. Des noms comme « oxblood » (#800020), « brass » (#cd7f32) et « deep sapphire » (#0f52ba) nécessitaient l'approche par comptage — on ne peut pas les deviner de manière fiable à partir des seules valeurs hexadécimales.

  3. La classification des formes à partir des chemins SVG nécessitait de comprendre les commandes de chemin SVG :

    • M (moveto), (lineto), (arc), (closepath)
Télécharger l’outil
FonctionRôle
startGameCrée une session de jeu, renvoie 200 images + 200 descriptions
scoreGameAccepte {matches: {}, sessionId: ""}, renvoie {correctCount: N}
generateTokenPrend sessionId, renvoie le jeton de complétion
getLeaderboardRenvoie les meilleurs scores
saveNicknameEnregistre le nom affiché pour le classement
Couleur de début de dégradé SVGNombreMot de descriptionNombre
#0a162828pitch28
#eef2f720frosted20
#0d0d0d19tenebrous19
#fef9f018pearlescent18
#1e0a2e18nocturnal18
#0a1a0a17midnight17
#1a0a0a15inky15
#f5f0eb14milky14
#f7f3ee14ethereal14
#f0f0f014radiant14
#f0f7f413glowing13
#1a1a2e10somber10
Motif SVGMéthode de détectionNombreNom de descriptionNombre
Éléments <line> horizontaux (dy=0)Vérifier x1,y1,x2,y235striped overlay35
Éléments <line> verticaux (dx=0)Vérifier x1,y1,x2,y234lattice pattern34
<circle> avec opacity="0.06" (points)Compter les cercles à faible opacité30stippled layer30
Aucun élément de superpositionAucune ligne, aucune forme d'arrière-plan28(aucun)28
Éléments <line> diagonauxVérifier la direction de la pente27tilted cross-lines27
Petits <polygon>/<path> avec opacity="0.06"Formes avec attribut de faible opacité25arrow-band texture25
<circle> avec fill="none" stroke="#888"Cercles avec contour uniquement21ringed pattern21
HexNom de descriptionHexNom de description
#708090blue-gray#cd7f32brass
#f5f5f5near white#dc143cfiery red
#00bcd4electric cyan#2196f3azure
#ff6b6bsalmon pink#228b22rich green
#98ff98pale green#b0b0b0platinum
#0f52badeep sapphire#800020oxblood
#ff8c00deep orange#4b0082dark purple
#e2725bterra rosa#ffbf00marigold
#b7410erusset#ff69b4candy pink
#ffd700bright gold#c0c0c0tin
#6b8e23moss#008080deep teal
#40e0d0pale teal#0047abroyal blue
#e34234burnt sienna#a0522ddeep red
#4a4a4adim gray#36454fdark gray
Élément SVGLogique de détectionNom de description
<polygon> à 3 sommetsCompter les paires de points séparées par des espacespike
<polygon> à 4 sommetstilted square
<polygon> à 5 sommetsquint form
<polygon> à 6 sommetsbee cell
<polygon> à 10 sommetsasterisk
<rect> (hors arrière-plan)Possède width/height, pas 200x200tilted square
<circle>Nom de la balisedisc
<path> avec fill-rule="evenodd"Deux chemins d'arc concentriquesdonut
<path> avec arc unique + ZChemin en demi-cerclehalf-disc
<path> avec 6 commandes L ou plusForme de croix/pluscrosshair
<path> avec 3 à 5 commandes LForme en pointe de flèchepointer
Plage de rayonNom de description
0–12minuscule
13–16modest
17–20mid-sized
21–25substantial
26+massive
0.5
faint
Valeur de rotate() SVGNom de description
0°upright
1–20°slightly tilted
21–55°diagonal
56–75°steeply angled
76–105°sideways
  • Pour chaque forme, extraire :
    • Couleur : depuis l'attribut fill, mappée via COLOR_MAP
    • Type : depuis la balise de l'élément + nombre de sommets / commandes de chemin
    • Taille : depuis les coordonnées des sommets ou le rayon
    • Opacité : depuis style="opacity:X"
    • Rotation : depuis transform="... rotate(X)"
    • Position : depuis transform="translate(X,Y) ..."
  • 200/200 correct
    ~21:15generateToken appelé — WKX-9245FA70-200
    ComposantTechnologie
    Plateforme du challengeBase44 (constructeur d'applications low-code)
    FrontendSPA React (bundle JS unique, ~410 Ko)
    BackendPython/uvicorn derrière Cloudflare
    CDN/ProxyCloudflare (HTTP/3, compression Brotli)
    AuthentificationJWT (HS256), stocké dans localStorage
    Format de donnéesSVG (base64 inline), API JSON
    Temps réelWebSocket (socket.io) pour les mises à jour de session en direct
    SolveurPython 3 (analyse regex, aucune bibliothèque externe)
    L
    A
    Z
  • Un donut = deux chemins d'arc concentriques avec fill-rule="evenodd"
  • Un demi-disque = un chemin d'arc unique
  • Un réticule = chemin avec 6 segments de ligne ou plus (forme de croix/plus)
  • Un pointeur = chemin avec 3 à 5 segments de ligne (forme de flèche)