Агар-агар (Agar-agar)
Агар-агар (Agar-agar)
Platform: hackerlab | Category: Steganography | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-07-10 | Status: Solved Techniques: upstream_source_diff, coordinate_grid_plotting, css_comment_stego, glyph_reading
Summary
Task: a deployed fork of huytd/agar.io-clone hides the flag as stego in a served static asset, teased by a hint about writing dots a special way since school. Solution: diff all served assets against the upstream repo via SHA-256 to isolate a modified css/main.css whose added comment stores ~86 (x,y) coordinate pairs; plotting them in order on a grid spells the phrase wrapped in CODEBY{}.
Агар-агар (Agar-agar) — hackerlab
Description
Лучший Агар-агар! А ещё я по-особенному записываю точки со школы...
(Best Agar-agar! And I record dots/points in a special way since school...)
The challenge deploys a fork of the open-source browser game huytd/agar.io-clone
(Express + Socket.IO), titled "Hacker's agar-Agar". Two instances were provided —
62.173.140.174:13002 and 192.169.2.100:13002 (same instance behind both). The
server is fragile and crashes after a few Socket.IO / spectator connections. Flag
format is CODEBY{...}.
Analysis
Client hint → flag wrapper. The client bundle js/app.js registers a -mass
chat command. Toggling it a second time prints a system line:
Something to decode and add between\tCODEBY{}
So the flag wrapper is CODEBY{} and some decoded text goes inside it.
False lead — in-game food dots. The obvious reading of "точки" (dots/points)
is to plot live in-game food coordinates. This is WRONG: food is randomly generated
server-side by map.balanceMass, so its positions carry no message. Organizer hints
confirmed the intended path:
- the flag is "in the sources, not in JavaScript",
- "a stego file was inserted into the distribution",
- "there are spaces in the flag".
Correct approach — diff distribution assets against the upstream repo. Because
the app is a known open-source project, any inserted stego must live in a file that
differs from the original huytd/agar.io-clone. The socket path crashes the
server, so all static assets were pulled with single gentle HTTP GETs, then compared
by SHA-256 against a fresh clone of the upstream repo:
bash read-only • copy allowed# gentle asset pull (avoid crash-prone socket path) for f in index.html css/main.css js/app.js \ img/feed.png img/split.png audio/split.mp3 audio/spawn.mp3; do curl -s "http://192.169.2.100:13002/$f" -o "server1/${f//\//__}" done git clone https://github.com/huytd/agar.io-clone upstream # compare each served asset to its upstream counterpart sha256sum server1/img__feed.png upstream/client/img/feed.png sha256sum server1/img__split.png upstream/client/img/split.png sha256sum server1/audio__split.mp3 upstream/client/audio/split.mp3 sha256sum server1/audio__spawn.mp3 upstream/client/audio/spawn.mp3
Result: all PNG and MP3 assets are byte-identical to upstream (SHA-256 match) —
no media steganography. The only meaningful diff is in css/main.css, which
contains an ADDED CSS comment holding a long list of integer coordinate pairs and
ending with the sentence We went over this in high school!:
css read-only • copy allowed*:focus { outline: 1px solid transparent; border-style: none; /* '5 5 5 0 1 0 1 2 2 ... 0 63 1 63 2 63 3. We went over this in high school!' */ }
The hint "по-особенному записываю точки со школы" ("record dots a special way
since school") plus "We went over this in high school" both point at
school-style coordinate-grid plotting: the integers are ordinary (x, y) pairs.
Solution
- Extract the quoted comment from
css/main.cssand pull every integer. - Interpret them as
(x, y)pairs and connect them in listed order. - Render with matplotlib on an equal-aspect grid — the polyline draws letters.
python read-only • copy allowed#!/usr/bin/env python3 import re from pathlib import Path import matplotlib.pyplot as plt css = Path("server1/css__main.css").read_text() match = re.search(r"/\*\s*'([^']+)'\s*\*/", css) # grab the quoted comment numbers = [int(n) for n in re.findall(r"-?\d+", match.group(1))] # ordinary school-graph coordinates stored as (x, y) points = list(zip(numbers[0::2], numbers[1::2])) x, y = zip(*points) fig, ax = plt.subplots(figsize=(16, 3)) ax.plot(x, y, "o-", color="black", linewidth=1.8, markersize=3) ax.set_aspect("equal") ax.grid(True) fig.tight_layout() fig.savefig("css_points.png", dpi=200) print(f"wrote css_points.png with {len(points)} points")
The exact coordinate list (from the CSS comment):
5 5 5 0 1 0 1 2 2 3 3 3 4 3 5 3 5 0 10 0 8 1 8 3 10 4 12 3 12 1
10 0 10 2 10 0 15 0 15 5 15 4 14 4 16 4 15 4 15 -1 17 -2 15 -1 15 0
20 0 19 1 20 0 21 1 20 2 19 3 20 4 21 3 20 4 19 3 21 1 20 0 30 0
30 4 31 5 30 4 29 4 31 4 30 4 30 0 35 0 33 1 33 3 35 4 37 3 37 1
35 0 40 0 40 4 41 5 40 4 40 0 50 0 48 2 50 0 52 2 50 0 50 -1 50 -2
49 -3 50 -2 50 0 57 0 55 1 55 3 57 4 59 3 59 1 57 0 57 2 57 0 65 0
65 4 65 0 64 0 63 1 63 2 63 3.
Reading the plot. Connecting the 86 points renders three space-separated words.
Careful glyph inspection matters: the first and last round glyphs each contain an
intentional internal vertical stroke from the baseline to their center, marking them
as the digit 0, while the middle word for uses a plain letter o. The rendered
phrase is:
REDACTED
(An initial naive reading dots for you — and an over-corrected d0ts f0r y0u —
were both wrong; the digit/letter distinction is per-glyph.)
Wrapping the decoded phrase per the -mass hint (add between CODEBY{}), preserving
the spaces: