← Back to Writeups
CUSTOMEasySteganography

Промпты на стол! (Prompts on the table!)

XESXOR9/2/20262 min read
#steganography#custom#easy#docx_xml_run_analysis#font_family_steganography#single_char_run_extraction

Промпты на стол! (Prompts on the table!)

Platform: bug-makers | Category: Steganography | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2025-10-19 | Status: Solved Techniques: docx_xml_run_analysis, font_family_steganography, single_char_run_extraction

Summary

Task: DOCX containing an LLM prompt/response hides a flag via font-based steganography. Solution: word/document.xml splits text into 264 runs; payload characters are single-char runs set in Liberation Serif (vs Times New Roman cover); concatenating them in document order yields the flag.

Промпты на стол! (Prompts on the table!) — bug-makers

Description

На рабочем столе у коллеги лежат файлы, в которых он сохраняет свои запросы к нейросети и ответы на них, вот один из них. Зачем он это делает — непонятно.

English summary: A colleague stores his queries to a neural network and the answers in files; one is provided as prompts.zip. The goal is to find the hidden flag inside the document.

Analysis

The ZIP extracts to a single artifact, prompt JSON to C# class.docx (plus a __MACOSX resource fork to ignore). Opened normally, the DOCX shows an innocent LLM conversation: a request to convert several JSON objects (User, Product, Order, Company, Event) into C# classes, followed by the assistant's generated C# class definitions. Visible text is only ~4 KB. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

A DOCX is just a ZIP archive, so the next step is to unzip it and inspect word/document.xml directly. The red flag: word/document.xml was ~102 KB — far larger than the small amount of visible text would justify.

Common document-stego vectors were checked and ruled out:

  • No zero-width / invisible Unicode — 0x200b, 0x200c, 0x200d, 0x2060, 0xfeff, 0xad all absent; visible text was pure ASCII.
  • No white/colored hidden text — every w:color value was 000000.
  • No w:vanish / hidden-text formatting.
  • docProps metadata (core.xml, app.xml) held nothing useful — just timestamps and a MyOffice application string. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The actual trick is in the run structure. The text was fragmented into 264 separate <w:r> runs, using two fonts:

  • Most runs used Times New Roman — the cover text.
  • Scattered single-character runs used Liberation Serif — visually identical when rendered, but a different font family.

This is font-family steganography: the font choice forms a hidden channel, and the alternate-font characters spell the payload. The flag content even self-references the technique — "how could you see the difference" — because the two fonts look identical to the eye. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Unzip the DOCX, parse word/document.xml, and extract (in document order) the text of every run whose w:rFonts w:ascii is Liberation Serif.

python
#!/usr/bin/env python3
import re, html

data = open('word/document.xml', encoding='utf-8').read()
runs = re.findall(r'<w:r>(.*?)</w:r>', data, re.S)

msg = ''
for r in runs:
    f = re.search(r'w:ascii="([^"]*)"', r)
    t = re.search(r'<w:t[^>]*>(.*?)</w:t>', r, re.S)
    if not t or not f:
        continue
    if 'Liberation' in f.group(1):
        msg += html.unescape(t.group(1))
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

print(msg)  # -> buGctF{REDACTED}

Concatenating the Liberation Serif characters in document order spells the flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍