← Back to Writeups
HTBN/AMisc

Terminal Diff

XESXOR8/23/20265 min read
#misc#htb#n/a

Terminal Diff

Platform: Broncoctf2026 | Category: Misc | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-07-11 | Status: Solved Techniques: ascii_art_ocr_via_pixel_render, braille_image_recognition, directional_arrow_spiral_reading, factoring_character_count_to_terminal_width, figlet_ticks_font_recognition, text_wrapping_at_97_columns

Summary

Task: a single 3395-char Unicode line (flag.txt) whose riddle hints at a terminal size. Solution: factor the length (3395 = 5797), wrap at 97 columns to reveal FIGlet 'ticks' block letters around a Braille oyster, render to pixels for OCR, then read the glyphs in a clockwise spiral dictated by edge arrows.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: broncoctf2026 | ID: 20260711_broncoctf2026_terminal_diff
  • Tags: steganography, ascii_art, wordplay, braille, misc, figlet, ticks_font, terminal_width, text_wrapping
  • Indicators: single very long line of Unicode text, characters limited to / \ _ < > ^ v plus U+2800 Braille block, riddle hints at terminal width/height, character count factors cleanly (3395 = 5797)
  • Source: 20260711_broncoctf2026_terminal_diff.md

Foothold

Vulnerability / Misconfiguration

  1. Ascii_art_ocr_via_pixel_render
  2. Braille_image_recognition
  3. Directional_arrow_spiral_reading
  4. Factoring_character_count_to_terminal_width
  5. Figlet_ticks_font_recognition
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • ascii_art_ocr_via_pixel_render
  • braille_image_recognition
  • directional_arrow_spiral_reading
  • factoring_character_count_to_terminal_width
  • figlet_ticks_font_recognition
  • text_wrapping_at_97_columns
  • Tags: steganography, ascii_art, wordplay, braille, misc, figlet, ticks_font, terminal_width, text_wrapping

Original Writeup

<details><summary>Click to expand original content</summary>

Description

I used to be too big picture, never focusing on the details (keeping track of like 90 things at once). Then, I starting looking into things a little bit too much (this phase only lasted like 7 days). Nowadays though, I am primed to look at things with just the right width (and height too). Anyways, here's the flag! You should be able to read it just fine, as long as you align with my mindset.

The only artifact is flag.txt from the CTFd instance: one very long single line of Unicode text. The riddle is a disguised hint about the correct terminal width (and height) at which the line should be wrapped so that the ASCII art becomes readable.

Analysis

file flag.txt reports "Unicode text, UTF-8, with very long lines". The file is a single line of 3395 characters (4140 bytes). The character set is small:

  • / \ _ < > ^ v — ASCII art strokes and directional markers
  • many codepoints in the Braille block (U+2800) — used to draw a small picture

The riddle encodes a terminal size through numeric decoys:

  • "keeping track of like 90 things" → width 90 is too big
  • "this phase only lasted like 7 days" → width 7 is too small
  • "just the right width (and height too)" + "align with my mindset" → wrap the single line at the correct terminal width

The clean way to find the width is to factor the character count:

3395 = 5 × 7 × 97

Wrapping the line every 97 characters yields exactly 35 rows (5 × 7). The intended terminal is 97 columns × 35 rows.

Solution

1. Factor the length and wrap

import sympy
s = open('flag.txt').read().rstrip('\n')
print(len(s), sympy.factorint(len(s)))   # 3395  {5:1, 7:1, 97:1}
w = 97
print('\n'.join(s[i:i+w] for i in range(0, len(s), w)))

Wrapped at 97 columns, the / and \ characters form large block letters — a narrow FIGlet "ticks"-style font (the standard figlet -f ticks uses /\/\ per stroke; this puzzle uses a half-width /\ variant). The _ characters are the font's background padding. The Braille block (U+2800) in the middle draws an oyster — a thematic nod to the idiom "the world is your oyster".

2. Render to pixels for OCR

Treat / and \ as filled pixels and everything else as background, then rasterize to a scaled PNG so the letters become legible:

from PIL import Image
rows = [s[i:i+97] for i in range(0, len(s), 97)]
scale = 9
img = Image.new('1', (97*scale, 35*scale), 1)
px = img.load()
for r in range(35):
    line = rows[r] if r < len(rows) else ''
    for c in range(97):
        if c < len(line) and line[c] in '/\\':
            for dy in range(scale):
                for dx in range(scale):
                    px[c*scale+dx, r*scale+dy] = 0
img.save('clean.png')

3. The letter grid

The glyphs are laid out in a 6×8 grid of FIGlet letters wrapping around the central oyster:

bronco{r
le_world
o      as     <- oyster image occupies the middle of these three lines
h      ti
w       z
_say_...

4. Directional arrows define reading order

Non-letter markers appear at the edges of the grid:

  • >> at the left of band le_world → read rightward
  • vvvv down the right side → read downward
  • << at the bottom-right → read leftward
  • ^^^^ up the left side → read upward

These trace a clockwise spiral around the oyster. Reading the grid naively in row-major order produces gibberish (bronco{rle_worldoashtiwz_say_...); the arrows are mandatory.

5. Decode the words

Verified glyph-by-glyph from the render:

  • Rightmost column top→bottom, continuing into the bottom-right corner = resizing (r-e-s-i-z-i-n-g)
  • Left column bottom→top, turning right into band le_world = whole_world (who + le + _world)
  • Bottom-middle = say (appears reversed as _yas_ because the spiral crosses the bottom right-to-left)

Assembling the spiral in natural reading order yields the sentence matching the riddle's theme — fitting "the whole world" into the terminal by resizing it:

bronco{REDACTED}
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR