
شرح مفصّل لتحدي CTF: هندسة عكسية لتطبيق SPA مبني على React، وتحليل صور SVG، وبناء حلّال آلي باستخدام المطابقة الجشعة، وتحقيق علامة كاملة.
النتيجة: 200/200 (100%) — الرمز: WKX-9245FA70-200 التاريخ: 24 مارس 2026
أعلن منشور على LinkedIn عن تحدٍّ خاص لبرنامج Wix KickstartX — وهو برنامج للمطوّرين المبتدئين. القواعد:
wix-kickstartx-challenge-2026.base44.app/ و wixkickstart.comالموقع الرئيسي wixkickstart.com هو موقع مستضاف على Wix (يُقدَّم من جانب الخادم بواسطة محرك Thunderbolt الخاص بـ Wix). لا يُرجع أمر curl العادي سوى هيكل JavaScript — دون أي محتوى فعلي. استخدمنا متصفح Chromium بدون واجهة رسومية (headless) لعرضه:
chromium --headless --disable-gpu --no-sandbox --virtual-time-budget=10000 \
--dump-dom "https://wixkickstart.com" > /tmp/wix_rendered.html
من DOM المعروض، استخرجنا iframe مضمّنًا:
اتضح أنه رسم متحرك جسيمات p5.js (عنصر جمالي في القسم الرئيسي)، وليس التحدي نفسه. التحدي الفعلي موجود في تطبيق Base44.
يعمل التحدي على Base44 — وهي منصة تطبيقات منخفضة الكود (low-code). التطبيق هو تطبيق React أحادي الصفحة، وكل المنطق في حزمة JS واحدة:
https://wix-kickstartx-challenge-2026.base44.app/assets/index-oTG160r9.js
الحجم: 410,809 بايت (React مُصغَّر + منطق التطبيق + Base44 SDK)
من خلال البحث في حزمة JS، قمنا بتخطيط الـ API بالكامل:
الكيانات: GameSession، Participant
نمط عنوان API:
POST /api/apps/{appId}/functions/{functionName}
معرّف التطبيق: 69aea07cbcb9a3dd1039a58d
const or = 1800; // Time limit: 1800 seconds (30 minutes)
const rm = 200; // Total items: 200 image-description pairs
اكتشاف بالغ الأهمية — التطبيق يحتوي على وضع إرسال جماعي JSON. من المصدر المُصغَّر:
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
}
النص النموذجي داخل النافذة المنبثقة:
{
"IMG-001": "DESC-042",
"IMG-002": "DESC-017",
...
}
هذا يعني أننا لا نحتاج إلى النقر 200 مرة في الواجهة — يمكننا إرسال خريطة JSON لجميع المطابقات الـ200 برمجيًا.
يستخدم SDK الخاص بـ Base44 مصادقة JWT:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
حمولة JWT تحتوي على:
{
"sub": "[email protected]",
"exp": 1782162460,
"iat": 1774386460
}
الترويسات المطلوبة لجميع استدعاءات 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)
بنية الاستجابة:
{
"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
}
كل صورة هي SVG بأبعاد 200x200 تحتوي على:
مثال لـ 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>
كل وصف يتبع نمطًا صارمًا:
{background} field[, overlaid with {overlay}]. {N} elements total:
{size} {opacity} {color} {shape} ({rotation}, at {position}) ·
{size} {opacity} {color} {shape} ({rotation}, at {position}) · ...
مثال:
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) · ...
احتجنا إلى ربط خصائص SVG بمفردات الوصف. تطلّب ذلك تحليل جميع الصور الـ200 وجميع الأوصاف الـ200 للعثور على التطابقات الدقيقة.
من خلال عدّ مرات الظهور في الجانبين، أنشأنا تطابقًا 1:1:
الطريقة: عدّ ألوان بداية التدرج الفريدة في جميع ملفات SVG الـ200، وعدّ الكلمة الأولى الفريدة في جميع الأوصاف الـ200، ثم المطابقة بناءً على العدد.
تراكبات SVG مُنفَّذة بطرق مختلفة حسب النوع:
الملاحظة الأساسية: عناصر نمط الخلفية تستخدم opacity كـ سمة XML (مثل opacity="0.06")، بينما تستخدم الأشكال الأمامية opacity داخل سمة style (مثل style="opacity:0.7"). كان هذا التمييز حاسمًا لفصل أنماط الخلفية عن الأشكال الفعلية.
جميع ملفات SVG الـ200 تستخدم 28 لون تعبئة hex فريدًا بالضبط. وجميع الأوصاف الـ200 تستخدم 28 اسم لون فريدًا بالضبط.
استنادًا إلى أقصى نصف قطر لرؤوس الشكل من نقطة الأصل:
| قيمة شفافية SVG | اسم الوصف |
|---|---|
| 1.0 | solid |
| 0.85 | nearly solid |
| 0.7 | semi-transparent |
| 0.5 |
تُوضع الأشكال على شبكة 7x7 عند الإحداثيات:
[14.3, 42.9, 71.4, 100.0, 128.6, 157.1, 185.7]
هذه تُرتبط بأسماء الأعمدة: far-left, left, inner-left, center, inner-right, right, far-right
وبأسماء الصفوف: top, upper, upper-mid, center, lower-mid, lower, bottom
لكل صورة من صور SVG الـ200:
<stop> من <linearGradient id="bg"><line> أو <circle fill="none" stroke> أو نقاط/أشكال منخفضة الشفافية<polygon> و <circle> و <path> و <rect> التي:
width="200")fill="url(#vig)")opacity="0.0x" كسمة)style="opacity:..." (جميع الأشكال الأمامية تملك هذا)لكل وصف نصي من الأوصاف الـ200:
(\d+) elements total (minuscule|modest|mid-sized|substantial|massive)
(solid|nearly solid|semi-transparent|faint)
(color name)
(shape type)
((rotation), at (position))
تستخدم خوارزمية المطابقة نهج التسجيل الجشع:
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
ثم نخصّص الأزواج الأعلى درجة أولًا بطريقة جشعة:
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
1. Start Game
POST /api/apps/69aea07cbcb9a3dd1039a58d/functions/startGame
Body: {"email": "[email protected]"}
Response: 200 OK (2.2 MB — all game data)
2. Score Game
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. Generate Token
POST /api/apps/69aea07cbcb9a3dd1039a58d/functions/generateToken
Body: {"sessionId": "69c2fd218e5b26f307c941c9"}
Response: 200 OK
{"token": "WKX-9245FA70-200", "score": 200}
| الخطوة | الإجراء |
|---|---|
| 21:07:45 UTC | بدأت اللعبة (تم استدعاء startGame) |
| 21:07–21:15 | تطوير سكربت الحلّال (التحليل + المطابقة) |
| ~21:15 | تم إرسال scoreGame — |
إجمالي وقت الحل: ~8 دقائق من نافذة مدتها 30 دقيقة.
نقطة الإرسال JSON كانت العامل الحاسم — دونها، كنا سنحتاج إلى أتمتة المتصفح للنقر 400 مرة (اختيار الصورة + اختيار الوصف × 200).
التخطيط القائم على العدّ كان الاختراق للألوان والخلفيات والتراكبات. بدلًا من تخمين ما يقابله اللون السداسي #cd7f32 بالإنجليزية، عددنا مرات ظهوره في ملفات SVG فوجدناه 65 مرة، ووجدنا "brass" تظهر 65 مرة في الأوصاف — العدد الفريد = تطابق مضمون.
القيود الصارمة تستبعد المرشحين بسرعة. كل صورة لها تركيبة فريدة من (نوع الخلفية، نوع التراكب، عدد الأشكال). مع 12 خلفية × 7 تراكبات × أعداد أشكال متفاوتة، معظم الصور لديها حفنة فقط من أوصاف المرشحين المحتملين، وليس 200.
SVG بيانات منظمة. على عكس الصور النقطية (PNG/JPG)، فإن SVGs هي XML — كل شكل ولون وموضع ودوران مشفَّر صراحةً كنص. لا حاجة لرؤية حاسوبية.
كان اكتشاف التراكب صعبًا. استخدمت أنماط الخلفية 5 تقنيات SVG مختلفة: عناصر <line>، و <circle> بحدود فقط، و <circle> بسمة شفافية منخفضة، و <polygon> بسمة شفافية منخفضة، وغياب كل هذه. التمييز الأساسي كان opacity كسمة XML (للخلفية) مقابل داخل style (لأشكال المقدمة).
مفردات الألوان لم تكن واضحة. أسماء مثل "oxblood" (#800020) و"brass" (#cd7f32) و"deep sapphire" (#0f52ba) تطلبت نهج العدّ — لا يمكنك تخمينها بشكل موثوق من القيم السداسية وحدها.
تصنيف الأشكال من مسارات SVG تطلّب فهم أوامر مسار SVG:
M (moveto)، L (lineto)، A (arc)، Z (closepath)| Function | الغرض |
|---|
startGame | ينشئ جلسة لعبة، ويعيد 200 صورة + 200 وصف |
scoreGame | يقبل {matches: {}, sessionId: ""}، ويعيد {correctCount: N} |
generateToken | يأخذ معرّف الجلسة، ويعيد رمز الإكمال |
getLeaderboard | يعيد أعلى الدرجات |
saveNickname | يحفظ الاسم المعروض للوحة المتصدرين |
| لون بداية تدرج SVG | العدد | كلمة الوصف | العدد |
|---|
#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 |
| نمط SVG | طريقة الاكتشاف | العدد | اسم الوصف | العدد |
|---|
عناصر <line> أفقية (dy=0) | فحص x1,y1,x2,y2 | 35 | striped overlay | 35 |
عناصر <line> عمودية (dx=0) | فحص x1,y1,x2,y2 | 34 | lattice pattern | 34 |
<circle> مع opacity="0.06" (نقاط) | عدّ الدوائر منخفضة الشفافية | 30 | stippled layer | 30 |
| لا توجد عناصر تراكب إطلاقًا | لا خطوط، ولا أشكال خلفية | 28 | (none) | 28 |
عناصر <line> قطرية | فحص اتجاه الميل | 27 | tilted cross-lines | 27 |
<polygon>/<path> صغيرة مع opacity="0.06" | أشكال بسمة شفافية منخفضة | 25 | arrow-band texture | 25 |
<circle> مع fill="none" stroke="#888" | دوائر بحدود فقط | 21 | ringed pattern | 21 |
| Hex | اسم الوصف | Hex | اسم الوصف |
|---|
#708090 | blue-gray | #cd7f32 | brass | |
#f5f5f5 | near white | #dc143c | fiery red | |
#00bcd4 | electric cyan | #2196f3 | azure | |
#ff6b6b | salmon pink | #228b22 | rich green | |
#98ff98 | pale green | #b0b0b0 | platinum | |
#0f52ba | deep sapphire | #800020 | oxblood | |
#ff8c00 | deep orange | #4b0082 | dark purple | |
#e2725b | terra rosa | #ffbf00 | marigold | |
#b7410e | russet | #ff69b4 | candy pink | |
#ffd700 | bright gold | #c0c0c0 | tin | |
#6b8e23 | moss | #008080 | deep teal | |
#40e0d0 | pale teal | #0047ab | royal blue | |
#e34234 | burnt sienna | #a0522d | deep red | |
#4a4a4a | dim gray | #36454f | dark gray |
| عنصر SVG | منطق الاكتشاف | اسم الوصف |
|---|
<polygon> 3 رؤوس | عدّ أزواج النقاط المفصولة بمسافات | pike |
<polygon> 4 رؤوس | tilted square | |
<polygon> 5 رؤوس | quint form | |
<polygon> 6 رؤوس | bee cell | |
<polygon> 10 رؤوس | asterisk | |
<rect> (ليس خلفية) | له width/height، وليس 200x200 | tilted square |
<circle> | اسم الوسم | disc |
<path> مع fill-rule="evenodd" | مساران قوسيان متحدا المركز | donut |
<path> مع قوس واحد + Z | مسار نصف دائرة | half-disc |
<path> مع 6+ أوامر L | شكل صليب/زائد | crosshair |
<path> مع 3-5 أوامر L | شكل شبيه بالسهم | pointer |
| نطاق نصف القطر | اسم الوصف |
|---|
| 0–12 | minuscule |
| 13–16 | modest |
| 17–20 | mid-sized |
| 21–25 | substantial |
| 26+ | massive |
faint |
| قيمة rotate() في SVG | اسم الوصف |
|---|
| 0° | upright |
| 1–20° | slightly tilted |
| 21–55° | diagonal |
| 56–75° | steeply angled |
| 76–105° | sideways |
fill، ويُربط عبر COLOR_MAPstyle="opacity:X"transform="... rotate(X)"transform="translate(X,Y) ..."| ~21:15 | تم استدعاء generateToken — WKX-9245FA70-200 |
| المكوّن | التقنية |
|---|
| منصة التحدي | Base44 (أداة بناء تطبيقات منخفضة الكود) |
| الواجهة الأمامية | React SPA (حزمة JS واحدة، ~410KB) |
| الواجهة الخلفية | Python/uvicorn خلف Cloudflare |
| CDN/وكيل | Cloudflare (HTTP/3، ضغط Brotli) |
| المصادقة | JWT (HS256)، مخزَّنة في localStorage |
| تنسيق البيانات | SVG (مضمّن base64)، JSON API |
| الوقت الفعلي | WebSocket (socket.io) لتحديثات الجلسة المباشرة |
| الحلّال | Python 3 (تحليل بالتعبيرات النمطية، دون مكتبات خارجية) |
fill-rule="evenodd"