← Back to Writeups
CUSTOMEasyWeb

Первые шаги

XESXOR9/10/20263 min read
#web#custom#easy#source_code_review#client_side_reward_decoding#linux

Первые шаги

Platform: avitoctf | Category: Web | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-07-22 | Status: Solved Techniques: source_code_review, client_side_reward_decoding

Summary

Task: A static honeycomb game exposes fixed coordinates, reward bytes, and its decoder in a public JavaScript asset. Solution: Reproduce the coordinate mixing and xorshift32 XOR routine locally, decode all levels, and concatenate their rewards.

First Steps — avitoctf

Description

А вот кстати и первый самый лёгкий флаг — до него можно дошагать несколькими способами.

The target presents a honeycomb browser game. The apparent goal is to finish its levels, but the real objective is to inspect the client-side implementation and recover the reward generated for each level.

Reconnaissance

Requesting the HTTP URL returns a 303 redirect to HTTPS. The HTTPS root is a static page that loads /assets/firststeps.js. There are no API calls or server-side game-completion endpoints involved in producing the reward.

The standard discovery file is also a dead end:

text
User-agent: *
Disallow: /

Thus, robots.txt neither discloses the reward nor points to a hidden endpoint. Its blanket disallow rule does not prevent direct inspection of the JavaScript asset.

JavaScript Analysis

The served source contains everything required to reproduce the reward calculation:

  • LEVEL_ORDER fixes the processing order as easy, medium, then hard.
  • The board dimensions and twelve hole coordinates are constants.
  • LEVEL_REWARD_LENGTHS gives the number of characters produced for each level.
  • LEVEL_REWARD_DATA stores the encoded reward bytes.
  • levelReward(level) implements the complete decoder.

For a selected level, levelReward() starts with a 32-bit state derived from fixed constants, the level index, and the board dimensions. It then mixes every hole's linear cell index into that state with Math.imul() and an 11-bit rotation.

For each output character, the function advances a xorshift32 generator using shifts of 13, 17, and 5 bits. It selects the level's lane from the interleaved reward array and XORs that byte with the low byte of the updated state. JavaScript's unsigned >>> 0 behavior must be retained so that every operation remains within 32 bits.

The game eventually calls the same public function after marking levels complete. Because its inputs and algorithm are already available to the browser, reproducing the function locally gives exactly the same result without playing the game.

Solution

  1. Save the served /assets/firststeps.js asset.
  2. Copy the public board constants and the logic of levelReward() into a standalone Node.js script.
  3. Evaluate the function for each entry in LEVEL_ORDER.
  4. Concatenate the three returned rewards in that order and wrap the result in the required avito{...} format.

The complete, verified solver is preserved as tasks/avitoctf/pervye_shagi/solve.js. It mirrors the served function directly, including Math.imul(), rotations, xorshift32 updates, lane selection, and unsigned coercions.

Run it from the task directory:

bash
node solve.js

Verification output:

text
avito{REDACTED}

Why Other Approaches Are Unnecessary

  • Browser game play: completion only gates the UI path to code that is already shipped to every client. It adds no secret input.
  • robots.txt enumeration: the file contains only a blanket disallow directive and no useful path.
  • Path traversal: no user-controlled file-read sink exists. The static JavaScript already provides a deterministic solution.
  • Hidden shared endpoint: source and traffic inspection reveal no API responsible for calculating or returning the reward.