
Ausführlicher Write-up einer CTF-Challenge: Reverse Engineering einer React-SPA, Parsen von SVG-Bildern, Erstellen eines automatisierten Solvers mit Greedy-Matching und Erzielen einer perfekten Punktzahl.
Ergebnis: 200/200 (100 %) — Token: WKX-9245FA70-200 Datum: 24. März 2026
Ein LinkedIn-Post kündigte eine spezielle Challenge für Wix KickstartX an — ein Junior-Entwicklerprogramm. Die Regeln:
wix-kickstartx-challenge-2026.base44.app/ und wixkickstart.comDie Hauptseite wixkickstart.com ist eine von Wix gehostete Seite (serverseitig gerendert von Wix' Thunderbolt-Engine). Ein Standard-curl-Aufruf gibt nur die JavaScript-Hülle zurück — keine tatsächlichen Inhalte. Wir verwendeten headless Chromium, um sie zu rendern:
chromium --headless --disable-gpu --no-sandbox --virtual-time-budget=10000 \
--dump-dom "https://wixkickstart.com" > /tmp/wix_rendered.html
Aus dem gerenderten DOM extrahierten wir einen eingebetteten iframe:
Das entpuppte sich als p5.js-Partikelanimation (Augenschmaus im Hero-Bereich), nicht als die Challenge selbst. Die eigentliche Challenge befindet sich in der Base44-App.
Die Challenge läuft auf Base44 — einer Low-Code-App-Plattform. Die App ist eine Single-Page-React-Anwendung, deren gesamte Logik in einem einzigen JS-Bundle steckt:
https://wix-kickstartx-challenge-2026.base44.app/assets/index-oTG160r9.js
Größe: 410.809 Bytes (minifiziertes React + App-Logik + Base44-SDK)
Indem wir das JS-Bundle per grep durchsuchten, bildeten wir die gesamte API ab:
| Funktion | Zweck |
|---|---|
startGame | Erstellt eine Spielsitzung, gibt 200 Bilder + 200 Beschreibungen zurück |
scoreGame | Akzeptiert {matches: {}, sessionId: ""}, gibt {correctCount: N} zurück |
generateToken | Nimmt sessionId entgegen, gibt Abschluss-Token zurück |
getLeaderboard | Gibt die besten Punktzahlen zurück |
saveNickname | Speichert den Anzeigenamen für die Bestenliste |
Entitäten: GameSession, Participant
API-URL-Muster:
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
Eine entscheidende Entdeckung — die App verfügt über einen JSON-Stapelübermittlungsmodus. Aus dem minifizierten Quellcode:
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
}
Platzhaltertext im Modal:
{
"IMG-001": "DESC-042",
"IMG-002": "DESC-017",
...
}
Das bedeutet, dass wir nicht 200 Mal in der UI klicken müssen — wir können ein JSON-Mapping aller 200 Zuordnungen programmatisch übermitteln.
Das Base44-SDK nutzt JWT-Authentifizierung:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Das JWT-Payload enthält:
{
"sub": "[email protected]",
"exp": 1782162460,
"iat": 1774386460
}
Erforderliche Header für alle API-Aufrufe:
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)
Antwortstruktur:
{
"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
}
Jedes Bild ist ein 200x200-SVG mit:
Beispiel für ein dekodiertes 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>
Jede Beschreibung folgt einem strengen Muster:
{background} field[, overlaid with {overlay}]. {N} elements total:
{size} {opacity} {color} {shape} ({rotation}, at {position}) ·
{size} {opacity} {color} {shape} ({rotation}, at {position}) · ...
Beispiel:
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) · ...
Wir mussten SVG-Eigenschaften auf das Beschreibungsvokabular abbilden. Dafür analysierten wir alle 200 Bilder und alle 200 Beschreibungen, um exakte Entsprechungen zu finden.
Durch Zählen der Vorkommen auf beiden Seiten stellten wir eine 1:1-Zuordnung her:
| SVG-Verlauf-Startfarbe | Anzahl | Beschreibungswort | Anzahl |
|---|---|---|---|
#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 |
Methode: Eindeutige Verlaufs-Startfarben in allen 200 SVGs zählen, eindeutige erste Wörter in allen 200 Beschreibungen zählen, anhand der Anzahl zuordnen.
SVG-Überlagerungen werden je nach Typ unterschiedlich implementiert:
| SVG-Muster | Erkennungsmethode | Anzahl | Beschreibungsname | Anzahl |
|---|---|---|---|---|
<line>-Elemente, horizontal (dy=0) | x1,y1,x2,y2 prüfen | 35 | striped overlay | 35 |
<line>-Elemente, vertikal (dx=0) | x1,y1,x2,y2 prüfen | 34 | lattice pattern | 34 |
<circle> mit opacity="0.06" (Punkte) | Kreise mit niedriger Deckkraft zählen | 30 | stippled layer | 30 |
| Überhaupt keine Überlagerungselemente | Keine Linien, keine Hintergrundformen | 28 | (keine) | 28 |
<line>-Elemente, diagonal | Steigungsrichtung prüfen | 27 | tilted cross-lines | 27 |
Kleine <polygon>/<path>-Elemente mit opacity="0.06" | Formen mit niedrigem Opacity-Attribut | 25 | arrow-band texture | 25 |
<circle> mit fill="none" stroke="#888" | Kreise nur mit Stroke | 21 | ringed pattern | 21 |