
Detailed write-up of a CTF challenge: reverse engineering a React SPA, parsing SVG images, building an automated solver with greedy matching, and achieving a perfect score.
Result: 200/200 (100%) — Token: WKX-9245FA70-200 Date: March 24, 2026
A LinkedIn post announced a special challenge for Wix KickstartX — a junior developer program. The rules:
wix-kickstartx-challenge-2026.base44.app/ and wixkickstart.comThe main site wixkickstart.com is a Wix-hosted site (server-side rendered by Wix's
Thunderbolt engine). Standard curl only returns the JavaScript shell — no actual content.
We used headless Chromium to render it:
chromium --headless --disable-gpu --no-sandbox --virtual-time-budget=10000 \
--dump-dom "https://wixkickstart.com" > /tmp/wix_rendered.html
From the rendered DOM we extracted an embedded iframe:
This turned out to be a p5.js particle animation (hero section eye candy), not the challenge itself. The actual challenge lives at the Base44 app.
The challenge runs on Base44 — a low-code app platform. The app is a single-page React application with all logic in one JS bundle:
https://wix-kickstartx-challenge-2026.base44.app/assets/index-oTG160r9.js
Size: 410,809 bytes (minified React + app logic + Base44 SDK)
By grepping the JS bundle, we mapped the entire API:
| Function | Purpose |
|---|---|
startGame | Creates a game session, returns 200 images + 200 descriptions |
scoreGame | Accepts {matches: {}, sessionId: ""}, returns {correctCount: N} |
generateToken | Takes sessionId, returns completion token |
getLeaderboard | Returns top scores |
saveNickname | Saves display name for leaderboard |
Entities: GameSession, Participant
API URL pattern:
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
A critical finding — the app has a JSON bulk submission mode. From the minified source:
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
}
Placeholder text in the modal:
{
"IMG-001": "DESC-042",
"IMG-002": "DESC-017",
...
}
This means we don't need to click 200 times in the UI — we can submit a JSON mapping of all 200 matches programmatically.
The Base44 SDK uses JWT authentication:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
The JWT payload contains:
{
"sub": "[email protected]",
"exp": 1782162460,
"iat": 1774386460
}
Required headers for all API calls:
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)
Response structure:
{
"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
}
Each image is a 200x200 SVG containing:
Example decoded 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>
Each description follows a strict pattern:
{background} field[, overlaid with {overlay}]. {N} elements total:
{size} {opacity} {color} {shape} ({rotation}, at {position}) ·
{size} {opacity} {color} {shape} ({rotation}, at {position}) · ...
Example:
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) · ...
We needed to map SVG properties to description vocabulary. This required analyzing all 200 images and all 200 descriptions to find exact correspondences.
By counting occurrences on both sides, we established a 1:1 mapping:
| SVG Gradient Start Color | Count | Description Word | Count |
|---|---|---|---|
#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 |
Method: Count unique gradient start colors in all 200 SVGs, count unique first-word in all 200 descriptions, match by count.
SVG overlays are implemented differently depending on type:
| SVG Pattern | Detection Method | Count | Description Name | Count |
|---|---|---|---|---|
<line> elements, horizontal (dy=0) | Check x1,y1,x2,y2 | 35 | striped overlay | 35 |
<line> elements, vertical (dx=0) | Check x1,y1,x2,y2 | 34 | lattice pattern | 34 |
<circle> with opacity="0.06" (dots) | Count low-opacity circles | 30 | stippled layer | 30 |
| No overlay elements at all | No lines, no bg shapes | 28 | (none) | 28 |
<line> elements, diagonal | Check slope direction | 27 | tilted cross-lines | 27 |
Small <polygon>/<path> with opacity="0.06" | Low-opacity attr shapes | 25 | arrow-band texture | 25 |
<circle> with fill="none" stroke="#888" | Stroke-only circles | 21 | ringed pattern | 21 |
Key insight: Background pattern elements use opacity as an XML attribute
(e.g., opacity="0.06"), while foreground shapes use opacity inside the style
attribute (e.g., style="opacity:0.7"). This distinction was critical for separating
background patterns from actual shapes.
All 200 SVGs use exactly 28 unique hex fill colors. All 200 descriptions use exactly 28 unique color names.