← Back to Writeups
CUSTOMMediumForensics

Ох уж этот кладмен (Oh, this stash guy)

XESXOR9/2/20264 min read
#forensics#custom#medium#arithmetic_decoding#docx_structure_analysis#label_length_key#ooxml_bookmark_steganography

Ох уж этот кладмен (Oh, this stash guy)

Platform: cyberareactf | Category: Forensics | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-07-15 | Status: Solved Techniques: arithmetic_decoding, docx_structure_analysis, label_length_key, ooxml_bookmark_steganography

Summary

Task: a DOCX access-control migration report hides a flag with no standard stego vector (no macros, embeddings, or hidden text). Solution: a customXml part reveals an arithmetic rule M = B + L + K; the hidden channel is OOXML bookmark names NB_<seq>_<M> around English labels, decoded as B = M - len(label) - key_digit(Request ID 23063).

Ох уж этот кладмен (Oh, this stash guy) — cyberareactf

Description

В ходе внутренней проверки был обнаружен DOCX-файл с отчётом по миграции реестра управления доступом. Один из бывших сотрудников оставил странную пометку: «Внутри этого документа спрятан клад:)». Формат флага: CYBERAREA{}.

English summary: An internal review found a DOCX report about migrating an access-control registry. A former employee left an odd note: "Inside this document a treasure is hidden :)". The title puns on клад (treasure/stash) and кладмен (drug-stash courier). The flag is hidden inside the OOXML package via a non-standard steganographic channel and must be recovered with a custom arithmetic decoding rule. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Analysis

1. Unwrap the containers

The file is delivered as report.docx.zip, which is a ZIP wrapping the real report.docx plus a macOS resource-fork artifact:

report.docx.zip
├── report.docx          (the real OOXML package)
└── __MACOSX/._report.docx (resource fork, ignore)

Unzipping yields outer/report.docx, a valid Microsoft Word 2007+ (OOXML) package with 21 standard parts. file confirms the type; there is no trailing/appended data, no ZIP comment, and no archive comment. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

2. Rule out standard DOCX stego vectors

Systematically eliminating the usual DOCX hiding places — none of them carry the payload:

  • No word/embeddings/, no media, no macros (vbaProject.bin absent).
  • No external relationships → no Remote Template Injection.
  • No OLE embedded objects.
  • No <w:vanish/>, no tracked revisions, no hidden-text payload. (White text exists but is ordinary dashboard/table-header styling, not a channel.)
  • No font/color/size character channel.
  • Nothing in metadata, footer, empty footnotes/endnotes, or styles. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

3. customXml holds the instructions, not the ciphertext

word/customXml/item1.xml and item2.xml are identical hint parts. They tell the analyst to:

  1. Inspect the English labels in the document.

  2. Follow the entries in order.

  3. Apply the "legacy rule":

    M = B + L + K
    

    where

    • B = the original ASCII byte (the value we want),
    • L = the UTF-8 byte length of the associated English label,
    • K = a key digit taken by repeating the Request ID digits 23063. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

4. The hidden channel = OOXML bookmark names

word/document.xml contains invisible bookmarks wrapped around 38 selected English control labels (e.g. "Access governance", "Account lifecycle", ...). Each relevant bookmark is named:

NB_<sequence>_<M>
  • <sequence> = ordering index 1..38,
  • <M> = the encoded number from the rule M = B + L + K.

Examples span NB_01_086 … NB_38_136. AUX_* bookmarks are decoys, and REF_* bookmarks are unrelated navigation markers — both are ignored. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Solution

For each NB_* bookmark:

  1. Read its <w:name> and parse sequence and M.
  2. Take the English label text from the paragraph the bookmark sits in; L = len(label.encode()) (UTF-8 byte length).
  3. Derive the key digit by cycling the Request ID: K = int("23063"[(sequence-1) % 5]).
  4. Recover the byte: B = M - L - K, which is a printable ASCII character.

Sorting by sequence and concatenating the recovered bytes yields the flag.

python
#!/usr/bin/env python3
import re, zipfile
from xml.etree import ElementTree as ET
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

DOCX = "outer/report.docx"
W = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
REQUEST_ID_DIGITS = "23063"

root = ET.fromstring(zipfile.ZipFile(DOCX).read("word/document.xml"))

entries = []
for p in root.iter(W + "p"):
    label = "".join(n.text or "" for n in p.iter(W + "t"))
    for m in p.iter(W + "bookmarkStart"):
        mm = re.fullmatch(r"NB_(\d+)_(\d+)", m.get(W + "name", ""))
        if mm:
            seq, M = map(int, mm.groups())
            entries.append((seq, M, label))
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

entries.sort()

flag = ""
for seq, M, label in entries:
    L = len(label.encode())              # UTF-8 byte length of the English label
    K = int(REQUEST_ID_DIGITS[(seq - 1) % 5])
    flag += chr(M - L - K)               # B = M - L - K

print(flag)  # CYBERAREA{REDACTED}

The recovered plaintext is leetspeak Russian: "кладмен больше так не будет" ("the stash guy won't do this again"), tying back to the challenge's клад/кладмен wordplay. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍