Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
wix-kickstartx-challenge-writeup — CTF 챌린지 상세 해설: React SPA 리버스 엔지니어링, SVG 이미지 파싱, 그리디 매칭 기반 자동화 솔버 구축 및 만점 달성. | Kitploit
도구/GitHubGitHub/d0rb/wix-kickstartx-challenge-writeup
Reverse EngineeringCTFLearning & Education
GitHubd0rb/wix-kickstartx-challenge-writeup

wix-kickstartx-challenge-writeup

CTF 챌린지 상세 해설: React SPA 리버스 엔지니어링, SVG 이미지 파싱, 그리디 매칭 기반 자동화 솔버 구축 및 만점 달성.

저장소 보기

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유
3개월 전아직 검토되지 않음

Wix KickstartX Challenge 2026 — 전체 라이트업

결과: 200/200 (100%) — 토큰: WKX-9245FA70-200 날짜: 2026년 3월 24일


1. 발견 및 정찰

챌린지 공지

LinkedIn 게시물을 통해 주니어 개발자 프로그램인 Wix KickstartX를 위한 특별 챌린지가 공지되었습니다. 규칙:

  • 30분, 단 한 번의 시도만 허용
  • 상위 10명의 최고 점수자는 시험 단계로 직접 진출
  • 두 개의 링크 제공: wix-kickstartx-challenge-2026.base44.app/ 및 wixkickstart.com

챌린지 진입점 찾기

메인 사이트 wixkickstart.com은 Wix에서 호스팅하는 사이트입니다(Wix의 Thunderbolt 엔진으로 서버 사이드 렌더링됨). 일반적인 curl은 JavaScript 셸만 반환할 뿐 실제 콘텐츠는 포함하지 않습니다. 이를 렌더링하기 위해 헤드리스 Chromium을 사용했습니다:

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

렌더링된 DOM에서 임베디드 iframe을 추출했습니다:

root@kitploit:~

이것은 챌린지 자체가 아니라 p5.js 파티클 애니메이션(히어로 섹션의 시각적 장식)이었습니다. 실제 챌린지는 Base44 앱에 있습니다.


2. 챌린지 앱 리버스 엔지니어링

플랫폼: Base44

챌린지는 Base44 — 로우코드 앱 플랫폼에서 실행됩니다. 이 앱은 모든 로직이 하나의 JS 번들에 포함된 단일 페이지 React 애플리케이션입니다:

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

크기: 410,809바이트(압축된 React + 앱 로직 + Base44 SDK)

API 표면 추출

JS 번들을 grep하여 전체 API를 매핑했습니다:

엔티티: GameSession, Participant

API URL 패턴:

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

앱 ID: 69aea07cbcb9a3dd1039a58d

번들에서 추출한 핵심 상수

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

JSON 제출 방식 발견

중요한 발견 — 이 앱에는 JSON 대량 제출 모드가 있습니다. 압축된 소스에서:

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
}

모달의 플레이스홀더 텍스트:

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

즉, UI에서 200번 클릭할 필요 없이 프로그래밍 방식으로 200개 매칭 전체의 JSON 매핑을 제출할 수 있습니다.

인증 흐름

Base44 SDK는 JWT 인증을 사용합니다:

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

JWT 페이로드에는 다음이 포함됩니다:

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

모든 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. 게임 데이터 이해하기

startGame 응답

root@kitploit:~
POST /api/apps/69aea07cbcb9a3dd1039a58d/functions/startGame
Status: 200
Transferred: 62.87 kB compressed (2.20 MB decompressed)
Content-Encoding: br (Brotli)

응답 구조:

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
}

이미지 형식: 인라인 SVG (base64 인코딩)

각 이미지는 다음을 포함하는 200x200 SVG입니다:

  1. 배경 그라데이션 (밝은 또는 어두운 테마)
  2. 선택적 오버레이 패턴 (선, 점, 링 등)
  3. 특정 색상, 크기, 불투명도, 회전, 위치를 가진 기하학적 도형

디코딩된 SVG 예시 (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>

설명 형식: 구조화된 자연어

각 설명은 엄격한 패턴을 따릅니다:

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

예시:

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. 자동화 솔버 구축

1단계: 모든 상수 매핑

SVG 속성을 설명 어휘에 매핑해야 했습니다. 이를 위해 정확한 대응 관계를 찾기 위해 200개 모든 이미지와 200개 모든 설명을 분석해야 했습니다.

배경 매핑 (12가지 유형)

양쪽의 출현 횟수를 세어 1:1 매핑을 확립했습니다:

방법: 200개 모든 SVG에서 고유한 그라데이션 시작 색상을 세고, 200개 모든 설명에서 고유한 첫 단어를 센 다음 개수로 매칭합니다.

오버레이 매핑 (7가지 유형)

SVG 오버레이는 유형에 따라 다르게 구현됩니다:

핵심 통찰: 배경 패턴 요소는 opacity를 XML 속성으로 사용하는 반면(예: opacity="0.06"), 전경 도형은 style 속성 내부에서 opacity를 사용합니다(예: style="opacity:0.7"). 이 구분은 배경 패턴과 실제 도형을 분리하는 데 결정적이었습니다.

색상 매핑 (28가지 색상)

200개 모든 SVG는 정확히 28개의 고유한 hex 채우기 색상을 사용하며, 200개 모든 설명은 정확히 28개의 고유한 색상 이름을 사용합니다.

도형 유형 매핑 (10가지 유형)

크기 매핑

원점에서 도형 꼭짓점까지의 최대 반경 기준:

불투명도 매핑

SVG 불투명도 값설명 이름
1.0solid
0.85nearly solid
0.7semi-transparent
0.5

회전 매핑

위치 그리드

도형은 X와 Y 모두에 대해 다음 좌표의 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

2단계: 모든 SVG 파싱

200개 SVG 이미지 각각에 대해:

  1. 디코딩 — base64를 원시 SVG XML로 변환
  2. 배경 추출 — <linearGradient id="bg">에서 첫 번째 <stop> 색상 읽기
  3. 오버레이 분류 — <line>, <circle fill="none" stroke>, 낮은 불투명도의 점/도형 확인
  4. 도형 추출 — 다음 조건을 충족하는 모든 <polygon>, <circle>, <path>, <rect> 요소 찾기:
    • 배경 rect가 아님 (width="200" 건너뛰기)
    • 비네트 rect가 아님 (fill="url(#vig)" 건너뛰기)
    • 오버레이 패턴 요소가 아님 (속성 opacity="0.0x" 건너뛰기)
    • style="opacity:..."를 가짐 (모든 전경 도형이 이 속성을 가짐)
  5. 각 도형에 대해 다음을 추출:
    • 색상: fill 속성에서 가져와 COLOR_MAP으로 매핑

3단계: 모든 설명 파싱

200개 텍스트 설명 각각에 대해:

  1. 배경: 첫 번째 단어 (예: "milky", "pitch", "tenebrous")
  2. 오버레이: 첫 문장에서 "overlaid with" 이후의 텍스트
  3. 도형 개수: 정규식 (\d+) elements total
  4. 개별 도형: 정규식 패턴:
    root@kitploit:~
    (minuscule|modest|mid-sized|substantial|massive)
    (solid|nearly solid|semi-transparent|faint)
    (color name)
    (shape type)
    ((rotation), at (position))
    

4단계: 점수 기반 매칭

매칭 알고리즘은 탐욕적 점수 계산 방식을 사용합니다:

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

그런 다음 가장 높은 점수를 받은 쌍부터 탐욕적으로 할당합니다:

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. 실행 및 제출

수행한 API 호출

1. 게임 시작

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

2. 점수 제출

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. 토큰 생성

root@kitploit:~
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:15scoreGame 제출 — 200/200 정답
~21:15generateToken 호출 —

총 해결 시간: 30분 제한 시간 중 약 8분.


6. 기술 스택 요약


7. 핵심 인사이트

이것이 성공한 이유

  1. JSON 제출 엔드포인트가 핵심 가능 요인이었습니다 — 이것이 없었다면 브라우저 자동화로 400번 클릭해야 했을 것입니다 (이미지 선택 + 설명 선택 x 200).

  2. 개수 기반 매핑이 색상, 배경, 오버레이에 대한 돌파구였습니다. hex #cd7f32가 영어로 무엇에 매핑되는지 추측하는 대신, SVG에서 65회 나타나고 설명에서 "brass"가 65회 나타나는 것을 세었습니다 — 고유한 개수 = 확실한 매칭.

  3. 하드 제약 조건이 후보를 빠르게 제거합니다. 각 이미지는 (background_type, overlay_type, shape_count)의 고유한 조합을 가집니다. 12가지 배경 x 7가지 오버레이 x 다양한 도형 개수로 대부분의 이미지는 200개가 아닌 소수의 가능한 설명 매칭만 갖습니다.

  4. SVG는 구조화된 데이터입니다. 래스터 이미지(PNG/JPG)와 달리 SVG는 XML이므로 모든 도형, 색상, 위치, 회전이 텍스트로 명시적으로 인코딩됩니다. 컴퓨터 비전이 필요 없습니다.

어려웠던 점

  1. 오버레이 감지가 까다로웠습니다. 배경 패턴은 5가지 서로 다른 SVG 기법을 사용했습니다: <line> 요소, stroke만 있는 <circle>, 낮은 불투명도 속성을 가진 <circle>, 낮은 불투명도 속성을 가진 <polygon>, 그리고 이들 모두가 없는 경우. 핵심 구분은 XML 속성으로서의 opacity(배경)와 style 내부의 opacity(전경 도형)였습니다.

  2. 색상 어휘가 명확하지 않았습니다. "oxblood"(#800020), "brass"(#cd7f32), "deep sapphire"(#0f52ba) 같은 이름은 개수 기반 접근 방식이 필요했습니다 — hex 값만으로는 신뢰성 있게 추측할 수 없습니다.

  3. SVG 경로에서 도형 분류는 SVG 경로 명령에 대한 이해가 필요했습니다:

    • M (moveto), L (lineto), A (arc), Z (closepath)
도구 다운로드
함수용도
startGame게임 세션을 생성하고 200개의 이미지 + 200개의 설명을 반환
scoreGame{matches: {}, sessionId: ""}를 받아 {correctCount: N}을 반환
generateTokensessionId를 받아 완료 토큰을 반환
getLeaderboard상위 점수를 반환
saveNickname리더보드용 표시 이름을 저장
SVG 그라데이션 시작 색상개수설명 단어개수
#0a162828pitch28
#eef2f720frosted20
#0d0d0d19tenebrous19
#fef9f018pearlescent18
#1e0a2e18nocturnal18
#0a1a0a17midnight17
#1a0a0a15inky15
#f5f0eb14milky14
#f7f3ee14ethereal14
#f0f0f014radiant14
#f0f7f413glowing13
#1a1a2e10somber10
SVG 패턴탐지 방법개수설명 이름개수
<line> 요소, 수평 (dy=0)x1,y1,x2,y2 확인35striped overlay35
<line> 요소, 수직 (dx=0)x1,y1,x2,y2 확인34lattice pattern34
opacity="0.06"인 <circle> (점)낮은 불투명도 원 개수 세기30stippled layer30
오버레이 요소가 전혀 없음선 없음, 배경 도형 없음28(없음)28
<line> 요소, 대각선기울기 방향 확인27tilted cross-lines27
opacity="0.06"인 작은 <polygon>/<path>낮은 불투명도 속성 도형25arrow-band texture25
fill="none" stroke="#888"인 <circle>stroke만 있는 원21ringed pattern21
Hex설명 이름Hex설명 이름
#708090청회색#cd7f32놋쇠색
#f5f5f5거의 흰색#dc143c불타는 빨강
#00bcd4전기 시안#2196f3하늘색
#ff6b6b연어 핑크#228b22짙은 초록
#98ff98옅은 초록#b0b0b0플래티넘
#0f52ba진한 사파이어#800020옥스블러드
#ff8c00진한 주황#4b0082진한 보라
#e2725b테라 로사#ffbf00메리골드
#b7410e적갈색#ff69b4캔디 핑크
#ffd700밝은 금색#c0c0c0주석색
#6b8e23이끼색#008080진한 틸
#40e0d0옅은 틸#0047ab로열 블루
#e34234번트 시에나#a0522d진한 빨강
#4a4a4a어두운 회색#36454f짙은 회색
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
fill-rule="evenodd"인 <path>두 개의 동심원 호 경로donut
단일 호 + Z를 가진 <path>반원 경로half-disc
L 명령 6개 이상인 <path>십자/플러스 모양crosshair
L 명령 3-5개인 <path>화살표 모양pointer
반경 범위설명 이름
0–12minuscule
13–16modest
17–20mid-sized
21–25substantial
26+massive
faint
SVG rotate() 값설명 이름
0°upright
1–20°slightly tilted
21–55°diagonal
56–75°steeply angled
76–105°sideways
  • 유형: 요소 태그 + 꼭짓점 개수 / 경로 명령에서
  • 크기: 꼭짓점 좌표 또는 반경에서
  • 불투명도: style="opacity:X"에서
  • 회전: transform="... rotate(X)"에서
  • 위치: transform="translate(X,Y) ..."에서
  • WKX-9245FA70-200
    구성 요소기술
    챌린지 플랫폼Base44 (로우코드 앱 빌더)
    프론트엔드React SPA (단일 JS 번들, ~410KB)
    백엔드Cloudflare 뒤의 Python/uvicorn
    CDN/프록시Cloudflare (HTTP/3, Brotli 압축)
    인증JWT (HS256), localStorage에 저장
    데이터 형식SVG (base64 인라인), JSON API
    실시간실시간 세션 업데이트용 WebSocket (socket.io)
    솔버Python 3 (정규식 파싱, 외부 라이브러리 없음)
  • 도넛 = fill-rule="evenodd"를 가진 두 개의 동심원 호 경로
  • 반원 = 단일 호 경로
  • 십자선 = 6개 이상의 선분을 가진 경로 (십자/플러스 모양)
  • 포인터 = 3-5개의 선분을 가진 경로 (화살표 모양)