Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
wix-kickstartx-challenge-writeup — Write-up detallado de un desafío CTF: ingeniería inversa de una SPA de React, parseo de imágenes SVG, creación de un solucionador automático con emparejamiento voraz (greedy) y logro de una puntuación perfecta. | Kitploit
Herramientas/GitHubGitHub/d0rb/wix-kickstartx-challenge-writeup
Ingeniería InversaCTFAprendizaje y Educación
GitHubd0rb/wix-kickstartx-challenge-writeup

wix-kickstartx-challenge-writeup

Write-up detallado de un desafío CTF: ingeniería inversa de una SPA de React, parseo de imágenes SVG, creación de un solucionador automático con emparejamiento voraz (greedy) y logro de una puntuación perfecta.

Ver Repositorio

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir
hace 3 mesesAún no revisado

Wix KickstartX Challenge 2026 — Writeup completo

Resultado: 200/200 (100%) — Token: WKX-9245FA70-200 Fecha: 24 de marzo de 2026


1. Descubrimiento y reconocimiento

El anuncio del desafío

Una publicación de LinkedIn anunció un desafío especial para Wix KickstartX, un programa para desarrolladores júnior. Las reglas:

  • 30 minutos, un solo intento
  • Los 10 puntajes más altos avanzan directamente a la etapa de examen
  • Dos enlaces proporcionados: wix-kickstartx-challenge-2026.base44.app/ y wixkickstart.com

Encontrar el punto de entrada al desafío

El sitio principal wixkickstart.com es un sitio alojado en Wix (renderizado del lado del servidor por el motor Thunderbolt de Wix). Un curl estándar solo devuelve el shell de JavaScript, sin contenido real. Usamos Chromium sin interfaz gráfica para renderizarlo:

root@kitploit:~
chromium --headless --disable-gpu --no-sandbox --virtual-time-budget=10000 \
  --dump-dom "https://wixkickstart.com" > /tmp/wix_rendered.html

Del DOM renderizado extrajimos un iframe incrustado:

root@kitploit:~

Resultó ser una animación de partículas p5.js (decoración de la sección hero), no el desafío en sí. El desafío real está en la app de Base44.


2. Ingeniería inversa de la app del desafío

Plataforma: Base44

El desafío se ejecuta en Base44, una plataforma de aplicaciones low-code. La app es una aplicación React de una sola página con toda la lógica en un único bundle de JS:

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

Tamaño: 410 809 bytes (React minificado + lógica de la app + SDK de Base44)

Extraer la superficie de la API

Haciendo grep al bundle de JS, mapeamos toda la API:

Entidades: GameSession, Participant

Patrón de URL de la API:

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

ID de la app: 69aea07cbcb9a3dd1039a58d

Constantes clave del bundle

root@kitploit:~
const or = 1800;   // Límite de tiempo: 1800 segundos (30 minutos)
const rm = 200;    // Total de elementos: 200 pares imagen-descripción

Descubrimiento del envío JSON

Un hallazgo crítico: la app tiene un modo de envío masivo JSON. Del código fuente minificado:

root@kitploit:~
function Ik({onSubmit:r, onClose:n}) {
    // ...
    h = JSON.parse(s)  // Parsear entrada JSON
    // Validación: debe ser un objeto como { "IMG-001": "DESC-042", ... }
    r(h)  // Enviar todas las coincidencias de una vez
}

Texto de marcador de posición en el modal:

root@kitploit:~
{
  "IMG-001": "DESC-042",
  "IMG-002": "DESC-017",
  ...
}

Esto significa que no necesitamos hacer clic 200 veces en la interfaz: podemos enviar un mapeo JSON de las 200 coincidencias mediante programación.

Flujo de autenticación

El SDK de Base44 usa autenticación JWT:

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

El payload del JWT contiene:

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

Cabeceras requeridas para todas las llamadas a la 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. Comprender los datos del juego

Respuesta 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)

Estructura de la respuesta:

root@kitploit:~
{
  "status": "active",
  "sessionId": "69c2fd218e5b26f307c941c9",
  "startedAt": "2026-03-24T21:07:45.101Z",
  "imageOrder": ["IMG-154", "IMG-102", ...],   // 200 elementos (orden de visualización)
  "descOrder": ["DESC-125", "DESC-109", ...],  // 200 elementos (orden de visualización)
  "imagesData": { "IMG-001": "data:image/svg+xml;base64,...", ... },  // 200 SVGs
  "descriptionsData": { "DESC-105": "milky field, overlaid with...", ... }  // 200 textos
}

Formato de imagen: SVG en línea (codificado en base64)

Cada imagen es un SVG de 200x200 que contiene:

  1. Un gradiente de fondo (tema claro u oscuro)
  2. Un patrón de superposición opcional (líneas, puntos, anillos, etc.)
  3. Formas geométricas con colores, tamaños, opacidades, rotaciones y posiciones específicos

Ejemplo de SVG decodificado (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>

Formato de descripción: lenguaje natural estructurado

Cada descripción sigue un patrón estricto:

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

Ejemplo:

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. Construcción del solucionador automatizado

Paso 1: Mapear todas las constantes

Necesitábamos mapear las propiedades SVG al vocabulario de las descripciones. Esto requirió analizar las 200 imágenes y las 200 descripciones para encontrar las correspondencias exactas.

Mapeo de fondos (12 tipos)

Contando las ocurrencias en ambos lados, establecimos un mapeo 1:1:

Método: contar los colores únicos de inicio de gradiente en los 200 SVG, contar la primera palabra única en las 200 descripciones y emparejar por conteo.

Mapeo de superposiciones (7 tipos)

Las superposiciones SVG se implementan de forma diferente según el tipo:

Idea clave: los elementos de patrón de fondo usan opacity como atributo XML (por ejemplo, opacity="0.06"), mientras que las formas del primer plano usan opacity dentro del atributo style (por ejemplo, style="opacity:0.7"). Esta distinción fue fundamental para separar los patrones de fondo de las formas reales.

Mapeo de colores (28 colores)

Los 200 SVG usan exactamente 28 colores de relleno hex únicos. Las 200 descripciones usan exactamente 28 nombres de color únicos.

Mapeo de tipos de forma (10 tipos)

Mapeo de tamaños

Basado en el radio máximo de los vértices de la forma desde el origen:

Mapeo de opacidades

Valor de opacidad SVGNombre en la descripción
1.0solid
0.85nearly solid
0.7semi-transparent

Mapeo de rotaciones

Cuadrícula de posiciones

Las formas se colocan en una cuadrícula de 7x7 en las coordenadas: [14.3, 42.9, 71.4, 100.0, 128.6, 157.1, 185.7] tanto para X como para Y.

Estas se corresponden con los nombres de columna: far-left, left, inner-left, center, inner-right, right, far-right Y nombres de fila: top, upper, upper-mid, center, lower-mid, lower, bottom

Paso 2: Parsear todos los SVG

Para cada una de las 200 imágenes SVG:

  1. Decodificar base64 a XML SVG sin procesar
  2. Extraer el fondo — leer el color del primer <stop> de <linearGradient id="bg">
  3. Clasificar la superposición — verificar <line>, <circle fill="none" stroke>, puntos/formas de baja opacidad
  4. Extraer las formas — encontrar todos los elementos <polygon>, <circle>, <path>, <rect> que:
    • NO sean el rect de fondo (omitir width="200")
    • NO sean el rect de viñeta (omitir fill="url(#vig)")
    • NO sean elementos del patrón de superposición (omitir opacity="0.0x" como atributo)
    • SÍ tengan style="opacity:..." (todas las formas del primer plano lo tienen)

Paso 3: Parsear todas las descripciones

Para cada una de las 200 descripciones de texto:

  1. Fondo: primera palabra (por ejemplo, "milky", "pitch", "tenebrous")
  2. Superposición: texto después de "overlaid with" en la primera oración
  3. Conteo de formas: regex (\d+) elements total
  4. Formas individuales: patrón regex:
    root@kitploit:~
    (minuscule|modest|mid-sized|substantial|massive)
    (solid|nearly solid|semi-transparent|faint)
    (color name)
    (shape type)
    ((rotation), at (position))
    

Paso 4: Emparejamiento basado en puntuación

El algoritmo de emparejamiento usa un enfoque de puntuación voraz:

root@kitploit:~
def score_match(img, desc):
    # Restricciones duras: deben coincidir exactamente
    if img['num'] != desc['num']:      return -10000  # Conteo de formas
    if img['bg'] != desc['bg']:        return -10000  # Tipo de fondo
    if img['overlay'] != desc['overlay']: return -10000  # Tipo de superposición

    score = 100  # Puntuación base por cumplir las restricciones duras

    # Puntuación suave: solapamiento de colores
    for color in img_colors:
        if color in desc_colors:
            score += 10

    # Puntuación suave: solapamiento de tipos
    for type in img_types:
        if type in desc_types:
            score += 8

    # Emparejamiento de detalles por forma (voraz estilo húngaro)
    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

Luego asignar vorazmente los pares de mayor puntuación primero:

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. Ejecución y envío

Llamadas a la API realizadas

1. Iniciar juego

root@kitploit:~
POST /api/apps/69aea07cbcb9a3dd1039a58d/functions/startGame
Body: {"email": "[email protected]"}
Response: 200 OK (2.2 MB — todos los datos del juego)

2. Puntuar juego

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. Generar token

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

Cronología

PasoAcción
21:07:45 UTCJuego iniciado (se llamó a startGame)
21:07–21:15Desarrollo del script solucionador (parseo + emparejamiento)
~21:15

Tiempo total de resolución: ~8 minutos de una ventana de 30 minutos.


6. Resumen del stack técnico


7. Ideas clave

Por qué funcionó

  1. El endpoint de envío JSON fue el habilitador crítico: sin él, habríamos necesitado automatización de navegador para hacer clic 400 veces (seleccionar imagen + seleccionar descripción x 200).

  2. El mapeo basado en conteos fue el avance para colores, fondos y superposiciones. En lugar de adivinar a qué corresponde #cd7f32 en inglés, contamos que aparece 65 veces en los SVG y "brass" aparece 65 veces en las descripciones: un conteo único = coincidencia garantizada.

  3. Las restricciones duras eliminan candidatos rápidamente. Cada imagen tiene una combinación única de (tipo de fondo, tipo de superposición, conteo de formas). Con 12 fondos x 7 superposiciones x conteos de formas variables, la mayoría de las imágenes solo tienen un puñado de descripciones posibles, no 200.

  4. El SVG son datos estructurados. A diferencia de las imágenes rasterizadas (PNG/JPG), los SVG son XML: cada forma, color, posición y rotación está explícitamente codificado como texto. No se necesita visión por computadora.

Qué lo hizo desafiante

  1. La detección de superposiciones fue complicada. Los patrones de fondo usaban 5 técnicas SVG diferentes: elementos <line>, <circle> solo con trazo, <circle> con atributo de baja opacidad, <polygon> con atributo de baja opacidad y ausencia de todo lo anterior. La distinción clave fue opacity como atributo XML (fondo) frente a dentro de style (formas del primer plano).

  2. El vocabulario de colores no era obvio. Nombres como "oxblood" (#800020), "brass" (#cd7f32) y "deep sapphire" (#0f52ba) requirieron el enfoque de conteo: no se pueden adivinar de forma fiable solo a partir de los valores hex.

  3. La clasificación de formas a partir de rutas SVG requirió comprender los comandos de ruta SVG:

    • M (moveto), L (lineto), A (arc), (closepath)
Descargar herramienta
FunciónPropósito
startGameCrea una sesión de juego, devuelve 200 imágenes + 200 descripciones
scoreGameAcepta {matches: {}, sessionId: ""}, devuelve {correctCount: N}
generateTokenToma sessionId, devuelve el token de finalización
getLeaderboardDevuelve los puntajes más altos
saveNicknameGuarda el nombre para mostrar en la tabla de clasificación
Color inicial del gradiente SVGConteoPalabra de descripciónConteo
#0a162828pitch28
#eef2f720frosted20
#0d0d0d19tenebrous19
#fef9f018pearlescent18
#1e0a2e18nocturnal18
#0a1a0a17midnight17
#1a0a0a15inky15
#f5f0eb14milky14
#f7f3ee14ethereal14
#f0f0f014radiant14
#f0f7f413glowing13
#1a1a2e10somber10
Patrón SVGMétodo de detecciónConteoNombre en la descripciónConteo
Elementos <line> horizontales (dy=0)Comprobar x1,y1,x2,y235striped overlay35
Elementos <line> verticales (dx=0)Comprobar x1,y1,x2,y234lattice pattern34
<circle> con opacity="0.06" (puntos)Contar círculos de baja opacidad30stippled layer30
Sin elementos de superposiciónSin líneas, sin formas de fondo28(ninguno)28
Elementos <line> diagonalesComprobar la dirección de la pendiente27tilted cross-lines27
<polygon>/<path> pequeños con opacity="0.06"Formas con atributo de baja opacidad25arrow-band texture25
<circle> con fill="none" stroke="#888"Solo círculos con trazo21ringed pattern21
HexNombre en la descripciónHexNombre en la descripción
#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
Elemento SVGLógica de detecciónNombre en la descripción
<polygon> con 3 vérticesContar pares de puntos separados por espaciospike
<polygon> con 4 vérticestilted square
<polygon> con 5 vérticesquint form
<polygon> con 6 vérticesbee cell
<polygon> con 10 vérticesasterisk
<rect> (no fondo)Tiene width/height, no 200x200tilted square
<circle>Nombre de la etiquetadisc
<path> con fill-rule="evenodd"Dos rutas de arco concéntricasdonut
<path> con un solo arco + ZRuta de semicírculohalf-disc
<path> con 6+ comandos LForma de cruz/máscrosshair
<path> con 3-5 comandos LForma de flechapointer
Rango de radioNombre en la descripción
0–12minuscule
13–16modest
17–20mid-sized
21–25substantial
26+massive
0.5faint
Valor de rotate() SVGNombre en la descripción
0°upright
1–20°slightly tilted
21–55°diagonal
56–75°steeply angled
76–105°sideways
  • Para cada forma, extraer:
    • Color: del atributo fill, mapeado mediante COLOR_MAP
    • Tipo: de la etiqueta del elemento + número de vértices / comandos de ruta
    • Tamaño: de las coordenadas de los vértices o el radio
    • Opacidad: de style="opacity:X"
    • Rotación: de transform="... rotate(X)"
    • Posición: de transform="translate(X,Y) ..."
  • scoreGame enviado — 200/200 correctos
    ~21:15generateToken llamado — WKX-9245FA70-200
    ComponenteTecnología
    Plataforma del desafíoBase44 (constructor de apps low-code)
    FrontendReact SPA (un único bundle JS, ~410 KB)
    BackendPython/uvicorn detrás de Cloudflare
    CDN/ProxyCloudflare (HTTP/3, compresión Brotli)
    AutenticaciónJWT (HS256), almacenado en localStorage
    Formato de datosSVG (base64 en línea), API JSON
    Tiempo realWebSocket (socket.io) para actualizaciones de sesión en vivo
    SolucionadorPython 3 (parseo con regex, sin librerías externas)
    Z
  • Un donut = dos rutas de arco concéntricas con fill-rule="evenodd"
  • Un half-disc = una única ruta de arco
  • Un crosshair = ruta con 6+ segmentos de línea (forma de cruz/más)
  • Un pointer = ruta con 3-5 segmentos de línea (forma de flecha)